Lec7 Sorting(1)
Lec7 Sorting(1)
排序:计算机科学中的核心问题
排序问题的定义
给定一组记录 $ r_1, r_2, , r_n $ 和它们对应的关键值 $ k_1, k_2, , k_n $,通过排序操作,将这些记录按照以下规则排列:
$ k_{s_1} k_{s_2} k_{s_n} $
注意事项:
- 关键值是否允许重复:
- 可根据应用需求决定是否允许重复关键值。
- 输出结果:
- 排列后的记录满足从小到大的排序顺序(也可以根据需求改为降序)。
排序分类
1. 比较排序(Comparison-Based Sorting)
这种排序算法基于比较操作来决定元素之间的顺序。它要求:
- 输入:
- 一个存储在数组 $ A $ 中的记录集合。
- 每条记录至少包含一个关键字段。
- 一个比较函数,用于确定关键值之间的相对顺序。
- 输出:
- 排序后的数组 $ A $,满足以下条件:
- 对任意 $ i < j $,如果 $ A[i] $ 的关键值小于 $ A[j] $ 的关键值,则 $ A[i] $ 出现在 $ A[j] $ 前面。
- 排序后的数组 $ A $,满足以下条件:
2. 内部排序(Internal Sorting)
- 适用于数据量较小,能够完全存储在内存中的情况。
- 常见算法:
- 冒泡排序(Bubble Sort)
- 插入排序(Insertion Sort)
- 归并排序(Merge Sort)
- 快速排序(Quick Sort)
- 堆排序(Heap Sort)
3. 外部排序(External Sorting)
- 当数据量太大以至于不能完全加载到内存时,采用外部排序。
- 通常依赖磁盘存储和分块操作,典型算法是外部归并排序。
排序算法的空间与时间复杂度以及稳定性分析
空间复杂度(Space Complexity)
- 排序算法需要多少额外空间?
- 排序算法所需的额外空间决定了算法的实现效率和适用场景。
- 是否需要复制数据?
- 需要复制数据:使用额外的 $ O(n) $ 空间(例如,归并排序需要辅助数组)。
- 原地排序(In-place Sorting):
- 不需要复制数据。
- 额外空间复杂度为 $ O(1) $。
- 例如,快速排序(Quick Sort)、堆排序(Heap Sort)。
- 临时空间使用:
- 某些算法介于两者之间,例如快速排序需要递归栈空间,平均为 $ O(n) $。
时间复杂度(Time Complexity)
算法的运行速度有多快?
- 一个排序算法的性能取决于它需要比较和移动元素的次数。
最少需要的比较次数:
- 在最理想情况下,每个元素至少需要检查一次,时间复杂度下限为 $ (n) $。
最坏情况下的比较次数:
- 每个元素可能需要与所有其他元素进行比较,时间复杂度为 $ O(n^2) $(例如冒泡排序、选择排序在未优化时的最坏表现)。
关键问题:
- 排序算法能多接近 $ O(n) $?
- 线性时间排序的实现通常需要利用非比较排序算法,例如计数排序(Counting Sort)、基数排序(Radix Sort)。
稳定性(Stability)
什么是排序算法的稳定性?
- 稳定排序算法:不会改变相同关键值记录的相对顺序。
- 例如,在按姓名排序的电话簿中,如果再次按县排序,仍希望同一县的记录保持原姓名排序。
为什么重要?
- 对于数据库等场景,稳定性非常重要,特别是当需要多次排序时。
案例分析:
输入记录:
$ r_1(7), r_2(5), r_3(8), r_4(3), r_5(7) $输出 1(稳定排序):
$ r_4(3), r_2(5), r_1(7), r_5(7), r_3(8) $- 重复关键值 $ 7 $ 的记录顺序未被改变:$ r_1 $ 在 $ r_5 $ 前。
输出 2(不稳定排序):
$ r_4(3), r_2(5), r_5(7), r_1(7), r_3(8) $- 重复关键值 $ 7 $ 的记录顺序被打乱:$ r_5 $ 在 $ r_1 $ 前。
性质 | 分析 |
---|---|
空间复杂度 | - $ O(n) $:需要额外存储(如归并排序)。 - $ O(1) $:原地排序(如快速排序、堆排序)。 - $ O(n) \(:临时空间(如快速排序递归栈)。 | | **时间复杂度** | - 最优:\) (n) \(。- 最差:\) O(n^2) $(比较排序)。 - 非比较排序可实现 $ O(n) $(如计数排序)。 |
稳定性 | - 稳定:保持重复关键值的记录相对顺序(如归并排序)。 - 不稳定:可能改变相对顺序(如快速排序、堆排序)。 |
选择排序算法时,需综合考虑时间、空间和稳定性三个方面的权衡。
冒泡排序(Bubble Sort)算法分析
冒泡排序算法简介
核心思想:
通过两两比较相邻元素,将较大的元素“冒泡”到数组的正确位置。每次遍历后,未排序部分的最大元素会移动到它的最终位置。
算法特点:
- 逐步冒泡:
- 第 $ i $ 次迭代中,将当前未排序部分的第 $ i $ 小元素冒泡到第 $ i $ 个位置。
- 重复 $ n-1 $ 次,最终排序完成。
- 实现方式:
- 从数组末尾开始,每次比较 $ A[j] $ 和 $ A[j-1] $,如果 $ A[j] < A[j-1] $,则交换两者。
- 每次循环结束后,未排序部分的最大元素就会被“冒泡”到正确位置。
算法实现
模板代码:
1 | template <typename E, typename Comp> |
算法复杂度分析
- 比较次数:
- 第 $ i $ 次冒泡需要比较 $ n - i $ 次。
- 总比较次数为: $ _{i=1}^{n-1} (n - i) = = O(n^2) $
- 无论是最好、平均还是最坏情况,比较次数都接近 $ O(n^2) $。
- 交换次数:
- 最好情况(已排序): 不发生任何交换,交换次数为 0。
- 最坏和平均情况: 平均情况下,交换次数大约为总比较次数的一半,仍为 $ O(n^2) $。
- 时间复杂度:
- 最好情况:$ O(n) $(已排序时,可通过优化减少不必要的比较)。
- 平均和最坏情况:$ O(n^2) $。
优化建议
提前终止判断:
如果某次遍历中没有发生交换,则说明数组已经有序,可以提前结束算法。时间复杂度优化:
- 已排序数组: $ O(n) $。
- 未排序数组: 仍为 $ O(n^2) $。
选择排序(Selection Sort)算法分析
选择排序算法简介
核心思想:
每次从未排序部分中选择最小(或最大)的元素,将其放到已排序部分的末尾。
具体步骤:
- 第 $ i $ 轮遍历中,从未排序部分中找到最小值,将其放置在位置 $ i $。
- 重复 $ n-1 $ 次,最终完成排序。
特点:
- 交换次数少: 每轮只需一次交换操作。
- 与冒泡排序的区别:
冒泡排序需要多次交换,而选择排序通过减少交换次数提高效率。
算法实现
模板代码:
1 | template <typename E, typename Comp> |
算法复杂度分析
- 比较次数:
- 第 $ i $ 轮需要比较 $ n - i - 1 $ 次。
- 总比较次数为: $ _{i=0}^{n-2} (n - i - 1) = = O(n^2) $
- 在最佳、平均、最坏情况下,比较次数均为 $ (n^2) $。
- 交换次数:
- 每轮选择完成后只需一次交换操作。
- 最好情况(已排序): 交换次数为 0。
- 最坏和平均情况: 交换次数为 $ n-1 $ 次,复杂度为 $ (n) $。
- 时间复杂度:
- 最好、平均、最坏情况均为 $ O(n^2) $。
选择排序过程示例
输入数组:
$ 5, 3, 8, 6, 2 $
排序步骤:
迭代轮次 | 查找最小值索引 | 交换操作 | 数组状态 |
---|---|---|---|
第 1 轮 | 最小值索引 4 | 交换 5 ↔︎ 2 | $ 2, 3, 8, 6, 5 $ |
第 2 轮 | 最小值索引 1 | 无需交换 | $ 2, 3, 8, 6, 5 $ |
第 3 轮 | 最小值索引 4 | 交换 8 ↔︎ 5 | $ 2, 3, 5, 6, 8 $ |
第 4 轮 | 最小值索引 3 | 无需交换 | $ 2, 3, 5, 6, 8 $ |
选择排序的性质
性质 | 分析 |
---|---|
时间复杂度 | 最好情况:$ O(n^2) \(<br>平均情况:\) O(n^2) \(<br>最坏情况:\) O(n^2) $。 |
空间复杂度 | $ O(1) \((原地排序)。 | | **稳定性** | **不稳定**(交换可能改变相同元素的相对顺序)。 | | **交换次数** | 最好情况:0 次<br>最坏和平均情况:\) n-1 $ 次。 |
适用场景 | 小规模数据排序,或当交换操作代价较大时。 |
选择排序的优缺点
优点:
- 简单易实现。
- 与冒泡排序相比,交换次数更少。
缺点:
- 不稳定,可能打乱相同元素的相对顺序。
- 时间复杂度为 $ O(n^2) $,在处理大规模数据时效率较低。
插入排序(Insertion Sort)算法分析
插入排序算法简介
核心思想:
每次将一个新元素插入到已排序序列中,使得序列依然保持有序。
假设前 $ k $ 个元素已经排好序,在第 $ k+1 $ 步中,将第 $ k+1 $
个元素插入到合适的位置。
算法步骤
- 从数组的第二个元素开始,将其视为待插入的元素。
- 遍历已排序部分,将待插入元素向前移动,直到找到合适位置。
- 重复该过程,直到所有元素都插入完成。
示例
输入:
$ 4, 7, 12, 5, 19, 16 $
排序过程:
初始已排序部分:$ 4, 7, 12 $
待插入元素:5
插入后:$ 4, 5, 7, 12, 19, 16 $已排序部分:$ 4, 5, 7, 12, 19 $
待插入元素:16
插入后:$ 4, 5, 7, 12, 16, 19 $
算法实现
模板代码:
1 | template <typename E, typename Comp> |
时间复杂度分析
- 最坏情况:
- 输入数据完全逆序。
- 第 $ i $ 次插入操作需要 $ i $ 次比较。
- 总比较次数为: $ _{i=2}^n i = O(n^2) $
- 每次比较几乎都伴随一次交换操作,交换次数为 $ O(n^2) $。
- 最好情况:
- 输入数据已经有序。
- 每次比较直接退出,无需交换。
- 比较次数为 $ n-1 $,时间复杂度为 $ O(n) $。
- 平均情况:
- 假设每次插入时,约有一半的元素需要比较和移动。
- 比较次数和交换次数均为 $ (n^2/4) $,时间复杂度为 $ O(n^2) $。
- 空间复杂度:
- 插入排序是原地排序算法,额外空间复杂度为 $ O(1) $。
插入排序性质
性质 | 分析 |
---|---|
时间复杂度 | 最好情况:$ O(n) \(<br>平均情况:\) O(n^2) \(<br>最坏情况:\) O(n^2) $。 |
空间复杂度 | $ O(1) $(原地排序)。 |
稳定性 | 稳定,不会改变相同元素的相对顺序。 |
适用场景 | 适用于几乎有序的数据,或者数据量较小的场景。 |
插入排序过程示例
迭代次数 | 插入元素 | 已排序部分 | 数组状态 |
---|---|---|---|
第 1 次 | 7 | $ 4 $ | $ 4, 7, 12, 5, 19, 16 $ |
第 2 次 | 12 | $ 4, 7 $ | $ 4, 7, 12, 5, 19, 16 $ |
第 3 次 | 5 | $ 4, 7, 12 $ | $ 4, 5, 7, 12, 19, 16 $ |
第 4 次 | 19 | $ 4, 5, 7, 12 $ | $ 4, 5, 7, 12, 19, 16 $ |
第 5 次 | 16 | $ 4, 5, 7, 12, 19 $ | $ 4, 5, 7, 12, 16, 19 $ |
优缺点
优点:
- 简单易实现。
- 稳定性强。
- 在几乎有序数据中表现优异,接近 $ O(n) $。
缺点:
- 时间复杂度较高,处理大规模数据效率较低。
- 逆序数据的最坏情况性能差。
冒泡排序、选择排序、插入排序的比较
比较总结表格
算法 | 最优时间复杂度 | 平均时间复杂度 | 最坏时间复杂度 | 交换次数(最优) | 交换次数(平均) | 交换次数(最坏) | 是否稳定 | 是否原地排序 |
---|---|---|---|---|---|---|---|---|
冒泡排序 | $ O(n^2) $ | $ O(n^2) $ | $ O(n^2) $ | 0 | $ O(n^2) $ | $ O(n^2) $ | 是 | 是 |
选择排序 | $ O(n^2) $ | $ O(n^2) $ | $ O(n^2) $ | $ O(n) $ | $ O(n) $ | $ O(n) $ | 否 | 是 |
插入排序 | $ O(n) $ | $ O(n^2) $ | $ O(n^2) $ | 0 | $ O(n^2) $ | $ O(n^2) $ | 是 | 是 |
时间复杂度比较
- 冒泡排序:
每次冒泡需要比较 $ n-i $ 次,循环 $ n-1 $ 次,最优情况下输入已排序,仅比较,无交换。
时间复杂度:- 最优情况:$ O(n^2) $
- 平均和最坏情况:$ O(n^2) $。
- 最优情况:$ O(n^2) $
- 选择排序:
每次选择最小元素需扫描整个未排序部分,共 $ n-1 $ 次,比较次数固定为 $ n^2/2 $。
时间复杂度:- 最优、平均、最坏情况均为 $ O(n^2) $。
- 插入排序:
每次将一个新元素插入到已排序部分,最优情况仅需比较。
时间复杂度:- 最优情况:$ O(n) $。
- 平均和最坏情况:$ O(n^2) $。
- 最优情况:$ O(n) $。
交换次数比较
冒泡排序:
最优情况(已排序):0 次交换;
最坏情况:约 $ O(n^2) $ 次交换。选择排序:
每次只需 1 次交换,总交换次数固定为 $ n-1 $。插入排序:
最优情况(已排序):0 次交换;
最坏情况:约 $ O(n^2) $ 次交换。
稳定性比较
- 冒泡排序: 稳定,每次交换仅发生在相邻元素之间,且相等元素不会改变顺序。
- 选择排序: 不稳定。例如:$ 3, 9, 3, 2, 5 $,在选择过程中会破坏相同元素的相对顺序。
- 插入排序: 稳定,元素插入过程中不会破坏相等元素的顺序。
算法性质分析
- 共同点:
- 三种算法均为原地排序算法,空间复杂度为 $ O(1) $。
- 都是基于比较和交换的排序方法。
- 平均情况下,时间复杂度均为 $ O(n^2) $。
- 局限性:
- 三种算法都只对相邻元素进行比较和交换,适用于小规模数据,但对大规模数据性能较差。
- 冒泡排序和插入排序在输入接近有序时表现较好。
- 交换次数与反序对:
- 反序对定义为 $ i < j $ 且 $ A[i] > A[j] $。
- 平均情况下,一个数组的反序对数为 $ $。
- 排序时需要消除所有反序对,导致 $ O(n^2) $ 时间复杂度。
- 反序对定义为 $ i < j $ 且 $ A[i] > A[j] $。
适用场景
算法 | 适用场景 |
---|---|
冒泡排序 | 数据量较小或输入数据近乎有序时;学习排序算法原理的入门选择。 |
选择排序 | 数据量较小、对稳定性要求不高时;交换次数少的场景。 |
插入排序 | 输入数据接近有序时表现最佳;适用于小型数据集或实时排序场景。 |
Shell排序
1. 算法简介
Shell排序(缩小增量排序)是一种基于插入排序改进的排序算法,核心思想是通过比较和交换非相邻元素,逐步将序列调整为“基本有序”,最终利用插入排序完成排序工作。
Shell排序的特点:
- 增量序列(Increment Sequence): 定义步长 $ h_1, h_2, , h_t $,逐渐减小。
- 每一轮按照当前步长 $ h_k $,将数组分成若干个子序列,分别进行插入排序。
- 关键性质: 如果一个序列是 $ h_k \(-有序(\) h_k $-sorted),那么它保持 $ h_{k-1} $-有序的特性。
- 时间复杂度: 优于 $ O(n^2) $,取决于步长序列的选择。
2. 算法流程
- 初始步长选择: 通常选择 $ h = n/2 $,逐步缩小 $ h $ 为 $ h = h/2 $。
- 子序列划分: 根据当前步长 $ h_k $,将数组分为多个间隔为 $ h_k $ 的子序列。
- 对子序列排序: 每个子序列使用插入排序。
- 步长递减: 重复上述过程,直到步长减小到 1。
3. 核心代码(C++实现)
版本 1:标准 Shell排序
1 | template <typename Comparable> |
版本 2:带插入排序函数的 Shell排序
1 | // 插入排序(支持指定步长) |
4. 增量序列的选择
增量序列的选择直接影响 Shell排序的性能:
- Shell's 增量: $ h_k = n/2^k $。最常用,但性能不是最佳。
- Hibbard 增量: $ h_k = 2^k - 1 $。改善性能,但最坏情况仍为 $ O(n^2) $。
- Sedgewick 增量: 一组混合序列,如 $ 1, 5, 19, 41, 109, $,可达到 $ O(n^{4/3}) $。
- Knuth 增量: $ h_k = 3^k - 1 $。常用的高效序列,性能较好。
5. 时间复杂度分析
步长序列 | 最坏时间复杂度 | 说明 |
---|---|---|
Shell's 增量 | $ O(n^2) $ | 使用 $ h_k = n/2^k $,性能较差。 |
Hibbard 增量 | $ O(n^{3/2}) $ | 改善了比较次数,但依赖增量序列的性质。 |
Knuth 增量 | $ O(n^{5/3}) $ | 实现较为复杂,性能更优。 |
Sedgewick 增量 | $ O(n^{4/3}) $ | 理论上性能较好,适合大规模数据。 |
6. 算法特点
- 优点:
- 简单易实现。
- 空间复杂度低,为 $ O(1) $。
- 对大规模数据性能优于直接插入排序。
- 缺点:
- 性能受增量序列影响较大。
- 稳定性较差(非相邻元素交换会破坏顺序)。
7. 适用场景
- 数据量较大但不希望使用复杂排序(如快速排序、归并排序)时。
- 需要原地排序,且对稳定性要求不高的场景。
例如:中型数据集的排序、对性能要求适中的程序设计。