9.27 | 预:8.1 排序概念 + 8.2 插入排序(直接、折半、希尔) + 8.3 交换排序(冒泡、快速) |
---|---|
9.29 | 预:8.4 选择排序(简单、堆) + 8.5 (未完成)+ 笔记整理 |
10.1 | 预:8.6 + 错题整理 |
10.2 | 预:8.2-8.3的错题 |
10.3 | 预:8.4-8.5错题整理 |
10.4 | 预:8.6 错题整理 |
summary 第七章
- 顺序查找 折半查找 分块查找 以及利用判定树计算对应的ASL(成功 or 失败)
- 二叉排序树/ 二叉搜索树 已知n个结点 有多少种树形=>卡特兰数
- AVL树 构建/插入、删除操作 ASL的计算 和折半查找判定树类似
- 红黑树 不一定是AVL树,他是颜色平衡,插入操作,ASL的计算
- B树 构建插入删除操作 不要和分块查找混淆
- B+树概念和B树的差异
- 散列函数 主开放定址法 解决冲突的方法:线性探测 平方探测 双散列探测
填装因子α,ASL的计算
应用题较多,核心是ASL的计算
第八章 排序
堆排 快排 归排 为重难点
对每种排序方法掌握思想、排序的过程(手动模拟,笔记整理)和特征(初态的影响、复杂度、稳定性、适用性等)
常用排序需掌握关键代码并熟练编写
看到某特定序列,需有选择最优排序算法的能力=>排序算法的特征
本章对概念性内容多为填空,问答多记录排序的思想和时间复杂度分析等,可能会将一些不重要的点省略,不一定会完全按照书上的内容
另一部分 手动排序内容均为笔记的形式上传
考纲里目前没有外部排序,8.7不整理
计数排序不用背,去看视频知道是什么就好
文字内容
问
- 请说明排序算法的稳定性是如何判定的。
- 插入排序的基本思想。
- 直接插入排序的基本思想。
- 直接插入排序的时间复杂度分析。
- 折半插入排序的基本思想。
- 折半插入排序的时间复杂度分析。
- 直接插入排序和折半插入排序的比较。
- 希尔排序的基本思想。
- 希尔排序的时间复杂度分析。
- 冒泡排序的基本思想。
- 冒泡排序的时间复杂度分析。
- 快速排序的基本思想。
- 快速排序的时间复杂度分析。
- 选择排序的基本思想。
- 简单选择排序的基本思想。
- 简单选择排序的时间复杂度分析。
- 堆排序的基本思想。
- 如何创建初始堆。
- 输出堆顶元素后,如何调整堆。
- 描述堆的插入操作过程。不要和调整堆操作搞混
- 堆排序的时间复杂度分析。
- 归并排序的基本思想。
- 归并排序的时间复杂度分析。
- 基数排序的基本思想。
- 基数排序的时间复杂度分析。
- 当待排序元素个数较小时,采用哪种排序算法,为什么?
- 当待排序元素个数较大时,采用哪种排序算法,为什么?
答
- 若待排序表中有两个元素
R
i
R_i
Ri和
R
j
R_j
Rj,其对应的关键字相同,即
k
e
y
i
=
k
e
y
j
key_i=key_j
keyi=keyj,且在排序前
R
i
R_i
Ri在
R
j
R_j
Rj的前面,若使用某一排序算法排序后,
R
i
R_i
Ri仍在
R
j
R_j
Rj的前面,则称这个排序算法是稳定的,否则称这个排序算法是不稳定的。
算法的稳定性是对算法的性质描述,不能衡量一个算法的优劣,若待排序表中的关键字不允许重复,排序结果是唯一的,则对于排序算法的选择,稳定与否无关紧要。
-
每次将一个待排序的记录按其关键字大小插入前面已排好序的子序列,直到全部记录插入完成。
-
直接插入排序的基本思想是:将序列分为已排序部分和未排序部分,从未排序部分依次取出元素,并将其插入到已排序部分的适当位置,使整个序列保持有序。具体做法是,从第二个元素开始,每次将当前元素与前面已排好序的部分进行比较,找到合适的位置插入,直到所有元素都插入完成。
-
最坏情况:当输入序列是完全逆序时,每个元素都需要与前面的所有元素进行比较和移动,时间复杂度为 O ( n 2 ) O(n^2) O(n2);最好情况:当输入序列是完全有序时,不需要移动元素,每个元素只进行一次比较,时间复杂度为 O ( n ) O(n) O(n);一般情况:随机顺序的情况下,平均需要进行较多的比较和移动操作,时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
-
将序列分为已排序部分和未排序部分,从未排序部分依次取出元素,并通过折半查找(即二分查找)找到它在已排序部分的适当位置,然后将其插入到该位置,使整个序列保持有序。具体做法是,从第二个元素开始,每次使用折半查找在前面已排好序的部分中寻找插入位置,然后将当前元素插入到该位置,直到所有元素都插入完成。折半查找减少了比较次数,但元素的移动操作与直接插入排序相同。
6 插入排序的核心问题是元素的移动。
-
最坏情况:当输入序列是完全逆序时,虽然折半查找可以将查找插入位置的时间复杂度为 O ( l o g n ) O(logn) O(logn),但元素的移动操作依旧需要 O ( n ) O(n) O(n)次,因此时间复杂度仍为 O ( n 2 ) O(n^2) O(n2);最好情况:当输入序列是完全有序时,不需要移动元素,时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn);一般情况:随机顺序的情况下,元素的移动次数没有减少,时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
-
折半插入排序仅减少了比较元素的次数,时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),该比较次数与待排序表的初始状态无关,仅取决于表中元素个数n;元素的移动次数并未改变,它依赖于待排序表的初始状态。因此折半插入排序的时间复杂度仍为 O ( n 2 ) O(n^2) O(n2),但对于
折半插入排序仅适用于顺序存储的线性表。 -
先将待排序表分割成若干形如L[i,i+d,i+2d,…,i+kd]的特殊子表,即把相隔某个增量的记录组成一个子表,对各个子表分别进行直接插入排序,当整个表中的元素已呈基本有序时,再对全体记录进行依次直接插入排序。
-
最坏情况,当输入序列是逆序时,尽管希尔排序通过逐步减少gap来优化排序过程,但当gap缩小到1时,它退化为普通的插入排序,时间复杂度 O ( n 2 ) O(n^2) O(n2);
最好情况,输入序列有序,在这种情况下每次递减gap,所需的比较和移动次数都非常少,且每次分组排序的代价较低,时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn);
对于一般情况下,希尔排序的平均时间复杂度介于 O ( n l o g n ) O(nlogn) O(nlogn)和 O ( n 2 ) O(n^2) O(n2)之间,时间复杂度为 O ( n 1.5 ) O(n^{1.5}) O(n1.5)。
-
冒泡排序的基本思想是:通过多次遍历序列,每次比较相邻的两个元素,如果它们的顺序不对就交换位置,较大的元素逐步"冒泡"到序列的末尾。经过多轮遍历,最终所有元素都会按顺序排列。
-
最坏的情况,输入序列为完全逆序,每一趟遍历中,所有相邻的元素都会进行比较和交换,因此每次都需要进行最大次数的比较:第一次n-1次比较,第二次n-2次比较,依此类推,总共比较次数为(1+n-1)*(n-1)/2=n(n-1)/2,因此最坏情况的时间复杂度为 O ( n 2 ) O(n^2) O(n2);
最好的情况,输入序列有序,冒泡排序依次比较相邻元素,但由于每次都不需要交换,只需要一趟遍历来确认序列是有序的,bubbleSort()每次遍历后检查是否发生了交换,如果没有交换,说明序列已经有序,可以提前终止排序,因此最好情况下只需要n-1次比较,即时间复杂度为 O ( n ) O(n) O(n);
平均时间复杂度为 O ( n 2 ) O(n^2) O(n2)。 -
快速排序的基本思想是:选择一个基准元素(通常是第一个元素或随机选取),将序列划分为两部分,使得左边部分的元素都小于基准,右边部分的元素都大于基准。然后递归地对左右两部分继续进行相同的操作,最终将整个序列排序。
快排是所有内部排序算法中平均性能最优的排序算法。
- 最坏情况发生在每次划分时pivot元素将序列分成极度不平衡的两部分,例如当基准元素总是选择到当前序列的最小或最大值时,导致划分出的子序列长度为n-1和1.此时,递归深度达到n,每次划分都需要进行n次比较,比较次数总和为n(n-1)/2,时间复杂度为
O
(
n
2
)
O(n^2)
O(n2);
最好情况下,每次划分pivot元素都恰好将序列分为两部分,且两部分的大小大致相等,则每次递归都会将问题规模减半,递归深度为 O ( l o g n ) O(logn) O(logn),即每次划分时,左右子序列的大小为n/2,每次划分需要 O ( n ) O(n) O(n)次比较,总的比较次数为 n + n 2 + n 4 + . . . = O ( n l o g n ) n+\frac{n}{2}+\frac{n}{4}+... = O(nlogn) n+2n+4n+...=O(nlogn),时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)。
平均时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)。
- 每一趟(第 i i i趟)在后面 n − i + 1 n-i+1 n−i+1个待排序元素中选取关键字最小的元素,作为有序子序列的第 i i i个元素,直到第 n − 1 n-1 n−1趟做完,待排序元素只剩下一个,就不用再选了。
- 假设排序表 L [ 1... N ] L[1...N] L[1...N],第 i i i趟排序即从 L [ i . . . n ] L[i...n] L[i...n]中选择关键字最小的元素与 L ( i ) L(i) L(i)交换,每趟排序可以确定一个元素的最终位置,这样经过 n − 1 n-1 n−1趟排序就可使得整个排序表有序。
- 简单选择排序在最好、最坏和平均情况下的时间复杂度是相同的,均为
O
(
n
2
)
O(n^2)
O(n2)。
每一轮排序中,第 i i i轮需要对剩下的 n − i n-i n−i个元素进行比较,因此需要 n − i n-i n−i次比较操作,整个比较次数为 ( n − 1 ) + ( n − 2 ) + . . . + 1 = n ( n − 1 ) 2 (n-1)+(n-2)+...+1=\frac{n(n-1)}{2} (n−1)+(n−2)+...+1=2n(n−1),因此,比较操作的总时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
简单选择排序每一轮只进行依次交换操作,因此交换次数最多为 n − 1 n-1 n−1次,时间复杂度为 O ( n ) O(n) O(n)。
但总体复杂度依然由比较次数主导,仍未 O ( n 2 ) O(n^2) O(n2)。 - 首先将存放在 L [ 1... n ] L[1...n] L[1...n]中的 n n n个元素建成初始堆,以大根堆为例,所以堆顶元素为最大值。输出堆顶元素后,通常将堆底元素送入堆顶,此时根结点已不满足大堆顶的性质,堆被破坏,将堆顶元素向下调整使其继续保持大顶堆的性质,再输出堆顶元素。如此重复,直到堆中仅剩一个元素为止。
- 创建初始堆的过程是将无序数组转换成一个大根堆or小根堆,具体步骤如下:
n个结点的完全二叉树,最后一个结点是第 ⌊ n 2 ⌋ \lfloor \frac{n}{2}\rfloor ⌊2n⌋个结点的孩子。对以第 ⌊ n 2 ⌋ \lfloor \frac{n}{2}\rfloor ⌊2n⌋个结点为根的子树筛选,对于大根堆,若根节点的关键字小于左右孩子中关键字较大者,则交换,使该子树成为堆。
之后向前依次对以各结点( ⌊ n 2 ⌋ \lfloor \frac{n}{2}\rfloor ⌊2n⌋-1~1)为根的子树进行筛选,看该结点值是否大于其左右子节点的值,若不大于,则将左右子节点中的较大值与之交换,交换后可能会破坏下一级的堆,于是采用上述方法构造下一级的堆,直到以该结点为根的子树构成堆为止。
反复利用上述调整堆的方法建堆,直到根结点。 - 输出堆顶元素后,将堆的最后一个元素与堆顶元素交换,此时堆的性质被破坏,需要向下进行筛选,具体做法是比较该元素与它左右子节点,选择较大的子节点与之交换,确保堆的结构正确。
- 对堆进行插入操作时,先将新结点放在堆的末端,再对这个新结点向上执行调整操作。
21 不用全记,只需要记关键的堆创建、堆调整、堆排序的时间复杂度就好,剩余理解即可。
- 堆排序主要包含两个步骤:构建初始堆,排序过程。具体时间复杂度分析如下:
初始堆的创建是从最后一个非叶结点开始,向上依次进行堆化调整,非叶子节点的数量为 n 2 \frac{n}{2} 2n,在堆化过程中,调节每个节点的时间取决于该结点的高度。
结点高度为 h h h,堆化调整的时间复杂度为 O ( h ) O(h) O(h),而高度为h的结点数量约为 2 h 2^h 2h。初始堆的构建过程总时间复杂度为 O ( n ) O(n) O(n),尽管堆化单个结点的复杂度为 O ( l o g n ) O(logn) O(logn),但总的构建过程由于是从下向上的,因此复杂度降低为O(n)。
构建好初始堆后,输出堆顶元素并将其与堆的最后一个元素交换,从堆中移除该元素。对新的堆顶元素进行堆化调整,时间复杂度为 O ( l o g n ) O(logn) O(logn),整个堆的大小逐次减少,每次都需要做一次堆化调整,共进行 n − 1 n-1 n−1次,因此这一过程的总时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)。
因此堆排序的整体时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)。
22 23 这里讲的都是二路归并排序,k路归并排序算法基本一致要跑 ⌈ l o g k N ⌉ \lceil log_kN \rceil ⌈logkN⌉趟,N为元素个数。
- 归并排序是一种分治算法,其核心思想是将数组不断地划分成两个子数组,分别对每个子数组进行排序,然后通过合并有序子数组来获得排序结果。具体过程如下:
首先,将待排序数组从中间划分为两半,直到每个子数组只有一个元素为止。
然后,递归地对划分后的子数组进行排序。
最后,合并两个已排序的子数组,合并过程将两个有序数组按照顺序合并成一个大的有序数组。 - 归并排序的过程分为分解和合并。
分解数组的过程是将数组不断对半分,每次递归将数组的规模缩小一般,直到数组长度为1,这个分解过程的时间复杂度为 O ( l o g n ) O(logn) O(logn);
在每一层递归中,合并两个子数组的过程需要遍历所有元素,因此合并每一层所需的时间是 O ( n ) O(n) O(n)。一共需要进行logn层合并。
因此,归并排序的总时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)。
如果一个桶中有多个元素,则按照先后顺序进行收集,类似于队列。
- 基数排序不基于比较和移动进行排序,而基于关键字各位的大小进行排序。
具体过程包含两个核心步骤:分配和收集。
分配:对线性表的每个元素,根据当前要处理的位的关键字,将元素分配到对应的桶/队列中。桶的数量取决于关键字可能的取值范围。LSD是从个位开始分配。
收集:将分配到各个桶中的元素按桶的顺序依次收集,形成一个新的线性表。
重复上述分配和收集的过程,依次处理更高位的关键字。所有位的排序都完成后,整个序列就是有序的。 - 分配操作中,需要对所有元素的当前位进行分配,每次都需要遍历n个元素;收集操作,需要将元素从各个桶中收集回来,如果使用r个桶,手机操作需要将这r个桶中的元素依次合并。
基数排序需要进行d轮排序,其中d为元素的最大位数,在每一轮中,需要分配和收集操作,因此总的时间复杂度为 O ( d ( n + r ) ) O(d(n+r)) O(d(n+r))。
- 若n较小,可采用直接插入排序或简单选择排序。由于直接插入排序所需的记录移动过次数较简单选择排序的多,因此当记录本身信息量较大时,用简单选择排序较好。
- 若n较大,应采用时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)的排序算法:快速排序、堆排序和归并排序。当待排序的关键字随机分布时,快速排序被认为是目前基于比较的内部排序算法中最好的算法。堆排序所需的辅助空间少于快速排序,且不会出现快速排序可能的最坏情况,这两种排序都是不稳定的。若要求稳定且时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),可选用直接插入或冒泡排序为宜。
挖
- 在排序过程中,根据数据元素【】,可将排序算法分为两类:【】和【】。
- 内部排序是指在排序期间元素【】的排序,在执行过程中都要进行【】和【】操作。
- 外部排序是指在排序期间无法【】,必须在排序的过程中根据要求【】。
- 内部排序包含【】、【】、【】、【】和【】等,【】不基于比较操作。
- 内部排序算法的性能取决于算法的【】和【】,而前者一般是由【】决定的。
- 插入排序可以引申出三个重要的排序算法:【】、【】和【】。
- 直接插入排序算法的空间复杂度【】,时间复杂度【】,稳定性【】,适合用于【】的线性表。
- 折半插入排序算法的空间复杂度【】,时间复杂度【】,稳定性【】,适合用于【】的线性表。
- 希尔排序又称【】,空间复杂度【】,时间复杂度【】,稳定性【】,适合用于【】的线性表。
- 交换排序是一种通过比较序列中两个元素的关键字,确定是否需要交换它们位置的排序方法,常见的交换排序算法包括【】和【】等。
- 冒泡排序算法的空间复杂度【】,时间复杂度【】,稳定性【】,适合用于【】的线性表。
- 冒泡排序中所产生的有序子序列一定是【】,不同于直接插入排序,也就是说,有序子序列中的所有元素的关键一定【】无序子序列中的所有元素的关键字,这样每趟排序都会将一个元素放置到其最终位置上,之后都不会改变。
- 快速排序的性能主要取决于【】。
- 快速排序算法的空间复杂度【】,时间复杂度【】,稳定性【】,适合用于【】的线性表。
- 简单选择排序算法的空间复杂度【】,时间复杂度【】,稳定性【】,适合用于【】的线性表,以及【】的情况。
- 堆是一种【】,并且满足一下性质:大根堆,对于堆中每个结点,值总是【】;小根堆,对于堆中每个结点,值总是【】。
- 堆的典型应用是用于快速查找【】,其中大根堆适合找【】,小根堆适合找【】。
- 堆排序算法的空间复杂度【】,时间复杂度【】,稳定性【】,适合用于【】的线性表,以及【】的情况。
- 归并排序算法的空间复杂度【】,时间复杂度【】,稳定性【】,适合用于【】的线性表。
- 为实现多关键字排序,通常由两种方式:MSD【】和LSD【】,这两种基数排序方法本质上并不会直接导致排序结果的递减或递增,前者适合【】的情况,后者适合【】的情况。
- 基数排序算法的空间复杂度【】,时间复杂度【】,稳定性【】,适合用于【】的线性表。
排序算法与序列的初始状态无关的意思是,无论待排序的序列是有序、部分有序还是完全无序,该排序算法的时间复杂度都不会受到输入序列的初始排列顺序的影响。换句话说,排序的效率仅依赖于输入序列的长度或其他固定因素,而不是输入数据的排列状态。
- 平均情况下,时间复杂为 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 n ) O(nlogn) O(nlogn)的稳定排序算法只有【】。
- 从适用性来看,【】、【】、【】和【】适用于顺序存储;【】、【】、【】、【】和【】既适用于顺序存储,又适用于链式存储。
- 从过程特征来看,采用不同的排序算法,在一趟或者几趟处理后的排序结果通常不同。【】、【】和【】在每趟处理后都能产生当前的最大值或最小值,而【】一趟处理至少能确定一个元素的最终位置等。
- 选择排序算法时需要考虑的因素:【】、【】、【】、【】、【】。
- 若文件的初始状态已按关键字基本有序,则选用【】或【】为宜。
- 若待排序元素很大,记录的关键字位数较少且可以分解时,采用【】较好。
- 当记录本身信息量较大时,为避免耗费大量时间【】,可采用【】作为存储结构。
空
- 是否完全存放在内存中、内部排序、外部排序
- 全部存放在内存中、比较、移动
- 全部同时存放在内存中、不断地在内、外存之间移动的排序
- 插入排序、交换排序、选择排序、归并排序、基数排序、基数排序
- 时间复杂度、空间复杂度、比较和移动的次数
- 直接插入排序、折半插入排序、希尔排序
- O ( 1 ) O(1) O(1)、 O ( n 2 ) O(n^2) O(n2)、稳定、基本有序的和数据量不大
- O ( 1 ) O(1) O(1)、 O ( n 2 ) O(n^2) O(n2)、稳定 、基本有序的和数据量不大
- 缩小增量排序、 O ( 1 ) O(1) O(1)、 O ( n 2 ) O(n^2) O(n2)、不稳定 、顺序存储
- 冒泡排序、快速排序
- O ( 1 ) O(1) O(1)、 O ( n 2 ) O(n^2) O(n2)、稳定、顺序存储和链式存储
- 全局有序的、小于(非递减排序)
- 划分操作的好坏
- O ( l o g n ) O(logn) O(logn)、 O ( n 2 ) O(n^2) O(n2)、不稳定、顺序存储(大规模且无序)
-
O ( 1 ) O(1) O(1)、 O ( n 2 ) O(n^2) O(n2)、不稳定、顺序存储和链式存储、关键字较少
-
完全二叉树、大于或等于其子节点的值、小于或等于其子节点的值
-
最大值或最小值、最大值、最小值
-
O ( n l o g n ) O(nlogn) O(nlogn)、 O ( 1 ) O(1) O(1)、不稳定、顺序存储、关键字较多
-
O ( n l o g n ) O(nlogn) O(nlogn)、 O ( n ) O(n) O(n)、稳定、顺序存储和链式存储
-
最高位优先法、最低位优先法、数字位数不等、数字位数相等
-
O ( d ( n + r ) ) O(d(n+r)) O(d(n+r))、 O ( r ) O(r) O(r)、稳定、顺序存储和链式存储
顺序:插入、选择、交换
- 直接插入排序、简单选择排序、冒泡排序、直接插入排序、冒泡排序、简单选择排序
- 希尔排序、较大规模
线性时间内完成建堆指的是在堆排序或其他涉及堆的数据结构操作中,通过特定的算法可以在𝑂(𝑛) 的时间复杂度内将一个无序数组构建成一个堆结构(通常是大根堆或小根堆)。
- 堆、线性、 O ( n l o g n ) O(nlogn) O(nlogn)
- 快速排序、 O ( n 2 ) O(n^2) O(n2)、 O ( n l o g n ) O(nlogn) O(nlogn)
- 归并排序、分割子序列、初始序列、 O ( n l o g n ) O(nlogn) O(nlogn)
- 插入排序、希尔排序、简单选择排序、冒泡排序、堆排序、快速排序、 O ( l o g n ) O(logn) O(logn)、 O ( n ) O(n) O(n)、归并排序、元素复制、 O ( n ) O(n) O(n)
- 插入排序、冒泡排序、归并排序、基数排序、希尔排序、简单选择排序、快速排序、堆排序、归并排序
- 折半插入排序、希尔排序、快速排序、堆排序、直接插入排序、简单选择排序、冒泡排序、归并排序、基数排序
- 简单选择排序、冒泡排序、堆排序、快速排序
- 待排序的元素个数n、待排序的元素的初始状态、关键字的结构及其分布情况、稳定性的要求、存储结构及辅助空间的大小限制等
- 直接插入排序、冒泡排序
- 基数排序
- 移动记录、链表
选择
注意:
- 堆排序中堆的构建是向上走,堆的调整是向下走。
结论
- 3 A 算法的稳定性不能衡量一个算法的优劣。
- 3 B 对同一线性表使用不同的排序算法进行排序,得到的排序结果可能不同。
- 3 C 链表和顺序表都可以实现排序算法。
- 3 D 在顺序表上实现的排序算法不一定能在链表上实现,例如折半插入排序。
- 16 希尔排序是不稳定的排序算法。
- 17 直接插入排序是稳定的排序算法。
- 5 快速排序算法在要排序的数据已基本有序的情况下最不利于发挥其长处。
分析:等同于当pivot选取序列中的最大值/最小值时,处于快速排序算法最坏情况。 - 6 就平均性能而言,目前最好的内部排序算法是快速排序。
- 7 快速排序第n趟的结果有至少n个元素在最终位置上。 17 18
- 13 对n个关键字进行快速排序,最大递归深度为n,最小递归深度为 l o g 2 n log_2n log2n 。
- 15 采用递归方式对顺序表进行快速排序,递归次数与每次划分后得到的分区的处理顺序无关。
- 2 简单选择排序算法的比较次数为 O ( n 2 ) O(n^2) O(n2),移动次数为 O ( n ) O(n) O(n)。
- 3 若只想得到100,000个元素组成的序列中第10个最小元素之前的部分的序列,用堆排序最快。
- 7 构建n个记录的初始堆,其时间复杂度为O(n);对n个记录进行堆排序,最坏情况下其时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)。
- 8 排序过程中的比较次序与序列初始状态无关的是简单选择排序,直接插入排序、快速排序、冒泡排序会受到影响,快排为最坏情况,复杂度达到 O ( n 2 ) O(n^2) O(n2),其余两个都是最好情况,复杂度为O(n)。
- 9 A 二叉排序树的高度 大于或等于 小根堆的高度。
- 13 删除堆顶元素后需要重新调整堆,是向下调整,建立初始堆或者插入新结点是向上调整。
- 14 堆 从根结点到任一叶结点的路径都是有序的。
- 19 Ⅰ 大根堆(至少含2个元素)可以将堆视为一个完全二叉树。
- 19 Ⅱ 大根堆(至少含2个元素)可以采用顺序存储方式。
- 19 Ⅳ 大根堆(至少含2个元素)中的次大值一定在根的下一层。
- 1 归并排序在一趟结束后不一定能选出一个元素放在其最终位置,简单选择排序、冒泡排序、堆排序都一定可以选出一个元素放在其最终位置。
- 4 归并排序过程中比较次数的数量级与序列初始状态无关,插入排序、快速排序、冒泡排序都有关。
- 5 二路归并排序中,归并趟数的数量级是 O ( l o g 2 n ) O(log_2n) O(log2n)。
- 10 若将中国人按照生日(只考虑月、日)来排序,则使用基数排序最快。
- 14 归并排序需要的附加存储空间最大。
- 17 基数排序的移动次数与关键字的初始状态无关。
- 19 使用二路归并排序堆含n个元素的数组M进行排序时,二路归并排序的功能是 将两个有序表合并为一个新的有序表。
- 9 一般情况下,堆查找效率最低,相较于有序顺序表、二叉排序树、平衡二叉树。
- 15 希尔排序、二路归并排序 每趟排序结束 都不一定能确定一个元素最终位置。
- 17 若将顺序存储更换为链式存储,则算法的时间效率会降低的是希尔排序、堆排序。【丢失了随机访问的特性】
- 19 对大部分元素已有序的的数组排序时,直接插入排序比简单选择排序效率更高,其原因是:直接插入排序过程中元素之间的比较次数更少。
错题
【错误选项】D
【思路】比较排序算法可以通过比较元素之间的大小关系来构建一个决策树,决策树的叶子结点就是比较后的序列结果。
由于一个有n个元素的集合,可能的排序数为n!,则决策树有n!个叶子节点,则决策树的最小高度为log(n!),其最小高度也就是至少要进行多少次key之间的两两比较,即 h>=log(n!)为log(5040)约等于12.3,所以选A 13。
【错误选项】C
【思路】逆序为最坏情况,54321,则从第二个元素4开始,比较1次;第三个元素3,比较2次;以此类推,一共是1+2+3+4则一共是10次,选B。
【错误选项】D B
【思路】最好情况是顺序,不需要做任何调整的情况,举个例子,12345,从第二个元素2开始,只需要比较1次;第三个元素比较1次,依次类推,只需要比较4次,所以应该是n-1选A。
【错误选项】D
【思路】希尔分完组就是直接插入排序;第一趟分组为{E,E’}{A,S’}{S,T}{Y,I}{Q,O}{U,N},每组进行一次比较,共比较6次;第一堂结果为{E,A,S,I,O,N,E’,S’,T,Y,Q,U},第二趟分组为{E,I,E’,Y}{A,O,S’,Q}{S,N,T,U},第一组比较4次,第二组比较4次,第三组比较3次,一共比较4+4+3+6=17次,所以选B。
![【错误选项】D
【思路】冒泡、选择都是会在每一步找到min/max值,这个序列显然不符合;在A和D中选,快速排序需要找到至少两个元素在最终位置,最终结果应当是1 2 4 6 8 9 10 20,满足条件;插入排序,需要保证前三个元素有序,显然不符合,所以选A。
【错误选项】A
【思路】第一趟 -> 4 7 8 3 5 6 9 1 2 10;第二趟 <- 1 4 7 8 3 5 6 9 2 10;第三趟 -> 1 4 7 8 3 5 6 2 9 10;第三趟 <- 1 2 4 7 8 3 5 6 9 10;第四趟 -> 1 2 4 7 3 5 6 8 9 10;第五趟 <- 1 2 3 4 7 5 6 8 9 10;第六趟 -> 1 2 3 4 5 6 7 8 9 10。一共是6趟,所以选B。
【错误选项】C D
【思路】快速排序的速度最快的情况是:当每次的pivot值都把表等分为长度相近的两个子表;最坏情况是逆序或有序,所以选D。
这个不是很理解,为什么选A不选C。
【错误选项】C
【思路】A 以92为pivot,35移动到第一个位置,96移动到倒数第三个位置,30移动到第二个位置,最后将92移动到第五个位置,4次;B 8次;C 4次;D 2次。选B。
【错误选项】B
【思路】最好情况,每次将待排序划分为等长的两部分。第一趟,将第一个元素与其余7个元素比较,将序列划分为3和4表厂的子表,比较7次;第二趟,对两个子表划分,将长度为3的子表划分两个1长度的子表,比较2次,长度4的子表划分为长度1和2的两个子表,比较3次;将长度为2的子表划分后,比较1次。一共比较7+2+3+1 =13 选D。
【错误选项】C
【思路】每次选取最小key,则简单选择排序、堆排序;加入已排序记录的末尾,则为应该为简单选择排序,而不是堆排序,堆排序是与未排序记录的末尾交换位置再输出。
【错误选项】B C
【思路】因为向堆中插入一个新元素是向上调整,只需要比较其父结点即可,所以时间复杂度应当是堆的高度,即为
O
(
l
o
g
2
n
)
O(log_2n)
O(log2n),选C。
【错误选项】D D
【思路】最坏情况下,快速排序的空间复杂度会增加至O(n),快速排序的最坏情况是序列基本有序或完全有序。
【错误选项】?
【思路】题目要求是在k1相同的前提下,将k2小的排在前面,所以对应的排序算法应先将k2从小到大排序,再找一个稳定的排序算法对k1排序。简单选择排序不稳定,直接插入排序稳定,所以选D。
【错误选项】B
【分析】将两个升序链表合并成一个降序链表,使用头插法,将小的结点先插入,最坏情况下应该是二者元素交叉对比,因为2max(m,n)>=m+n,所以时间复杂度为O(max(m,n)),所以选D。
【错误选项】B【将升序想成降序,思路直接乱掉】
【思路】第一趟分配、收集后的结果:151 301 372 892 93 43 485 946 146 236 327,所以372前为301 后为892,选C。
【补充】使用LSD基数排序得到降序序列,只需要在收集的时候从最大的桶开始收集,桶中元素还是按照先进先出的原则收集。
【错误选项】B
【思路】在基本有序的情况下,快速排序是处于最坏情况应为
O
(
n
2
)
O(n^2)
O(n2),直接插入排序处于最好情况应为
O
(
n
)
O(n)
O(n),归并排序不受影响,该分分所以还是
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2n),归并排序的时间复杂度不受序列初始状态的影响。所以选C。
【错误选项】D
【思路】序列基本有序,要想使得排序过程中的元素比较次数最少。肯定找受序列初始状态影响的算法:插入排序、冒泡排序、希尔排序。
希尔排序的每趟gap = 5 2 1,第一趟,比较5次,第二趟比较4+6次,第三趟比较9次,共比较24次。
插入排序从第二个元素开始,向前比较,2-10各比较1次,6-9各比较2次,总共13次;冒泡排序,从后向前交换,一共要走4趟,比较次数逐趟递减,共为9+8+7+6 = 30次,所以应该是插入排序比较次数最少,选A。
【错误选项】B
【思路】同等大小的不同初始序列,总比较次数一定,则不受初始状态影响的排序算法有:简单选择排序。折半插入排序每一趟比较次数都为
O
(
l
o
g
2
m
)
O(log_2m)
O(log2m),m为当前已排好的子序列长度,基数排序不进行比较,冒泡、快排、归并、堆排序都受序列初始状态影响。所以选A。
【错误选项】D
【分析】基数排序每趟需要利用前一趟已排好的序列,无法并行执行;
快速排序每趟划分的子序列互不影响,可以并行执行;
冒泡排序每趟对未排序的所有元素进行一趟处理,无法并行执行;
堆排序可以并行执行,因为根结点的左右子树构成的子堆在执行过程中是互不影响的。
所以选A。
【错误选项】D
【分析】归并排序的代码比插入排序更为复杂。所以Ⅰ不对。【不是和递归有关就简单!】
真的很无语!!昨天没保存啊啊啊(╯°□°)╯︵ ┻━┻(╯‵□′)╯炸弹!•••*~●
笔记【自用】
代码内容(非递减)
插入排序
直接插入排序
比较项置哨兵、找到插入位置A[j+1]、之后的项往后稍、插入(一一对比)A[0]<A[j]
void InsertSort(ElemType A[] , int n){
int i,j;
for(i = 2 ; i <= n ; i++){
if(A[i] < A[i-1]){
A[0] = A[i]; //A[0]为哨兵,不存放元素,保存待被插入的元素L(i)
for(j = i-1 ;A[0]<A[j];--j)
A[j+1] = A[j];
A[j+1] = A[0];
}
}
}
折半插入排序
比较项置哨兵A[0]、找到插入位置A[low] A[high]、之后的项往后稍j>=high、插入(折半插入)A[high+1]
void InsertSort(ElemType A[],int n){
int i,j,low,high,mid;
for(i = 2 ; i<=n ;i++){
A[0] = A[i];
low = 1;
high = i-1;
while(low <= high){
mid = (low+high)/2;
if(A[mid] > A[0]) high=mid-1;
else low = mid+1;
}
for(j = i-1 ; j >= high ;--j)//往后稍
A[j+1] = A[j];
A[high+1] = A[0];
}
}
希尔排序
这里关键字都存储于下标[1,n]之间
e.g.{49,38,65,97,76,13,27,49‘,55,04}
第一趟gap = 5 分组{49,13},{38,27},{65,49’},{97,55},{76,04}
结果{13,27,49’,55,04,49,38,65,97,76}
第二趟gap = 3 分组{13,55,38,76},{27,04,65},{49‘,49,97}
…
void ShellSort(ElemType A[] , int n){
int gap,i,j;
//选择初始增量,通常从n/2开始,然后逐步减小为1
for(gap = n/2 ; gap >=1 ; gap/=2){
//对每个增量序列进行插入排序
for(i = gap+1; i<=n ;++i){//相同gap,不同组
A[0] = A[i]; //存储当前元素
//j从后往前比较,如果A[0]小于A[j],那就需要将其后退gap个单位,满足条件就移动
//不需要将同一组单独拎出来,上述例子中第二趟的{13,55,38,76},先判断13 55 然后27 04再49‘ 49,轮到55 38后才会将13 55 38拉出来判断。
for(j = i-gap;j>0 && A[0]<A[j];j-=gap)
A[j+gap] = A[j]; //元素的移动
A[j+gap] = A[0]; //找到插入位置,插入
}
}
}
记忆:
- 第一个for确定gap,每次gap/=2;
- 第二个for确定比较项,i=gap+1,每次i++,并将其存储与A[0];
- 第三个for准备往前比较满足条件后就移动 j = i - gap,每次j-=gap,满足的条件为j>0 &&
A[0]<A[j]; - 在第二个for的最后,A[0]插入到位置A[j+gap]。
交换排序
冒泡排序
从后向前比较,遵循前小后大,若交换,标记flag=true
void BubbleSort(ElemType A[] , int n){
for(int i = 0 ; i < n-1 ;i++){
bool flag = false;
for(int j = n-1 ; j > i ; j--){
if(A[j-1] > A[j]){ //逆序
swap(A[j-1],A[j]); //交换
flag = true; //标记本趟冒泡发生交换
}
if(!flag)
return; //若本趟冒泡没有发生交换,说明表已经有序,结束循环
}
}
}
快速排序
快排通常总以表中的第一个元素作为枢轴来对表进行划分,则将表中比枢轴大的元素向右移动,将比枢轴小的元素向左移动,使得一趟Partition()操作后,表中的元素被枢轴一分为二。
pivot 为基准、枢轴的意思。
int Partition(ElemType A[] ,int low ,int high){
ElemType pivot = A[low];
while(low < high){
while(low<high && A[high] >= pivot) --high;
A[low] = A[high]; //将比pivot小的元素移动到左端low
while(low<high && A[low] <= pivot) ++low;
A[high] = A[low]; //将比pivot大的元素移动到右端high
}
}
void QuickSort(ElemType A[] ,int low ,int high){
if(low < high){ //递归跳出条件
int pivotpos = Partition(A , low ,high); //划分
//左子数组
QuickSort(A , low , pivotpos-1);//依次对这两个子数组进行递归排序
//右子数组
QuickSort(A , pivotpos+1 , high);
}
}
选择排序
简单选择排序
void SelectSort(ElemType A[] ,int n){
for(int i = 0 ; i < n-1 ;i++){
int min = i; //记录最小位置
for(int j = i+1 ; j<n ;j++){
if(A[j] < A[min]) min = j;
}
if(min!=j) swap(A[i] , A[min]);
}
}
堆排序
void BuildMaxHeap(ElemType A[],int len){
for(int i = len/2 ; i>0 ;i--){ //将以n/2 - 1为根的子树 反复调整为大根堆/小根堆
HeadAdjust(A,i,len);
}
}
void HeadAdjust(ElemType A[],int k,int len){
//func.对以元素k为根的子树进行调整
A[0] = A[k]; //暂存子树的根结点
for(int i = k*2 ; i <= len ;i*=2){ //沿key较大的子节点向下shaix
if(i<len && A[i] < A[i+1])
i++;
if(A[0]>=A[i]) break;
else{
A[k] = A[i];
k=i;
}
}
A[k] = A[0]; //被筛选结点的值放入最终位置
}
void HeapSort(ElemType A[],int len){
BuildMaxHeap(A,len);
for(int i = len ; i>1 ;i--){ //堆顶元素为A[1]
Swap(A[i] , A[1]); //输出堆顶元素,并将堆底元素交换至堆顶
HeadAdjust(A , 1 ,i-1); //向下调整出新的堆
}
}
【自写】堆的插入操作
void InsertMaxHeap(ElemType A[],int &len,ElemType value){
//将其放在堆的末端
len++;
A[len] = value;
//向上调整堆
for(int i = len/2 ; i>0 ;i--){
HeadAdjust(A , i ,len);
}
}
这种写法在逻辑上可以工作,但存在一个性能上的问题。在插入一个元素后,使用了 HeadAdjust 函数从堆的中间部分开始向上调整整个堆的每个节点,这会导致每次插入都需要调整多个节点,可能会重复调整不需要修改的子树,从而增加了不必要的开销。
改进建议:
插入新元素时,实际上只需要调整新插入的元素与它的父节点的关系,而不是调整整个堆。因此,你应该从新插入的节点开始进行“上滤操作”,直到堆的性质得到维护为止。调整的步骤是沿着新插入节点向上,只修改与其相关的父节点,减少不必要的操作。
void InsertMaxHeap(ElemType A[],int &len,ElemType value){
//将其放在堆的末端
len++;
A[len] = value;
int i = len;
while(i>1 && A[i] > A[i/2]){ //父结点还是小于当下结点
Swap(A[i] ,A[i/2]);
i = i/2;
}
}
归并排序
Merge()的功能是将前后相邻的两个有序表归并为一个有序表。
设两段有序表A[low…mid]、A[mid+1…high]存放在同一顺序表中的相邻为止,先将他们复制到辅助数组B中。
每次从B的两段中取出一个记录进行关键字比较,将较小者放入A中,当B中有一段的下标超出其对应的表长,此时该段所有的元素都已复制到A中,将另一段的剩余部分直接复制到A中。
【类似于有序链表的合并】
ElemType *B = (ElemType *) malloc((n+1)*sizeof(ElemType));
void Merge(ElemType A[], int low ,int mid ,int high){
//将A[low...mid]和A[mid+1...high]合并成一个有序表
int i,j,k;
for(k = low ; k <=high ;k++) //将A的所有元素复制给B
B[k] = A[k];
for(i = low , j =mid+1 ,k=i; i<=mid && j<=high ;k++){
if(B[i]<=B[j]) //用B判断大小
A[k] = B[i++]; //在A中做排序
else
A[k] = B[j++];
}
while(i<=mid) A[k++] = B[i++]; //将剩余内容导入A
while(j<=high) A[k++] = B[j++];
}
void MergeSort(ElemType A[],int low ,int high){
if(low<high){
int mid = (low+high)/2;
MergeSort(A,low,mid); //拆分
MergeSort(A,mid+1,high);
Merge(A,low,mid,high); //归并
}
}
计数排序
B数组存放输出的排序序列,C数组存储计数值。
用A中的元素作为数组C的下标,而该元素出现的次数存储在该元素作为下标的数组C中。
void CountSort(ElemType A[],ElemType B[],int n,int k){
int i,C[k];
for(i=0;i<k;i++)
C[i]=0;
for(i=0;i<k;i++)
C[A[i]]++;
for(i=1;i<k;i++)
C[i]=C[i]+C[i-1];
for(i=n-1;i>=0;i--){
B[C[A[i]-1]] = A[i];
C[A[i]] = C[A[i]]-1;
}
}