您的位置:首页 > 理论基础 > 数据结构算法

数据结构--队列-泛型OC&C++混编-泛型编程

2016-01-02 20:50 513 查看
在这篇文章里, 您可以学习到:

数据结构简介
数据结构的逻辑结构和物理结构
队列
OC和C++在Xcode中的混编
泛型编程思想
泛型编程实现循环队列和链表队列
博客中使用的图片均来自网络


一.数据结构简介

数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。数据结构往往同高效的检索算法和索引技术有关。

帮你理解

数据结构的实现与语言无关, 它可以被任何语言实现.
一个好的抽象, 可以适用普遍数据类型. 除了特定场合和教学示例外, 你不能只支持某种特定的数据类型.

谈到数据结构, 一定会涉及到的两个概念: 数据 和 数据之间的关系.

数据: 我们前面提到, 数据结构不分数据类型, 这个数据可以是任何数据, 你可以把它想象成是一个收纳盒, 盒子内可以放任何东西.

关系: 按照相面的比喻, 关系就是指这些收纳盒之间的映射关系. 比如我规定一种关系: 把这些盒子排成一列, 对于中间的某个盒子来讲, 它的前面和后面都有一个和它紧挨的盒子. 我们可以说, 这个盒子和它前面和后面的盒子有关系, 和它不相邻的盒子没有关系. 那么我们可以把这样的模型剥离出来, 得到一种数据结构 – 线性结构, 又叫线性表.

推理

按照这种抽象模型的过程, 我们可以把下面的关系抽离出另一种数据结构树.



file-list

还可以继续抽象, 元素之间的关系式任意的, 就得到了网状结构, 也就是我们所说的图.



file-list

抽象出状结构之后, 我们基本已经无法再从对应关系上来细分了, 不信您就想想, 你所成想到的关系, 全部包含在这几种抽象中了

吗?

当然不是, 空间还能抽象到11维呢!

事实上, 上述数据结构的共性–每个元素至少与其他元素建立了联系. 这种规则是人为规定的, 没人要求我们一定这样抽象数据, 所以, 我们可以假设所有的盒子之间两两都没有关系, 所有盒子都是独立的, 就像一堆硬币, 我们把这种数据结构成为集合:



file-list

至此, 我们得到了四种结构

线性表: 数据结构中的元素存在一对一的相互关系

树: 数据结构中的元素存在一对多的相互关系

图: 数据结构中的元素存在多对多的相互关系

散列(集合): 数据结构中的元素之间除了“同属一个集合” 的相互关系外,别无其他关系

注意: 数据结构只有这四种吗?

现实世界中的结构千变万化,没有通用结构可以解决所有问题。甚至一个小问题,某个适应此类问题很优秀的结构却未必就适合它。

我们提炼的4种结构, 相对来讲是最具普遍性的, 能适应现实世界的大多数场景.

你可以提炼你自己的数据结构.


二. 数据结构的逻辑结构和物理结构

书承上文

数据结构指同一数据元素类中各数据元素之间存在的关系。数据结构分别为逻辑结构、存储结构(物理结构)和数据的运算. 数据的逻辑结构是从具体问题抽象出来的数学模型,是描述数据元素及其关系的数学特性的,有时就把逻辑结构简称为数据结构.

逻辑结构: 我们上面赘述了那么多, 都是描述的逻辑结构(不涉及实现问题, 只是描述它们的关系).

存储结构(物理结构): 真正有关数据在计算机中是如何存储方式.

这个’存储’是指在内存中的存在形式(并非外设, 如硬盘/U盘/光盘).

假设我们有10个盒子, 它们是线性结构, 这10个盒子应该如何摆放呢?

既然是线性结构, 难道不是依次摆放吗? 一个挨着一个的样子.

您要是这么想得话, 说明您没有明白物理结构和逻辑结构的区别.

假如我们把盒子分别放到不同的地方, 然后在每个盒子上写明: 它上一个盒子是放在哪里, 下一个盒子放在哪里.

