数据结构快速复习
在这篇文章中可以看到很多基本的数据结构的定义、实现的思路。
目录:
一、线性表
二、栈
三、队列
四、树
五、优先级队列
六、集合与静态查找表
七、动态查找表
八、哈希法
九、不相交集合类
十、图
十一、最小生成树
一、线性表
线性表是N个具有相同特征的结点 A0,A1,...,AN−1 构成的集合。在这个集合中,除了 A0 和 AN−1 外,每个元素都有唯一的前趋和后继。对于每个 Ai ,它的前驱是 Ai−1 ,它的后继是 Ai+1 。 A0 只有后继没有前驱, AN−1 只有前驱没有后继。
1.1基本操作
创建一个线性表create()
清除一个线性表clear()
求线性表的长度length()
在第i个位置插入一个元素insert(i,x)
- 删除第i个位置的元素remove(i)
- 搜索某个元素在线性表中是否出现search(x)
- 访问线性表的第i个元素visit(i)
- 遍历线性表运算traverse()
1.2线性表的实现
线性表有两种实现方式:顺序实现和链接实现。
顺序实现
线性表中结点存放在存储器上一块连续的空间中。借助存储空间的连续性,结点可以按照其逻辑顺序依次存放。一块连续的存储空间可以用一个数组实现。由于线性表中的元素个数是动态的,因此,因此采用了动态数组。
insert():
在插入时,表长会增加。当表长等于容量时,新增加的元素将无法存储。此时有两种解决方法:一种是不执行插入,报告一个错误消息;另一种是扩大数组的容量
链表实现
将每个结点放在一个独立的存储单元中,结点间的逻辑关系依靠存储单元中附加的指针来给出。
单链表
每个结点附加了一个指针字段,如next,该指针指向它的直接后继结点,最后一个结点的next字段为空。为了消除特殊情况,通常在表头额外增加一个相同类型的特殊结点,称之为头结点。它们不是线性表中的组成部分。
双链表
每个结点附加了两个指针字段,如prior和next,prior字段给出直接前驱结点的地址,next给出直接后继结点的地址。为了消除在表头、表尾插入删除的特殊情况,通常双链表设一头结点,设一尾节点。
循环链表
单循环链表
双循环链表:头结点中prior字段给出尾结点的地址,尾结点中next字段给出头结点的地址
1.3STL中表的实现
STL中线性表的实现有两种:
vector:线性表的顺序实现
list:线性表的双链表的实现
二、栈
后进先出(LIFO,Last In First Out)或先进后出(FILO,First In Last Out)结构,最先(晚)到达栈的结点将最晚(先)被删除。
2.1基本操作
创建一个栈create():创建一个空的栈;
进栈push(x):将x插入栈中,使之成为栈顶元素;
出栈pop():删除栈顶元素并返回栈顶元素值;
读栈顶元素top():返回栈顶元素值但不删除栈顶元素;
判栈空isEmpty():若栈为空,返回true,否则返回false。
2.2栈的实现
顺序实现
用连续的空间存储栈中的结点,即数组。进栈和出栈总是在栈顶一端进行,不会引起类似顺序表中的大量数据的移动。用数组的后端表示栈顶。
create():按照用户估计的栈的规模申请一个动态数组,将数组地址保存在data中,数组规模保存在maxSize中,并设top_p的值为-1。
push(x):将top_p加1,将x放入top_p指出的位置中。但要注意数组满的情况。
pop():返回top_p指出的位置中的值并将top_p减1。
top():与pop类似,只是不需要将top_p减1。
isEmpty():若top_p的值为-1,返回true,否则返回false。
链接实现
栈的操作都是在栈顶进行的,因此不需要双链表,用单链表就足够了,而且不需要头结点。对栈来讲,只需要考虑栈顶元素的插入删除。从栈的基本运算的实现方便性考虑,可将单链表的头指针指向栈顶。
2.3STL中的栈
在STL中栈类是借助于线性表类实现的。STL中的栈提供四个标准运算:push、pop、top和empty。
在STL中,借助于其他容器存储数据的容器称为容器适配器。栈就是一个容器适配器
定义一个栈对象要指明栈中元素的类型以及借助于哪一个容器,因此栈的类模板有两个模板参数:栈元素类型和所借助的容器类型。可以借助的容器有vector,list和deque。
2.4栈的应用
递归函数的非递归实现
递归的本质是函数调用,而函数调用是通过栈实现的。因此,递归可以用栈消除。
快速排序的非递归实现。
符号平衡检查
表达式的计算
表达式有前缀式,中缀式和后缀式。
后缀式的工作方式。
中缀式转换为后缀式的算法。
三、队列
到达越早的结点,离开的时间越早。所以队列通常称之为先进先出(FIFO:First In First Out)队列。
3.1基本操作
创建一个队列create():创建一个空的队列;
入队enQueue(x):将x插入队尾,使之成为队尾元素;
出队deQueue():删除队头元素并返回队头元素值;
读队头元素getHead():返回队头元素的值;
判队列空isEmpty():若队列为空,返回true,否则返回false。
3.2队列的实现
顺序实现
使用数组存储队列中的元素。队列中的结点个数最多为MaxSize个,元素下标的范围从0到MaxSize-1。
顺序队列的三种组织方式:
- 队头位置固定:队头固定在下标0,用一个变量指出队尾位置,队列为空时,队尾位置为-1。
- 队头位置不固定:使用队首指针front和队尾指针rear,分别指示队首结点的前一位置和队尾结点存放的下标地址。队列初始化时,设front= rear(都为-1),即队空的标志:front= rear。队满标志:rear= MaxSize- 1
- 循环队列:“牺牲”一个单元,规定front指向的单元不能存储队列元素,只起到标志作用,表示后面一个是队头元素。队列满的条件是:(rear + 1) % MaxSiz == front。队列为空的条件是front
== rear,即队头追上了队尾。
链接实现
队列的操作是在队列的两端进行的,不会对队列中的其他元素进行操作,用单链表就足够了。同时记住头尾结点的位置。
用无头结点的单链表表示队列,表头为队头,表尾为队尾。队列为空时,单链表中没有结点存在,即头尾指针都为空指针。保存一个链接队列只需要两个指向单链表结点的指针front和rear,分别指向头尾结点。
3.3STL中的队列
STL中的队列类是一个容器适配器,队列可以借助的容器有vector,list和deque。
定义一个队列对象要指明队列中元素的类型以及借助于哪一个容器,因此队列的类模板有两个模板参数:队列元素类型和所借助的容器类型。
STL中的队列提供六个运算:
- 入队操作push:调用
- 出队操作pop:调用
- 获得队头元素的操作front
- 获得队尾元素的函数back
- 判队列为空的函数empty
- 获得队列长度的函数size
3.4队列的应用
排队系统的模拟。
四、树
树是n (n≥1) 个结点的有限集合T,并且满足:
- 有一个被称之为根(root)的结点
- 其余的结点可分为m(m≥0)个互不相交的集合 T1,T2,...,Tm ,这些集合本身也是一棵树,并称它们为根结点的子树(Subree)。每棵子树同样有自己的根结点。
树的术语:根结点、叶结点、内部节点;结点的度和树的度;儿子结点;父亲结点;兄弟结点;祖先结点;子孙结点;结点所处层次;树的高度;有序树无序树;森林
4.1基本操作
建树create():创建一棵空树;
清空clear():删除树中的所有结点;
判空IsEmpty():判别是否为空树;
找根结点root():找出树的根结点。如果树是空树,则返回一个特殊的标记;
找父结点parent(x):找出结点x的父结点;
找子结点child(x,i):找结点x的第i个子结点;
剪枝delete(x,i):删除结点x的第i棵子树;
构建一棵树MakeTree(x,T1, T2, ……,Tn):构建一棵以x为根结点,以T1,T2, ……,Tn为第i棵子树的树;
遍历traverse():访问树上的每一个结点。
4.2二叉树
二叉树(BinaryTree)是结点的有限集合,它或者为空,或者由一个根结点及两棵互不相交的左、右子树构成,而其左、右子树又都是二叉树。
二叉树的性质
- 一棵非空二叉树的第i层上最多有 2i−1 个结点(i≥1)。
- 一棵高度为k的二叉树,最多具有 2k-1 个结点。
- 对于一棵非空二叉树,如果叶子结点数为n0,度数为2的结点数为n2,则有 n0=n2+1成立。
- 具有n个结点的完全二叉树的高度 k=[log2n]+1
二叉树的遍历
- 前序遍历:如果二叉树为空,则操作为空,否则,访问根结点,前序遍历左子树,前序遍历右子树。
- 中序遍历:如果二叉树为空,则操作为空,否则,中序遍历左子树,访问根结点,中序遍历右子树。
- 后序遍历:如果二叉树为空,则操作为空,否则,后序遍历左子树,后序遍历右子树,访问根结点。
前序+中序能唯一确定一棵二叉树;后续+中序也能唯一确定一棵二叉树;前序+后序不能确定一棵二叉树。
二叉树的实现
顺序实现
链接实现
二叉树是一个递归结构,因此二叉树中的许多运算的实现都是基于递归函数。
size():树的规模应该为:左子树的规模 +右子树的规模 + 1(根)
height():树的高度应该为:1+max(左子树高度,右子树高度)
创建一棵树:先输入根结点的值,创建根节点对已添加到树上的每个结点,依次输入它的两个儿子的值。如果没有儿子,则输入一个特定值实现工具。使用一个队列,将新加入到树中的结点放入队列依次出队,对每个出队的元素输入它的儿子。
二叉树遍历的非递归实现
前序遍历
设置一个栈,保存将要访问的树的树根。
开始时,把二叉树的根结点存入栈中。然后重复以下过程,直到栈为空:
- 从栈中取出一个结点,输出根结点的值
- 然后把右子树,左子树放入栈中
中序遍历
在中序遍历中根结点要进栈两次。
当要遍历一棵树时,将根结点进栈。
根结点第一次出栈时,它不能被访问,必须重新进栈,并将左子树也进栈,表示接下去要访问的是左子树。
根结点第二次出栈时,才能被访问,并将右子树进栈,表示右子树可以访问了。
后序遍历
当以后序遍历一棵二叉树时,先将树根进栈,表示要遍历这棵树。
根结点第一次出栈时,根结点不能访问,应该访问左子树。于是,根结点重新入栈,并将左子树也入栈。
根结点第二次出栈时,根结点还是不能访问,要先访问右子树。于是,根结点再次入栈,右子树也入栈。
当根结点第三次出栈时,表示右子树遍历结束,此时,根结点才能被访问。
4.3表达式树
4.4哈夫曼树与哈夫曼编码
前缀编码:
字符只放在叶结点中
字符编码可以有不同的长度
由于字符只放在叶结点中,所以每个字符的编码都不可能是其他字符编码的前缀
前缀编码可被惟一解码
哈夫曼算法:
- 给定一个具有n个权值{ w1,w2,………wn }的结点的集合F= { T1,T2,………Tn}
- 初始时,设集合A = F。
- 执行i= 1 至n -1 的循环,在每次循环时执行以下操作:
- 从当前集合中选取权值最小、次最小的两个结点,以这两个结点作为内部结点bi的左右儿子,bi 的权值为其左右儿子权值之和。
- 在集合中去除这两个权值最小、次最小的结点,并将内部结点bI 加入其中。这样,在集合A中,结点个数便减少了一个。
- 这样,在经过了n-1次循环之后,集合A中只剩下了一个结点,这个结点就是根结点。
哈夫曼编码:
每个字符的编码是根节点到该字符的路径,左枝为0,右枝为1
哈夫曼类的实现:
哈夫曼树可以用一个大小为2n的数组来存储。0节点不用,根存放在节点1。叶结点依次放在n到2n-1的位置。
每个数组元素保存的信息:结点的数据、权值和父结点和左右孩子的位置
4.5树和森林
树的存储实现
孩子链表示法
将每个结点的所有孩子组织成一个链表。树的节点由两部分组成:存储数据元素值的数据部分;指向孩子链的指针。
孩子兄弟链表示法
实质上是用二叉树表示一棵树。树中的每个结点有数据字段、指向它的第一棵子树树根的指针字段、指向它的兄弟结点的指针字段。
树、森林和二叉树
树的孩子兄弟链表示法就是将一棵树表示成二叉树的形态,这样就可以将二叉树中的许多方法用在树的处理中。
森林的二叉树存储:
将每棵树Ti转换成对应的二叉树Bi;将Bi作为Bi-1根结点的的右子树。
五、优先级队列
结点之间的关系是由结点的优先级决定的,而不是由入队的先后次序决定。优先级高的先出队,优先级低的后出队。这样的队列称为优先级队列。
二叉堆
堆是一棵完全二叉树,且满足下述关系之一
ki≤k2i且ki≤k2i+1(i=1,2,...,[n/2])
或者:
ki≥k2i且ki≥k2i+1(i=1,2,...,[n/2])
其中,下标是树按层次遍历的次序。
二叉堆可以采用顺序存储
基于二叉堆的优先级队列的实现
以最小化堆为例
插入:堆的插入是在具有最大序号的元素之后插入新的元素或结点,否则将违反堆的结构性。如果新元素放入后,没有违反堆的有序性,那么操作结束。否则,让该节点向父节点移动,直到满足有序性或到达根节点。新节点的向上移动称为向上过滤(percolate up)
删除:当最小元素被删除时,在根上出现了一个空结点。堆的大小比以前小1,最后一个结点应该删掉。如果最后一项可以放在此空结点中,就把它放进去。然而,这通常是不可能的。找到空结点的一个较小的子结点,如果该儿子的值小于我们要放入的项,则把该儿子放入空结点,把空结点往下推一层,重复这个动作,直到该项能被放入正确的位置。向下过滤。
建堆:利用堆的递归定义。如果函数buildHeap可以将一棵完全二叉树调整为一个堆,那么,只要对左子堆和右子堆递归调用buildHeap。至此,我们能保证除了根结点外,其余的地方都建立起了堆的有序性。然后对根结点调用percolateDown,以创建堆的有序性。
六、集合与静态查找表
在集合中,每个数据元素有一个区别于其他元素的唯一标识,通常称为键值或关键字值。
集合的主要运算:
查找某一元素是否存在。
将集合中的元素按照它的唯一标识排序。
6.1查找
用于查找的集合称之为查找表。
查找表分为静态查找表、动态查找表、内部查找、外部查找。
6.2无序表的查找
6.3有序表的查找
顺序查找
二分查找
每次检查待查数据中排在最中间的那个元素。如中间元素等于要查找的元素,则查找完成;否则,确定要找的数据是在前一半还是在后一半,然后缩小范围,在前一半或后一半内继续查找。
差值查找
适用于数据的分布比较均匀的情况。
查找位置的估计:
next=low+[x−a[low]a[high]−a[low]×(high−low+1)]
分块查找
分块查找也称为索引顺序块的查找,是处理大量数据查找的一种方法。它把整个有序表分成若干块,块内的数据元素可以是有序存储,也可以是无序的,但块之间必须是有序的。
6.4STL中的静态表
对应于顺序查找和二分查找,C++的标准模板库中提供两个模板函数:find和binary_search。这两个函数模板都位于标准库algorithm中。
find函数顺序查找一个序列
binary_search函数用二分查找的方式查找一个有序序列
七、动态查找表
7.1二叉查找树
二叉查找树或者为空,或者具有如下性质:对任意一个结点p而言:
如果p的左子树若非空,则左子树上的所有结点的关键字值均小于p结点的关键字值。
如果p的右子树若非空,则右子树上的所有结点的关键字值均大于p结点的关键字值。
结点p的左右子树同样是二叉查找树。
基本操作
- 特定节点在树上是否存在:若根结点的关键字值等于查找的关键字,成功。否则,若关键字值小于根结点,查其左子树。若关键字值大于根结点,查其右子树。在左右子树上的操作类似。
- 插入一个节点:若二叉树为空。则新插入的结点成为根结点。如二叉树非空,首先执行查找算法,找出被插结点的父亲结点。 判断被插结点是其父亲结点的左、右儿子。将被插结点作为叶子结点插入。(新插入的结点总是叶子结点)
- 删除一个结点:
- 删除叶结点
- 删除有一个儿子的结点:将儿子取代被删结点的位置。
- 删除有两个儿子的结点:选取“替身”取代被删结点,替身选取左子树中最大的结点或者右子树中最小的结点。先将替身的数据字段复制到被删结点;将原替身的另一儿子作为它的父亲结点的儿子,究竟是作为左儿子还是右儿子依原替身结点和其父亲结点的关系而定;释放原替身结点的空间。
查找树的性能
7.2AVL树(二叉平衡树)
平衡因子(平衡度):结点的平衡度是结点的左子树的高度-右子树的高度。
空树的高度定义为-1。
平衡二叉树:每个结点的平衡因子都为+1、-1、0 的二叉树。或者说每个结点的左右子树的高度最多差1的二叉树。
插入
删除
7.3红黑树
7.4AA树
7.5伸展树
7.6B+树
B+树是满足某些平衡条件的M叉树。
M阶的B+树是具有以下性质的B叉树:
数据项被存贮在叶子中。
非叶子结点至多保存M-1个键来引导查找,键i表示了子树i+1中键的最小值。
根或者是叶子,或者是有2到M个儿子。
除根之外所有的非叶结点的儿子数为[M/2] 到M之间。这保证了B树不会退化成二叉树。
所有的叶子都在同一层上,并且对于某个L要有[L/2] 到L个数据项
7.7STL中的动态查找表
STL中的查找表成为关联容器。关联容器支持高效的通过关键字的读写。
最基本的两个关联容器是set和map。
八、哈希法
哈希法,也称散列法。它不用比较的办法,而是直接根据所求结点的关键字值KEY 找到这个结点。
8.1哈希函数
每个结点在表中的存储位置是由一个函数H确定。该函数以结点的关键字值为参数,计算出该关键字对应的结点的存储位置。该函数称为哈希函数。
直接地址法
H(key)= key 或 H(key)= a×key +b
除留取余法
H(key)=key MOD p 或 H(key)= key MOD p + c 这里p 为小于等于m素数。
数字分析法
对关键字集合中的所有关键字,分析每一位上数字分布。取数字分布均匀的位作为地址的组成部分。
平方取中法
将关键字平方后,取其结果的中间各位作为散列函数值。
折叠法
是选取一个长度后,将关键字按此长度分组相加。
8.2冲突解决
要选择一个一一对应的哈希函数很困难。一般的哈希函数都是多对一。当两个以上的关键字映射到一个存储单元时,称为冲突或碰撞。
闭散列表
利用本散列表中的空余单元
线性探测法
当散列发生冲突时,探测下一个单元,直到发现一个空单元。
删除:一般来讲,删除某一元素,先要找到该元素,然后把该空间的内容清空。但这样就给查找带来了问题,某些元素会查不到。解决的方案是采用迟删除,即不真正删除元素,而是做一个删除标记。
二次探测法
地址序列为 k+12,k+22,k+32,...
再次散列法
采用第二个散列函数。第i次碰撞时,地址为f(i) = i*hash2(x)
开散列表
将碰撞的结点存放在散列表外的各自的线性表。
将具有同一散列地址的结点保存于M 存区的各自的链表之中。
九、不相交集合类
9.1等价关系和等价类
等价关系(equivalence relation)是一种满足以下三个特性关系R。
自反性(Reflexive):对所有的a∈R,aRa为真。
对称性(Symmetric):当且仅当bRa时,aRb。
传递性(Transitive):aRb和bRc隐含aRc。
等价关系的基本操作:
判断两个元素之间是否有等价关系
在两个元素间添加等价关系
等价关系的一种表示方法是采用等价类。集合S中的元素x的等价类是S的一个子集,它包含了所有与x有关的元素。这些等价类形成了不相交的集合。等价类形成了S的一种分割。
9.2不相交集(并查集)
不相交集合的基本操作:
find操作:找出特定元素属于哪个等价类
union操作:用于添加关系。如果要把序偶(a, b)加到关系表中,则与a相关的元素都与b相关,与b相关的元素也都与a相关。即a的等价类与b的等价类合并为一个等价类。
存储实现
每个等价类表示为一棵树,等价类的名字为根结点的名字。
每个节点只需要知道父节点,可以采用双亲表示法,用一个数组保存。数组s[i]的值为i的父节点的下标。如s[i]=-1,表示i是某棵树的根。
运算实现
为了执行两个集合的union操作,我们归并这两棵树,将一棵树的根作为另一棵树的孩子。
元素x的find操作返回包含x的树的根。
改进的union算法:改进的思想:尽量避免树的增高
按规模并:将规模小的树作为规模大的树的子树,对规模相同的树可以任意选择。
按高度并:将较矮的树作为较高的树的子树完成归并操作。
改进的find算法:路径压缩
- 当对find(x)采用路径压缩方法的话,那么在从x到根结点的路径上的每一个结点都将自己的父结点改为根结点。
9.3不相交集的应用
生成迷宫
最近共同祖先问题
十、图
图可以用G=(V, E)表示。其中,V是顶点的集合,E是连接顶点的边(弧)的集合。
有向图:边是有方向的。有向图的边用<>表示。\
10.1图的基本术语
邻接;度;入度;出度;子图;路径和路径长度;连通,连通图,连通分量;强连通图,强联通分量,弱连通图;完全图;生成树
10.2图的运算
构造一个由若干个结点、若干条边组成的图。
判断两个结点之间是否有边存在。
在图中添加或删除一条。
返回图中的结点数或边数。
按某种规则遍历图中的所有结点。
- 拓扑排序。
- 找最小生成树。
- 找最短路径。
10.3图的存储
邻接矩阵
设有向图具有 n个结点,则用 n 行n 列的布尔矩阵A 表示该有向图。如果i至 j
有一条有向边,A[i,j] = 1 ,如果 i 至j 没有一条有向边,A[i,j] = 0。
分别用0、1、2、3 分别标识结点A、B、C、D。而将真正的数据字段之值放入一个一维数组之中。
邻接表
设有向图或无向图具有n 个结点,则用结点表、边表表示该有向图或无向图。
结点表:用数组或单链表的形式存放所有的结点值。如果结点数n已知,则采用数组形式,否则应采用单链表的形式。
边表(边结点表):每条边用一个结点进行表示。同一个结点出发的所有的边形成它的边结点单链表。
10.4图的遍历
设有向图或无向图具有n 个结点,用结点表、边表表示该有向图或无向图。
深度优先搜索
- 选中第一个被访问的顶点;
- 对顶点作已访问过的标志;
- 依次从顶点的未被访问过的第一个、第二个、第三个…… 邻接顶点出发,进行深度优先搜索;
- 如果还有顶点未被访问,则选中一个起始顶点,转向2;
- 所有的顶点都被访问到,则结束。
必须对访问过的顶点加以标记
广度优先搜索
- 选中第一个被访问的顶点;
- 对顶点作已访问过的标志;
- 依次访问已访问顶点的未被访问过的第一个、第二个、第三个……第 m个邻接顶点 W1 、W2、W3…… Wm ,进行访问且进行标记,转向3;
- 如果还有顶点未被访问,则选中一个起始顶点,转向2;
- 所有的顶点都被访问到,则结束。
需要记录每个结点是否已被访问。需要记住每个已被访问的结点的后继结点,然后依次访问这些后继结点。这可以用一个队列来实现。
- 将序号最小的顶点放入队列;
- 重复取队列的队头元素进行处理,直到队列为空。对出队的每个元素,首先检查该元素是否已被访问。如果没有被访问过,则访问该元素,并将它的所有的没有被访问过的后继入队;
- 检查是否还有结点未被访问。如果有,重复上述两个步骤。
10.5图遍历的应用
无向图的连通性
欧拉回路
有向图的连通性
拓扑排序
十一、最小生成树
生成树是无向连通图的极小连通子图。包含图的所有 n 个结点,但只含图的n-1 条边。在生成树中添加一条边之后,必定会形成回路或环。
最小生成树:加权无向图的所有生成树中边的权值(代价)之和最小的树。
11.1Kruscal算法
基本思想:考虑图中权值最小的边。如果加入这条边不会导致回路,则加入;否则考虑下一条边,直到包含了所有的顶点
实现:初始时,设置生成树为(V,Φ),如果V有n个顶点,则初始的生成树为具有n个连通分量的树。按权值的大小逐个考虑所有的边,如果改变的加入能连接两个连通分量,则加入。当生成树只有一个连通分量时,算法结束。
使用优先级队列选择代价最小的边。用并查集判断加入一条边后会不会形成回路。
11.2Prim算法
基本思想:从顶点的角度出发。初始时,顶点集U为空,然后逐个加入顶点,直到包含所有顶点。
实现:首先选择一个顶点,加入顶点集。然后重复下列工作,直到U = V
- 选择连接U 和V-U 中代价最小的边(u,v)
- 把(u,v)加入生成树的边集,v加入到U