文章目录
7 图
【有向完全图和无向完全图】
若有向图中有 n n n 个顶点,则最多有 n ( n − 1 ) n(n-1) n(n−1) 条边(图中任意两个顶点都有两条边相连),将具有 n ( n − 1 ) n(n-1) n(n−1) 条边的有向图称为有向完全图
若无向图中有 n n n 个顶点,则最多有 n ( n − 1 ) 2 \frac{n(n-1)}{2} 2n(n−1) 条边(图中任意两个顶点都有一条边相连),将具有 n ( n − 1 ) 2 \frac{n(n-1)}{2} 2n(n−1) 条边的无向图称为无向完全图
【简单路径和回路】
简单路径: 如果路径上的各顶点均不互相重复,称这样的路径为简单路径
回路: 若一条路径中第一个顶点和最后一个顶点相同,则这条路径是一条回路
【连通图和连通分量】
如果图中任意两个顶点之间都连通,则称该图为连通图;否则,图中的极大连通子图称为连通分量,如
【强连通图和强连通分量】
如果对于每一对顶点 < v i , v j > <v_i,v_j> <vi,vj>,从 v i v_i vi 到 v j v_j vj 和 从 v j v_j vj 到 v i v_i vi 都有路径,则称该图为强连通图;否则,将其中的极大强连通子图称为强连通分量
- 有 n n n 个顶点的强连通图最多有 n ( n − 1 ) n(n-1) n(n−1) 条边(每一对顶点一条边),最少 n n n 条边(成环)
【无向连通图】
- 一个无向图 G = ( V , E ) G=(V,E) G=(V,E) 是连通的,那么边的数目大于等于顶点的数目减一: ∣ E ∣ > = ∣ V ∣ − 1 |E|>=|V|-1 ∣E∣>=∣V∣−1,而反之不成立
- 所有顶点的度之和为偶数,且 D = 2 E D = 2E D=2E
【邻接表/逆邻接表】
邻接表不说了,很熟悉。逆邻接表,即对每个顶点 v i v_i vi 建立一个链接以 v i v_i vi 为头的弧的表
【十字邻接表】(对有向图)
【邻接多重表】
顶点表由两个域组成,vertex 域存储和该顶点先关的信息,firstedge 域指示第一条依附于该顶点的边
边表结点由 6 个域组成,mark 为标记域,可以用于标记该边是否被搜索过,ivex 和 jvex 为该边依附于的两个顶点的编号,ilink 指向下一条依附于顶点 ivex 的边,jlink 指向下一条依附于顶点 vjex 的边
【图的两种遍历的适用范围】
DFS、BFS 对任何图都适用,并没有限制是针对有向图还是无向图
【图遍历与二叉树遍历的联系】
DFS 相当于二叉树中的先序遍历,BFS 相当于二叉树中的层次遍历
【关于一些图算法的时间复杂度】
- 对有
n
n
n 个结点、
e
e
e 条边且使用邻接表存储的有向图进行广度优先遍历,其算法时间复杂度是
O
(
n
+
e
)
\mathcal O(n+e)
O(n+e)
解释: 广度优先遍历需要借助队列实现,在对图进行广度优先遍历时,每个顶点均需要入队一次(顶点表遍历),在搜索所有顶点的邻接点的过程中每条边至少访问一次(出边表遍历)* - 对有
n
n
n 个结点、
e
e
e 条边且使用邻接表存储的有向图进行拓扑排序,其算法时间复杂度是
O
(
n
+
e
)
\mathcal O(n+e)
O(n+e)
解释: 输出每个顶点的同时还要删除以它为起点的边,这样对各顶点和边都要进行遍历*
【最小生成树】
1)最小生成树
含有 n n n 个顶点的单权连通图的最小生成树是指图中任意一个由 n n n 个顶点构成的边的权值之和最小的连通子图
何时最小生成树唯一? 当带权连通图的任意一个环中所包含的边的权值均不相同时,该图的最小生成树唯一
2)普利姆算法
普利姆算法思想: 从图中任意取出一个顶点,把它当成一棵树,然后从与这棵树相连的边中选择一条最短的边,并将这条边及其所连的顶点并入树中
普利姆算法执行过程:
- 将 v v v 到其他顶点的所有边作为侯选边
- 从候选边中挑出权值最小的边输出,并将与该边相连的另一端顶点并入生成树中
- 考察所有剩余顶点 v i v_i vi,如果 ( v , v i ) (v,v_i) (v,vi) 的权值比 l o w c o s t [ v i ] lowcost[v_i] lowcost[vi] 小,则用 ( v , v i ) (v,v_i) (v,vi) 的权值更新 l o w c o s t [ v i ] lowcost[v_i] lowcost[vi]
- 重复上述过程直到所有点都被并入生成树中
复杂度分析: 普里姆算法的复杂度为 O ( n 2 ) \mathcal O(n^2) O(n2),只与图中的顶点数有关,与边数没有关系,所以适合稠密图
注意,普里姆算法中边上的权可正可负
3)克鲁斯卡尔算法
克鲁斯卡尔算法思想: 每次找出候选边中权值最小的边,就将该边并入生成树中
克鲁斯卡尔算法执行过程:
- 将图中边按照权值从小到大排序
- 从最小边开始扫描各边,并检测当前边是否为侯选边,即是否该边的并入会构成回路,如不构成回路,则将该边并入当前生成树中
- 重复上述过程,直到所有边都被检查完
并查集:
判断是否产生回路要用到并查集。并查集中保存了一棵或者几棵树,这些树有这样的特点:通过树中一个结点,可以找到其双亲结点,进而找到根结点。这种特性有两个好处:
- 可以快速地将两个含有很多元素的集合并为一个。两个集合就是在并查集中的两棵树,其合并只需找到其中一棵树的根,然后将其作为另一棵树中任何一个结点的孩子结点即可(具体如下图所示)
- 可以方便地判断两个元素是否属于同一个集合
复杂度分析: 克鲁斯卡尔算法时间主要花费在对边的排序上,所以时间复杂度与边数有关,适合稀疏图
【迪杰斯特拉算法】
求图中某一顶点到其余各顶点的最短路径
三个数组:
- d i s t [ v i ] dist[v_i] dist[vi] 表示当前已找到的从 v 0 v_0 v0 到每个终点 v i v_i vi 的最短路径长度
- p a t h [ v i ] path[v_i] path[vi] 中保存从 v 0 v_0 v0 到 v i v_i vi 最短路径上 v i v_i vi 的前一个顶点
- s e t [ ] set[] set[] 为标记数组, s e t [ v i ] set[v_i] set[vi] 表示 v i v_i vi 还没有被并入最短路径
执行过程:
- 从当前 d i s t [ ] dist[] dist[] 中选出最小值,假设为 d i s t [ v u ] dist[v_u] dist[vu],将 s e t [ v u ] set[v_u] set[vu] 置为 1,表示当前新并入的结点为 v u v_u vu
- 循环扫描图中顶点
假设当前顶点为 v j v_j vj ,检查 s e t [ v j ] set[v_j] set[vj],若为 1,则略过;若为 0,比较 d i s t [ v j ] dist[v_j] dist[vj] 和 d i s t [ v u ] + w u j dist[v_u]+w_{uj} dist[vu]+wuj,若前者大于后者,则用后者更新前者,并把 v u v_u vu 加入路径,且作为路径上 v j v_j vj 之前的那个顶点,反之则什么也不做 - 重复上述过程直到所有顶点都并入最短路径
考虑如下的例子,
p
a
t
h
[
]
path[]
path[] 中其实保存了一棵树,这是一棵用双亲存储结构存储的树,通过这棵树可以打印出从源点到任何一个顶点的最短路径。但是,注意,在双亲存储结构中只能输出从叶子结点到根结点,所以在利用
p
a
t
h
[
]
path[]
path[] 打印路径的时候需要借助栈实现逆向输出
复杂度分析: 迪杰斯特拉算法的时间复杂度为 O ( n 2 ) \mathcal O(n^2) O(n2)
【弗洛伊德算法】
求图中任意一对顶点间的最短路径
两个矩阵:
- A \mathbf A A 用来记录当前已经求得的任意两个顶点最短路径的长度
- P a t h \mathbf{Path} Path 用来记录当前两个顶点间最短路径上要经过的中间顶点
执行过程:
- 初始化 A \mathbf A A 和 P a t h \mathbf{Path} Path
- 以
k
k
k 为中间顶点,
k
k
k 取
0
∼
n
−
1
0\sim n-1
0∼n−1,对图中所有顶点对
{
i
,
j
}
\left \{i, j\right\}
{i,j} 进行如下检查:
如果 A [ i ] [ j ] > A [ i ] [ k ] + A [ k ] [ j ] A[i][j]>A[i][k]+A[k][j] A[i][j]>A[i][k]+A[k][j],则 A [ i ] [ j ] = A [ i ] [ k ] + A [ k ] [ j ] A[i][j]=A[i][k]+A[k][j] A[i][j]=A[i][k]+A[k][j], P a t h [ i ] [ j ] = k Path[i][j]=k Path[i][j]=k,反之则什么也不做
考虑下面的例子,
复杂度分析: 弗洛伊德算法的时间复杂度为 O ( n 3 ) \mathcal O(n^3) O(n3)
【拓扑排序】
当有向图中无环的时候,还可以采用 DFS 进行拓扑排序。由于图中无环,当从图中某个顶点出发进行 DFS 时,最先退出算法的顶点即为出度为 0 的顶点,它是拓扑排序中的最后一个顶点。因此,按照 DFS 算法出栈的先后次序记录下的顶点序列即为逆拓扑排序序列
对上面我们做出如下的解释:
最先退出算法的顶点指的是程序运行过程中最先退出系统栈的顶点,按照 DFS 出栈的先后次序同样也是指退出系统栈的先后顺序,此顺序并不是 DFS 最终遍历的结果序列(具体参考如下)
【关键路径算法】
v e ( k ) ve(k) ve(k) 为事件 k k k 的最早发生时间, v l ( k ) vl(k) vl(k) 为事件 k k k 的最迟发生时间, e ( a k ) e(ak) e(ak) 表示当前活动 a k ak ak 的最早发生时间, l ( a k ) l(ak) l(ak) 表示当前活动 a k ak ak 的最迟发生时间
算法流程:
- 正向走,求 v e ( j ) ve(j) ve(j), v e ( j ) = v e ( i ) + a i j ve(j)=ve(i)+a_{ij} ve(j)=ve(i)+aij,取 m a x max max
- 反向走,求 v l ( i ) vl(i) vl(i), v e ( i ) = v l ( j ) − a i j ve(i)=vl(j)-a_{ij} ve(i)=vl(j)−aij,取 m i n min min
- 求 e ( a i j ) e(a_{ij}) e(aij), e ( a i j ) = v e ( i ) e(a_{ij})=ve(i) e(aij)=ve(i)
- 求 l ( a i j ) l(a_{ij}) l(aij), l ( a i j ) = v l ( j ) − a i j l(a_{ij})=vl(j)-a_{ij} l(aij)=vl(j)−aij
- e ( a i j ) = l ( a i j ) e(a_{ij})=l(a_{ij}) e(aij)=l(aij),即为关键路径
活动的剩余时间等于活动的最迟发生时间减去活动的最早发生时间,即 l ( a k ) − e ( a k ) l(ak)-e(ak) l(ak)−e(ak),剩余时间反应了活动完成的一种松弛度。根据前面的描述,易知,关键活动的剩余时间为 0
【无向图的邻接矩阵幂】
若已知具有 n n n 个顶点的无向图的邻接矩阵为 B B B,则 B m B^m Bm 中的非零元素 b i j b_{ij} bij 表示顶点 i i i 到顶点 j j j 的长度为 m m m 的路径有 b i j b_{ij} bij 条
【有向图的邻接矩阵幂】
若已知具有 n n n 个顶点的无向图的邻接矩阵为 B B B,则 B m B^m Bm 中的非零元素 b i j b_{ij} bij 表示顶点 i i i 到顶点 j j j 经由顶点 m m m 的路径有 b i j b_{ij} bij 条
与最小生成树相关的习题
【习题】
【解析】C,克鲁斯卡尔选择当前图中权值最小的边,即
(
V
3
,
V
4
)
,
(
V
1
,
V
3
)
,
(
V
2
,
V
3
)
(V_3,V_4),(V_1,V_3),(V_2,V_3)
(V3,V4),(V1,V3),(V2,V3),而普利姆选择与当前生成树(包含顶点
V
4
,
V
1
V_4,V_1
V4,V1)相连的权值最小的边,显然 C)选项所对应的边不与当前的生成树相连
【习题】
【解析】其实这是最小生成树的问题,按照克鲁斯卡尔算法可以得到两个结果
【习题】(最小生成树的唯一性)
【解析】
与拓扑排序相关的习题
【习题】
【解析】A,理论在上面知识点的对应讲解中有说明
【习题】
【解析】
对于DFS,向下搜索过程中如果搜索到前面已经走过的节点,即可以说明有环
对于拓扑排序,访问不到图中所有的节点,亦可以说明存在回路
【习题】
【解析】C,考虑下面的两个例子
与图的基本概念相关的习题
【习题】
【解析】C,和顶点 v 相关的边包括出边和入边,对于出边只需要遍历 v 的顶点表即可,对于入边则需要遍历整个邻接表
【习题】
【解析】B
无向图 G 的极大连通子图称为 G 的连通分量。任何连通图的连通分量只有一个,即是其自身,非连通的无向图有多个连通分量
(如果题目没有说连通图,则具有 n n n 个顶点的无向图最少有 1 1 1 个连通分量,最多有 n n n 个连通分量)
【习题】
【解析】 s s s,在有向图中,对于每一条边都有对应的入点和出点,因此入度之和与出度之和一致
【习题】
【解析】B,有向图连通要求任意两个顶点之间都有路径,即
v
i
v_i
vi 到
v
j
v_j
vj,故有向图连通比无向图连通在最少边时多一条,因为要连回去
【习题】
【解析】C,如果路径上的各顶点均不互相重复,称这样的路径为简单路径,故 Ⅰ 错误;稀疏图应该采用邻接表更省空间,故 Ⅱ 错误
【习题】
【解析】A,一个无向图
G
=
(
V
,
E
)
G=(V,E)
G=(V,E) 是连通的,那么边的数目大于等于顶点的数目减一,即
∣
E
∣
>
=
∣
V
∣
−
1
|E|>=|V|-1
∣E∣>=∣V∣−1,而反之不成立,故 Ⅱ 错误;无向完全图中不存在度为1的顶点
,故 Ⅲ 错误
【习题】
【解析】D,注意题干的要求是 保证图 G 在任何情况下都是连通,即给定 n 条边,用这 n 条边不管怎么连接这 8 个顶点所构成的图 G 始终是连通的。那么,首先需要 G 的 7 个结点构成完全连子通图 G1,然后再添加一条边将第 8 个结点与 G1 连接起来,故为 7×(7-1)/2+1=22
【习题】
【解析】D,
4
×
n
4
+
3
×
n
3
+
2
×
n
2
=
23
×
2
⇒
n
2
=
7
4\times n_4+3\times n_3+2\times n_2=23\times 2\Rightarrow n_2 = 7
4×n4+3×n3+2×n2=23×2⇒n2=7
与有向无环图应用相关的习题
【习题】
【解析】B
对 A,关键路径延期完成,必将导致关键路径长度增加,即整个工程的最短完成时间增加,故正确
对 B,但是关键路径不止一条,当存在多条关键路径的时候,其中一条上的关键活动时间缩短,只能导致该条关键路径变成非关键路径,而不会影响整个工程的最短完成时间,故错误
总结的来说,
- 如果关键路径上的某个关键活动延期完成,必然导致其他的关键路径变为非关键路径
- 缩短某些关键活动不一定缩短整个工程的完成时间
【习题】
【解析】C,详细分析如下所示
【习题】
【解析】C,计算过程如下
【习题】
【解析】
【习题】
【解析】A
先看如下的一个例子立理解一下有向无环图描述表达式
求过程可描述为:
先将表达式转化为二叉树,再将二叉树去重转换成有向无环图,这里的去重是指去除重复的节点,故对本题有
与最短路径相关的习题
【习题】
【解析】C,详细分析过程如下
【习题】(多源迪杰斯特拉)
【解析】其实就是一个多源迪杰斯特拉
我们设从起点到
A
A
A 中各结点的距离均为
L
L
L(私认为重点不要是没啥问题的),于是根据迪杰斯特拉算法可以求出从起点到图中其余各点的最短路径,那么
a
→
b
a\rightarrow b
a→b 的最短路径就是上述序列中第二个顶点为
a
a
a、最后一个顶点为
b
b
b 的最短路,然后最短距离减去
L
L
L 即为
a
,
b
a,b
a,b 间的最短距离
【习题】
【解析】显然是不正确的,如
8 排序
【各排序方法复杂度总结】
排序方式 | 平均情况 | 最坏情况 | 最好情况 | 空间复杂度 |
---|---|---|---|---|
插入排序 | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) |
希尔排序 | O ( n 1.3 ) O(n^{1.3}) O(n1.3) | O ( 1 ) O(1) O(1) | ||
冒泡排序 | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) |
快速排序 | O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) | O ( n 2 ) O(n^2) O(n2) | O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) | O ( l o g 2 n ) O(log_2n) O(log2n) |
选择排序 | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) |
堆排序 | O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) | O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) | O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) | O ( 1 ) O(1) O(1) |
归并排序 | O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) | O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) | O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) | O ( n ) O(n) O(n) |
基数排序 | O ( d ( n + r ) ) O(d(n+r)) O(d(n+r)) | O ( d ( n + r ) ) O(d(n+r)) O(d(n+r)) | O ( d ( n + r ) ) O(d(n+r)) O(d(n+r)) | O ( r ) O(r) O(r) |
细节上的问题:
- 希尔排序的时间是所取“增量”序列的函数,当 n n n 在某个特定范围内,希尔排序所需的比较和移动次数约为 n 1.3 n^{1.3} n1.3
- 不稳定的: 情绪不稳定,快些选一堆好友来聊天
- 复杂度 O ( n l o g n ) \mathcal O(nlogn) O(nlogn): 快些 以nlogn 归队
【一些总结】
- 各排序算法什么情况下最好,什么情况下最坏:
① 插入排序(直接,折半):最好:正序,最坏:逆序
② 冒泡排序:最好:正序,最坏:逆序
③ 快速排序:最坏:数组有序(在分解时每次选取的主元素为最小元素或最大元素) - 一趟排序后能选出一个关键字放在其最终位置上的算法: 简单选择、堆排、快排、冒泡
- 排序趟数和序列的初始状态有关的算法: 交换类的排序(冒泡、快排),其趟数和原始序列状态有关
- 排序趟数固定的算法: 直接插入排序趟数固定为 n − 1 n-1 n−1;简单选择排序趟数固定为 n − 1 n-1 n−1;基排每趟都要分配和收集,排序趟数固定为 d d d
- 比较次数与序列原始状态无关: 简单选择(还有一个折半插入),因为无论序列初始状态如何,每趟排序选择最小(大)值,都要顺序遍历序列,依次用当前最小值和序列中的当前值比较;简单选择排序的比较次数为 O ( n 2 ) \mathcal O(n^2) O(n2),交换次数为 O ( n ) \mathcal O(n) O(n)
- 元素移动次数与关键字的初始排列次序无关: 基数排序
【直接插入】
- 从空间来看,它只需要一个记录的辅助空间(即 j j j)
- 当待排序列中记录按关键字正序排列,所需进行关键字间比较的次数达最小值,即 n − 1 n-1 n−1,记录不需要移动
- 当待排序列中记录按关键字逆序排列时,总比较次数达最大值,即 ∑ i = 2 n i = ( n + 2 ) ( n − 1 ) 2 \sum_{i=2}^n i=\frac{(n+2)(n-1)}{2} i=2∑ni=2(n+2)(n−1) 记录移动的次数也达最大值 ∑ i = 2 n ( i + 1 ) = ( n + 4 ) ( n − 1 ) 2 \sum_{i=2}^n(i+1)=\frac{(n+4)(n-1)}{2} i=2∑n(i+1)=2(n+4)(n−1)
【折半插入】
-
折半插入适合关键字数较多的场景,与直接插入相比,折半插入在查找插入关键位置上面所花费的时间更少,折半插入在关键字移动次数方面和直接插入是一样的
-
折半插入排序的关键字比较次数和初始序列无关,因为每趟排序折半查找插入位置时,折半次数是一定的,折半一次就要比较一次,所以比较次数一定
【2-路插入排序】
2-路插入排序是在折半插入的基础上再改进,其目的是减少排序过程中移动记录的次数,但为此需要 n n n 个记录的辅助空间
具体做法: 另设一个数组 d d d,将待排序列的第一个元素赋值给 d [ 1 ] d[1] d[1],然后从待排序列的第 2 个记录开始起依次插入到 d [ 1 ] d[1] d[1] 之前或之后的有序序列中。先将待插入元素的 d [ 1 ] d[1] d[1] 比较,如果小于 d [ 1 ] d[1] d[1],则将待插入元素插入到 d [ 1 ] d[1] d[1] 之前的有序表中,反之则插入到 d [ 1 ] d[1] d[1] 之后的有序表中。在实现算法时,可将 d d d 看成一个循环向量,并设两个指针 f i r s t first first 和 f i n a l final final 分别指示排序过程中得到的有序序列中的第一个记录和最后一个记录在 d d d 中的位置,具体如下所示
需要注意的是
- 2-路插入排序只能减少移动记录的次数,而不能绝对避免移动记录
- 如果 d [ 1 ] d[1] d[1] 赋值的是待排序列中的最小或最大记录,则 2-路插入排序就完全失去其优越性
设数组中下标为 0 的分量为表头结点,并令表头结点记录的关键字取最大整数 M A X I N T MAXINT MAXINT,于是,表插入排序的过程描述如下:
首先将静态链表中的数组下标为 1 的分量(结点)和表头结点构成一个循环链表,然后依次将下标 2 至 n 的分量(结点)按关键字非递减有序插入到循环链表中
和直接插入相比,表插入仅是修改 2 n 2n 2n 次指针值代替移动记录,排序过程中所需进行的关键字比较次数相同,因此表插入排序的时间复杂度仍为 O ( n 2 ) O(n^2) O(n2)
表插入排序的结果只是求得一个有序链表,则只能对它进行顺序查找,不能进行随机查找,为了能实现有序表的折半查找,需要对记录进行重新排序(即让元素在数组中正序排列,此时 next 域将不再具有实际用处)
【希尔排序】
希尔排序的思想是: 先将待排序元素序列分割成若干子序列(由相隔某个增量的元素组成),分别进行插入排序 ,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序
希尔排序考研中的重点是执行的流程,在执行的流程中要注意,增量 k k k 是从当前元素往后数的第 k k k 个元素(当前元素的下一个元素是第 1 1 1 个元素),具体可以参考下面的例子
关于希尔排序的增量选取有两个需要注意的地方:
- 增量序列的最后一个值一定取 1
- 增量序列中的值应尽量没有除 1 之外的公因子
【冒泡排序】
- 冒泡排序算法结束的条件是在一趟排序过程中美而有发生关键字交换
- 若初始序列为正序序列,则只需要进行一趟排序,在排序过程中进行 n − 1 n-1 n−1 次关键字间的比较,不移动记录;反之,若初始序列为逆序序列,则需要进行 n − 1 n-1 n−1 趟排序,比较次数为 ∑ i = n 2 ( i − 1 ) = n ( n − 1 ) 2 \sum_{i=n}^2(i-1)=\frac{n(n-1)}{2} i=n∑2(i−1)=2n(n−1)
【快速排序】
- 快速排序最好情况下的时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n),待排序列越接近无序,快排的效率越高;最怀情况在的时间内复杂度为 O ( n ) O(n) O(n),待排序列越接近有序,快排的效率越低
- 对长度为 L L L 的子序列进行一次划分,其基本操作可取为两指针的移动,总共 L − 1 L-1 L−1 次
- 在诸多复杂度为同为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) 的排序算法中,只有这里的算法叫做快速排序,原因是这些算法的基本操作执行次数的多项式最高项为 X ⋅ n l o g 2 n X\cdot nlog_2n X⋅nlog2n,快排的 X X X 最小,在同级别的算法中最好
- 快排是递归进行的,需要用到栈空间。若每一趟排序都将记录均匀地分割成长度相近的两个子序列,则栈的最大深度为 ⌊ l o g 2 n ⌋ + 1 \left \lfloor log_2n \right \rfloor+1 ⌊log2n⌋+1;但是,若每趟排序后,枢轴位置均偏向子序列的一段,则为最坏情况,栈的深度为 n n n,此时考虑作出一些修改:在一趟排序后比较分割所得两部分的长度,且先对长度短的子序列中的记录进行快排,则栈的最大深度可降为 O ( l o g 2 n ) O(log_2n) O(log2n)
【简单选择】
无论记录的初始排列如何,所需进行的关键字间的比较次数相同,均为
n
(
n
−
1
)
2
\frac{n(n-1)}{2}
2n(n−1)
【堆排序(以大顶堆为例)】
1)插入结点
需要在插入结点后保持堆的特性,因此需要先将要插入的结点 x 放在最底层的最右边,插入后满足完全二叉树的特点:然后把 x 依次向上调整到合适的位置以满足父大子小的性质
2)删除结点
当删除堆中的一个结点时,原来的位置会出现一个小孔,填充这个孔的办法是:把最底层最右边的叶子结点的值赋给该孔并下调到合适位置,最后把该叶子结点删除
3)建堆
注意一点:调整堆从无序序列所确定的完全二叉树的第一个非叶子结点开始,从右至左,从下至上
4)堆排序
- 建堆: 从无序序列所确定的完全二叉树的第一个非叶子结点开始,从右至左,从下至上,每个结点进行调整,最终得到一个大顶堆
(对结点的调整方法:假设当前结点为 a,将其值与孩子结点进行比较,如果存在大于 a 值的孩子结点,则从中选出最大的一个与 a 交换。当 a 来到下一层的时候重复上述过程,直到 a 的孩子结点值都小于 a 的值为止) - 删除堆顶元素,调整堆: 将当前无序序列中的第一个关键字,反映在树中是根结点(假设为 a)与无序序列中的最后一个关键字交换(假设为 b),a 进入有序序列,到达最终位置。无序序列中关键字减少 1 个,有序序列中关键字增加 1 个。此时只有结点 b 可能不满足堆的定义,对其进行调整
- 重复步骤 2,直到无序序列中的关键字剩下 1 个时排序结束
【归并排序】
【基数排序】
基数排序的时间复杂度为
O
(
d
(
n
+
r
)
)
O(d(n+r))
O(d(n+r)),其理解过程为:基数排序每一趟都要进行分配和收集。分配需要依次对序列中的每个关键字进行,即需要扫描整个序列,所以有
n
n
n 这一项;收集需要依次对每个桶进行,而同的数量取决于关键字的取值范围,即
r
r
r,因此一趟分配和收集需要的时间为
n
+
r
n+r
n+r,整个排序需要多少趟分配和收集呢?需要
d
d
d 趟,即关键字的位数有几位就需要几趟。于是,最终得出基数排序的时间复杂度为
O
(
d
(
n
+
r
)
)
O(d(n+r))
O(d(n+r))
【外部排序】
外部排序指待排文件较大,内存一次性放不下,需存放在外部介质中。外部排序通常采用归并排序法
- 置换-选择排序
其中我们称 1)中的 m m m 个有序记录段为初始归并段,如果被划分的每个小记录段规模不够小,仍然无法完全读入内存,则无法使用内排序得到初始归并段,因此需要一种适用于初始归并段规模的、高效的且不受内存空间限制的排序算法,即置换-选择排序 - 最佳归并树
将当前的 m m m 组(每组含有 k k k 个有序记录段)记录归并为 m m m 个有序记录段的过程称为一趟归并,可见每个记录在每趟归并中需要两次 I/O 操作。读写操作时非常耗时的,可见减少归并次数可以提高效率。为了使得归并次数最少,需用到最佳归并树 - 失败者树
归并排序算法中有一个多次出现的步骤是从当前 k k k 个值中用某种算法选出最值,可见提高选最值的效率也可以提高整个归并排序算法的效率,这时需要用到失败者树
【置换-选择排序】
采用置换-选择排序算法构造初始归并段的过程:
根据缓冲区大小,由外存读入记录,当记录充满缓冲区后,选择最小的(假设升序排序)写回外存,其空缺位置由下一个读入记录来取代,输出的记录称为当前初始归并段的一部分。如果新输入的记录不能成为当前生成的归并段的一部分,即它比生成的当前归并段中最大的记录要小,则它将生成其他初始归并段的选择。重复上述过程,直到缓冲区中的所有记录都比当前初始归并段最大的记录小时,就生成了一个初始归并段
【最佳归并树】
归并过程可以用一棵树来形象第描述,这棵树就称为归并树,树中结点代表当前归并段长度。初始记录经过置换-排序后,得到的是长度不等的初始归并段,归并策略不同,所得的归并树也不同,树的带权路径长度也不同 (带权路径长度与 I/O 次数的关系为:I/O 次数 = 带权路径长度×2) ,为了优化归并树的带权路径长度,可以运用哈夫曼树的知识,对于 k 路归并算法,可以用构造 k 叉哈夫曼树的方法来构造最佳归并树
给出如下的一道例题,
在讨论 k 叉哈夫曼树时,出现给定序列无法构造 k 叉哈夫曼树而需要补充一个权值为 0 的结点的情况。同样,在最佳归并树中也会出现类似的情况。当初始归并段的数目不足时,需附加长度为 0 的虚段,按照哈夫曼树构成的原则,权为 0 的叶子应离树根最远
若按最佳归并树的归并方案进行磁盘归并排序,需在内存建立一张载有归并段长度和它在磁盘上的物理位置的索引表
那么,如何判定附加虚段的数目呢?当 3 叉树中只有度为 3 和度为 0 的结点时,必有 n 3 = n 0 − 1 2 n_3=\frac{n_0-1}{2} n3=2n0−1(考虑 3 n 3 + 1 = N , n 0 + n 3 = N 3n_3+1= N,n_0+n_3=N 3n3+1=N,n0+n3=N),由于 n 3 n_3 n3 必为整数,则 ( n 0 − 1 ) m o d 2 = 0 (n_0-1)\ mod\ 2=0 (n0−1) mod 2=0,也就是说,对 3 路归并而言,只有当初始归并段的个数为偶数时,才需要增加 1 个虚段
在一般情况下,
设需要补充的虚段个数为 n 补 n_{补} n补,则 n 0 = m + n 补 n_0=m+n_{补} n0=m+n补( m m m 是初始归并段)
又 n 0 + n 12 = N , 12 × n 12 + 1 = N n_0+n_{12}=N,12\times n_{12}+1=N n0+n12=N,12×n12+1=N,即有 n 0 = 11 n 12 + 1 n_0=11n_{12}+1 n0=11n12+1
于是可得, n 12 = 120 + n 补 − 1 11 n_{12}=\frac{120+n_{补}-1}{11} n12=11120+n补−1
由于 n 12 n_{12} n12 为整数,故 n 补 n_{补} n补 是使得上式整除的最小整数,求得 n 补 = 2 n_{补}=2 n补=2
【败者树】
1)基本概念
在 k 路归并中,若不使用败者树,则每次对读入的 k 个值需进行 k-1 次比较才能得到最值。引入败者树(由 k 个关键字构造成败者树)只需要约 l o g 2 k log_2k log2k 次即可,因此在归并排序中选最值那一步常用败者树来完成
败者树中两种不同类型的结点:
- 叶子结点,其值为从当前参与归并的归并段中读入的段首记录,叶子结点的个数为当前参与归并的归并段数,即 k 路归并叶子数为 k
- 非叶子结点,都是度为 2 的结点,其值为叶子结点的序号,同时也指示了当前参与选择的记录所在的归并段
2)建立败者树(以最小值败者树为例)
3)调整败者树
【外部排序的时间与空间复杂度问题】
- m m m 个初始归并段进行 k k k 路归并,归并的趟数为 ⌈ l o g k m ⌉ \left \lceil log_km \right \rceil ⌈logkm⌉
- 每一次归并,所有记录都要进行两次 I/O 操作
- 置换-选择排序这一步中,所有记录都要进行两次 I/O 操作
- 置换-选择排序中,选最值那一步的时间复杂度视题干而定
- k k k 路归并的败者树的高度为 ⌈ l o g 2 k ⌉ + 1 \left \lceil log_2k \right \rceil+1 ⌈log2k⌉+1,因此利用败者树从 k k k 个记录中选出最值需要进行 ⌈ l o g 2 k ⌉ \left \lceil log_2k \right \rceil ⌈log2k⌉ 次比较,即时间复杂度为 O ( l o g 2 k ) O(log_2k) O(log2k)
- k k k 路归并败者树的建树时间复杂度为 O ( k l o g 2 k ) O(klog_2k) O(klog2k)
- 显然所有步骤中的空间复杂度都是常量,因此是 O ( 1 ) O(1) O(1)
需要注意的是, k k k 路归并败者树不是 k k k 叉败者树,而是一棵二叉树,且高度不包含最上层选出的结点,如图 8-27 中最上边的结点 2
与排序算法性质相关的习题
【习题】
【解析】D,希尔排序和堆排序都利用了顺序存储的随机访问特性
【习题】
【解析】
(1)堆排序 O ( 1 ) O(1) O(1),快排 O ( l o g 2 n ) O(log_2n) O(log2n),归并 O ( n ) O(n) O(n)
(2)只有归并排序是稳定排序
(3)在平均情况下来看,在时间复杂度同为 O ( n l o g n ) O(nlogn) O(nlogn) 的所有算法中,快排的基本操作执行次数最少,虽然数量级是一样的,但是实际中快排会更快一些
(4)堆排序,因为其最坏情况下也是 O ( n l o g n ) O(nlogn) O(nlogn),空间复杂度为 O ( 1 ) O(1) O(1)
与插入排序相关的习题
【习题】
【解析】D
对 A,排序的总趟数取决于元素个数 n n n,两者都为 n − 1 n-1 n−1 趟,故错误
折半插入排序适合关键字数较多的场景,与直接插入排序相比,折半插入排序在查找插入位置上面所花的时间大大减少,折半插入排序和直接插入排序在关键字移动次数方面和直接插入是一样的,所以其时间复杂度和直接插入排序还是一样的
折半插入排序的关键字比较次数和初始序列无关。因为每趟排序折半查找插入位置时,折半次数是一定的,折半一次就要比较一次,所以比较次数是固定的
需要指出的是,直接插入排序最好情况下的时间复杂度为 O ( n ) O(n) O(n),折半插入排序最好情况下的时间复杂度仍为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
与希尔排序相关的习题
【习题】
【解析】D,具体分析如下
与快排相关的习题
【习题】
【解析】D
这道题如果按照快排一趟有一个元素就位的想法分析的话会发现四个选项均有两个元素就位,显然仅想到这一步是不够的
对于快排,第一趟划分为两个子序列后,第二趟要对这两个子序列都完成划分才算第二趟结束,即正常情况下第二趟会有 3 个元素在最终位置上,除非有第一趟是最大值或者最小值,此时就只有 2 个元素在最终位置上
对于 A、B、C 选项均有两个元素在最终位置上,且其中一个为最大/最小元素,故正确;而 D 选项,只有两个中间元素就位,与上述内容矛盾,故错误
【习题】
【解析】C、D,有序的结果为
{
11
,
18
,
23
,
68
,
69
,
73
,
93
}
\left\{11,18,23,68,69,73,93\right\}
{11,18,23,68,69,73,93} 或者
{
93
,
73
,
69
,
68
,
23
,
18
,
11
}
\left\{93,73,69,68,23,18,11\right\}
{93,73,69,68,23,18,11} 两种情况
【习题】
【解析】D,快排的递归次数与初始数据的排列次序是有关的。且,每次划分后分区比较平衡,则递归次数少;划分后分区不平衡,则递归次数多。但是,快排的递归次数与分区处理顺序无关,即先处理较长分区或先处理较短分区都不影响递归次数
【习题】
【解析】A,对绝大部分内部排序而言,只只用于顺序存储结构。快排在排序的过程中,既要从后向前查找,也要从前向后查找,因此宜采用顺序存储
与简单选择排序相关的习题
【习题】
【解析】A,由 (2)、(3)、(4)可知,每一趟中待排序列中的最小元素都被选出。这里要注意区分一下简单选择和冒泡排序,冒泡的第一趟结果应该是
{
47
,
25
,
15
,
21
,
84
}
\left\{ 47, 25,15,21,84\right\}
{47,25,15,21,84}
与堆相关的习题
【习题】
【解析】B,具体分析如下
【习题】
【解析】A,具体分析如下所示
【习题】
【解析】C,删除 8 后,将 12 移到堆顶,第一次是 15 和 10 比较,第二次是 10 和 12 比较,第三次是12 和 16 比较
【习题】
【解析】B,第一比较: 10 < 18 10<18 10<18,第二次比较: 18 < 25 18<25 18<25
但是本题答案选项有一点小问题,因为堆排序代码中需要对子树根结点的两个孩子结点做一次比较,以选出较大的,再与子树根结点比较。所以,严格的比较次数应该多出来一次 13 13 13 和 18 18 18 的比较,即通过比较挑出较大 18 18 18 再和根结点 25 25 25 比较,所以正确的关键字比较次数应该是 3 3 3
与基数排序相关的习题
【习题】
【解析】C,具体分析如下
与外部排序相关的习题
【习题】
【解析】A,注意这里的归并排序是指外部排序中的归并,将记录读入内存是在生成初始归并段之后的事情
【习题】
【解析】B
设需要补充的虚段个数为 n 补 n_{补} n补,则 n 0 = 120 + n 补 n_0=120+n_{补} n0=120+n补
又 n 0 + n 12 = N , 12 × n 12 + 1 = N n_0+n_{12}=N,12\times n_{12}+1=N n0+n12=N,12×n12+1=N,即有 n 0 = 11 n 12 + 1 n_0=11n_{12}+1 n0=11n12+1
于是可得, n 12 = 120 + n 补 − 1 11 n_{12}=\frac{120+n_{补}-1}{11} n12=11120+n补−1
由于 n 12 n_{12} n12 为整数,故 n 补 n_{补} n补 是使得上式整除的最小整数,求得 n 补 = 2 n_{补}=2 n补=2
【习题】
【解析】其思想和最佳归并树的思想是一样的
9 查找
【 顺序查找】
对于顺序查找,假设每个元素等概率被查找,于是,查找成功情况下的平均查找长度 A S L 成 功 = ∑ i = 1 n i n = n + 1 2 ASL_{成功}= \sum_{i=1}^n\frac{i}{n}=\frac{n+1}{2} ASL成功=i=1∑nni=2n+1
查找不成功情况下的平均查找长度 A S L 不 成 功 = n ASL_{不成功}=n ASL不成功=n
【折半查找】
- 折半查找表要求元素有序,但没有要求是递增有序还是递减有序,且对关键字的数据类型没有规定
- 折半查找是基于随机存储方式的算法,必须用顺序表而不能用链表
- 折半查找的平均长度为 l o g 2 n log_2n log2n
- 折半查找的时间复杂度为 O ( l o g 2 n ) O(log_2n) O(log2n),但并不表示折半查找的速度就一定快于顺序查找
描述折半查找的判定树
折半查找的过程可以用二叉树来表示。把当前查找区间中的中间位置上的记录作为树根,左子表和右子表中的记录分别作为根的左子树和右子树,由此即可得到描述折半查找的判定树。折半查找的比较次数即为从根结点到待查找元素所经过的结点数,因此,算法的时间复杂度可以用树的高度来表示,具有 n n n 个关键字的折半查找的判定树高度为 ⌊ l o g 2 n ⌋ + 1 \left \lfloor log_2n \right \rfloor+1 ⌊log2n⌋+1,即时间复杂度为 O ( l o g 2 n ) O(log_2n) O(log2n)
关于 m i d mid mid 的取值的问题,如果待查找序列中节点总数是偶数:
-
向下取整(下大,右多一)
① 如果待查找序列中节点总数是偶数,且向下取整,那么 m i d mid mid 作为排序树的根节点,它的左子树中节点总数一定比右子树中节点总数小1
② 如果待查找序列只剩下两个元素,且向下取整, m i d mid mid 一定是其中较小的那一个,剩下的的那一个节点变成 m i d mid mid 的右子树,如下图结构:
-
向上取整 (上大,左多一)
① 如果待查找序列中节点总数是偶数,且向上取整,那么 m i d mid mid 作为排序树的根节点,它的左子树中节点总数一定比右子树中节点总数大1
② 如果待查找序列只剩下两个元素,且向上取整, m i d mid mid 一定是其中较大的那一个,剩下的的那一个节点变成 m i d mid mid 的左子树,如下图结构:
【分块查找】
分块查找把线性表分成若干块,每一块中的元素存储顺序是任意的,但是块与块之间必须按照关键字大小有序排列,即前一块中的最大关键字要小于后一块的最小关键字。分块查找实际上进行两次查找,整个算法的平均查找长度是两次查找的平均长度之和,即二分查找平局查找长度+顺序查找平均长度
【二叉排序树】
1)二叉排序树的定义
关于二叉排序树查找路径的问题,牢牢抓住二叉排序树的定义:
- 左子树不空,左子树上所有关键字的值均小于根关键字的值
- 右子树不空,右子树上所有关键字的值均大于根关键字的值
2)查找关键字
二叉排序树的中序遍历序列是递增有序的。对某个关键字的查找过程类似于折半查找,实际上折半查找法的判定树就是一棵二叉排序树
3)插入关键字
对于一个不存在与二叉排序树中的关键字,其查找不成功的位置即为该关键字的插入位置
4)删除关键字
假设在二叉排序树上被删除结点为 p p p, f f f 为其双亲结点,则删除结点 p p p 分以下 3 种情况
- p p p 为叶子结点,直接删除
-
p
p
p 只有右子树而无左子树,或者只有左子树而无右子树,具体操作如下所示
- p p p 既有左子树又有右子树,先沿着 p p p 的左子树根结点的右指针一直往右走,直到来到其左子树的最右边的一个结点 r r r(也可以沿着 p p p 的右子树根结点的左指针一直往左走,直到来到其左子树的最左边的一个结点),然后将 p p p 中的关键字用 r r r 中的关键字替代。最后判断, r r r 是叶子结点还是只有一个孩子结点的哪种情况,对应上面两个小点的方法进行删除
【平衡二叉树】
1)定义
二叉平衡树 AVL 是一种特殊的二叉排序树,要求左右子树高度差的绝对值不超过 1
一个结点的平衡因子为其左子树的高度减去右子树高度的差,对于平衡二叉树,树中的所有结点的平衡因子的取值只能是 − 1 , 0 , 1 -1,0,1 −1,0,1
最小不平衡子树即所谓的失去平衡的最小子树,是以距离插入结点最近,且以平衡因子绝对值大于 1 1 1 的结点作为根的子树。当失去平衡的最小子树被调整为平衡子树后,无须调整原有其他所有的不平衡子树,整个二叉排序树就会成为一棵平衡二叉树
2)平衡二叉树高度和结点的关系
设 N h N_h Nh 表示高度为 h h h 的平衡二叉树中含有的最少结点数(换言之所有非叶结点的平衡因子),则有
N 1 = 1 , N 2 = 2 , N 3 = 4 , N 4 = 7 , N 5 = 12 , ⋯ , N h = N h − 1 + N h − 2 + 1 N_1=1,N_2=2,N_3=4,N_4=7,N_5=12,\cdots,N_h=N_{h-1}+N_{h-2}+1 N1=1,N2=2,N3=4,N4=7,N5=12,⋯,Nh=Nh−1+Nh−2+1
3)平衡调整
在最前面需要指出的是,LL、RR、LR、RL 的命名并不是对调整过程的描述,而是对不平衡状态的描述。如 LL,表示新插入结点落在最小不平衡子树根节点的左孩子的左子树上
下面我们给出一个平衡二叉树建立的过程,在其中穿插解释各平衡调整方式
【B-树】
1)B-树的定义与性质
- 若根结点不是叶子结点,则至少有 2 2 2 棵子树
- 树中每个结点至多有 m m m 棵子树,即至多有 m − 1 m-1 m−1个关键字
- 除根以外的所有非叶结点至少有 ⌈ m / 2 ⌉ ⌈ m/ 2 ⌉ ⌈m/2⌉ 棵子树,即至少含有 ⌈ m / 2 ⌉ − 1 ⌈ m/ 2 ⌉ − 1 ⌈m/2⌉−1 个关键字
- 结点内各关键字互不相等且按从小到大排列
- 叶结点处于同一层,可以用空指针表示,是查找失败到达的位置
B-树是 m m m 叉平衡查找树,但是限制更强,要求所有叶结点在同一层
2)关键字的插入
插入位置一定出现在终端结点上,然后直接插入。插入后检查被插入结点内的关键字的个数,如果大于 m − 1 m-1 m−1,则需进行拆分。注意,插入操作只会使得 B-树逐渐变高而不会改变叶子结点在同一层的特性(下面以 5 阶 B-树为例,关键字个数的范围为 2 ~ 4)
3)结点的删除
- 如果要删除的关键字在终端结点上
① 直接删除
② 向兄弟结点借关键字
③ 向父结点借关键字
- 如果要删除的关键字不在终端结点上,则需先将其转化成在终端结点上,在按照上述方法删除:对于不在终端结点上的关键字 a,需要先沿着 a 的左指针来到其子树根结点,然后沿着根结点中最右端的关键字的右指针往下走,一直走到终端结点上,终端结点上的最右端的关键字即为 a 的相邻关键字,也就是替代 a 的关键字
(接着以上面构造的 5 阶 B-树为例)
【B+树】
- 具有
n
n
n 个关键字的结点含有
n
n
n 个分支
(B-树中, n + 1 n+1 n+1 个分支) - 每个结点(除根结点以外)中的关键字个数
n
n
n 的取值范围
⌈
m
/
2
⌉
∼
m
\left \lceil m/2 \right \rceil \sim m
⌈m/2⌉∼m,根结点的取值范围为
2
∼
m
2\sim m
2∼m
(B-树中的取值范围分别为 ⌈ m / 2 ⌉ − 1 ∼ m − 1 \left \lceil m/2 \right \rceil-1 \sim m-1 ⌈m/2⌉−1∼m−1 和 2 ∼ m − 1 2\sim m-1 2∼m−1) - 叶子结点包含信息,并且包含了全部的关键字,叶子结点引出的指针指向记录
- 所有非叶结点仅起到一个索引的作用
- 有一个指向关键字最小的叶子结点的指针,所有叶子结点链接成一个线性表
B+树是应文件系统所需而产生的 B-树的变形,前者比后者更加适用于实际应用中的操作系统的文件索引和数据库索引,因为前者磁盘读写代价更低、查询效率更加稳定
【哈希】
1)常用 Hash 函数的构造方法
- 直接定址法, H ( k e y ) = a × k e y + b H(key)=a\times key+b H(key)=a×key+b
- 数字分析法,假设关键字是 r r r 进制数,并且 Hash 表中可能出现的关键字都是事先知道的,则可选取关键字的若干位组成 Hash 地址。选取的原则是使得到的 Hash 地址尽量减少冲突,即所选数位上的数字尽可能是随机的
- 平方取中法,取关键字平方后的中间几位作为 Hash 地址。通常在选定 Hash 函数的时候不一定能直到关键字的全部情况,仅取其中的几位为地址不一定合适,而去一个数平方后的中间几位数和数的每一位都相关,由此得到的 Hash 地址随机性大,取的位数表长决定
- 除留余数法, H ( k e y ) = k e y m o d p H(key)=key\ mod\ p H(key)=key mod p
2)冲突处理方法
- 开放地址法
① 线性探查法, d + 1 , d + 2 , ⋯ d+1,d+2,\cdots d+1,d+2,⋯,问题是容易产生堆积
② 平方探查法, d + 1 2 , d − 1 2 , d + 2 2 , d − 2 2 , ⋯ d+1^2, d-1^2, d+2^2, d-2^2,\cdots d+12,d−12,d+22,d−22,⋯,问题是至少只能探查到一半的单元 - 链地址法
3)计算平均查找长度
- 要求 A S L 成 功 ASL_{成功} ASL成功,看关键字
- 要求 A S L 不 成 功 ASL_{不成功} ASL不成功, 看表中每个可以通过 Hash 函数计算得到的地址(强调,不是表内的所有地址)
4)装填因子
装填因子 a = n m a=\frac{n}{m} a=mn, n n n 为关键字个数, m m m 为表长
5)两个细节问题
-
问链地址法会不会产生堆积现象,答:不会
-
问堆积现象可不可以完全避免,答:不可以
与哈希相关的习题
【习题】
【解析】C,不成功看的是初始位置 0 ~ 6,
A
S
L
不
成
功
=
(
9
+
8
+
7
+
6
+
5
+
4
+
3
)
/
7
=
6
ASL_{不成功}=(9+8+7+6+5+4+3)/7=6
ASL不成功=(9+8+7+6+5+4+3)/7=6
【习题】
【解析】
(1)装填因子为 0.7 0.7 0.7,故表长为 10 10 10,散列后的结果如下所示
(2)查找成功时,是根据每个元素查找次数来计算平均长度,在等概率的情况下,各关键字的查找次数为:
故,查找成功时的平均查找长度为:
A
S
L
成
功
=
(
1
+
1
+
1
+
1
+
3
+
3
+
2
)
/
7
=
12
/
7
ASL_{成功}=(1+1+1+1+3+3+2)/7 = 12/7
ASL成功=(1+1+1+1+3+3+2)/7=12/7
这里要特别防止惯性思维。查找失败时,是根据查找失败位置计算平均次数,根据散列函数 MOD 7,初始只可能在 0 ~ 6 的位置。等概率情况下,查找 0 ~ 6 位置查找失败的查找次数为:
故,查找不成功时的平均查找长度为: A S L 不 成 功 = ( 3 + 2 + 1 + 2 + 1 + 5 + 4 ) / 7 = 18 / 7 ASL_{不成功}=(3+2+1+2+1+5+4)/7 = 18/7 ASL不成功=(3+2+1+2+1+5+4)/7=18/7
(这里给出一个说明,如果哈希表在以顺序表为存储结构的情况,如果空位置作为结束标记,则与空位置的比较次数也要计算在内;在以链表为存储结构的情况下,与空指针的比较次数不计算在内)
【习题】(考虑偏移)
【解析】该题是一般的除留余数法的变形,可以先将关键字散列到
0
∼
9
0\sim9
0∼9 的范围,然后向右移
100
100
100 个单位,即可散列到
100
∼
109
100\sim109
100∼109 内,故
H
(
k
e
y
)
=
k
e
y
M
o
d
p
+
100
H(key)=key\ Mod\ p+100
H(key)=key Mod p+100,因表长为
10
10
10,
p
p
p 取不大于表长的最大素数即
7
7
7,最终结果即为
【习题】
【解析】B,首先,装填因子越大(字多表短),容易发生冲突,① 是不正确的;② 没有太多争议;主要看 ③,堆积现象是没有办法完全避免的,所以 ③ 不够正确
【习题】
【解析】D,同义词即我们认为会发生冲突的关键字,显然第
i
i
i 个同义词存入时要进行
i
i
i 次探测
【习题】(链地址下的分析)
【解析】
a
=
1
2
=
8
m
⇒
m
=
16
a=\frac{1}{2} = \frac{8}{m}\Rightarrow m=16
a=21=m8⇒m=16
A
S
L
成
功
=
(
1
+
1
+
1
+
1
+
2
+
1
+
1
+
2
)
/
8
=
1.25
ASL_{成功}=(1+1+1+1+2+1+1+2)/8=1.25
ASL成功=(1+1+1+1+2+1+1+2)/8=1.25
A
S
L
不
成
功
=
(
1
+
1
+
1
+
+
2
+
1
+
2
)
/
8
=
0.62
ASL_{不成功}=(1+1+1++2+1+2)/8=0.62
ASL不成功=(1+1+1++2+1+2)/8=0.62
【习题】
【解析】表长为
⌈
11
0.75
⌉
=
15
\left \lceil \frac{11}{0.75} \right \rceil=15
⌈0.7511⌉=15
A
S
L
1
=
(
1
+
1
+
2
+
1
+
1
+
1
+
1
+
1
+
2
+
3
+
4
)
/
11
=
18
/
11
ASL_1=(1+1+2+1+1+1+1+1+2+3+4)/11=18/11
ASL1=(1+1+2+1+1+1+1+1+2+3+4)/11=18/11
A S L 2 = ( 1 + 0 + 2 + 1 + 0 + 1 + 1 + 0 + 0 + 0 + 1 + 0 + 4 ) / 13 = 11 / 13 ASL_2=(1+0+2+1+0+1+1+0+0+0+1+0+4)/13=11/13 ASL2=(1+0+2+1+0+1+1+0+0+0+1+0+4)/13=11/13
【习题】
【解析】C
【习题】
【解析】D
对 A,哈希表是一个在时间和空间上做出权衡的经典例子。如果没有内存限制,那么可以直接将键作为数组的索引,那么所有的查找时间复杂度为O(1);如果没有时间限制,那么我们可以使用无序数组并进行顺序查找,这样只需要很少的内存
平衡二叉查找树的效率分析
- 查找代价:查找效率最好、最坏情况都是 O ( l o g n ) O(logn) O(logn) 数量级的
- 插入代价:在执行每个插入操作时最多需要1次旋转,其时间复杂度在 O ( l o g n ) O(logn) O(logn) 左右(插入结点需要首先查找插入的位置)
- 删除代价:每一次删除操作最多需要 O ( l o g n ) O(logn) O(logn) 次旋转,因此,删除操作的时间复杂度为 O ( l o g n ) + O ( l o g n ) = O ( 2 l o g n ) O(logn)+O(logn)=O(2logn) O(logn)+O(logn)=O(2logn),总体上是 O ( l o g n ) O(logn) O(logn)
与B-树和B+树相关的习题
【习题】
【解析】根结点最少可以只有两棵子树
【习题】
【解析】D,除根节点外,每个结点至少含有 ⌈ 4/ 2 ⌉ − 1 = 1 个关键字,那么一个结点一个关键字,此时含关键字的结点数量最多
【习题】
【解析】A,根结点至少包含一个关键字,且至少两个孩子,孩子结点最少有 ⌈ 5/ 2 ⌉ − 1 = 2 个关键字,于是根结点的两个孩子就有 4 个关键字,算上根结点 1 个关键字,故总共至少有 5 个关键字
【习题】
【解析】B,每个非叶结点至少包含一个关键字,也就是有两个子结点,那么高度为 5 的含关键字最少的 B-树,即类似于一棵满二叉树,每个结点一个关键字
【习题】
【解析】D,每个结点至多有 2 个关键字,3 棵子树,因此,关键字至多为 (1+3+9+27+81)×2=242
【习题】
【解析】A,由于B+树的所有叶结点中包含了全部的关键字信息,且叶结点本身依关键字从小到大顺序链接,可以进行顺序查找,而B-树不支持顺序查找(只支持多路查找)
【习题】
【解析】详细过程如下所示
与平衡二叉树相关的习题
【习题】(从 AVL 树结点和高度的关系判断查找序列)
【解析】D,根据 5 层平衡二叉树最少有 12 个结点,则一定最多通过比较 5 次就可以找到关键字,故排除 A、B、C 选项(排除B、C 的原因是第 5 次查找的关键字不是 35,意味着还会有之后的查找,故而查找次数是大于 5 的)
【习题】
【解析】C, 5 层 AVL 树最少有 12 个结点,6 层 AVL 树最少有 20 个结点,因此 15 个结点 AVL 树最大有 5 层,故比较次数最多为 5 次,故排除 D 选项;这题跟上一题一点不同的是,对于接下来的 A、B、C 选项需要考其查询路径是否符合二叉排序树
【习题】
【解析】C,具体分析如下,注意 RL 是对不平衡状态的描述
【习题】
【解析】平衡二叉树首先是二叉排序树。基于二叉排序树,发现树越矮查找效率越高,进而发明了二叉平衡树
【习题】
【解析】C
在节点最少的情况下,左右子树的高度差为1,故 N k = N k − 1 + N k − 2 + 1 N_k=N_{k-1}+N_{k-2}+1 Nk=Nk−1+Nk−2+1,此数列与斐波那契数列 F ( n ) = F ( n − 1 ) + F ( n − 2 ) F(n)=F(n-1)+F(n-2) F(n)=F(n−1)+F(n−2) 相似,由归纳法可得 N k = F ( k + 2 ) − 1 N_k=F(k+2)-1 Nk=F(k+2)−1(斐波那契数列: 1 , 1 , 2 , 3 , 5 , 8 , 13 , ⋯ 1,1,2,3,5, 8,13,\cdots 1,1,2,3,5,8,13,⋯)
【习题】
【解析】B,高度为 6 的平衡二叉树最少有 20 个结点。每个非叶子结点的平衡因子均为 1,即暗示了结点最少这种极端情况,因为增加一个结点可以使得某个结点的平衡因子变为 0,而不会破坏平衡性
【习题】
【解析】D,AVL 树是一棵二叉排序树,中序遍历得到的是降序序列,于是有 v a l 左 > v a l 根 > v a l 右 val_左 > val_根 > val_右 val左>val根>val右 这样的关系式
对 A),只有两个结点的二叉平衡树的根结点的度为 1
对 B)D),中序遍历后可以得到一个降序序列,树中最大元素一定无左子树(可能有右子树),故 B)错 D)对
对 C),最后插入的结点可能会导致平衡调整,而不一定是叶结点
【习题】
【解析】A,不管是删除叶结点或非叶结点再插入不变很容易理解,下面分析一下结构改变了的情况
【习题】
【解析】这里补充说下字典顺序,如果第一个字母相同,将取第二个字母按照字典序比较,故建树过程如下
与二叉排序树相关的习题
【习题】
【解析】
直接在平衡二叉树中查找关键字的话,相当于走了一条从根到叶子结点的路径,时间复杂度为 O ( l o g 2 n ) O(log_2n) O(log2n);在中序遍历输出的序列中查找关键字,其实就相当于在有序表中查找关键字,如果采用顺序查找,时间复杂度为 O ( n ) O(n) O(n),如果采用折半查找,时间复杂度为 O ( l o g 2 n ) O(log_2n) O(log2n)
按序建立的二叉排序树,因为插入元素是有序的,因此所有的插入操作都会发生在最左边的叶子结点的左指针上,或者最右边的叶子结点的右指针上,取决于按递减有序还是递增有序。这样,所形成的二叉排序树就蜕变为了单枝树,折半查找也蜕变为了顺序查找,故其平均查找长度为 ( n + 1 ) / 2 (n+1)/2 (n+1)/2,时间复杂度为 O ( n ) O(n) O(n)
【习题】
【解析】
(1)最小元素必无左子女,最大元素必无右子女,此命题正确
(2)最小元素和最大元素不一定是叶节点,因为具有最小关键字值的结点可以有右子树,具有最大关键字值的结点可以有左子树
(3)一个新元素总是作为叶结点插入到二叉排序树的
【习题】
【解析】CA,同折半查找判定树查找不成功时算的是方框的叶子节点的个数
【习题】
【解析】C,首先需要明确的是,二叉搜索树即二叉排序树
对 A,也可用右子树的最小值结点替代
对 B,对于二叉排序树来说,只要知道前序遍历的结果就可以还原树了,第一个结点是根结点,之后的连续 k 个值小于根结点值的结点为左子树,后面的都是右子树,然后递归构造树
对 D,如果允许额外的存储空间,可先按照 C)选项生成一个排好序的数组,然后不断地找到数组中处于 mid 位置的结点作为根来构造平衡树,此过程的时间复杂度即为线性的;如果不允许使用额外的存储空间,只能靠旋转的话是无法在线性时间内完成操作的
【习题】
【解析】构造出来的二叉树排序树与原来的相同,因为:二叉排序树的前序序列的第一个元素一定是二叉排序树的根,而对应前序序列的根后面的所有元素分为两组:从根的后一元素开始,其值小于根的一组元素就是树的左子树的结点的前序序列,剩下的元素的值大于树的根,,即为树的右子树的结点的前序序列。在把前序序列的元素依次插入初始为空的二叉排序树时,第一个元素就称为树的根,它后面第一组元素的值都小于根结点的值,可以递归建立根的左子树;第二组元素的值都大于根结点的值,可以递归地建立右子树。最后,插入的结果就是一棵与原来二叉排序树相同的二叉排序树
【习题】
【解析】D,具体分析如下
【习题】
【解析】A,根据 A)选项构造的二叉排序树为
【习题】
【解析】C
如果对二叉排序树的构造更加熟悉,可以很容易发现 C)选项中根节点的左右孩子是 60 和 120, 明显不同于其他三个选项
【习题】
【解析】C,根据二叉排序树的定义,可以构造如下的特例
【习题】
【解析】B,如果是最一般最基础的二叉树的话,因为深度不平衡,所以会发展成单链的形状,就是一条线 n 个点那么深,最差情况下就是
O
(
n
)
O(n)
O(n) ,如果是深度平衡的二叉树(即 AVL)插入一个结点的时间复杂度为
O
(
l
o
g
n
)
O(logn)
O(logn)
【习题】
【解析】C,分析如下
与折半查找相关的习题
【习题】
【解析】D,向下取整构造判定树,但是不需要构造全部的判定树
【习题】
【解析】A,折半查找的判定树是一棵二叉排序树,于是看按照比较序列构成的二叉树是否满足二叉排序树的要求,对 A)有
【习题】
【解析】A,折半查找判定树是一棵二叉排序树,它的中序序列是一个有序序列,因此我们可以对四个选项填上相应的元素
然后根据【折半查找】中的理论可知,B)中 { 4 , 5 } \left \{4,5\right \} {4,5} 和 { 7 , 8 } \left \{7,8 \right \} {7,8}取整不统一,C)中 { 3 , 4 } \left \{3,4 \right \} {3,4}和 { 6 , 7 } \left \{6,7\right \} {6,7} 取整不统一,D)中 { 6 , 7 } \left \{6,7\right \} {6,7} 和 { 1 , 10 } \left \{1,10\right \} {1,10} 取整不统一(注意这里 { 1 , 10 } \left \{1,10\right \} {1,10} 不是那么好发现)
【习题】
【解析】B,具有 n n n 个关键字的折半查找的判定树高度为 ⌊ l o g 2 n ⌋ + 1 \left \lfloor log_2n \right \rfloor+1 ⌊log2n⌋+1
【习题】
【解析】C,其判定树的高度,也就是为最坏一次查找时,需要比较的次数,所以为
⌈
l
o
g
2
(
n
+
1
)
⌉
\left \lceil log_2(n+1) \right \rceil
⌈log2(n+1)⌉
【习题】
【解析】A,取关键字为
{
1
,
2
,
3
,
4
,
5
,
6
,
7
,
8
,
9
,
10
,
11
,
12
}
\left\{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12\right\}
{1,2,3,4,5,6,7,8,9,10,11,12} 构建折半查找判定树为
A S L 成 功 = 3 + 4 + 2 + 3 + 4 + 1 + 3 + 4 + 2 + 4 + 3 + 4 12 = 37 12 ASL_{成功}= \frac{3+4+2+3+4+1+3+4+2+4+3+4}{12}=\frac{37}{12} ASL成功=123+4+2+3+4+1+3+4+2+4+3+4=1237
与顺序查找相关的习题
【习题】
【解析】
(1)查找长度不同,如果对一个递减有序的顺序表,只要发现查找字比第一个关键字还要大,那么即可结束扫描
(2)查找成功时,对于两种表都是逐个扫描关键字,直到找到与给定查找字相同的关键字为止,因此平均查找长度都是 n ( n + 1 ) 2 \frac{n(n+1)}{2} 2n(n+1)
(3)在有序的顺序表中相同关键字的位置一定是相邻的,而在无序表中则有可能相邻也有可能不相邻
【习题】
【解析】注意之前我们讨论的查找长度都是在等概率的情况下,这道题不再是了
(1)采用顺序存储结构时,元素按照查找概率降序排列,可使得平均查找长度更短。采用顺序查找法,查找成功时的平均长度为 0.35 × 1 + 0.35 × 2 + 0.15 × 3 + 0.15 × 4 = 2.1 0.35\times1+0.35\times2+0.15\times3+0.15\times4=2.1 0.35×1+0.35×2+0.15×3+0.15×4=2.1
(2)答案一:采用链式存储结构时,元素还是按照查找概率降序排列,此时构成单链表,情况同 1)
答案二:采用二叉链表存储结构时,按照字典序构造二叉排序树,结果如下:
采用二叉排序树的查找方式,查找成功时的平均长度为
0.15
×
1
+
0.35
×
2
+
0.35
×
2
+
0.15
×
3
=
2
0.15\times1+0.35\times2+0.35\times2+0.15\times3=2
0.15×1+0.35×2+0.35×2+0.15×3=2
4 串
【KMP】
1)KMP 的不同之处: 当匹配过程中产生失配时,指针 i i i 不变,指针 j j j 退回到 n e x t [ j ] next[j] next[j] 所指示的位置上重新进行比较,并且当指针 j j j 退至 0 时,指针 i i i 和指针 j j j 需要同时自增 1,即若主串的第 i i i 个字符和模式的第 1 个字符不等,应从主串的第 i + 1 i+1 i+1 个字符重新进行匹配
2)求 next 数组
3)求 nextval 数组
【习题】
【解析】C,注意一下,第一次出现 “ 失配 ”( s [ i ] ≠ t [ j ] s[i]\neq t[j] s[i]=t[j])时, i = j = 5 i=j=5 i=j=5,此时题中主串和模式串的位序都是从 0 开始的(要灵活应变),于是,此时的 next 数组为
于是下一次开始匹配时,
i
=
5
,
j
=
2
i = 5,j = 2
i=5,j=2
【习题】
【解析】B
假设位序都是从 0 开始的,按照 next 数组生成算法,对 S 有
根据 KMP 算法,第一趟连续 6 次比较,在模式串的 5 号位和主串的 5 号位匹配失败,模式串的下一个比较位置为 next[5] ,即下一次比较从模式串的 2 号位和主串的 5 号位开始,然后直到模式串 5 号位和主串的 8 号位匹配,第二趟比较 4 次,模式串匹配成功,单个字符的比较次数为 10 次
【习题】
【解析】A
5 数组、矩阵与广义表
【二维数组的行优先和列优先】
【稀疏矩阵】
1)定义
稀疏矩阵中的相同元素 c c c 在矩阵中的分布不像在特殊矩阵(如对称矩阵、上三角、下三角)中那么有规律可循
常用的稀疏矩阵顺序存储方法有三元组表示法和伪地址表示法
常用的稀疏矩阵链式存储方法有邻接表表示法和十字链表表示法
2)三元组表示法
3)伪地址表示法
4)邻接表表示法
5)十字链表表示法
【习题】
【解析】C
【习题】
【解析】A,具体分析如下
【习题】
【解析】B
第 i i i 行之前的元素个数为 2 + ( i − 2 ) × 3 2+(i-2)\times3 2+(i−2)×3, a i , i − 1 a_{i,i-1} ai,i−1 在一维数组中的下标即为 2 + ( i − 2 ) × 3 2+(i-2)\times3 2+(i−2)×3
于是, a 30 , 29 a_{30,29} a30,29 在一维数组中的下标为 2 + ( 30 − 2 ) × 3 = 86 2+(30-2)\times3=86 2+(30−2)×3=86, a 30 , 30 a_{30,30} a30,30 的下标即为 87 87 87
【习题】
【解析】A,按照下图的方式推导下去,
m
6
,
6
,
m_{6,6,}
m6,6, 前面有
12
+
11
+
10
+
9
+
8
=
50
12+11+10+9+8 = 50
12+11+10+9+8=50 个元素
【习题】
【解析】
A [ ] [ ] A[][] A[][] 按行优先存储,那么,对于 A [ 3 ] [ 5 ] A[3][5] A[3][5] 前面的 3 3 3 行全部存满,于是有 3 × 10 = 30 3\times10=30 3×10=30 个元素,再加上 A [ 3 ] A[3] A[3] 这一行在 A [ 3 ] [ 5 ] A[3][5] A[3][5] 前面有 5 5 5 个元素,一共在 A [ 3 ] [ 5 ] A[3][5] A[3][5] 前面有 35 35 35 个元素,设初始地址为 x x x,有 x + 35 × 4 = 1000 ⇒ x = 860 x+35\times4=1000\Rightarrow x=860 x+35×4=1000⇒x=860
【习题】
【解析】