那么您是不是可以根据盒子上的位置找到下一个盒子呢? 当然可以了.

这就是逻辑结构(线性关系)和物理结构(摆放位置)的区别.

事实上, 我们在内存中存放数据的格式确实有两种: 顺序存储 和
链式存储

顺序存储: 顾名思义, 数据放在一块儿连续的内存空间, 工作指针的移动可以很方便的找到其他元素(我说的是其他元素, 而不是下一个元素, 因为这样的存储方式也不一定完全为线性表存储)

链式存储: 所有元素均任意存放在内存中, 元素之间是通过元素内的指针互相寻找对方的.


三.队列

队列是一种特殊的线性表,特殊之处在于它只允许在表的 前端(head) 进行删除操作,而在表的
后端(tail) 进行插入操作,队列是一种操作受限制的线性表。进行插入操作的端称为
队尾 ,进行删除操作的端称为
队头 。

队列特点是先进先出,后进后出.



file-list

队列的定义, 除了点名了它元素之间的逻辑关系, 也定义了它的部分算法.

算法与数据结构密不可分, 它依附于数据结构而存在, 对于任何算法的编写,必须依赖一个已经存在的数据结构来对它进行操作,数据结构成为算法的操作对象,这也是为什么算法和数据结构两门分类不分家的概念,算法在没有数据结构的情况下,没有任何存在的意义. 而数据结构没有算法就等于是一个尸体而没有灵魂

对于一个线性表来说, 必须实现下面5种基本方法才能被称为是队列:

初始化队列:Init_Queue(q) ,初始条件:队q 不存在。操作结果:构造了一个空队
入队操作: In_Queue(q,x),初始条件: 队q 存在。操作结果:对已存在的队列q,插入一个元素x 到队尾,队发生变化
出队操作: Out_Queue(q,x),初始条件: 队q 存在且非空,操作结果: 删除队首元素,并返回其值,队发生变化
读队头元素:Front_Queue(q,x),初始条件: 队q 存在且非空,操作结果: 读队头元素,并返回其值,队不变
读队头元素:Front_Queue(q,x),初始条件: 队q 存在且非空,操作结果: 读队头元素,并返回其值,队不变

以上只是要求实现的基本方法, 你可以为队列添加其他的方法,我们规定上面的方法是普世大多数应用场景, 如果你的需求特殊, 完全可以进行扩展.


队列的实现方式


队列是一种受限制的线性表, 所以它的实现和线性表的实现一样, 可以分为
顺序存储 和 链式存储

队列的顺序存储分为两种: 顺序队列和循环队列, 具体我们在下面详解.

队列的链式存储是一种链表实现.


顺序队列


建立顺序队列结构必须为其静态分配或动态申请 一片连续的存储空间 ,并设置两个指针(下标)进行管理。一个是队头指针head,它指向队头元素;另一个是队尾指针tail,它指向下一个入队元素的存储位置.



file-list

每次在队尾插入一个元素是,tail增1每次队头删除一个元素时,head增1。
随着插入和删除操作的进行,队列元素的个数不断变化,队列所占的存储空间也在为队列结构所分配的连续空间中移动。
当head==tail时,队列中没有任何元素,称为 空队。
当tail增加到指向分配的连续空间之外时,队列无法再插入新元素.成为 满队
这时往往还有大量可用空间未被占用,这些空间是已经出队的队列元素曾经占用过得存储单元。这种现象叫做 “溢出”

事实上, 顺序队列有三种溢出:

“下溢”现象:当队列为空时,做出队运算产生的溢出现象。“下溢”是正常现象,常用作程序控制转移的条件.
“真上溢”现象:当队列满时,做进栈运算产生空间溢出的现象。“真上溢”是一种出错状态,应设法避免.
“假上溢”现象:由于入队和出队操作中,头尾指针只增加不减小,致使被删元素的空间永远无法重新利用。当队列中实际的元素个数远远小于向量空间的规模时,也可能由于尾指针已超越向量空间的上界而不能做入队操作。该现象称为”假上溢”现象.

