在这篇文章里, 您可以学习到:
- 数据结构简介
- 数据结构的逻辑结构和物理结构
- 队列
- OC和C++在Xcode中的混编
- 泛型编程思想
- 泛型编程实现循环队列和链表队列
- 博客中使用的图片均来自网络
数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。数据结构往往同高效的检索算法和索引技术有关。
帮你理解
- 数据结构的实现与语言无关, 它可以被任何语言实现.
- 一个好的抽象, 可以适用普遍数据类型. 除了特定场合和教学示例外, 你不能只支持某种特定的数据类型.
谈到数据结构, 一定会涉及到的两个概念: 数据 和 数据之间的关系.
数据: 我们前面提到, 数据结构不分数据类型, 这个数据可以是任何数据, 你可以把它想象成是一个收纳盒, 盒子内可以放任何东西.
关系: 按照相面的比喻, 关系就是指这些收纳盒之间的映射关系. 比如我规定一种关系: 把这些盒子排成一列, 对于中间的某个盒子来讲, 它的前面和后面都有一个和它紧挨的盒子. 我们可以说, 这个盒子和它前面和后面的盒子有关系, 和它不相邻的盒子没有关系. 那么我们可以把这样的模型剥离出来, 得到一种数据结构 – 线性结构, 又叫线性表.
推理
按照这种抽象模型的过程, 我们可以把下面的关系抽离出另一种数据结构树.
还可以继续抽象, 元素之间的关系式任意的, 就得到了网状结构, 也就是我们所说的图.
抽象出状结构之后, 我们基本已经无法再从对应关系上来细分了, 不信您就想想, 你所成想到的关系, 全部包含在这几种抽象中了
吗?
当然不是, 空间还能抽象到11维呢!
事实上, 上述数据结构的共性–每个元素至少与其他元素建立了联系. 这种规则是人为规定的, 没人要求我们一定这样抽象数据, 所以, 我们可以假设所有的盒子之间两两都没有关系, 所有盒子都是独立的, 就像一堆硬币, 我们把这种数据结构成为集合:
至此, 我们得到了四种结构
线性表: 数据结构中的元素存在一对一的相互关系
树: 数据结构中的元素存在一对多的相互关系
图: 数据结构中的元素存在多对多的相互关系
散列(集合): 数据结构中的元素之间除了“同属一个集合” 的相互关系外,别无其他关系
书承上文
数据结构指同一数据元素类中各数据元素之间存在的关系。数据结构分别为逻辑结构、存储结构(物理结构)和数据的运算. 数据的逻辑结构是从具体问题抽象出来的数学模型,是描述数据元素及其关系的数学特性的,有时就把逻辑结构简称为数据结构.
逻辑结构: 我们上面赘述了那么多, 都是描述的逻辑结构(不涉及实现问题, 只是描述它们的关系).
存储结构(物理结构): 真正有关数据在计算机中是如何存储方式.
这个’存储’是指在内存中的存在形式(并非外设, 如硬盘/U盘/光盘).
假设我们有10个盒子, 它们是线性结构, 这10个盒子应该如何摆放呢?
既然是线性结构, 难道不是依次摆放吗? 一个挨着一个的样子.
您要是这么想得话, 说明您没有明白物理结构和逻辑结构的区别.
假如我们把盒子分别放到不同的地方, 然后在每个盒子上写明: 它上一个盒子是放在哪里, 下一个盒子放在哪里.
那么您是不是可以根据盒子上的位置找到下一个盒子呢? 当然可以了.
这就是逻辑结构(线性关系)和物理结构(摆放位置)的区别.
事实上, 我们在内存中存放数据的格式确实有两种: 顺序存储 和 链式存储
顺序存储: 顾名思义, 数据放在一块儿连续的内存空间, 工作指针的移动可以很方便的找到其他元素(我说的是其他元素, 而不是下一个元素, 因为这样的存储方式也不一定完全为线性表存储)
链式存储: 所有元素均任意存放在内存中, 元素之间是通过元素内的指针互相寻找对方的.
队列是一种特殊的线性表,特殊之处在于它只允许在表的 前端(head) 进行删除操作,而在表的 后端(tail) 进行插入操作,队列是一种操作受限制的线性表。进行插入操作的端称为 队尾 ,进行删除操作的端称为 队头 。
队列特点是先进先出,后进后出.
队列的定义, 除了点名了它元素之间的逻辑关系, 也定义了它的部分算法.
算法与数据结构密不可分, 它依附于数据结构而存在, 对于任何算法的编写,必须依赖一个已经存在的数据结构来对它进行操作,数据结构成为算法的操作对象,这也是为什么算法和数据结构两门分类不分家的概念,算法在没有数据结构的情况下,没有任何存在的意义. 而数据结构没有算法就等于是一个尸体而没有灵魂
对于一个线性表来说, 必须实现下面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,它指向下一个入队元素的存储位置.
- 每次在队尾插入一个元素是,tail增1
- 每次队头删除一个元素时,head增1。
- 随着插入和删除操作的进行,队列元素的个数不断变化,队列所占的存储空间也在为队列结构所分配的连续空间中移动。
- 当head==tail时,队列中没有任何元素,称为 空队。
- 当tail增加到指向分配的连续空间之外时,队列无法再插入新元素.成为 满队
- 这时往往还有大量可用空间未被占用,这些空间是已经出队的队列元素曾经占用过得存储单元。这种现象叫做 “溢出”
事实上, 顺序队列有三种溢出:
- “下溢”现象:当队列为空时,做出队运算产生的溢出现象。“下溢”是正常现象,常用作程序控制转移的条件.
- “真上溢”现象:当队列满时,做进栈运算产生空间溢出的现象。“真上溢”是一种出错状态,应设法避免.
- “假上溢”现象:由于入队和出队操作中,头尾指针只增加不减小,致使被删元素的空间永远无法重新利用。当队列中实际的元素个数远远小于向量空间的规模时,也可能由于尾指针已超越向量空间的上界而不能做入队操作。该现象称为”假上溢”现象.
“下溢”和”真上溢”是我们无法避免的, 他们本身也不属于异常现象. 但是”假上溢”会使我们程序发生异常,我们要避免它.
如何利用使用过的空间呢?
为充分利用空间,克服”假溢出”现象的方法是:将空间想象为一个首尾相接的圆环,并称这种空间为循环队列。循环队列可以有效的使用空间.
从图中我们可以看到
- 当有元素入队时, tail(此时head和tail不是指针, 而是下标), 向后移动一位. tail 永远在队尾元素的下一个位置.
- 如果tail的值超过我们的空间总大小, 则对tail对数组长度取模. 使得tail永远不会越界.
- 出队时, 返回head下标所在的值, 之后head向后移动一位, 与tail一样, 一旦超出数组长度, 则对其取模.
- 开始时, head和tail指向同一位置, 此时队列为空.
- 当满队时, tail移动到head位置, 此时 满队.
如果按照上述逻辑, 在判断满队时, 因为tail和head均指向同一位置, 所以我们不能作出区分. 在这里, 判断满队的逻辑发生了问题.
1.通过一个计数器,记录元素个数, 当元素个数与数组最大值相等,则为满列;
2.少用一个存储空间,也就是数组的最后一个存数空间不用,当(tail+1)%maxsiz = head时,队列满;
我们会在后面的示例中演示循环队列的Demo, 这个Demo采用的是第一种方案.
在队列的形成过程中,可以利用线性链表的原理,来生成一个队列.
新元素(等待进入队列的元素)总是被插入到链表的尾部,而读取的时候总是从链表的头部开始读取。每次读取一个元素,释放一个元素。所谓的动态创建,动态释放。因而也不存在溢出等问题。由于链表由结构体间接而成,遍历也方便。
队列的链表实现原理图:
插入数据时:
因为链表的存储方式数据动态申请空间, 插入元素的时候才会申请空间, 所以不存在满队的情况
- 你可以人为的为链表队列设置一个上限. 一旦达到这个上限, 则满队.
后面的demo回详细的为您解释基本算法的实现.
苹果的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 |
上述代码中, 我们定义了一个结构体, 结构体中有两个成员变量 data 和 next
next:指向自身结构体类型的指针.
data:的类型是T, 这个T是什么呢?
template < typename T >: 这条语句表示T的类型定义还没有给出,需要在使用Node类型时, 通过”<>” 符号来指明 typename T 的类型.
1
|
Node |
1 2 3 4 5 |
template |
template 是来修饰 struct Node 的, 您不能在他们中间插入任何代码, 比如:
1 2 3 4 5 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.新建工程
2.新建C++文件, 我们没有创建头文件
3.创建完成后, 手动修改文件类型为.mm类型.
4.循环队列的实现
我们在LinkListByArray.mm中写下面代码, 注意, 这个是C++语法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 |
// // LinkListByArray.cpp // QueueByLinkList // // Created by LO3G on 15/11/29. // Copyright © 2015年 LO3G. All rights reserved. // #include |
main.mm中调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 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 |
在LinkList.mm中写下面代码, 注意, 这个是C++语法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
// // LinkedQueue.cpp // QueueByLinkList // // Created by LO3G on 15/11/26. // Copyright © 2015年 LO3G. All rights reserved. // #include |
在main.mm中调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
// // main.mm // QueueByLinkList // // Created by LO3G on 15/11/27. // Copyright © 2015年 LO3G. All rights reserved. // #import |