目录
- 0.抽象数据类型ADT——Abstract Data Type
- 1.表ADT及其实现
1.1 表ADT
1.2 表的实现
1.2.1 数组实现
1.2.2 链表
1.2.3 双链表
1.2.4 循环链表
1.3 表的应用
1.3.1 多项式
1.3.2 基数排序
1.3.3 多重表 - 2.栈ADT及其实现
2.1 栈ADT
2.2 栈的实现
2.3 栈的应用
2.3.1 平衡符号
2.3.2 后缀表达式
2.3.3 中缀到后缀的转换
2.3.4 函数调用 - 3.队列ADT及其实现
3.1 队列ADT
3.2 队列的实现
3.3 队列的应用
3.3.1 排队论
3.3.2 任务队列
3.3.3 在图论中有大量的应用
3.3.4 消息队列
3.3.5 linux内核进程执行队列(按优先级轮转) - 4.哈希表ADT及其实现
- 5.查找树ADT及其实现
- 6.优先队列(堆)ADT及其实现
- 7.不相交集ADT及其实现——并查集
0.抽象数据类型ADT——Abstract Data Type
ADT是一些操作的集合。抽象数据类型是数学的抽象,在ADT的定义中不涉及如何实现操作的结合。
对诸如表、集合、图和它们的操作一起可以看作是抽象数据类型,就像整数、实数是数据类型一样。对于集合ADT,可以有诸如并(union)、交(intersection)、获取大小(size)以及取余(complement)等操作。
1.表ADT及其实现
1.1 表ADT
1)表ADT的数据结构
形如A1, A2, ..., An的表。表的大小是n。大小为0的表为空表。
对除空表外的任何表,说Ai+1后继Ai并称Ai-1前驱Ai。表中第一个元素是A1,最后一个元素是An。不定义A1的前驱元和An的后继元。元素Ai在表中的位置为i。
2)表ADT上进行的操作的集合
- PrintList
- MakeEmpty
- Find返回关键字首次出现的位置
- FindKth返回某个位置上的元素
- Insert/Delete从表的某个位置插入和删除某个关键字
1.2 表的实现
1.2.1 数组实现(连续存储)
表的大小实现需要已知(除非实现位动态数组)
插入和删除是昂贵的,最坏情况为O(N)
1.2.2 链表
链表允许不连续存储。
链表有一系列不在内存中相连的结构组成。每一个结构均含有表元素和指向包含该元素后继元的结构的指针(Next指针)。
实现时,采用带有表头的链表:
1)链表的声明和一些简单的判断
2)查找
3)删除
4)插入
5)删除整个链表
1.2.3 双链表
1.2.4 循环链表
1.3 表的应用
1.3.1 多项式
使用数组(适合稠密)和链表(适合稀疏)实现。
1.3.2 基数排序
1.3.3 多重表
2.栈ADT及其实现
2.1 栈ADT——LIFO(后进先出)表
栈是限制插入和删除只能在一个位置上进行的表,该位置是表的末端,叫做栈的顶。
2.2 栈的实现
任何实现表的方法都能实现栈。
2.2.1 栈的链表实现
通过在表顶端插入来实现push,通过删除表顶端元素实现pop。
2.2.2 栈的数组实现
2.3 栈的应用
2.3.1 平衡符号
针对括号类的检查:
- 做一个空栈。读入字符直到文件尾
- 如果字符是一个开发符号,则将其推入栈中
如果字符是一个封闭符号,则当空栈时报错;否则,将栈元素弹出。
如果弹出的符号不是对应的开放符号,则报错。 - 在文件尾,如果栈非空则报错。
2.3.2 后缀表达式
后缀表达式: a b c * + d e * f + g * +
- 当见到一个数是就把它推入栈中
- 遇到一个运算符时,从栈中弹出两个数,并运用该运算符计算,并将所得结果推入栈中
2.3.3 中缀到后缀的转换(栈的操作本质上是从左到右,将优先级高的先弹出,或者说是按照计算的次序进行弹出的)
将中缀表达式 a + b * c + (d * e + f) * g
转成后缀表达式 a b c * + d e * f + g * +
- 当读到一个操作数的时候,立即把它放到输出中
- 操作符不立即输出,将其放到栈中。当遇到做括号时,也将其推入栈中
- 见到一个右括号,将栈元素弹出,将弹出的符号写出,直到遇到一个对应的左括号。左括号弹出,不输出。
- (如果栈顶符号的优先级高于当前符号,要弹出)如果见到任何其他的符号: + * (,从栈中弹出栈元素直到发现优先级更低的元素位置。例外:除非在处理),否则不从栈中移走(。当从栈弹出元素的工作完成后,在将操作符压入栈中。
- 读到输入的末尾,将栈元素弹出知道该栈变成空栈,将符号写到输出中。
a + b * c + (d * e + f) * g:
2.3.4 函数调用
函数调用是,需要存储当前所有重要信息(活动记录、栈帧),诸如寄存器的值和返回地址。
将这些信息放到栈中,然后控制转移到新函数,新函数可以自由地用它的一些值代替这些寄存器。
栈溢出:许多系统中是不检测溢出的。由于有太多同时在运行着的函数,用尽栈空间情况总是可能发生的。
当栈太大是,可能触及到你的程序部分。
- 如果撞进了你的程序部分,产生一些无意义的指令并在这些指令执行时程序崩溃
- 如果撞进了你的数据部分,你将一些信息写入数据时,将毁坏栈的信息,很可能是返回地址,则你的程序将返回到某个奇怪的地址,从而程序崩溃。
发生栈溢出一般是由失控递归(忘记基准情形)引起的。
消除递归一般方法是使用一个栈,而且仅当你能够把最低限度的最小值放到栈上时,这个方法才值得一用。
3.队列ADT及其实现
3.1 队列ADT——FIFO表
队列也是表,插入在一端(队尾rear),删除在另一端(对头front)。
3.2 队列的实现
同栈一样,任何表的实现都是合法的。
3.2.1 队列的链表实现
3.2.2 队列的数组实现
3.3 队列的应用
3.3.1 排队论
- 当所有的接线员忙得不可开交的时候,对大公司的传呼一般都被放到一个队列中
- 在大规模的大学里,如果所有的终端都被占用,由于资源有限,学生们必须在一个等代表上签字。在终端上待得时间最长的学生将首先被强制离开,而等待时间最长的学习则将是下一个被允许使用终端的用户。
处理用概率的方法计算用户排队预计等待时间、等待服务的队列能够排多长,以及其他一些诸如此列的问题将用到被称为排队类的整个数学分支。
问题的答案依赖于用户加入队列的频率以及一旦用户得到服务时处理服务花费的时间。这两个参数作为概率分布函数给出。
如果有k个接线员,直接解决比较困难,可以用模拟的方法(使用一个队列)进行进行求解。
模拟方法:(当k变大时需要模拟)
- 模拟由处理中的事件组成。这里的两个事件:
1)一位顾客的到达
2)一位顾客的离开,从而腾出一名出纳员 - 使用过概率函数来生成一个输入流,由每位顾客的到达时间和服务时间的序偶组成,并通过到达时间排序。不必使用一天中的准确时间,而是使用单位时间量,称之为一个滴答(tick)
- 使用队列进行模拟
3.3.2 任务队列
将任务按照顺序放到一个队列中,用一个线程池(多个处理线程)轮流冲任务队列中取出任务进行处理。
3.3.3 在图论中有大量的应用
3.3.4 消息队列
3.3.5 linux内核进程执行队列(按优先级轮转)
4.哈希表ADT及其实现
4.1 哈希表ADT
散列(hashing)是一种用于以常数平均时间执行插入、删除、查找的技术。
那些需要元素间任何排序信息的操作将不会得到有效支持。因此诸如FindMin、FindMax等操作都是哈希表不支持的。
4.2 哈希表的实现
实现参见:http://www.jianshu.com/p/6dfd8c4c2b50
5.查找树ADT及其实现
参见http://www.jianshu.com/p/bdd7442f54e2
5.1 查找树ADT
5.2 查找树的实现
5.2.1 二叉查找树
5.2.2 AVL树
5.2.3 伸展树
5.2.4 B-树
5.2.5 红黑树
参考红黑树专题
参考2-3-4树及2-3树的总结
6.优先队列(堆)ADT及其实现
6.1 优先队列ADT
优先队列是允许至少下列两种操作的数据结构:
1)Insert
2)DeleteMin 找出、返回和删除优先队列中最小的元素
6.2 优先队列的实现
参见http://www.jianshu.com/p/f62787325788
6.2.1 二叉堆
6.2.2 d-堆
6.2.3 左式堆
6.2.4 斜堆
6.2.5 二项队列
6.2.6 斐波那契堆
6.2.7 van Emde Boas树
7.不相交集ADT及其实现——并查集
7.1 不相交集ADT
一些应用涉及将n个元素分成一组不相交的集合。这些应用经常需要进行两种特别的操作:寻找包含给定元素的唯一集合和合并两个集合。
支持以下三个操作:
- MAKE-SET(x):建立一个新的集合,它的唯一成员是x。因为各个集合是不相交的,故x不会出现在别的某个集合中。
- UNION(x, y):将包含x和y的两个动态集合(Sx和Sy)合并成一个新的集合。
- FIND-SET(x):返回一个指针,这个指针指向包含x的(唯一)结合的代表。
7.2 不相交集的实现
7.2.1 用链表表示集合
1)表示方法
每个集合用一个链表来表示。
- head指向表的第一个对象,也是集合的代表
- tail属性指向最后一个对象
- 集合每个对象包括:集合成员、指向下一个对象的指针、指向集合对象的指针
2)操作
- MAKE-SET——O(1)时间
- FIND-SET——O(1)时间
x对象的指向集合对象的指针,然后通过集合对象返回head指向对象的成员。 -
UNION——链表合并,但是必须更新一个链表(集合)中素有对象指向集合对象的指针。最坏情况下的UNION平均摊还时间为Θ(n)。
UNION的一种方法:总是将较短的表拼接到较长的链表中。
7.2.2 用有根树表示集合——不相交集合森林
1)基础表示法
使用有根树来表示集合,树中每个结点包含一个成员,每棵树代表一个集合。
在一个不相交集合森林(disjoint-set forest)中,每个成员仅指向它的父节点。
- MAKE-SET
- FIND-SET:沿着指向父节点的指针找到书的根。
这一通向根节点的简单路径上所访问的结点构成了查找路径。 - UNION:使得一棵树的根指向另外一颗树的根
2)按秩合并与路径压缩
按秩合并
对于每个结点,维护一个秩,它表示该节点高度(从该结点到某一后代叶节点的最长简单路径上边的数目)的一个上界。
在使用按秩合并策略的UNION操作中,让具有较小的秩的根指向具有较大秩的根。-
路径压缩
在FIND-SET操作中,使用这种策略可以使查找路径中的每个结点直接指向根。路径压缩并不改变任何结点的秩。
-
MAKE-SET
-
UNION(x, y)
-
FIND-SET(x)——递归的精妙作用(Amazing!)
该递归是一种两趟方法:
1)第一趟沿着查找路径向上直到找到根
2)递归回溯时,第二趟沿着搜索向下更新每个结点,使其直接指向根。
3)运行时间分析
- n——表示MAKE-SET操作的次数,这里假设n个MAKE-SET操作总是最先执行的n个操作。
- m——表示MAKE-SET、UNION和FIND-SET操作的总次数(m >= n)
当同时使用按秩合并和路径压缩时,最坏情况的运行时间为O(mα(n)),这里α(n)是一个增长非常慢的函数,在任何一个可以想得到的不相交集合数据结构的应用中,都有α(n) <= 4.
详细分析参见摊还分析:3.2.8 不相交集合——并查集
7.3 不相交集合的应用
7.3.1 确定无向图的连通分量
7.3.2 同一个人的多个账号
- 以前做的某个投票网站当中需要查封小号刷票的作弊账号,就在后台悄悄做了一个设置,如果某个人不小心带着相同的cookies登录了不同的账号(也就是在同一个浏览器上退出一个账号之后又登入了另一个账号),登录系统就会悄悄标记这两个账号属于同一个人,如果A和B是同一个人,B和C也是同一个人,那么A、B、C都是同一个人,我们最后只关心哪些账号是同一个人而哪些不是,并不关心究竟是通过怎样的步骤发现它们是同一个人的,这就是并查集最典型的应用。
- 这个问题就可以在MySQL里面创建一个表,它有两个字段,第一个叫做ID,主键,第二个叫做Parent,当前等价的父节点,这就是一个MySQL数据表表示的并查集,然后随便用SELECT和UPDATE实现一下,并查集接近O(1)的复杂度,一般也就几次MySQL查询