biancheng-数据结构
目录http://c.biancheng.net/data_structure/
1数据结构入门
2线性表
3栈和队列
4字符串
5数组和广义表
6树存储结构
7图存储结构
8动态内存管理
9查找表结构
10排序算法
11外部排序算法
数据结构大致包含以下几种存储结构:
运用时间复杂度和空间复杂度来衡量一个算法的运行效率。
在满足准确性和健壮性的基础上,还有一个重要的筛选条件,即通过算法所编写出的程序的运行效率。程序的运行效率具体可以从 2 个方面衡量,分别为:
- 程序的运行时间。
- 程序运行所需内存空间的大小。
根据算法编写出的程序,运行时间更短,运行期间占用的内存更少,该算法的运行效率就更高,算法也就更好。
时间复杂度
判断一个算法所编程序运行时间的多少,并不是将程序编写出来,通过在计算机上运行所消耗的时间来度量。原因很简单,一方面,解决一个问题的算法可能有很多种,一一实现的工作量无疑是巨大的,得不偿失;另一方面,不同计算机的软、硬件环境不同,即便使用同一台计算机,不同时间段其系统环境也不相同,程序的运行时间很可能会受影响,严重时甚至会导致误判。
如下列举了常用的几种时间复杂度,以及它们之间的大小关系:
O(1)常数阶 < O(logn)对数阶 < O(n)线性阶 < O(n2)平方阶 < O(n3)(立方阶) < O(2n) (指数阶)
空间复杂度
和时间复杂度类似,一个算法的空间复杂度,也常用大 O 记法表示。
要知道每一个算法所编写的程序,运行过程中都需要占用大小不等的存储空间,例如:
- 程序代码本身所占用的存储空间;
- 程序中如果需要输入输出数据,也会占用一定的存储空间;
- 程序在运行过程中,可能还需要临时申请更多的存储空间。
线性表
线性表结构存储的数据往往是可以依次排列的,就像小朋友手拉手,每位学生的前面和后面都仅有一个小朋友和他拉手,具备这种“一对一”关系的数据就可以使用线性表来存储。
例如,存储类似 {1,3,5,7,9} 这样的数据时,各元素依次排列,每个元素的前面和后边有且仅有一个元素与之相邻(除首元素和尾元素),因此可以使用线性表存储。
线性表并不是一种具体的存储结构,它包含顺序存储结构和链式存储结构,是顺序表和链表的统称。
顺序表
顺序表,简单地理解,就是常用的数组,只是换了个名字而已,例如使用顺序表存储 {1,3,5,7,9},如图 1 所示:
由于顺序表结构的底层实现借助的就是数组,因此对于初学者来说,可以把顺序表完全等价为数组,但实则不是这样。数据结构是研究数据存储方式的一门学科,它囊括的都是各种存储结构,而数组只是各种编程语言中的基本数据类型,并不属于数据结构的范畴。
链表
我们知道,使用顺序表(底层实现靠数组)时,需要提前申请一定大小的存储空间,这块存储空间的物理地址是连续的,如图 1 所示。
链表则完全不同,使用链表存储数据时,是随用随申请,因此数据的存储位置是相互分离的,换句话说,数据的存储位置是随机的。
为了给各个数据块建立“依次排列”的关系,链表给各数据块增设一个指针,每个数据块的指针都指向下一个数据块(最后一个数据块的指针指向 NULL),就如同一个个小学生都伸手去拉住下一个小学生的手,这样,看似毫无关系的数据块就建立了“依次排列”的关系,也就形成了链表,如图 2 所示:
栈和队列
栈和队列隶属于线性表,是特殊的线性表,因为它们对线性表中元素的进出做了明确的要求。
栈中的元素只能从线性表的一端进出(另一端封死),且要遵循“先入后出”的原则,即先进栈的元素后出栈。
栈结构如图 3 所示,像一个木桶,栈中含有 3 个元素,分别是 A、B 和 C,从在栈中的状态可以看出 A 最先进的栈,然后 B 进栈,最后 C 进栈。根据“先进后出”的原则,3 个元素出栈的顺序应该是:C 最先出栈,然后 B 出栈,最后才是 A 出栈。
队列中的元素只能从线性表的一端进,从另一端出,且要遵循“先入先出”的特点,即先进队列的元素也要先出队列。
![](https://img2022.cnblogs.com/blog/2084611/202203/2084611-20220308104155269-2132631954.png)
队列结构如图 4 所示,队列中有 3 个元素,分别是 A、B 和 C,从在队列中的状态可以看出是 A 先进队列,然后 B 进,最后 C 进。根据“先进先出”的原则,3 个元素出队列的顺序应该是 A 最先出队列,然后 B 出,最后 C 出。
树存储结构
树存储结构适合存储具有“一对多”关系的数据。
![](https://img2022.cnblogs.com/blog/2084611/202203/2084611-20220308104211452-368508563.png)
如图 5 所示,其中张平只有一个父亲,但他却有两(多)个孩子,这就是“一对多”的关系,满足这种关系的数据可以使用树存储结构。
图存储结构
图存储结构适合存储具有“多对多”关系的数据。
如图 6 所示,从 V1 可以到达 V2、V3、V4,同样,从 V2、V3、V4 也可以到达 V1,这就是“多对多”的关系,满足这种关系的数据可以使用图存储结构。
线性表
将具有“一对一”关系的数据“线性”地存储到物理空间中,这种存储结构就称为线性存储结构(简称线性表)。
使用线性表存储的数据,如同向数组中存储数据那样,要求数据类型必须一致,也就是说,线性表存储的数据,要么全部都是整形,要么全部都是字符串。一半是整形,另一半是字符串的一组数据无法使用线性表存储。
顺序存储结构和链式存储结构
数据依次存储在连续的整块物理空间中,这种存储结构称为顺序存储结构(简称顺序表);
数据分散的存储在物理空间中,通过一根线保存着它们之间的逻辑关系,这种存储结构称为链式存储结构(简称链表);
顺序表的初始化
使用顺序表存储数据之前,除了要申请足够大小的物理空间之外,为了方便后期使用表中的数据,顺序表还需要实时记录以下 2 项数据:
- 顺序表申请的存储容量;
- 顺序表的长度,也就是表中存储数据元素的个数;
顺序表插入元素
向已有顺序表中插入数据元素,根据插入位置的不同,可分为以下 3 种情况:
- 插入到顺序表的表头;
- 在表的中间位置插入元素;
- 尾随顺序表中已有元素,作为顺序表中的最后一个元素;
虽然数据元素插入顺序表中的位置有所不同,但是都使用的是同一种方式去解决,即:通过遍历,找到数据元素要插入的位置,然后做如下两步工作:
- 将要插入位置元素以及后续的元素整体向后移动一个位置;
- 将元素放到腾出来的位置上;
例如,在 {1,2,3,4,5}
的第 3 个位置上插入元素 6,实现过程如下:
顺序表删除元素
从顺序表中删除指定元素,实现起来非常简单,只需找到目标元素,并将其后续所有元素整体前移 1 个位置即可。
后续元素整体前移一个位置,会直接将目标元素删除,可间接实现删除元素的目的。
顺序表查找元素
顺序表中查找目标元素,可以使用多种查找算法实现,比如说二分查找算法、插值查找算法等。
顺序表更改元素
顺序表更改元素的实现过程是:
- 找到目标元素;
- 直接修改该元素的值;
单链表的基本操作
链表的节点
从图 2 可以看到,链表中每个数据的存储都由以下两部分组成:
- 数据元素本身,其所在的区域称为数据域;
- 指向直接后继元素的指针,所在的区域称为指针域;
即链表中存储各数据元素的结构如图 3 所示:
![](https://img2022.cnblogs.com/blog/2084611/202204/2084611-20220410200051122-142326503.png)
图 3 所示的结构在链表中称为节点。也就是说,链表实际存储的是一个一个的节点,真正的数据元素包含在这些节点中,如图 4 所示:
![](https://img2022.cnblogs.com/blog/2084611/202204/2084611-20220410200103348-1110431863.png)
头节点,头指针和首元节点
其实,图 4 所示的链表结构并不完整。一个完整的链表需要由以下几部分构成:
- 头指针:一个普通的指针,它的特点是永远指向链表第一个节点的位置。很明显,头指针用于指明链表的位置,便于后期找到链表并使用表中的数据;
- 节点:链表中的节点又细分为头节点、首元节点和其他节点:
- 头节点:其实就是一个不存任何数据的空节点,通常作为链表的第一个节点。对于链表来说,头节点不是必须的,它的作用只是为了方便解决某些实际问题;
- 首元节点:由于头节点(也就是空节点)的缘故,链表中称第一个存有数据的节点为首元节点。首元节点只是对链表中第一个存有数据节点的一个称谓,没有实际意义;
- 其他节点:链表中其他的节点;
链表插入元素
同顺序表一样,向链表中增添元素,根据添加位置不同,可分为以下 3 种情况:
- 插入到链表的头部(头节点之后),作为首元节点;
- 插入到链表中间的某个位置;
- 插入到链表的最末端,作为链表中最后一个数据元素;
虽然新元素的插入位置不固定,但是链表插入元素的思想是固定的,只需做以下两步操作,即可将新元素插入到指定的位置:
- 将新结点的 next 指针指向插入位置后的结点;
- 将插入位置前结点的 next 指针指向插入结点;
例如,我们在链表 {1,2,3,4}
的基础上分别实现在头部、中间部位、尾部插入新元素 5,其实现过程如图 1 所示:
![](https://img2022.cnblogs.com/blog/2084611/202203/2084611-20220308104633077-1895028712.png)
从图中可以看出,虽然新元素的插入位置不同,但实现插入操作的方法是一致的,都是先执行步骤 1 ,再执行步骤 2。
静态链表
静态链表,也是线性存储结构的一种,它兼顾了顺序表和链表的优点于一身,可以看做是顺序表和链表的升级版。
使用静态链表存储数据,数据全部存储在数组中(和顺序表一样),但存储位置是随机的,数据之间"一对一"的逻辑关系通过一个整形变量(称为"游标",和指针功能类似)维持(和链表类似)。
静态链表中的节点
通过上面的学习我们知道,静态链表存储数据元素也需要自定义数据类型,至少需要包含以下 2 部分信息:
- 数据域:用于存储数据元素的值;
- 游标:其实就是数组下标,表示直接后继元素所在数组中的位置;
双向链表
双向链表添加节点
根据数据添加到双向链表中的位置不同,可细分为以下 3 种情况:
添加至表头
将新数据元素添加到表头,只需要将该元素与表头元素建立双层逻辑关系即可。
换句话说,假设新元素节点为 temp,表头节点为 head,则需要做以下 2 步操作即可:
- temp->next=head; head->prior=temp;
- 将 head 移至 temp,重新指向新的表头;
例如,将新元素 7 添加至双链表的表头,则实现过程如图 2 所示:
![](https://img2022.cnblogs.com/blog/2084611/202203/2084611-20220308104747691-1569260248.png)
添加至表的中间位置
同单链表添加数据类似,双向链表中间位置添加数据需要经过以下 2 个步骤,如图 3 所示:
- 新节点先与其直接后继节点建立双层逻辑关系;
- 新节点的直接前驱节点与之建立双层逻辑关系;
![](https://img2022.cnblogs.com/blog/2084611/202203/2084611-20220308104757087-1784006679.png)
栈(Stack)和队列(Queue)详解
栈存储结构详解
即栈是一种只能从表的一端存取数据且遵循 "先进后出" 原则的线性存储结构。
通常,栈的开口端被称为栈顶;相应地,封口端被称为栈底。因此,栈顶元素指的就是距离栈顶最近的元素,拿图 2 来说,栈顶元素为元素 4;同理,栈底元素指的是位于栈最底部的元素,图 2 中的栈底元素为元素 1。
![](https://img2022.cnblogs.com/blog/2084611/202204/2084611-20220410201107018-9208695.png)
进栈和出栈
基于栈结构的特点,在实际应用中,通常只会对栈执行以下两种操作:
- 向栈中添加元素,此过程被称为"进栈"(入栈或压栈);
- 从栈中提取出指定元素,此过程被称为"出栈"(或弹栈);
顺序栈及基本操作(包含入栈和出栈)
如果你仔细观察顺序表(底层实现是数组)和栈结构就会发现,它们存储数据的方式高度相似,只不过栈对数据的存取过程有特殊的限制,而顺序表没有。
例如,我们先使用顺序表(a 数组)存储 {1,2,3,4}
,存储状态如图 1 所示:
如果你仔细观察顺序表(底层实现是数组)和栈结构就会发现,它们存储数据的方式高度相似,只不过栈对数据的存取过程有特殊的限制,而顺序表没有。
例如,我们先使用顺序表(a 数组)存储 {1,2,3,4}
,存储状态如图 1 所示:
![](https://img2022.cnblogs.com/blog/2084611/202203/2084611-20220308105056526-709418125.png)
同样,使用栈存储结构存储 {1,2,3,4}
,其存储状态如图 2 所示:
![](https://img2022.cnblogs.com/blog/2084611/202203/2084611-20220308105103804-613736209.png)
链栈的实现思路同顺序栈类似,顺序栈是将数顺序表(数组)的一端作为栈底,另一端为栈顶;链栈也如此,通常我们将链表的头部作为栈顶,尾部作为栈底,如图 1 所示:
![](https://img2022.cnblogs.com/blog/2084611/202203/2084611-20220308105127303-476836652.png)
将链表头部作为栈顶的一端,可以避免在实现数据 "入栈" 和 "出栈" 操作时做大量遍历链表的耗时操作。
链表的头部作为栈顶,意味着:
- 在实现数据"入栈"操作时,需要将数据从链表的头部插入;
- 在实现数据"出栈"操作时,需要删除链表头部的首元节点;
什么是队列(队列存储结构)
与栈结构不同的是,队列的两端都"开口",要求数据只能从一端进,从另一端出,如图 1 所示:
![](https://img2022.cnblogs.com/blog/2084611/202204/2084611-20220410201411809-987299056.png)
图 1 队列存储结构
队列存储结构的实现有以下两种方式:
顺序队列简单实现
由于顺序队列的底层使用的是数组,因此需预先申请一块足够大的内存空间初始化顺序队列。除此之外,为了满足顺序队列中数据从队尾进,队头出且先进先出的要求,我们还需要定义两个指针(top 和 rear)分别用于指向顺序队列中的队头元素和队尾元素,如图 1 所示:
![](https://img2022.cnblogs.com/blog/2084611/202203/2084611-20220308105228179-530563509.png)
由于顺序队列初始状态没有存储任何元素,因此 top 指针和 rear 指针重合,且由于顺序队列底层实现靠的是数组,因此 top 和 rear 实际上是两个变量,它的值分别是队头元素和队尾元素所在数组位置的下标。
链式队列及基本操作
链式队列数据入队
链队队列中,当有新的数据元素入队,只需进行以下 3 步操作:
- 将该数据元素用节点包裹,例如新节点名称为 elem;
- 与 rear 指针指向的节点建立逻辑关系,即执行 rear->next=elem;
- 最后移动 rear 指针指向该新节点,即 rear=elem;
由此,新节点就入队成功了。
例如,在图 1 的基础上,我们依次将 {1,2,3}
依次入队,各个数据元素入队的过程如图 2 所示:
![](https://img2022.cnblogs.com/blog/2084611/202203/2084611-20220308105322981-1633644298.png)
字符串,数据结构中的串存储结构
串存储结构的具体实现
数据结构中,字符串要单独用一种存储结构来存储,称为串存储结构。这里的串指的就是字符串。
存储一个字符串,数据结构包含以下 3 种具体存储结构:
- 定长顺序存储:实际上就是用普通数组(又称静态数组)存储。例如 C 语言使用普通数据存储字符串的代码为 char a[20] = "data.biancheng.net";
- 堆分配存储:用动态数组存储字符串;
- 块链存储:用链表存储字符串;
链表各节点存储数据个数的多少可参考以下几个因素:
- 串的长度和存储空间的大小:若串包含数据量很大,且链表申请的存储空间有限,此时应尽可能的让各节点存储更多的数据,提高空间的利用率(每多一个节点,就要多申请一个指针域的空间);反之,如果串不是特别长,或者存储空间足够,就需要再结合其他因素综合考虑;
- 程序实现的功能:如果实际场景中需要对存储的串做大量的插入或删除操作,则应尽可能减少各节点存储数据的数量;反之,就需要再结合其他因素。
数组和广义表详解
根据数组中存储数据之间逻辑结构的不同,数组可细分为一维数组、二维数组、...、n 维数组:
- 一维数组,指的是存储不可再分数据元素的数组,如图 1 所示;
- 二维数组,指的存储一维数组的一维数组,如图 2 所示;
数据结构树,树存储结构详解
本章所介绍的树结构是一种非线性存储结构,存储的是具有“一对多”关系的数据元素的集合。
![](https://img2022.cnblogs.com/blog/2084611/202203/2084611-20220308110017262-1448468194.png)
![](http://c.biancheng.net/uploads/allimg/190427/0944301493-0.png)
图 1(A) 是使用树结构存储的集合 {A,B,C,D,E,F,G,H,I,J,K,L,M} 的示意图。对于数据 A 来说,和数据 B、C、D 有关系;对于数据 B 来说,和 E、F 有关系。这就是“一对多”的关系。
将具有“一对多”关系的集合中的数据元素按照图 1(A)的形式进行存储,整个存储形状在逻辑结构上看,类似于实际生活中倒着的树(图 1(B)倒过来),所以称这种存储结构为“树型”存储结构。
树的结点
结点:使用树结构存储的每一个数据元素都被称为“结点”。例如,图 1(A)中,数据元素 A 就是一个结点;
父结点(双亲结点)、子结点和兄弟结点:对于图 1(A)中的结点 A、B、C、D 来说,A 是 B、C、D 结点的父结点(也称为“双亲结点”),而 B、C、D 都是 A 结点的子结点(也称“孩子结点”)。对于 B、C、D 来说,它们都有相同的父结点,所以它们互为兄弟结点。
树根结点(简称“根结点”):每一个非空树都有且只有一个被称为根的结点。图 1(A)中,结点 A 就是整棵树的根结点。
叶子结点:如果结点没有任何子结点,那么此结点称为叶子结点(叶结点)。例如图 1(A)中,结点 K、L、F、G、M、I、J 都是这棵树的叶子结点。
子树和空树
子树:如图 1(A)中,整棵树的根结点为结点 A,而如果单看结点 B、E、F、K、L 组成的部分来说,也是棵树,而且节点 B 为这棵树的根结点。所以称 B、E、F、K、L 这几个结点组成的树为整棵树的子树;同样,结点 E、K、L 构成的也是一棵子树,根结点为 E。
注意:单个结点也是一棵树,只不过根结点就是它本身。图 1(A)中,结点 K、L、F 等都是树,且都是整棵树的子树。
知道了子树的概念后,树也可以这样定义:树是由根结点和若干棵子树构成的。
空树:如果集合本身为空,那么构成的树就被称为空树。空树中没有结点。
结点的度和层次
对于一个结点,拥有的子树数(结点有多少分支)称为结点的度(Degree)。例如,图 1(A)中,根结点 A 下分出了 3 个子树,所以,结点 A 的度为 3。
结点的层次:从一棵树的树根开始,树根所在层为第一层,根的孩子结点所在的层为第二层,依次类推。对于图 1(A)来说,A 结点在第一层,B、C、D 为第二层,E、F、G、H、I、J 在第三层,K、L、M 在第四层。
如果两个结点的父结点虽不相同,但是它们的父结点处在同一层次上,那么这两个结点互为堂兄弟。例如,图 1(A)中,结点 G 和 E、F、H、I、J 的父结点都在第二层,所以之间为堂兄弟的关系。
有序树和无序树
如果树中结点的子树从左到右看,谁在左边,谁在右边,是有规定的,这棵树称为有序树;反之称为无序树。
拿图 1(A)来说,如果是其本身是一棵有序树,则以结点 B 为根结点的子树为整棵树的第一个孩子,以结点 D 为根结点的子树为整棵树的最后一个孩子。
森林
由 m(m >= 0)个互不相交的树组成的集合被称为森林。图 1(A)中,分别以 B、C、D 为根结点的三棵子树就可以称为森林。
前面讲到,树可以理解为是由根结点和若干子树构成的,而这若干子树本身是一个森林,所以,树还可以理解为是由根结点和森林组成的。用一个式子表示为:
其中,root 表示树的根结点,F 表示由 m(m >= 0)棵树组成的森林。
树的表示方法
除了图 1(A)表示树的方法外,还有其他表示方法:
![](http://c.biancheng.net/uploads/allimg/190427/0944303N4-1.png)
![](https://img2022.cnblogs.com/blog/2084611/202203/2084611-20220308110037485-322979152.png)
图 2(A)是以嵌套的集合的形式表示的(集合之间绝不能相交,即图中任意两个圈不能相交)。
图 2(B)使用的是凹入表示法(了解即可),表示方式是:最长条为根结点,相同长度的表示在同一层次。例如 B、C、D 长度相同,都为 A 的子结点,E 和 F 长度相同,为 B 的子结点,K 和 L 长度相同,为 E 的子结点,依此类推。
最常用的表示方法是使用广义表的方式。图 1(A)用广义表表示为:
总结
树型存储结构类似于家族的族谱,各个结点之间也同样可能具有父子、兄弟、表兄弟的关系。本节中,要重点理解树的根结点和子树的定义,同时要会计算树中各个结点的度和层次,以及树的深度。
二叉树
简单地理解,满足以下两个条件的树就是二叉树:
- 本身是有序树;
- 树中包含的各个节点的度不能超过 2,即只能是 0、1 或者 2;
例如,图 1a) 就是一棵二叉树,而图 1b) 则不是。
二叉树具有以下几个性质:
- 二叉树中,第 i 层最多有 2i-1 个结点。
- 如果二叉树的深度为 K,那么此二叉树最多有 2K-1 个结点。
- 二叉树中,终端结点数(叶子结点数)为 n0,度为 2 的结点数为 n2,则 n0=n2+1。
如果二叉树中除了叶子结点,每个结点的度都为 2,则此二叉树称为满二叉树。
如果二叉树中除去最后一层节点为满二叉树,且最后一层的结点依次从左到右分布,则此二叉树被称为完全二叉树。
二叉树还可以继续分类,衍生出满二叉树和完全二叉树。
满二叉树
如果二叉树中除了叶子结点,每个结点的度都为 2,则此二叉树称为满二叉树。
如图 2 所示就是一棵满二叉树。
满二叉树除了满足普通二叉树的性质,还具有以下性质:
- 满二叉树中第 i 层的节点数为 2n-1 个。
- 深度为 k 的满二叉树必有 2k-1 个节点 ,叶子数为 2k-1。
- 满二叉树中不存在度为 1 的节点,每一个分支点中都两棵深度相同的子树,且叶子节点都在最底层。
- 具有 n 个节点的满二叉树的深度为 log2(n+1)。
完全二叉树
如果二叉树中除去最后一层节点为满二叉树,且最后一层的结点依次从左到右分布,则此二叉树被称为完全二叉树。
如图 3a) 所示是一棵完全二叉树,图 3b) 由于最后一层的节点没有按照从左向右分布,因此只能算作是普通的二叉树。
完全二叉树除了具有普通二叉树的性质,它自身也具有一些独特的性质,比如说,n 个结点的完全二叉树的深度为 ⌊log2n⌋+1。
⌊log2n⌋ 表示取小于 log2n 的最大整数。例如,⌊log24⌋ = 2,而 ⌊log25⌋ 结果也是 2。
对于任意一个完全二叉树来说,如果将含有的结点按照层次从左到右依次标号(如图 3a)),对于任意一个结点 i ,完全二叉树还有以下几个结论成立:
- 当 i>1 时,父亲结点为结点 [i/2] 。(i=1 时,表示的是根结点,无父亲结点)
- 如果 2*i>n(总结点的个数) ,则结点 i 肯定没有左孩子(为叶子结点);否则其左孩子是结点 2*i 。
- 如果 2*i+1>n ,则结点 i 肯定没有右孩子;否则右孩子是结点 2*i+1 。
总结
本节介绍了什么是二叉树,以及二叉树的性质,同时还介绍了满二叉树和完全二叉树以及各自所特有的性质,初学者需理解并牢记这些性质,才能更熟练地使用二叉树解决实际问题。
二叉树的存储结构有两种,分别为顺序存储和链式存储。本节先介绍二叉树的顺序存储结构。
二叉树的顺序存储,指的是使用顺序表(数组)存储二叉树。需要注意的是,顺序存储只适用于完全二叉树。换句话说,只有完全二叉树才可以使用顺序表存储。因此,如果我们想顺序存储普通二叉树,需要提前将普通二叉树转化为完全二叉树。
二叉树先序遍历的实现思想是:
- 访问根节点;
- 访问当前节点的左子树;
- 若当前节点无左子树,则访问当前节点的右子树;
![](http://c.biancheng.net/uploads/allimg/190427/094T644B-0.png)
二叉树中序遍历的实现思想是:
- 访问当前节点的左子树;
- 访问根节点;
- 访问当前节点的右子树;
本节介绍另外一种遍历方式:按照二叉树中的层次从左到右依次遍历每层中的结点。具体的实现思路是:通过使用队列的数据结构,从树的根结点开始,依次将其左孩子和右孩子入队。而后每次队列中一个结点出队,都将其左孩子和右孩子入队,直到树中所有结点都出队,出队结点的先后顺序就是层次遍历的最终结果。
数据结构图,图存储结构详解
数据之间的关系有 3 种,分别是 "一对一"、"一对多" 和 "多对多",前两种关系的数据可分别用线性表和树结构存储,本节学习存储具有"多对多"逻辑关系数据的结构——图存储结构。
![](https://img2022.cnblogs.com/blog/2084611/202203/2084611-20220308110140236-701633251.png)
图 1 所示为存储 V1、V2、V3、V4 的图结构,从图中可以清楚的看出数据之间具有的"多对多"关系。例如,V1 与 V4 和 V2 建立着联系,V4 与 V1 和 V3 建立着联系,以此类推。
与链表不同,图中存储的各个数据元素被称为顶点(而不是节点)。拿图 1 来说,该图中含有 4 个顶点,分别为顶点 V1、V2、V3 和 V4。
图存储结构中,习惯上用 Vi 表示图中的顶点,且所有顶点构成的集合通常用 V 表示,如图 1 中顶点的集合为 V={V1,V2,V3,V4}。
注意,图 1 中的图仅是图存储结构的其中一种,数据之间 "多对多" 的关系还可能用如图 2 所示的图结构表示:
![](https://img2022.cnblogs.com/blog/2084611/202203/2084611-20220308110149552-1302097449.png)
可以看到,各个顶点之间的关系并不是"双向"的。比如,V4 只与 V1 存在联系(从 V4 可直接找到 V1),而与 V3 没有直接联系;同样,V3 只与 V4 存在联系(从 V3 可直接找到 V4),而与 V1 没有直接联系,以此类推。
因此,图存储结构可细分两种表现类型,分别为无向图(图 1)和有向图(图 2)。
图存储结构基本常识
弧头和弧尾
有向图中,无箭头一端的顶点通常被称为"初始点"或"弧尾",箭头直线的顶点被称为"终端点"或"弧头"。
入度和出度
对于有向图中的一个顶点 V 来说,箭头指向 V 的弧的数量为 V 的入度(InDegree,记为 ID(V));箭头远离 V 的弧的数量为 V 的出度(OutDegree,记为OD(V))。拿图 2 中的顶点 V1来说,该顶点的入度为 1,出度为 2(该顶点的度为 3)。
(V1,V2) 和 <V1,V2> 的区别
无向图中描述两顶点(V1 和 V2)之间的关系可以用 (V1,V2) 来表示,而有向图中描述从 V1 到 V2 的"单向"关系用 <V1,V2> 来表示。
由于图存储结构中顶点之间的关系是用线来表示的,因此 (V1,V2) 还可以用来表示无向图中连接 V1 和 V2 的线,又称为边;同样,<V1,V2> 也可用来表示有向图中从 V1 到 V2 带方向的线,又称为弧。
集合 VR 的含义
并且,图中习惯用 VR 表示图中所有顶点之间关系的集合。例如,图 1 中无向图的集合 VR={(v1,v2),(v1,v4),(v1,v3),(v3,v4)},图 2 中有向图的集合 VR={<v1,v2>,<v1,v3>,<v3,v4>,<v4,v1>}。
路径和回路
无论是无向图还是有向图,从一个顶点到另一顶点途径的所有顶点组成的序列(包含这两个顶点),称为一条路径。如果路径中第一个顶点和最后一个顶点相同,则此路径称为"回路"(或"环")。
并且,若路径中各顶点都不重复,此路径又被称为"简单路径";同样,若回路中的顶点互不重复,此回路被称为"简单回路"(或简单环)。
拿图 1 来说,从 V1 存在一条路径还可以回到 V1,此路径为 {V1,V3,V4,V1},这是一个回路(环),而且还是一个简单回路(简单环)。
在有向图中,每条路径或回路都是有方向的。
权和网的含义
在某些实际场景中,图中的每条边(或弧)会赋予一个实数来表示一定的含义,这种与边(或弧)相匹配的实数被称为"权",而带权的图通常称为网。如图 3 所示,就是一个网结构:
![](https://img2022.cnblogs.com/blog/2084611/202203/2084611-20220308110201028-1806337745.png)
子图:指的是由图中一部分顶点和边构成的图,称为原图的子图。
动态内存管理(内存的分配与回收)详解
占用块和空闲块
对于计算机中的内存来说,称已经分配给用户的的内存区统称为“占用块”;还未分配出去的内存区统称为“空闲块”或者“可利用空间块”。
系统的内存管理
对于初始状态下的内存来说,整个空间都是一个空闲块(在编译程序中称为“堆”)。但是随着不同的用户不断地提出存储请求,系统依次分配。
整个内存区就会分割成两个大部分:低地址区域会产生很多占用块;高地址区域还是空闲块。如图 1 所示:
但是当某些用户运行结束,所占用的内存区域就变成了空闲块,如图 2 所示:
此时,就形成了占用块和空闲块犬牙交错的状态。当后续用户请求分配内存时,系统有两种分配方式:
- 系统继续利用高地址区域的连续空闲块分配给用户,不去理会之前分配给用户的内存区域的状态。直到分配无法进行,也就是高地址的空闲块不能满足用户的需求时,系统才会去回收之前的空闲块,重新组织继续分配;
- 当用户运行一结束,系统马上将其所占空间进行回收。当有新的用户请求分配内存时,系统遍历所有的空闲块,从中找出一个合适的空闲块分配给用户。
可利用空间表
当采用第 2 种方式时,系统需要建立一张记录所有空闲块信息的表。表的形式有两种:目录表和链表。各自的结构如图 3 所示:
目录表:表中每一行代表一个空闲块,由三部分组成:
- 初始地址:记录每个空闲块的起始地址。
- 空闲块大小:记录每个空闲块的内存大小。
- 使用情况:记录每个空闲块是否存储被占用的状态。
分配存储空间的方式
通常情况下系统中的可利用空间表是第 3 种情况。如图 3(C) 所示。由于链表中各结点的大小不一,在用户申请内存空间时,就需要从可利用空间表中找出一个合适的结点,有三种查找的方法:
- 首次拟合法:在可利用空间表中从头开始依次遍历,将找到的第一个内存不小于用户申请空间的结点分配给用户,剩余空间仍留在链表中;回收时只要将释放的空闲块插入在链表的表头即可。
- 最佳拟合法:和首次拟合法不同,最佳拟合法是选择一块内存空间不小于用户申请空间,但是却最接近的一个结点分配给用户。为了实现这个方法,首先要将链表中的各个结点按照存储空间的大小进行从小到大排序,由此,在遍历的过程中只需要找到第一块大于用户申请空间的结点即可进行分配;用户运行完成后,需要将空闲块根据其自身的大小插入到链表的相应位置。
- 最差拟合法:和最佳拟合法正好相反,该方法是在不小于用户申请空间的所有结点中,筛选出存储空间最大的结点,从该结点的内存空间中提取出相应的空间给用户使用。为了实现这一方法,可以在开始前先将可利用空间表中的结点按照存储空间大小从大到小进行排序,第一个结点自然就是最大的结点。回收空间时,同样将释放的空闲块插入到相应的位置上。
以上三种方法各有所长:
- 最佳拟合法由于每次分配相差不大的结点给用户使用,所以会生成很多存储空间特别小的结点,以至于根本无法使用,使用过程中,链表中的结点存储大小发生两极分化,大的很大,小的很小。该方法适用于申请内存大小范围较广的系统
- 最差拟合法,由于每次都是从存储空间最大的结点中分配给用户空间,所以链表中的结点大小不会起伏太大。依次适用于申请分配内存空间较窄的系统。
- 首次拟合法每次都是随机分配。在不清楚用户申请空间大小的情况下,使用该方法分配空间。
数据结构查找表详解(包含常用查找算法)
查找表是由同一类型的数据元素构成的集合。例如电话号码簿和字典都可以看作是一张查找表。
一般对于查找表有以下几种操作:
- 在查找表中查找某个具体的数据元素;
- 在查找表中插入数据元素;
- 从查找表中删除数据元素;
静态查找表和动态查找表
在查找表中只做查找操作,而不改动表中数据元素,称此类查找表为静态查找表;反之,在查找表中做查找操作的同时进行插入数据或者删除数据的操作,称此类表为动态查找表。
关键字
在查找表查找某个特定元素时,前提是需要知道这个元素的一些属性。例如,每个人上学的时候都会有自己唯一的学号,因为你的姓名、年龄都有可能和其他人是重复的,唯独学号不会重复。而学生具有的这些属性(学号、姓名、年龄等)都可以称为关键字。
关键字又细分为主关键字和次关键字。若某个关键字可以唯一地识别一个数据元素时,称这个关键字为主关键字,例如学生的学号就具有唯一性;反之,像学生姓名、年龄这类的关键字,由于不具有唯一性,称为次关键字。
9.1 什么是查找表9.2 顺序查找算法9.3 二分查找(折半查找)算法9.4 分块查找(索引顺序查找)算法9.5 静态树表查找算法9.6 二叉排序树(二叉查找树)9.7 平衡二叉树(AVL树)9.8 红黑树(更高级的二叉查找树)算法9.9 B-树及其基本操作(插入和删除)9.10 B+树及基本操作(插入和删除)9.11 键树查找法(双链树和字典树)9.12 什么是哈希表(散列表)9.13 哈希查找算法
排序算法详解
10.1 插入排序算法10.2 折半插入排序算法10.3 2路插入排序算法10.4 表插入排序算法10.5 希尔排序算法(缩小增量排序)10.6 冒泡排序算法10.7 快速排序算法(QSort,快排)10.8 简单选择排序算法10.9 树形选择排序(锦标赛排序)算法10.10 堆排序算法10.11 归并排序算法10.12 基数排序算法10.13 内部排序算法的优势分析
外部排序算法
11.1 什么是外部排序算法11.2 多路平衡归并排序(胜者树、败者树)算法11.3 置换选择排序算法11.4 最佳归并树