“下溢”和”真上溢”是我们无法避免的, 他们本身也不属于异常现象. 但是”假上溢”会使我们程序发生异常,我们要避免它.

如何利用使用过的空间呢?


循环队列


为充分利用空间,克服”假溢出”现象的方法是:将空间想象为一个首尾相接的圆环,并称这种空间为循环队列。循环队列可以有效的使用空间.



file-list



file-list

从图中我们可以看到

当有元素入队时, tail(此时head和tail不是指针, 而是下标), 向后移动一位. tail 永远在队尾元素的下一个位置.
如果tail的值超过我们的空间总大小, 则对tail对数组长度取模. 使得tail永远不会越界.
出队时, 返回head下标所在的值, 之后head向后移动一位, 与tail一样, 一旦超出数组长度, 则对其取模.
开始时, head和tail指向同一位置, 此时队列为空.
当满队时, tail移动到head位置, 此时 满队.

如果按照上述逻辑, 在判断满队时, 因为tail和head均指向同一位置, 所以我们不能作出区分. 在这里, 判断满队的逻辑发生了问题.

判断满队我们有两种方案:

1.通过一个计数器,记录元素个数, 当元素个数与数组最大值相等,则为满列;

2.少用一个存储空间,也就是数组的最后一个存数空间不用,当(tail+1)%maxsiz = head时,队列满;

我们会在后面的示例中演示循环队列的Demo, 这个Demo采用的是第一种方案.


队列的链表


在队列的形成过程中,可以利用线性链表的原理,来生成一个队列.

新元素(等待进入队列的元素)总是被插入到链表的尾部,而读取的时候总是从链表的头部开始读取。每次读取一个元素,释放一个元素。所谓的动态创建,动态释放。因而也不存在溢出等问题。由于链表由结构体间接而成,遍历也方便。

队列的链表实现原理图:



file-list

插入数据时:



file-list

因为链表的存储方式数据动态申请空间, 插入元素的时候才会申请空间, 所以不存在满队的情况

你可以人为的为链表队列设置一个上限. 一旦达到这个上限, 则满队.

后面的demo回详细的为您解释基本算法的实现.


四.OC与C++的混编

苹果的Objective-C编译器允许用户在同一个源文件里自由地混合使用C++和Objective-C,混编后的语言叫Objective-C++。有了它,你就可以在Objective-C应用程序中使用已有的C++类库。


五.泛型编程

您肯定遇到过如下情况, 编写好的函数因为类型原因, 往往要重写好多次, 造成代码重复, 如下面的例子:

// 一个返回两个整型数的和 addInt函数

1
2
3
4

int addInt(int a, int b)
{
return a+b;
}

// 一个返回两个浮点数的和 addFloat函数
1
2
3
4

float addFloat(float a, float b)
{
return a+b;
}

上面的代码, 除了返回值和参数的类型不同, 逻辑完全一样, 对于这种情况, C++率先提出了泛型编程的概念.

泛型即是指具有在多种数据类型上皆可操作的含意,与模板有些相似。

泛型编程让你编写完全一般化并可重复使用的算法,其效率与针对某特定数据类型而设计的算法相同。

关于泛型的理解, 您可以理解为”将数类型做为参数传递进来”

泛型编程(Generic Programming)最初提出时的动机很简单直接:发明一种语言机制,能够帮助实现一个通用的标准容器库。

我们看一个例子, 下面的代码使用C++语法:

1
2
3
4

template <typename T> struct Node{
T data;      //数据域
Node * next; //指针域
};

上述代码中, 我们定义了一个结构体, 结构体中有两个成员变量 data 和 next

next:指向自身结构体类型的指针.

data:的类型是T, 这个T是什么呢?

template < typename T >: 这条语句表示T的类型定义还没有给出,需要在使用Node类型时, 通过”<>” 符号来指明 typename T 的类型.

1

Node<int> node;

注意, 上述代码可以写成
1
2
3
45

