【有向完全图和无向完全图】
若有向图中有 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_i vi 建立一个链接以 v i v_i vi 为头的弧的表
【十字邻接表】(对有向图)
【邻接多重表】
顶点表由两个域组成,vertex 域存储和该顶点先关的信息,firstedge 域指示第一条依附于该顶点的边
边表结点由 6 个域组成,mark 为标记域,可以用于标记该边是否被搜索过,ivex 和 jvex 为该边依附于的两个顶点的编号,ilink 指向下一条依附于顶点 ivex 的边,jlink 指向下一条依附于顶点 vjex 的边
【图的两种遍历的适用范围】
DFS、BFS 对任何图都适用,并没有限制是针对有向图还是无向图
【图遍历与二叉树遍历的联系】
DFS 相当于二叉树中的先序遍历,BFS 相当于二叉树中的层次遍历
【关于一些图算法的时间复杂度】
【最小生成树】
1)最小生成树
含有 n n n 个顶点的单权连通图的最小生成树是指图中任意一个由 n n n 个顶点构成的边的权值之和最小的连通子图
何时最小生成树唯一? 当带权连通图的任意一个环中所包含的边的权值均不相同时,该图的最小生成树唯一
2)普利姆算法
普利姆算法思想: 从图中任意取出一个顶点,把它当成一棵树,然后从与这棵树相连的边中选择一条最短的边,并将这条边及其所连的顶点并入树中
普利姆算法执行过程:
复杂度分析: 普里姆算法的复杂度为 O ( n 2 ) \mathcal O(n^2) O(n2),只与图中的顶点数有关,与边数没有关系,所以适合稠密图
注意,普里姆算法中边上的权可正可负
3)克鲁斯卡尔算法
克鲁斯卡尔算法思想: 每次找出候选边中权值最小的边,就将该边并入生成树中
克鲁斯卡尔算法执行过程:
并查集:
判断是否产生回路要用到并查集。并查集中保存了一棵或者几棵树,这些树有这样的特点:通过树中一个结点,可以找到其双亲结点,进而找到根结点。这种特性有两个好处:
复杂度分析: 克鲁斯卡尔算法时间主要花费在对边的排序上,所以时间复杂度与边数有关,适合稀疏图
【迪杰斯特拉算法】
求图中某一顶点到其余各顶点的最短路径
三个数组:
执行过程:
考虑如下的例子,
p a t h [ ] path[] path[] 中其实保存了一棵树,这是一棵用双亲存储结构存储的树,通过这棵树可以打印出从源点到任何一个顶点的最短路径。但是,注意,在双亲存储结构中只能输出从叶子结点到根结点,所以在利用 p a t h [ ] path[] path[] 打印路径的时候需要借助栈实现逆向输出
复杂度分析: 迪杰斯特拉算法的时间复杂度为 O ( n 2 ) \mathcal O(n^2) O(n2)
【弗洛伊德算法】
求图中任意一对顶点间的最短路径
两个矩阵:
执行过程:
考虑下面的例子,
复杂度分析: 弗洛伊德算法的时间复杂度为 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 的最迟发生时间
算法流程:
活动的剩余时间等于活动的最迟发生时间减去活动的最早发生时间,即 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 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 间的最短距离
【习题】
【各排序方法总结】
排序方式 | 平均情况 | 最坏情况 | 最好情况 | 空间复杂度 |
---|---|---|---|---|
插入排序 | 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) |
细节上的问题:
【一些总结】
【直接插入】
【折半插入】
折半插入适合关键字数较多的场景,与直接插入相比,折半插入在查找插入关键位置上面所花费的时间更少,折半插入在关键字移动次数方面和直接插入是一样的
折半插入排序的关键字比较次数和初始序列无关,因为每趟排序折半查找插入位置时,折半次数是一定的,折半一次就要比较一次,所以比较次数一定
【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 中的位置,具体如下所示
设数组中下标为 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 个元素),具体可以参考下面的例子
关于希尔排序的增量选取有两个需要注意的地方:
【冒泡排序】
【快速排序】
【简单选择】
无论记录的初始排列如何,所需进行的关键字间的比较次数相同,均为 n ( n − 1 ) 2 \frac{n(n-1)}{2} 2n(n−1)
【堆排序(以大顶堆为例)】
1)插入结点
需要在插入结点后保持堆的特性,因此需要先将要插入的结点 x 放在最底层的最右边,插入后满足完全二叉树的特点:然后把 x 依次向上调整到合适的位置以满足父大子小的性质
2)删除结点
当删除堆中的一个结点时,原来的位置会出现一个小孔,填充这个孔的办法是:把最底层最右边的叶子结点的值赋给该孔并下调到合适位置,最后把该叶子结点删除
3)建堆
注意一点:调整堆从无序序列所确定的完全二叉树的第一个非叶子结点开始,从右至左,从下至上
4)堆排序
【归并排序】
【基数排序】
基数排序的时间复杂度为 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))
【外部排序】
外部排序指待排文件较大,内存一次性放不下,需存放在外部介质中。外部排序通常采用归并排序法
【置换-选择排序】
采用置换-选择排序算法构造初始归并段的过程:
根据缓冲区大小,由外存读入记录,当记录充满缓冲区后,选择最小的(假设升序排序)写回外存,其空缺位置由下一个读入记录来取代,输出的记录称为当前初始归并段的一部分。如果新输入的记录不能成为当前生成的归并段的一部分,即它比生成的当前归并段中最大的记录要小,则它将生成其他初始归并段的选择。重复上述过程,直到缓冲区中的所有记录都比当前初始归并段最大的记录小时,就生成了一个初始归并段
【最佳归并树】
归并过程可以用一棵树来形象第描述,这棵树就称为归并树,树中结点代表当前归并段长度。初始记录经过置换-排序后,得到的是长度不等的初始归并段,归并策略不同,所得的归并树也不同,树的带权路径长度也不同 (带权路径长度与 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 次即可,因此在归并排序中选最值那一步常用败者树来完成
败者树中两种不同类型的结点:
2)建立败者树(以最小值败者树为例)
3)调整败者树
【外部排序的时间与空间复杂度问题】
需要注意的是, 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
这道题如果按照快排一趟有一个元素就位的想法分析的话会发现四个选项均有两个元素就位,显然仅想到这一步是不够的
对于快排,第一趟划分为两个子序列后,第二趟要对这两个子序列都完成划分才算第二趟结束,即正常情况下第二趟会有 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}
【习题】
【习题】
【习题】
【解析】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
【习题】
【习题】
【解析】A,注意这里的归并排序是指外部排序中的归并,将记录读入内存是在生成初始归并段之后的事情
【习题】
设需要补充的虚段个数为 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
【习题】
【解析】其思想和最佳归并树的思想是一样的
【 顺序查找】
对于顺序查找,假设每个元素等概率被查找,于是,查找成功情况下的平均查找长度 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
【折半查找】
描述折半查找的判定树
折半查找的过程可以用二叉树来表示。把当前查找区间中的中间位置上的记录作为树根,左子表和右子表中的记录分别作为根的左子树和右子树,由此即可得到描述折半查找的判定树。折半查找的比较次数即为从根结点到待查找元素所经过的结点数,因此,算法的时间复杂度可以用树的高度来表示,具有 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 种情况
【平衡二叉树】
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,表示新插入结点落在最小不平衡子树根节点的左孩子的左子树上
下面我们给出一个平衡二叉树建立的过程,在其中穿插解释各平衡调整方式
1)B-树的定义与性质
B-树是 m m m 叉平衡查找树,但是限制更强,要求所有叶结点在同一层
2)关键字的插入
插入位置一定出现在终端结点上,然后直接插入。插入后检查被插入结点内的关键字的个数,如果大于 m − 1 m-1 m−1,则需进行拆分。注意,插入操作只会使得 B-树逐渐变高而不会改变叶子结点在同一层的特性(下面以 5 阶 B-树为例,关键字个数的范围为 2 ~ 4)
3)结点的删除
(接着以上面构造的 5 阶 B-树为例)
B+树是应文件系统所需而产生的 B-树的变形,前者比后者更加适用于实际应用中的操作系统的文件索引和数据库索引,因为前者磁盘读写代价更低、查询效率更加稳定
【哈希】
1)常用 Hash 函数的构造方法
2)冲突处理方法
3)计算平均查找长度
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
【习题】
【习题】
对 A,哈希表是一个在时间和空间上做出权衡的经典例子。如果没有内存限制,那么可以直接将键作为数组的索引,那么所有的查找时间复杂度为O(1);如果没有时间限制,那么我们可以使用无序数组并进行顺序查找,这样只需要很少的内存
平衡二叉查找树的效率分析
【习题】
【习题】
【解析】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-树不支持顺序查找(只支持多路查找)
【解析】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 选项需要考其查询路径是否符合二叉排序树
【习题】
【习题】
【解析】平衡二叉树首先是二叉排序树。基于二叉排序树,发现树越矮查找效率越高,进而发明了二叉平衡树
【习题】
在节点最少的情况下,左右子树的高度差为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,同折半查找判定树查找不成功时算的是方框的叶子节点的个数
【习题】
对 A,也可用右子树的最小值结点替代
对 B,对于二叉排序树来说,只要知道前序遍历的结果就可以还原树了,第一个结点是根结点,之后的连续 k 个值小于根结点值的结点为左子树,后面的都是右子树,然后递归构造树
对 D,如果允许额外的存储空间,可先按照 C)选项生成一个排好序的数组,然后不断地找到数组中处于 mid 位置的结点作为根来构造平衡树,此过程的时间复杂度即为线性的;如果不允许使用额外的存储空间,只能靠旋转的话是无法在线性时间内完成操作的
【习题】
【解析】构造出来的二叉树排序树与原来的相同,因为:二叉排序树的前序序列的第一个元素一定是二叉排序树的根,而对应前序序列的根后面的所有元素分为两组:从根的后一元素开始,其值小于根的一组元素就是树的左子树的结点的前序序列,剩下的元素的值大于树的根,,即为树的右子树的结点的前序序列。在把前序序列的元素依次插入初始为空的二叉排序树时,第一个元素就称为树的根,它后面第一组元素的值都小于根结点的值,可以递归建立根的左子树;第二组元素的值都大于根结点的值,可以递归地建立右子树。最后,插入的结果就是一棵与原来二叉排序树相同的二叉排序树
【习题】
【解析】A,根据 A)选项构造的二叉排序树为
如果对二叉排序树的构造更加熟悉,可以很容易发现 C)选项中根节点的左右孩子是 60 和 120, 明显不同于其他三个选项
【习题】
【解析】C,根据二叉排序树的定义,可以构造如下的特例
【习题】
【解析】B,如果是最一般最基础的二叉树的话,因为深度不平衡,所以会发展成单链的形状,就是一条线 n 个点那么深,最差情况下就是 O ( n ) O(n) O(n) ,如果是深度平衡的二叉树(即 AVL)插入一个结点的时间复杂度为 O ( l o g n ) O(logn) O(logn)
【习题】
【解析】C,分析如下
【习题】
【习题】
【解析】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
【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
【习题】
假设位序都是从 0 开始的,按照 next 数组生成算法,对 S 有
根据 KMP 算法,第一趟连续 6 次比较,在模式串的 5 号位和主串的 5 号位匹配失败,模式串的下一个比较位置为 next[5] ,即下一次比较从模式串的 2 号位和主串的 5 号位开始,然后直到模式串 5 号位和主串的 8 号位匹配,第二趟比较 4 次,模式串匹配成功,单个字符的比较次数为 10 次
【二维数组的行优先和列优先】
【稀疏矩阵】
1)定义
稀疏矩阵中的相同元素 c c c 在矩阵中的分布不像在特殊矩阵(如对称矩阵、上三角、下三角)中那么有规律可循
常用的稀疏矩阵顺序存储方法有三元组表示法和伪地址表示法
常用的稀疏矩阵链式存储方法有邻接表表示法和十字链表表示法
2)三元组表示法
5)十字链表表示法
【习题】
【解析】A,具体分析如下
【习题】
第 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
【习题】