涵盖了《王道论坛-2022年数据结构考研复习指导》里的几乎全部内容!
目录
1 绪论
基本概念 | 解释 |
数据 | 数据项(构成数据元素的不可分割的最小单位)∈数据元素(数据的基本单位)∈数据对象Ì数据 |
数据类型 | 原子类型、结构类型、抽象数据类型ADT |
逻辑结构 | 线性结构、非线性结构(集合、树、网) |
存储/物理结构/映像 | 顺序(可静态可动态分配,存储密度高,逻辑上相邻的元素物理上也相邻,适合线性结构、图、树等)、链式(比顺序存储更能方便表示各种逻辑结构,结点内的存储单元地址必须连续)、索引、散列/哈希存储(不能反映数据逻辑关系) |
数据结构三要素 | 逻辑结构、物理结构、运算;ADT定义了一个完整的数据结构 |
存取方式 | 指读写方式,顺序表可随机存取 |
算法——特性 | 有穷性(但程序可无穷)、确定性、可行性、输入、输出 |
算法——设计 | 正确性、可读性、健壮性、效率与低存储量要求 |
算法问题规模n | 算法中所有语句之和T(n)是n的函数 |
基本运算 | 最深层循环内的语句,其频度与T(n)同量级 |
时间复杂度T(n) | 最坏、最好、平均时间复杂度;一般指最坏情况下估计算法执行时间的一个上界;与实现算法的语言无关;O(n^k)<O(2^n)<O(n!)<O(n^n);递归程序的时间复杂度用递推公式来算 |
空间复杂度S(n) | Space |
算法原地工作 | 空间复杂度=O(1) |
✓ | 同一个算法,实现语言的级别越高,执行效率越低 |
✓ | 在相同规模下,复杂度为O(n)的算法在时间上总是优于复杂度为O(2^n)的算法 |
✓ | 逻辑结构独立于存储结构,而存储结构不能独立于逻辑结构 |
✓ | 既描述逻辑结构,又描述存储结构和数据运算:顺序表、哈希表、单链表 |
O(m+n)=O(max(m,n)) |
2 线性表
2.1 线性表基本概念
解释 | |
线性表L | L=(a1,a2,…,a_n),a1为表头元素,a_n为表尾元素;除第一个元素外,每个元素有且仅有一个直接前驱 |
位序 | 从1开始 |
其他基本概念 | 表长、表头元素、表尾元素、直接前驱、直接后继 |
头指针 | 若有头结点,则指向头结点 |
头结点 | 线性表的表长不含头结点 |
前插操作 | 在某结点前面插入一个新结点 |
后插操作 | 在某结点后面插入一个新结点 |
InitList(&L) | |
Length(L) | |
LocateElem(L,e) | |
GetElem(L,i) | |
ListInsert(&L,i,e) | |
ListDelete(&L,i,&e) | |
PrintList(L) | |
Empty(L) | 判空 |
DestroyList(&L) | |
合并两个长度分别为m和n的有序表,最坏情况下需要比较m+n-1次 |
2.2 线性表分类
解释 | 插入基本操作 | 插入时间复杂度 | 删除基本操作 | 删除时间复杂度 | |
顺序表 | 动态分配时若空间满,需另开辟一块更大的空间 | 元素移动 | O(n) | 元素移动 | O(n) |
单链表 | 可头插法、尾插法建立 | 查找 | O(n) | 查找 | O(n) |
双链表 | 每个结点有两个指针 | ||||
循环单链表 | 最后一个结点指向首个结点 | ||||
循环双链表 | |||||
静态链表 | 借助数组的链式存储结构,其指针称为游标 | ||||
比较操作的时间代价常弱于移动操作 |
3 栈和队列
3.1 栈和队列
栈 | 队列/队 | |
操作特性 | 后进先出LIFO | 先进先出FIFO |
操作位置 | 栈顶 | 队头/首出/离队,队尾入/进队 |
主要指针 | top=栈顶元素 | front=队头元素但初始也为0,rear=队尾元素的下一个位置 |
顺序存储结构 | 顺序栈、共享栈(减少上溢,有2个指向栈顶元素指针 | 顺序队列(存在假溢出,即Q.rear==Maxsize时元素无法进队但数组中仍有空位;队空时front==rear==0)、循环队列 |
链式存储结构 | 链栈:表头为栈顶的单链表,无头结点 | 链队列(尾指针rear=队尾结点)、双端队列 |
出来元素的不同排列个数 | Catalan数:1/(n+1) C(2n,n) | |
应用 | 括号匹配、表达式求值(编译原理)、中缀表达式变后缀表达式(运算符在操作数后)、递归(DFS)、进制转换、迷宫求解、调用函数及其局部变量 | 层次遍历、计算机系统中资源分配(如缓冲区) |
其他 | 只可能发生上溢(因为插入删除都在栈顶进行) | 栈和队列的逻辑结构相同,都是线性结构 |
双端队列 | 进队时前端进的排在后端进的元素前,出队时无论前、后端,先出的排在前面 | 没限制的双端序列输入序列为1234....n,则输出序列有n!个 |
输入受限的双端队列 | 允许在一端进行插入和删除,但在另一端只允许插入 | |
输出受限的双端队列 | 允许在一端进行插入和删除,但在另一端只允许删除 |
3.2 特殊矩阵的压缩存储
概念 | 解释 |
维界 | 数组元素的下标的取值范围 |
上、下三角区 | i<j、i>j(也叫对角线以下) |
主对角线 | i=j |
特殊矩阵 | 对称矩阵、三角矩阵(存储完一方的元素之后,再紧接着存储另一方的常量一次)、对角/带状矩阵、三对角矩阵 |
稀疏矩阵 | 逻辑:三元组;存储:数组、十字链表(将行单链表、列单链表结合) |
按行优先 | 先存储行较小的 |
按列优先 | 先存储列较小的 |
4 串
解释 | |
字符串/串 | 可在串后加一个不计入串长的结束标记字符”\0” |
空串Ø | |
子串/模式串 | 设为p_1 p_2 … p_m |
主串 | 包含子串的串,设为s_1 s_2 … s_n,子串在主串中的位置从1开始 |
存储结构 | 定长顺序(超过预定义长度的串值会被截断)、堆分配存储(地址连续但动态分配)、块链存储(每个结点(即块)可存放一个或多个字符) |
串的最小操作子集 | 串赋值StrAssign、StrCompare、Strlength、Concat、SubString |
前缀 | 除最后一个字符外,所有的头部子串 |
后缀 | 除第一个字符外,所有的尾部子串 |
部分匹配值PM | 前、后缀的最长相等长度 |
PM表 | 依次列出模式串中所有头部子串的PM值 |
next数组 | PM表右移一位且令next[1]=-1,相邻的后一个比前一个最多大1 |
next的另一种表示 | 将上面的这种表示方法的每个数组元素的值加一。此时变为j=next[j],含义为在模式串的第j个字符失配时,跳到模式串的next[j]位置重新与主串当前位置进行比较 |
nextval数组 | 若j和next[j]处字符相同,则nextval=next[next[j]],直到两字符不同 |
模式匹配 | 子串的定位 |
简单模式匹配 | 最坏时间复杂度O(mn) |
KMP | 模式串后移位数Move=已匹配的字符数-PM = (j-1)-PM[j-1] = (j-1)-next[j],相当于将子串的比较指针j回退到j=j-move,主串始终不回退,O(m+n) |
KMP的优化 | 用nextval取代next,即将next[j]修正为next[next[j]],直到两者对应位置的字符不等为止 |
5 树
5.1 树的基本概念
子孙 | 跟祖先反过来的概念 |
双亲/直接上层结点 | 最近的祖先 |
孩子 | 最近的子孙 |
兄弟 | 双亲相同 |
堂兄弟 | 双亲在同一层 |
树的度 | 树中结点的最大度数 |
分支结点/非终端结点、叶子结点/终端结点 | 结点数=分支数/所有结点的度数之和+1,因为分支跟孩子一一对应 |
结点的层次 | 根结点为第一层,以此类推 |
结点的深度 | 从根结点开始逐层累加 |
结点的高度 | 从叶结点开始逐层累加 |
树的高度/深度 | 树中结点的最大层数 |
路径 | 结点序列,方向是从上而下(因为分支是从双亲指向孩子,是有向的) |
路径长度 | 路径上经过的边的个数 |
树的路径长度 | 树根到每个结点的路径长的总和 |
结点的带权路径长度 | 根到该结点的路径长度×该结点上权值 |
树的带权路径长度WPL | 所有叶结点的带权路径长度之和 |
树——双亲表示法 | 连续空间,每个结点中增设伪指针指向双亲在数组中的位置。其中,根结点下标为0,其伪指针域为-1 |
树——孩子表示法 | 把每个结点的孩子结点用单链表链接起来,则n个结点有n个孩子链表 |
树——孩子兄弟/二叉树表示法 | 用二叉链表作为存储结构:data、firstchild、nextsibling,方便实现将树转为二叉树 |
结点在另一结点的左方 | 层数之差为0、正、负都是可以的 |
5.2 二叉树
概念 | 性质 | |
二叉树 | 树的度=2/1/0,度为1的结点的孩子也分左右次序(区别于有序树——不可为空、度为1的孩子无序) | 可顺序存储(h为树高,则要占近2^h-1个存储单位)、链式存储,n0=n2+1 |
满二叉树 | ∈完全二叉树,结点最多 | |
完全二叉树 | 与满二叉树的层次遍历一一对应,n1=1(n为偶)或=0(n奇),仅最大层结点可不满,仅最下两层可有叶结点 | 将结点按层序从1编号,结点i的双亲为⌊i/2⌋、左孩子为2i、右孩子为2i+1。分支结点i<=⌊n/2⌋ |
二叉排序/查找/搜索树BST | 结点的关键字:左子树<根结点<右子树,适用于有序的动态查找表 | 中序遍历得递增序列;插入的结点一定是叶结点(AVL不是这样),删除某结点时若其左右子树均不空需用其直接前驱或后继代替它,插入和删除为O(log_2 n);查找效率取决于树高h |
平衡(二叉)树AVL | ∈BST,任意结点的平衡因子=左右子树高度差<=1,最少结点数递推公式n_h=1+n_(h-1)+n_(h-2) | 若插入导致不平衡需调整最小不平衡子树:LL(左孩子的左子树(初始可为空)上插入一个新结点)右单(右转一次)、RR左单、LR先左后右双、RL先右后左双 |
红黑树RBT | ∈BST,①每个结点或是红色,或是黑色的 ②根节点是黑色的 ③叶结点(即外部结点、NULL结点、失败结点)均黑色 ④不存在两个相邻的红结点(即红结点的父节点和孩子结点均是黑色) ⑤对每个结点,从该节点到任一叶结点的简单路径上,所含黑结点的数目(称为该结点的黑高bh)相同 | 性质1:从根结点到叶结点的最长路径不大于最短路径的2倍; 性质2:有n个内部节点的红黑树高度h≤2log_2 (n+1) => 红黑树查找、删除、插入操作时间复杂度=O(log_2 n)。其他:在一棵红黑树中,如果所有结点都是黑的,那么它的形态一定是满二叉树;根节点黑高为h的红黑树,内部结点数(关键字)最少的情况是铺满h层黑结点的满树形态,最多是黑、红交叉铺满2h层 |
哈夫曼树/最优二叉树 | WPL最小。构造方法:不断选取权值最小的两个结点,并将和作为它们俩的双亲结点的权值 | 所有初始结点都变成叶结点,权值越小的结点到根结点的路径长度越大,n1=0,新建了n-1个结点。构造出的哈夫曼树可能不唯一,但WPL必然相同且最小 |
无论是二叉查找树、还是AVL树、红黑树。先删除一个关键字,紧接着插入相同关键字,树形态都有可能改变。 | ||
RBT与AVL:查找效率通常AVL树更好,插入、删除操作通常红黑树更优秀。查找/插入/删除的最坏时间复杂度都是O(log_2 n) | ||
二叉链表 | 包含data、lchild、rchild | 空链域个数=n1+2n0=n+1(所以之后线索数也是n+1个) |
三叉链表 | 二叉链表的基础上加上parent | |
线索链表 | 二叉链表的基础上加上ltag(=1时lchild指向前驱)、rtag(=1时rchild指向后继) | 原二叉链表的空链域里填入线索(即二叉树的线索化,即指向前驱或后继的指针),这使树变为线索二叉树(是加上线索后的链表,故∈物理结构) |
固定长度编码 | 每个字符用相等长度的二进制位表示 | |
可变长度编码 | 对频率高的字符赋短编码、低的赋长编码 | |
前缀编码 | 没有一个编码是另一个编码的前缀 | |
哈夫曼编码 | ∈可变长度编码,∈前缀编码,为从根至该字符的路径上的边的标记的序列 | 边标记为0表示转向左孩子、1表示转向右孩子。用哈夫曼树可设计出总长度最短的二进制前缀编码 |
5.3 树、森林的遍历
树 | 二叉树(一一对应) | 森林(可以只有一棵树) |
先根(先根后子树) | 先/前序 | 先序/先根(第一棵树的根结点、第一棵树的子树森林、其他树) |
后根(先子树后根) | 中序 | 中序/中根/后根(第一棵树的子树森林,第一棵树的根结点、其他树) |
并查集
概念 | 解释 | 数组实现 |
Union(S,Root1,Root2) | 把S的子集合Root2并入子集合Root1,要求Root1和2不相交 | 将Root2根结点的双亲指针指向Root1的根结点 |
Find(S,x) | 查找集合S中单元素所在的子集合 | 循环寻找x的根 |
Initial(S) | S中每个元素都初始化为只有一个单元素的子集 | 每个数组元素的值初始化为负数 |
存储结构 | 树、森林的双亲表示,每个子集合是一棵树 | 数组元素的下标:元素名,根结点的下标:子集合名 |
6 图
6.1 图的基本概念
基本元素 | G=(V,E) | V(G) | E(G) | |V|>0(跟线性表和树不同,图不可是空图) | |E| |
度 | 度TD(v)=ID+OD | 入度ID(v) | 出度OD(v) | ||
路径(定义为由顶点和相邻顶点序偶构成的边所形成的序列或顶点序列) | 回路/环(第一个、最后一个顶点相同的路径) | 简单路径(顶点不重复出现) | 简单回路(除第一个、最后一个顶点外其他定点不重复) | 路径长度(路径上边的数目) | 距离(最短路径的长度) |
有向图衍生概念 | 有向边/弧<v,w> | 弧尾v | 弧头w | v邻接到w | |
无向图衍生概念 | 无向边/边(v,w) | w和v互为邻接点 | 边(v,w)依附于w和v | 边(v,w)和w,v相关联 | |
AOE网衍生概念 | 开始顶点/源点 | 结束顶点/汇点 | 关键路径(路径长度最大) | 关键活动(关键路径上的活动) |
图的分类
不相交的概念1 | 不相交的概念2 | 衍生概念 |
有向图 | 无向图 | |
有向完全图(有n(n-1)条弧) | (简单/无向)完全图 | |
强连通图(有向图) | 连通图、非连通图(都是无向图) | |
强连通分量=极大强连通子图(有向图) | 连通分量=极大连通子图(边最多) | |
生成森林(非连通图的) | 生成树=极小连通子图(边最少) | 生成子图(顶点不减) |
简单图(不存在重复边和顶点到自身的边,数据结构中仅讨论简单图) | 多重图 | |
稀疏图 | 稠密图 | |
带权图/网 | 边的权值 | |
有向树 | ||
AOV网(用点表示活动)∈有向无环图DAG(可表示表达式) | AOE网(点表示事件、 边表示活动,边上权值为活动开销)∈DAG | 拓扑排序 |
6.2 图的存储
邻接矩阵 | 邻接表 | 十字链表 | 邻接多重表 | ||
顶点结点 | 一维数组(可省略) | 顶点表:data、firstarc | data、firstin、firstout | data、firstedge | 都是顺序存储 |
边结点 | 二维数组 | 顶点的边表、出边表(对于有向图):单链表,邻接点域adjvex、指针域nextarc | 弧结点:十字链表,表示不唯一,尾域tailvex、头域headvex、hlink、tlink、info | 链表,mark、ivex、ilink(指向下一条依附于顶点ivex的边)、jvex、jlink、info | |
优点 | 易得到顶点的出度 | 易求得顶点的出度和入度 | 比邻接表更易判断两点间的关系 | ||
缺点 | 占用空间大 | 不易得到顶点的入度 | |||
适用范围 | 无向图、有向图、带权图/网 | 无向图、有向图 | 有向图 | 无向图 | |
其他 | 邻接矩阵A^n的元素=由顶点i到顶点j的长度为n的路径数目 | 逆邻接表的优缺点与之相反 | firstin指向以该顶点为弧头的第一个弧结点;hlink指向弧头相同的下一条弧 | 所有依附于同一顶点的边串联在同一链表中;边结点跟边一一对应 |
6.3 图的遍历
广度优先搜索BFS | 深度优先搜索DFS | |
类似于树的遍历 | 层序遍历,按距离从近至远遍历 | 先序遍历 |
辅助 | 队列 | 递归工作栈 |
是否递归 | 否 | 是(因为需往回退) |
需辅助数组标记是否被访问过 | 是 | 是 |
原理——外循环 | 外循环实际次数=子图个数 | 同左 |
原理——内部——初始化 | 内循环第一步:对外循环的当前顶点先访问再压入队列中 | 访问当前顶点 |
原理——内部——循环 | 当队列不空时:出队列——其邻接点依次访问并入队列 | 遍历当前顶点的邻接点,依次对其递归重走“内部”函数 |
空间复杂度 | 辅助队列→O(|V|) | 递归工作栈→O(|V|) |
时间复杂度——邻接表 | 每个顶点至少入队一次+搜每个点的邻接点时每个边至少访问一次=O(|V|)+O(|E|) | 同左 |
时间复杂度——邻接矩阵 | 查找每个点的邻接点需O(|V|)→O(|V|^2) | 同左 |
应用 | 生成广度优先生成树(<=右者树高) | 生成深度优先生成树/森林 |
应用 | 求非带权图的单源最短路径d(v_0,v_i):令v_0为BFS外循环的第一个结点 | 顶点v入栈后,必先遍历完其后继顶点后v才会出栈,即v在其后继结点之后出栈 |
6.4 图的应用
最小生成树MST——Prim | 最小生成树MST——Kruskal | MST——破圈法 | 最短路径——Dijkstra | 最短路径——Floyd | 拓扑排序 | 关键路径 | |
使用范围 | 非带权图、带权图(MST唯一) | 同左 | 带权图:单源最短路径 | 带权图:每对顶点间 | AOV网 | AOE网 | |
类似方法 | Dijkstra | Prim | 最大路径 | ||||
原理——初始化 | 任选一个顶点 | 每个顶点自成一个连通分量 | 任取一圈,去掉圈上权最大的边 | 把源点放入集合S(记录已求得最短路径的顶点),即令S={0},dist[](原点到个顶点当前最短路径长度)初始值dist[i]=arcs[0][i] | 邻接矩阵(注意并非幂运算,而是迭代次数)![]() | 将没前驱的顶点入栈,选一个出栈 | ve(源点)=0;vl(汇点)=ve(汇点) |
原理——循环 | 从{(u,v)|u∈U, v∈V-U}中选权值最小且不构成回路的边 | 按边权值递增顺序不断选权值最小且不构成回路的边,不断把森林连起来,在此过程中不要求一直连通 | 重复执行,直到没圈为止 | 从顶点集合V-S中选出v_j使dist[j]最小并放入S;修改V-S中的每个顶点v_k:若dist[k]>dist[j]+arcs[j][k]则令左=右 | 依次把各个顶点作为中介顶点,即依次令k=0…n-1,并修改矩阵元素值:![]() | 将与其相连的点入度-1,并将入度变为0的点都入栈 | 递推公式: ve(k)=Max{ve(j)+Weight(v_j,v_k)}、 vl(k)=Min{vl(j)-Weight(v_k,v_j)} |
时间复杂度 | O(|V|^2) | 用堆存放边故每次选最小权边需O(log|E|)*用并查集描述生成树=O(|E|log|E|) | O(|V|^2) | O(|V|^3)。另一方法是对每个顶点|V|*做一次Dijkstra 即O(|V|^2)=O(|V|^3) | 用邻接表时删顶点+删相应的边=O(|V|+|E|);用邻接矩阵时O(|V|^2) | ||
适用于 | 边稠密的图 | 边稀疏而顶点较多的图 | 权值为正 | 不允许带负权的边成回路 | 邻接矩阵是三角矩阵时 | ||
其他 | 当带权连通图的任意一个环中所包含的边的权值均不相同时,其MST唯一 | 同左 | 虽是在内存中进行的,但不属于内部排序范畴 | 事件v_k的最早发生时间ve(k); 事件v_k的最迟发生时间vl(k); 活动a_i的最早开始时间e(i)=ve(k); 活动a_i的最迟开始时间l(i)=vl(j)-Weight(v_k,v_j); 活动a_i可拖延时间d(i)=l(i)-e(i)差额d()=0的活动构成关键路径 |
7 查找
7.1 查找的基本概念
概念 | 分类 | 举例 | |
查找表/查找结构 | 用于查找的数据集合 | 静态查找表(无需插入和删除,适合顺序、折半、散列查找)、动态查找表(适合BST、散列查找) | 数组、链表等 |
关键字 | |||
平均查找长度ASL | 查找过程中进行关键字比较次数的期望值 | 求查找失败的ASL有两种观点:认为比较到空结点才算失败,所以比较次数等于冲突次数加1;认为只有与关键字的比较才算比较次数 |
7.2 查找算法
顺序查找——无序表 | 顺序查找——有序表 | 折半查找 | 分块查找 | 散列查找 | |
别名 | 线性查找 | 同左 | 二分查找 | 索引顺序查找 | 哈希查找 |
适用范围 | 顺序表、链表 | 同左 | 有序∩顺序表、BST的二叉链表 | 查找表分块,块内无序、块间有序 | 散列表 |
ASL_成功 | (n+1)/2 | O(log_2 n) | 索引表查找(顺序或折半)+块内查找 | O(1),需要比较关键字以确定是否查找成功 | |
ASL_不成功 | n+1 | n/2+n/(n+1) | |||
相关概念 | 失败结点 | 判定树 | 索引表 | 散列函数Hash(key)=Addr | |
相关概念解释 | 若有n个结点则有n+1个失败结点;用于算ASL_不成功 | 可描述折半查找的二叉树,是唯一的 | 表中每个元素含有各块的最大关键字和首元素地址;分为b块,每块s个记录 | 直接定址法-H(key)=a×key+b; 除留余数法-H(key)=key%p;(p取不大于表长的最大素数) 数字分析法-观察key数字结构; 平方取中法-H(key)=key^2的中间几位 | |
其他相关概念 | 对有n个记录的分块表最理想块长:√n | 冲突、同义词(映射到同一地址到不同关键字);装填因子α=表中记录数/散列表长度;聚集/堆积:大量元素在相邻散列地址上 | |||
在散列表中,平均查找长度与装填因子α直接相关,但不直接依赖于表中已有表项个数或表长 | |||||
堆积:对存储效率、散列函数和装填因子均不会有影响,但会影响ASL |
处理散列函数冲突的方法
拉链法 | 开放地址法 | |
原理 | 把所有同义词存储在一个线性链表中 | H_i=(H(key)+d_i)%m,d_i为增量序列,m散列表表长 |
删散列表中记录 | 可以物理地删除 | 不能物理地删除,只能做删除标记 |
其他 | 线性探测法d_i=0,1,..,k; 平方/二次探测法d_i=0,1,-1,4,-4,…,k^2,-k^2; 伪随机序列法d_i=伪随机数序列; 再/双散列法d_i=i×Hash_2(key),用第2个散列函数 |
7.3 B树和B+树
B树/多路平衡查找树 | B+树 | |
概念 | 所有结点的平衡因子=0,且都满足子树、关键字个数要求。可为空树 | |
阶m | >=所有结点(包括终端结点)的孩子个数的最大值 | 同左 |
非终端根结点子树个数 | >=2(无论m多大都是这样) | 同左 |
非根非叶结点子树个数范围 | [⌈m/2⌉, m](m=2亦可) | 同左 |
每个结点的子树个数-关键字个数(即互相约束 | 1 | 0 |
关键字分布位置 | 非叶结点,即非终端+终端结点,即叶结点个数-1 | 叶结点,即终端结点 |
叶节点 | 属于外部结点,代表查找失败 | 包含全部关键字 |
非叶节点 | 某结点若有m-1个关键字的值,则该结点还有m个指向子树根结点的指针 | 仅包含m个相应子树关键字的最大值+m个指向子树根结点的指针 |
高度/磁盘存取次数 | 不含叶节点 | |
适用查找 | 随机查找 | 随机查找、顺序查找 |
适用场景 | 比左边更适于操作系统的文件索引、数据库索引。因为比左边的磁盘读写代价更低、查询效率更稳定 |
B树的插入和删除
插入 | 删除 | |
关键字位置 | 必然插入到终端结点(最底层的非叶节点) | 若删除非终端结点的关键字k则仅需用k的前驱或后继k'替代即可;若删除终端结点关键字则通常直接删掉即可 |
特殊情况 | 插入后使某结点内关键字个数>=m | 删除后使某结点内关键字个数=⌈m/2⌉-2 |
特殊情况处理 | 从中切开,并把中间位置的关键字传给父结点 | 兄弟够借:父子换位法——父→原关键字位置、兄→父; 兄弟不够借:父、兄合并到原关键字所在结点 |
8 排序
8.1 排序的基本概念
排序算法的稳定性 | 关键字相同的元素排序后相对位置不变,算法的稳定性与算法的优劣性无关 |
内部排序 | 在排序期间元素全部存放在内存中,主要操作:比较、移动,对任意n个关键字排序的比较次数至少为⌈log2(n!)⌉ |
外部排序 | 排序文件较大,内存一次放不下。时间代价主要考虑访问磁盘的次数:I/O次数 |
全局有序 | 有序子列中任一元素一定>或<无序子列全部元素 |
一趟 | 排序过程中,对尚未确定最终位置的所有元素进行一遍处理 |
堆 | n个关键字序列L[…],相当于一个完全二叉树 |
大根堆/大顶堆 | L(i)>=L(2i)且L(i)>=L(2i+1),根结点是最大元素,跟BST的区别在于两个孩子结点没有次序规定 |
小根堆/小顶堆 | L(i)<=L(2i)且L(i)<=L(2i+1),根结点是最小元素,跟BST的区别在于两个孩子结点没有次序规定 |
堆的插入 | 先插入到第n+1个位置(即叶结点),然后与其双亲进行比较及交换,直到不再大/小于其双亲 |
堆的删除 | 让最后一个叶结点填补被删除的空位,然后进行堆的调整 |
8.2 内部排序
插入排序——直接插入 | 插入排序——折半插入 | 插入排序——希尔排序/缩小增量排序 | 交换排序——冒泡排序 | 交换排序——快速排序 | 选择排序——简单选择排序 | 选择排序——堆排序 | 2路归并排序 | 基数排序 | |
算法 | 从左边第一个元素开始,向前查找待插入位置,找到后将中间经过的元素整体后移 | 与左边差别仅在于使用折半查找而非顺序查找 | 不断以增量d划分子表并对所有子表分别进行直接插入排序,d不断减半至1 | 两两比较相邻元素,边比较边交换 | 将比pivot小的移到其左边、比pivot大的移到右边(通过从两边向中间添空位)。然后对两个子表分别递归重复该过程 | 先找到最小元素,然后交换到首位 | 关键是构造大/小根堆。从最右下的非叶结点开始,依次遍历各个非叶结点 | 依次以2、4、8、…个(归并路数2的次方)元素为一组(要用到merge(),即将两个有序表归并为一个有序表) | 共有d趟分配(即入相应序号的队,形成r个链表)和收集(出队合成一个链表)。如对十进制数排序,要依次按个、十、百、…的入出队顺序 |
第一个操作的元素位置 | 左数第二个元素 | 左数第二个元素 | 每个子表L={i,i+d,i+2d,…i+kd}里的左数第二个元素 | 最左(/右)两个元素 | 任取一个元素pivot,称为枢轴/基准,并留下空位。通常取首元素。最右边比pivot小的和最左边比pivot大的最先移动 | 最小元素所在位置 | 以最后一个非叶结点为根的子树 | 每对相邻的两个元素 | |
空间复杂度 | O(1) | O(1) | O(1) | O(1) | 根据递归栈的深度(即递归树高度):最好及平均O(log_2 n)、最坏O(n)(当已有序 | O(1) | O(1) | O(n)(即merge()的辅助空间) | r个队列:O(r ) |
最好情况 | 已有序对每个元素只用比较一次 | 有序时比较次数为n-1(因该趟冒泡没发生交换,就直接结束了 | Partition()平衡划分,即两个子问题的大小相同 | 选择排序、归并排序的比较次数与序列的初始状态无关,而且比较是主要操作 | 建堆时间O(n)*与树高O(h)有关的向下调整 | 需进行⌈log_2 n⌉趟归并 | [每趟分配O(n)+收集O(r)]*d趟 | ||
最好情况时间复杂度 | O(n) | O(n)进行1次冒泡 | O(nlog_2 n) | O(n^2) | O(nlog_2 n) | O(nlog_2 n) | O(d(n+r)) | ||
最坏情况 | 刚好逆序,比较和移动均最多 | 正好正序或逆序,每次选取第n个元素为基准,会导致划分区间分配不均匀 | |||||||
最坏情况时间复杂度 | O(n^2) | O(n^2) | O(n^2)进行n-1次冒泡 | O(n^2) | O(n^2) | O(nlog_2 n) | O(nlog_2 n) | O(d(n+r)) | |
时间复杂度(即运行效率 | O(n^2) | 比较O(nlog_2 n)+移动O(n^2) | 约为O(n^1.3) | O(n^2) | O(nlog_2 n) | O(n^2) | O(nlog_2 n) | O(nlog_2 n) | O(d(n+r))与序列初始状态无关 |
稳定性 | 是 | 是 | 不是 | 是 | 不是 | 不是 | 不是 | 是 | 是 |
适用性 | 顺序表、链式存储的线性表 | 顺序表 | 顺序表 | ||||||
全局有序 | 不是 | 不是 | 不是 | 是 | 是 | ||||
一趟排序含义 | 找到某个元素的插入位置 | 使各个子表有序 | 所有相邻元素都比较过;交换类排序的趟数和原始序列有关 | 也叫一次划分,若只对一块子表进行排序而未处理另一子表,就不能算完整的一趟 | |||||
特点 | 初始序列基本有序时,插入排序比较次数较少 | 平均性能而言最好的内部排序方法; 第i趟完成时,会有i个以上的数出现在它最终将要出现的位置; | 常用于取k个最大/小元素;堆是用于排序的,在查找时它是无序的 | 不基于比较和移动进行排序;不能对float和double类型的实数进行排序 | |||||
相关概念 | 基数r:关键字为d元组时,每一元的元素种数,如关键字为十进制数时,基数为10;排序依据:最高位优先MSD、最低位优先LSD(即按实际数字大小)决定r个队列的顺序; | ||||||||
应用——取前k小的元素 | 插入排序、快速排序和归并排序只有在将元素全部排完序后,才能得到前k小的元素序列 | 冒泡排序、堆排序和简单选择排序则在每一趟中都确定一个最小的元素 | 对n个元素,建立堆排序初始堆的时间不超过4n,取得第k个最小元素之前的排序序列用时klog2_n,总时间为4n+klog2_n | ||||||
希尔排序和堆排序都利用了顺序存储的随机访问特性,而链式存储不支持这种性质,所以更换为链式存储后时间复杂度会增加 |
8.3 外部排序——归并排序
归并段/顺串 | 外部排序过程中的有序子文件,即通过内部排序方法排好序后放回外存的有序子文件 | 每一趟归并的IO读写磁盘次数相同,=待排文件总记录数/每块磁盘可容纳的记录数。初始归并段个数r=待排文件总记录数/内存可容纳的记录数 | 减少r可减少归并趟数(优化1) |
k路归并排序 | 最多只有k个段归并为一个。使用归并排序方法,最少只需在内存中分配k+1块大小的缓冲区(包括k个输入缓冲区和一个输出缓冲区)即可对任意大小的一个文件进行排序。这里的块与外存中磁盘块的大小一致。 | 归并趟数=⌈log_k r⌉。内部归并:在k个关键字中选择一个关键字需要比较k-1次。k+1=内存可容纳的记录数/每块磁盘可容纳的记录数 | 增大路数可减少I/O次数(优化2),但同时也会增加内存开销和内部归并所需时间 |
k路平衡归并 | 在每一趟归并中,若有m个归并段参与归并,则经过这一趟归并必须恰好得到⌈m/k⌉个新的归并段 | ||
败者树 | 是完全二叉树,k个叶结点(是外结点)存放k个归并段 | 内部归并:在k个关键字中选择一个关键字最多需要比较⌈log_2 k⌉次 | 令内部归并的比较次数与k无关 |
置换——选择算法 | 在内存工作区WA选择关键字最小的记录,即MINIMAX记录,并输出到初始归并段输出文件FO,同时从初始待排文件FI中输入下一个记录到WA中 | 生成更长的初始归并段 | |
最佳归并树 | 让记录最少的初始段最先归并,类似于哈夫曼树 | 总的I/O次数=读+写=2✖️WPL | 是m路归并排序的优化方案(总的I/O次数最少) |
虚段 | 即添加权为0的叶子结点 | 添加后使n0=(k-1)n_k+1且n_k为整数 | 为使初始归并段构成严格k叉树 |
推荐的Github库
2021王道数据结构课后大题代码全实现:
https://github.com/Ruvikm/Wangdao-Data-Structures
《数据结构-C语言版》[严蔚敏,吴伟民版]课本源码与习题解析:
GitHub - kangjianwei/Data-Structure: 《数据结构》-严蔚敏.吴伟民-教材源码与习题解析