template <typename T>
struct Node{
T data;      //数据域
Node * next; //指针域
};

template 是来修饰 struct Node 的, 您不能在他们中间插入任何代码, 比如:

错误的示例:
1
2
3
45
6

template <typename T>
int a = 0;  // 这里是错误的
struct Node{
T data;      //数据域
Node * next; //指针域
};

前面说到, 数据结构是不分语言和类型的, 我们能不能写一个队列, 适配所有的数据类型呢? 向队列插入int, float, double, char都可以处理, 甚至对象也能放进去.

我们引入泛型的概念就是为此目的.


STL模板


我们能想到这种思路, 前辈们早就想到了, 事实上, STL即为用泛型编程思想实现的一系列数据结构和算法的封装, 我们下面要写的例子, 实际上在C++的模板库中早已为我们写好. STL模板库有17个头文件,6个大部分.
STL可分为容器(containers)、迭代器(iterators)、空间配置器(allocator)、配接器(adapters)、算法(algorithms)、仿函数(functors)六个部分。


六. 泛型编程实现循环队列和链表队列

1.新建工程



file-list



file-list

2.新建C++文件, 我们没有创建头文件



file-list



file-list

3.创建完成后, 手动修改文件类型为.mm类型.



file-list

4.循环队列的实现

我们在LinkListByArray.mm中写下面代码, 注意, 这个是C++语法
1
2
3
45
6
7
8
9
10
1112
13
14
15
16
17
18
19
20
2122
23
24
25
26
27
28
29
30
3132
33
34
35
36
37
38
39
40
4142
43
44
45
46
47
48
49
50
5152
53
54
55
56
57
58
59
60
6162
63
64
65
66
67
68
69
70
7172
73
74
75
76
77
78
79
80
8182
83
84
85
86
87
88
89
90
9192
93
94
95
96
97
98
99
100
101102
103
104
105
106
107
108
109
110
111112
113
114
115
116
117
118
119
120
121122
123
124
125
126
127
128
129
130
131132
133
134
135
136
137
138
139
140
141142

//
//  LinkListByArray.cpp
//  QueueByLinkList
//
//  Created by LO3G on 15/11/29.
//  Copyright © 2015年 LO3G. All rights reserved.
//

#include <iostream>

using namespace std;

template <typename T>
class LinkListByArray {
private:

// 指向队列首地址, 该地址永远指向连续内存空间的首地址而不会发生变化
T * arrayPtr;

// 队头下标, 注意类型是个整型
int head;

// 队尾下标
int tail;
int maxCnt;

public:
// 队列当前元素个数, 将这个属性设为共有, 是为了外界能直接访问队列个数.
// c++没有oc中"属性"概念, 所有成员变量的语义需要用方法来控制.
int count;

// 初始化
LinkListByArray(int cnt);

// 入队
void pushItem(T item);

// 出队
T popItem();

// 空队
bool isEmpty();
// 满队
bool isFull();

// peek队头元素
T peekHead();

// 打印队列
void display();
};

// 初始化
template <typename T>
LinkListByArray<T>::LinkListByArray(int cnt)
{
// 参数cnt表示想要创建队列的最大长度.
maxCnt = cnt;

// 按照类型T*个数的方式, 在堆区内为队列开辟空间.
arrayPtr = (T *)malloc(cnt*sizeof(T));

// 头和尾下标均指向数组的第一个位置.
head = 0;
tail = 0;

// 队列内元素个数在初始化时为0.
count = 0;
}

// 空队
template <typename T>
bool LinkListByArray<T>::isEmpty()
{
// 这里使用一个计数器记录队列元素个数
if (count == 0) {
cout << "空队" << endl;
return true;
}else
{
return false;
}
}
// 满队
template <typename T>
bool LinkListByArray<T>::isFull()
{
if (count == maxCnt) {
cout << "满队" << endl;
return true;
}else
{
return false;
}
}

// 入队
template <typename T>
void LinkListByArray<T>::pushItem(T item)
{
// 如果满队的情况, 不能入队了, 直接返回.
if (isFull()) {
return;
}
// 入队操作只在队列尾部
arrayPtr[tail] = item;

// 注意, 这里为tail增加时, 要对其取模, 保证它永远不会大过数组总长度.
tail = (tail + 1) % maxCnt;

// 入队成功, 将元素个数记录+1.
count++;
}

// 出队
template <typename T>
T LinkListByArray<T>::popItem()
{
// 如果空队, 则不能返回任何元素, 返回NULL作为标记, 外部需判断
if (isEmpty()) {
return NULL;
}

// 出队时, 声明一个临时变量做一次值拷贝
T tmp = arrayPtr[head];

// 之后将head向后移动一位, 注意, 这里也要对head取模, 保证其值不会大于数组总长度.
head = (head + 1) % maxCnt;
count--;

return tmp;
}

// 打印队列
template <typename T>
void LinkListByArray<T>::display()
{
for (int i = 0; i <= count - 1; i++) {
cout << arrayPtr[(head + i) % maxCnt] << ' ';
}
cout << endl;
}

main.mm中调用

1
2
3
45
6
7
8
9
10
1112
13
14
15
16
17
18
19
20
2122
23
24
25
26
27
28
29
30
3132
33
34
35
36
37
38
39
40

//
//  main.mm
//  QueueByLinkList
//
//  Created by LO3G on 15/11/27.
//  Copyright © 2015年 LO3G. All rights reserved.
//
#import <iostream>
#import "LinkListByArray.mm"

using namespace std;

int main(int argc, const char * argv[]) {

// 初始化一个4个元素个数的队列
// 注意<char>指明队列内元素类型为char型, 这里你可以写任何类型, 但是需入队时类型一致.
LinkListByArray<char> queueArray(4);

queueArray.pushItem('a');  // 入队一个元素'a'
queueArray.pushItem('b');
queueArray.pushItem('c');
queueArray.pushItem('d');

// 出队, 我们没有用变量接受, 出队的元素被我们弄丢了.
queueArray.popItem();

queueArray.pushItem('a');
queueArray.pushItem('b');
queueArray.pushItem('c');
queueArray.pushItem('d');

queueArray.popItem();
queueArray.pushItem('a');
queueArray.popItem();

// 打印队列内所有元素, 看看结果与我们预期的是否一致.
queueArray.display();

return 0;
}

5.队列的链表实现

新建C++文件, 注意仍然没有创建头文件



file-list



file-list

在LinkList.mm中写下面代码, 注意, 这个是C++语法
1
2
3
45
6
7
8
9
10
1112
13
14
15
16
17
18
19
20
2122
23
24
25
26
27
28
29
30
3132
33
34
35
36
37
38
39
40
4142
43
44
45
46
47
48
49
50
5152
53
54
55
56
57
58
59
60
6162
63
64
65
66
67
68
69
70
7172
73
74
75
76
77
78
79
80
8182
83
84
85
86
87
88
89
90
9192
93
94
95
96
97
98
99
100
101102
103
104
105
106
107
108
109
110
111112
113
114
115
116
117
118
119
120
121122
123
124
125
126
127
128
129
130
131132
133
134
135
136
137
138
139

//
// LinkedQueue.cpp
// QueueByLinkList
//
// Created by LO3G on 15/11/26.
// Copyright © 2015年 LO3G. All rights reserved.
//

#include <iostream>

using namespace std;
// 这个demo的队列与上图中略有不同, 该队列没有链表结构中的head节点.
// 所有的节点均为数据节点, head和tail作为两根成员指针, 一旦指向相同位置, 则将其置空, 表示当前空队.

// 定义数据节点.
template <typename T> struct Node{ T data; //数据域 Node * next; //指针域 };
// 队列管理类
template <typename T>
class LinkList{
Node<T> * head; // 队列的头
Node<T> * tail; // 队列的尾巴
public:
// 重写了默认初始化方法
LinkList(){head=NULL;tail=NULL;count=0;}
// 队列中元素的个数, 这个成员是可以被外界访问的, 但是不能被外界修改. c++中没有这个语义.
int count;
// 入队
void pushItem(T item);
// 出队
T popItem();
// 判断队列是否为空
bool isEmpty();
// 打印队列元素
void display();
// 根据一个数组填装队列. 这个方法并不是队列的基本算法, 是我随意加上的.
void packingByCAarray(T * start,T * end);
};

// 入队
template <typename T>
void LinkList<T>::pushItem(T item){
// 如果空列, 此时入队一个元素的话, head和tail均需要指向这个元素.
if (this->isEmpty()) {
// 让head指向一个新的节点
head = (Node<T> *)new(Node<T>);

// head指向的节点的数据域为需要入队的值.
head->data = item;

// head指向节点的next指针域指向空
head->next = NULL;

// tail和head指向同一片天空.
tail = head;
}else{
// 如果不为空的话, head和tail必将指向了不同的元素, 此时应该单独移动tail, 而不移动head.

// 新建一个节点
Node<T> * newNode = (Node<T> *)new(Node<T>);

// 让当前tail的next域 指向这个新的节点
tail->next = newNode;

// 新节点的数据域为入队的值
newNode->data = item;

// 将tail移动(指向)新的节点.
tail = tail->next;

// 将tail(newNode)的next域指向空.
tail->next = NULL;
}

// 入队之后, 队列元素个数+1.
count++;
}

// 出队
template <typename T>
T LinkList<T>::popItem(){
// 如果空队, 没有元素能出队了, 直接返回空.
if (this->isEmpty()) {
return NULL;
}
// 用临时变量对head的数据区做一次值拷贝.
T nodeData = head->data;

// 定义一根和head相同的指针, 这个指针的作用是: 当head移动到下一个位置时, 仍能找到上一个返回的元素, 将其释放掉.
Node<T> * nodep = head;

// head移动到下一个元素位置
head = head->next;

// 刚才定义的指针有作用了, 释放掉出队的元素空间.
delete nodep;

// 出队了, 元素个数应该减一.
count--;

// 返回值.
return nodeData;
}

// 判断队列是否为空
template <typename T>
bool LinkList<T>::isEmpty(){
// 根据记录的元素个数来判断是否为空对.
return count == 0 ? true : false;
}

// 打印队列元素
template <typename T>
void LinkList<T>::display(){
// 定义一根可以移动的指针, 这个指针根据节点的next一直向下找, 一直找到next为NULL为止.
Node<T> *iter = head;
while(iter != NULL){
// 打印iter指针指向元素的数据域
cout << iter->data << ' ';
// iter移动到下一个位置.
iter = iter->next;
}
cout<<endl;
}

// 使用另外一个数组来初始化队列
// 来来来, 请您自己来分析一下这个方法的原理~!~
template <typename T>
void LinkList<T>::packingByCAarray(T *start,T *end){
if(start==end)return;

while(start!=end){
this->pushItem(*start++);
}
}

在main.mm中调用

1
2
3
45
6
7
8
9
10
1112
13
14
15
16
17
18
19
20
2122
23
24
25
26
27
28
29
30
3132
33
34
35
36
37
38

//
//  main.mm
//  QueueByLinkList
//
//  Created by LO3G on 15/11/27.
//  Copyright © 2015年 LO3G. All rights reserved.
//
#import <iostream>
#import "LinkList.mm"

using namespace std;

int main(int argc, const char * argv[]) {
LinkList<int> queue;

queue.pushItem('a');
queue.pushItem('b');
queue.pushItem('d');
queue.pushItem('e');
queue.pushItem('f');
queue.pushItem('g');
queue.pushItem('h');
queue.pushItem('i');
queue.pushItem('j');

queue.display();

cout << queue.popItem() << endl;
cout << queue.popItem() << endl;
cout << queue.popItem() << endl;
cout << queue.popItem() << endl;
cout << queue.popItem() << endl;
cout << queue.popItem() << endl;

queue.display();

return 0;
}

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: