文章目录
- 要点4:掌握不同排序算法的原理和方法,以及寻找一个长序列中前k个最大数所运用的方法
-
- [📚 学习路线图](#📚 学习路线图)
- 本文内容一览(快速理解)
- [一、基于比较的排序算法(Comparison-Based Sorting):理解排序的基本方法](#一、基于比较的排序算法(Comparison-Based Sorting):理解排序的基本方法)
-
- [1.1 插入排序(Insertion Sort):简单直观的贪心算法](#1.1 插入排序(Insertion Sort):简单直观的贪心算法)
- [1.2 堆排序(Heapsort):基于堆数据结构的排序](#1.2 堆排序(Heapsort):基于堆数据结构的排序)
- [1.3 归并排序(Merge Sort):分治法排序](#1.3 归并排序(Merge Sort):分治法排序)
- [1.4 快速排序(Quick Sort):基于数值的分治法排序](#1.4 快速排序(Quick Sort):基于数值的分治法排序)
- [二、不基于比较的排序算法(Non-Comparison Sorting):突破O(n log n)下界](#二、不基于比较的排序算法(Non-Comparison Sorting):突破O(n log n)下界)
-
- [2.1 计数排序(Counting Sort):统计元素出现次数](#2.1 计数排序(Counting Sort):统计元素出现次数)
- [2.2 基数排序(Radix Sort):按位排序](#2.2 基数排序(Radix Sort):按位排序)
- [2.3 桶排序(Bucket Sort):分桶后排序](#2.3 桶排序(Bucket Sort):分桶后排序)
- [三、寻找前k个最大数(Finding Top k Largest Numbers):多种解决方法](#三、寻找前k个最大数(Finding Top k Largest Numbers):多种解决方法)
-
- [3.1 问题定义(Problem Definition):理解前k个最大数问题](#3.1 问题定义(Problem Definition):理解前k个最大数问题)
- [3.2 方法1:重复找最大数(Repeated Maximum Finding)](#3.2 方法1:重复找最大数(Repeated Maximum Finding))
- [3.3 方法2:Select算法+比较(Select + Comparison)](#3.3 方法2:Select算法+比较(Select + Comparison))
- [3.4 方法3:堆方法(Heap Method):利用堆数据结构](#3.4 方法3:堆方法(Heap Method):利用堆数据结构)
- [3.5 方法4:锦标赛树方法(Tournament Tree Method):空间换时间](#3.5 方法4:锦标赛树方法(Tournament Tree Method):空间换时间)
- [3.6 方法比较(Method Comparison):选择合适的方法](#3.6 方法比较(Method Comparison):选择合适的方法)
- [📝 本章总结](#📝 本章总结)
要点4:掌握不同排序算法的原理和方法,以及寻找一个长序列中前k个最大数所运用的方法
📌 适合对象 :算法学习者、计算机科学学生
⏱️ 预计阅读时间 :60-70分钟
🎯 学习目标 :掌握基于比较和不基于比较的排序算法,理解各种排序方法的原理和特性,掌握寻找前k个最大数的多种方法
📚 参考PPT:第 4 章-PPT-N2_modified(排序算法)- 排序算法与前k个最大数相关内容
📚 学习路线图
排序算法 基于比较的排序
O(n log n)下界 不基于比较的排序
O(n)可能 插入排序
堆排序
归并排序
快速排序 计数排序
基数排序
桶排序 前k个最大数 方法1:重复找最大
方法2:Select+比较
方法3:堆方法
方法4:锦标赛树
本文内容一览(快速理解)
- 基于比较的排序算法:插入排序、堆排序、归并排序、快速排序,时间复杂度下界为Ω(n log n)
- 不基于比较的排序算法:计数排序、基数排序、桶排序,时间复杂度可优于O(n log n)
- 排序算法的特性:稳定性、原地性、时间复杂度、空间复杂度
- 前k个最大数问题:从n个数中找出前k个最大的数
- 多种解决方法:重复找最大、Select算法、堆方法、锦标赛树方法
一、基于比较的排序算法(Comparison-Based Sorting):理解排序的基本方法
这一章要建立的基础:理解基于比较的排序算法通过比较元素大小来决定相对位置,时间复杂度下界为Ω(n log n)。
核心问题:如何通过比较元素大小来排序?有哪些经典的排序算法?
!NOTE
📝 关键点总结:排序把 a 1 , a 2 , a 3 , ... , a n a_1, a_2, a_3, \ldots, a_n a1,a2,a3,...,an重排为一个递增序列。排序是很多算法的重要组成部分。用比较数字大小来排序的算法称为基于比较的排序算法。任何比较排序需要至少lg(n!) ≈ n log n次比较。
1.1 插入排序(Insertion Sort):简单直观的贪心算法
概念的本质:
插入排序是一个简单的用贪心法的算法。思路是:
- 初始 :把一个数 a 1 a_1 a1排好序(不需操作)
- 第1步 :把两个数 a 1 , a 2 a_1, a_2 a1,a2排好序
- 第2步 :把三个数 a 1 , a 2 , a 3 a_1, a_2, a_3 a1,a2,a3排好序
- 以此类推:每一步都将一个新元素插入到已排序的子序列中
图解说明:
初始:5 3 4 2 1 第1步:3 5 4 2 1
插入3 第2步:3 4 5 2 1
插入4 第3步:1 3 4 5 2
插入2 第4步:1 2 3 4 5
插入1
💡 说明:所谓"排好",是指"对应的几个数"排成了一个递增的子序列。每一步都是将当前元素插入到前面已排序的子序列的正确位置。
类比理解:
就像整理手中的扑克牌:
- 左手拿牌,右手从牌堆中取牌
- 每次取一张新牌,插入到左手已排序的牌中的正确位置
- 重复这个过程,直到所有牌都排序完成
实际例子:
插入排序过程:
初始:5 3 4 2 1
第1步后:3 5 4 2 1 (将3插入到5前面)
第2步后:3 4 5 2 1 (将4插入到3和5之间)
第3步后:1 3 4 5 2 (将2插入到1和3之间)
第4步后:1 2 3 4 5 (将1插入到最前面)
时间复杂度:O(n²)
优点:简单、稳定、原地排序
缺点:复杂度较高
1.2 堆排序(Heapsort):基于堆数据结构的排序
概念的本质:
堆排序利用堆数据结构进行排序:
- 建堆:将输入数据组织成一个最大堆
- 排序:反复取出堆中最大值(根节点),与堆尾交换,修复堆
- 重复:直到堆规模为1
图解说明:
是 否 输入数组 建堆
Build-Max-Heap 最大堆 heap-size > 1? 交换A[1]和A[heap-size] heap-size-- Max-Heapify(A, 1) 排序完成
💡 说明:每一轮,将堆中最大的数(即根中的数)与堆尾数字互换,即当前最大的数放到最后,然后堆规模减一、修复堆,为下一轮做准备。
类比理解:
就像选美比赛:
- 每次从所有选手中选出最美的(堆顶最大值)
- 把她放到最后的位置(已排序区域)
- 剩下的选手重新排名(修复堆)
- 重复直到所有人都排好
实际例子:
堆排序过程:
初始堆:[16, 14, 10, 8, 7, 9, 3, 2, 4, 1]
第1轮:交换16和1,修复堆
→ [14, 8, 10, 4, 7, 9, 3, 2, 1 | 16]
第2轮:交换14和1,修复堆
→ [10, 8, 9, 4, 7, 1, 3, 2 | 14, 16]
继续这个过程...
最终:[1, 2, 3, 4, 7, 8, 9, 10, 14, 16]
时间复杂度:O(n log n)
优点:原地排序
缺点:不稳定,比较次数较多(2n log n)
1.3 归并排序(Merge Sort):分治法排序
概念的本质:
归并排序采用分治策略:
- 分:把序列一分为二
- 递归:用分治法递归地将两子序列排好序
- 合:用合并算法把排好序的两个子序列合并为一
图解说明:
否 是 序列A[p..r] p < r? 底:单个元素已有序 计算中点mid 递归排序A[p..mid] 递归排序A[mid+1..r] 合并两个有序子序列 得到有序序列
💡 说明:归并排序采用了基于中点的分治策略。底是p==r,即只剩一个元素(暗含已有序)。分是基于中点的子问题划分,合是通过合并算法合并两个有序子序列。
类比理解:
就像合并两个有序的队伍:
- 先把大队伍分成两个小队伍
- 分别整理两个小队伍(递归)
- 当小队伍只有一个人时,自然有序(底)
- 然后把两个有序的小队伍合并成一个大队伍
实际例子:
归并排序过程:
数组:[9, 6, 2, 4, 1, 5, 3, 8]
第1层:分成[9,6,2,4]和[1,5,3,8]
第2层:
[9,6,2,4] → [9,6]和[2,4]
[1,5,3,8] → [1,5]和[3,8]
第3层:每个子序列只有1-2个元素,直接排序
合并:逐层向上合并
最终:[1,2,3,4,5,6,8,9]
时间复杂度:O(n log n),系数为1
优点:稳定排序,时间复杂度好
缺点:需要额外O(n)空间
1.4 快速排序(Quick Sort):基于数值的分治法排序
概念的本质:
快速排序采用基于数值的分治策略:
- 选择主元 :取序列中一个数(如 A [ r ] A[r] A[r])为基准
- 划分 :把序列分为左、右两部分,使左边任何数 ≤ A [ r ] ≤ \leq A[r] \leq ≤A[r]≤右边任何数
- 递归:分别对"左边部分"和"右边部分"再进行排序
图解说明:
序列A[p..r] 选择主元A[r] 划分:≤主元 | >主元 递归排序左部分 递归排序右部分 排序完成
💡 说明:选择主元元素的方法有很多,但排序性能都差不多,这里选择A[r]做为主元元素。一旦分成两部分后,则两部分的元素将永远不会再比较------这降低了比较次数,这也是分治法的特点,子问题互不交叠。
类比理解:
就像按身高排队:
- 选一个参考身高(主元)
- 比参考矮的站左边,比参考高的站右边
- 然后左边和右边分别再排队
- 最终整个队伍就排好了
实际例子:
快速排序过程:
数组:[9, 6, 2, 4, 1, 5, 3, 8]
选择主元8,划分:
- 左部分(≤8):[6, 2, 4, 1, 5, 3]
- 右部分(>8):[9]
递归排序左部分和右部分
最终得到有序序列
平均时间复杂度:O(n log n),系数1.39
最坏时间复杂度:O(n²)(已有序时)
优点:原地排序,平均性能最佳
缺点:不稳定,最坏情况较差
二、不基于比较的排序算法(Non-Comparison Sorting):突破O(n log n)下界
这一章要建立的基础:理解不基于比较的排序算法可以突破比较排序的Ω(n log n)下界,但往往有额外的条件限制。
核心问题:如何在不比较元素大小的情况下进行排序?这些算法有什么限制?
!NOTE
📝 关键点总结:任何比较排序需要至少lg(n!) ≈ n log n次比较。不基于比较的排序算法的复杂度可优于O(n log n)。不基于比较的排序算法往往有额外的条件限制。
2.1 计数排序(Counting Sort):统计元素出现次数
概念的本质:
计数排序的基本思想:
- 对每一个数组元素 x x x,确定数组中 < x < x <x的元素个数
- 利用这一信息,就可以直接将 x x x放到它在"输出数组"的对应位置上
- 例如:如果有17个元素小于 x x x,那么 x x x就应该在第18个输出位置上
要求:
- 数组元素的值都是整数
- 限制在一定范围内, 0 ≤ a 1 , a 2 , ... , a n ≤ k 0 \leq a_1, a_2, \ldots, a_n \leq k 0≤a1,a2,...,an≤k, k = O ( n ) k = O(n) k=O(n)
算法步骤:
- 统计 :统计 A A A里面有多少数等于0,有多少数等于1,...,有多少数等于 k k k
- 累计 :对数组 C C C做累计统计:有多少数是0、有多少数是小于等于1的、...、有多少数是小于等于 k k k的
- 输出 :从 A [ n ] A[n] A[n]到 A [ 1 ] A[1] A[1],把 A [ 1.. n ] A[1..n] A[1..n]中数字输出到 B [ 1.. n ] B[1..n] B[1..n]使得 B B B中数字是排好序的
图解说明:
输入数组A 统计每个值出现次数
数组C 累计统计
C[i] = 小于等于i的个数 从后向前输出
保证稳定性 输出数组B(有序)
💡 说明:从A向B拷贝数据时,是从第"d"大、第"d-1"大、第"d-2"大...这样一个从后向前的次序填放的;而第一行取数据时,同样也采用了"n downto 1"这样一个从后向前的次序,从而保证数值相同的元素,(在B中)排序后,还保持A中原序,从而保证计数排序是稳定排序。
类比理解:
就像统计投票:
- 统计每个候选人的得票数
- 累计统计:有多少人得票数≤1,有多少人得票数≤2,...
- 根据累计统计,确定每个候选人的排名位置
实际例子:
计数排序示例:
输入:A = [4, 5, 3, 0, 2, 3, 4, 2],k = 5
第1步:统计
C[0] = 1, C[1] = 0, C[2] = 2, C[3] = 2, C[4] = 2, C[5] = 1
第2步:累计
C[0] = 1, C[1] = 1, C[2] = 3, C[3] = 5, C[4] = 7, C[5] = 8
第3步:从后向前输出
A[8]=2, C[2]=3 → B[3]=2, C[2]=2
A[7]=4, C[4]=7 → B[7]=4, C[4]=6
...
输出:B = [0, 2, 2, 3, 3, 4, 4, 5]
时间复杂度:O(n)
优点:稳定排序,时间复杂度低
缺点:需要额外空间,要求整数且范围有限
2.2 基数排序(Radix Sort):按位排序
概念的本质:
基数排序要求每个数组元素的数值是由 d d d位数组成的整数,且每一位取 k k k个允许的值之一(如十进制数中每位可以取0到9)。
做法 :从右向左,即从最低位到最高位,逐位排序。这 n n n个数字被排序 d d d次,每次抽取一位作为关键字来对前一次排序的结果作进一步排序。
图解说明:
d位数 第1次:按最低位排序 第2次:按次低位排序 ... 第d次:按最高位排序 排序完成
💡 说明:因为每一位的取值范围是常数k,可以用计数排序来完成。基数排序中,低位排序得到的次序,只有高位相同时才需要尊重。假如采用从高位到低位的排序方式的话,结果会错误。
类比理解:
就像整理文件:
- 先按文件名的最后一个字符排序
- 再按倒数第二个字符排序
- 继续按前面的字符排序
- 最终所有文件都按完整文件名排序
实际例子:
基数排序示例:
输入:[329, 457, 657, 839, 436, 720, 355]
按个位排序(第1次):
720, 355, 436, 457, 657, 329, 839
按十位排序(第2次):
720, 329, 436, 839, 355, 457, 657
按百位排序(第3次):
329, 355, 436, 457, 657, 720, 839
时间复杂度:O(d(n+k)),通常d和k是常数,所以O(n)
优点:稳定排序,时间复杂度低
缺点:需要额外空间,要求是d位整数
2.3 桶排序(Bucket Sort):分桶后排序
概念的本质:
桶排序要求 0 ≤ A [ i ] < 1 0 \leq A[i] < 1 0≤A[i]<1( 1 ≤ i ≤ n 1 \leq i \leq n 1≤i≤n)。不满足时先做变换。
做法:
- 分发 :把这些数分发到编号为0到 ( n − 1 ) (n-1) (n−1)的 n n n个桶里
- 排序:把各桶中数字用插入排序法排序
- 串联 :从0号桶开始到 ( n − 1 ) (n-1) (n−1)号桶,依次把各个桶中排好的序列串连为一个序列
图解说明:
输入数组
0 ≤ A[i] < 1 分发到n个桶
j = ⌊n×A[i]⌋ 桶0
桶1
...
桶n-1 对每个桶用插入排序 串联所有桶 排序完成
💡 说明:桶排序假定输入数据服从均匀分布,但实际上,即使输入数据分布不满足均匀分布,只要所有的桶的大小的平方和与总的元素数呈线性关系,桶排序仍可以在线性时间内完成。
类比理解:
就像分拣邮件:
- 把邮件按邮编分发到不同的邮箱(桶)
- 每个邮箱内的邮件再排序
- 最后按邮箱顺序把所有邮件串联起来
实际例子:
桶排序示例:
输入:A = [0.78, 0.17, 0.39, 0.26, 0.72, 0.94, 0.21, 0.12, 0.23, 0.68],n = 10
分发到桶:
桶0: []
桶1: [0.17, 0.12]
桶2: [0.26, 0.21, 0.23]
桶3: [0.39]
桶6: [0.68]
桶7: [0.78, 0.72]
桶9: [0.94]
对每个桶排序后串联:
[0.12, 0.17, 0.21, 0.23, 0.26, 0.39, 0.68, 0.72, 0.78, 0.94]
时间复杂度:O(n)(平均情况)
优点:时间复杂度低
缺点:需要额外空间,要求数据在[0,1)范围内
三、寻找前k个最大数(Finding Top k Largest Numbers):多种解决方法
这一章要建立的基础 :理解从 n n n个数中找出前 k k k个最大数是一个重要的实际问题,有多种解决方法。
核心问题 :如何高效地从 n n n个数中找出前 k k k个最大的数?
!NOTE
📝 关键点总结:在许多实际问题中,往往需要从 n n n个数中找出 k k k个最大的顺序数。这是一个需要理论联系实际的问题。实际应用中,根据 n n n和 k k k的关系以及 n n n本身的大小来设计最佳算法。
3.1 问题定义(Problem Definition):理解前k个最大数问题
概念的本质:
从 n n n个数的集合或序列中,找出前 k k k个最大的数。
实际应用:
- 从众多的网站中找出访问次数最多的前100个网站
- 从众多的高考考生中找出前1000名高考考分最高的考生
- 从所有视频文件中找出访问次数最多的前1000个视频
图解说明:
n个数
例如:10亿个 找出前k个最大数
例如:前1000个 输出结果
k个最大数
💡 说明:实际应用中,根据n和k的关系以及n本身的大小来设计最佳算法。不同的方法适用于不同的场景。
类比理解:
就像选秀比赛:
- 有 n n n个选手参加
- 需要选出前 k k k名
- 可以用不同的方法:全部排序、部分排序、维护一个候选列表等
实际例子:
前k个最大数问题示例:
输入:n = 10⁹(十亿)个数,k = 1000
目标:找出前1000个最大的数
不同方法的比较:
- 方法1:重复找最大 → 约1000n次比较
- 方法2:Select+比较 → 约60n次比较
- 方法3:归并排序 → 约30n次比较,但需要O(n)空间
- 方法4:堆方法 → 约20n次比较
- 方法5:锦标赛树 → 约n次比较,但需要2n-1空间
3.2 方法1:重复找最大数(Repeated Maximum Finding)
概念的本质:
重复 k k k次使用找最大数的算法。
算法:
- 找出当前数组中的最大值
- 移除或标记该最大值
- 重复 k k k次
复杂度 :约 k n kn kn次比较
图解说明:
数组A[1..n] 第1次:找最大值 移除最大值 第2次:找最大值 移除最大值 ... 第k次:找最大值 得到k个最大数
💡 说明:找最大数需要n-1次比较(定理5.1:任何采用比较的方法在n个互不相等的数字中找出最大顺序数的算法至少需要n-1次比较)。重复k次,总比较次数约为kn。
类比理解:
就像选秀:
- 每次从所有选手中选出第一名
- 把第一名移出
- 重复 k k k次
实际例子:
重复找最大数方法:
输入:n = 10⁹,k = 1000
第1次:找最大值 → n-1次比较
第2次:找最大值 → n-2次比较
...
第1000次:找最大值 → n-1000次比较
总比较次数:约1000n次
时间复杂度:O(kn)
评价:简单但效率低,当k较大时不适用
3.3 方法2:Select算法+比较(Select + Comparison)
概念的本质:
先用Select算法找到第 k k k顺序数,然后与所有数比较,大于等于该数的就是前 k k k个最大数。
步骤:
- 调用Select算法找到第 k k k顺序数 x x x
- 遍历所有数,与 x x x比较
- 大于等于 x x x的数就是前 k k k个最大数
复杂度 :Select算法需要约 60 n 60n 60n次比较,再加上 n n n次比较,总共约 60 n 60n 60n次比较
图解说明:
是 否 数组A[1..n] Select算法
找第k顺序数x 遍历所有数 A[i] ≥ x? 加入结果集 丢弃 得到k个最大数
💡 说明:Select算法可以在O(n)时间内找到第i顺序数。找到第k顺序数后,所有大于等于它的数就是前k个最大数。
类比理解:
就像设定一个分数线:
- 先找到第 k k k名的分数(Select算法)
- 然后找出所有大于等于这个分数的学生
- 这些学生就是前 k k k名
实际例子:
Select+比较方法:
输入:n = 10⁹,k = 1000
步骤1:Select算法找第1000顺序数
→ 约60n次比较
步骤2:与所有数比较
→ n次比较
总比较次数:约60n次
时间复杂度:O(n)
评价:比方法1好,但Select算法实现复杂
3.4 方法3:堆方法(Heap Method):利用堆数据结构
概念的本质:
利用堆来找 k k k个最大的顺序数有两种方法:
方法3a:最小堆方法
- 先取出 k k k个数来建一个最小堆,来存放搜索过程中迄今为止最大的 k k k个数
- 将余下的每个数和堆的根比较
- 如果小于等于根里的数,则丢弃
- 否则,用这个数取代根中的数并修复堆结构
- 依次检查完所有 n n n个数之后,堆中的 k k k个数就是要找的 k k k个最大的数
方法3b:最大堆方法
- 用最大堆来对所有 n n n个数排序
- 从堆中输出 k k k个最大数字后,排序中止
图解说明:
是 否 方法3a:最小堆 建k个元素的最小堆 遍历剩余n-k个数 A[i] > 堆根? 替换堆根并修复 丢弃 堆中k个数即结果 方法3b:最大堆 建n个元素的最大堆 输出k个最大值 结果
💡 说明:最小堆方法最坏情况下大约需要2n log k = 20n次比较(当n=10⁹,k=1000时)。最大堆方法需要大约2n次比较建堆,而后输出k个最大数需要最多2k log n ≈ 60,000 << n次比较。最大堆方法的缺点是,当n很大时,堆所占用的空间很大。
类比理解:
最小堆方法:
- 就像维护一个"前k名"的名单
- 每次遇到更好的,就替换名单中最差的
- 最终名单就是前k名
最大堆方法:
- 就像把所有选手都排名
- 但只需要前k名,所以排到前k名就停止
实际例子:
堆方法示例:
输入:n = 10⁹,k = 1000
方法3a(最小堆):
- 建k个元素的最小堆:O(k)
- 遍历n-k个数,每次最多log k次比较
- 总比较次数:约2n log k = 20n次
方法3b(最大堆):
- 建n个元素的最大堆:约2n次比较
- 输出k个最大值:约2k log n ≈ 60,000次
- 总比较次数:约2n次
- 缺点:需要O(n)空间
评价:方法3a空间效率高,方法3b时间效率高但空间开销大
3.5 方法4:锦标赛树方法(Tournament Tree Method):空间换时间
概念的本质:
利用锦标赛树来找 k k k个最大顺序数:
- 锦标赛树是一个基于比较的排序算法,可以用一棵完全二叉树来描述
- n n n个叶子来存储 n n n个要排序的数字
- 每个内节点代表一次比较,胜者(较大的数)参加下一轮比较
- 根节点处的比较决出冠军,即最大的数
- 找到最大值后,将其原来叶子所在的值改为 − ∞ -\infty −∞,寻找下一个最大的数
图解说明:
n个叶子节点
存储n个数 第1轮:找最大值
n-1次比较 最大值在根节点 将最大值改为-∞ 更新路径
约log n次比较 第2轮:找第2大值 重复k次 得到k个最大数
💡 说明:寻找下一个最大的数值只需要O(log n)次比较,这是因为锦标赛树上内节点的比较,其实绝大部分都没有变,改变的只有上一轮最大值原所在叶子与根节点之间路径上的每个内节点。因此,只需要更新这条路径上的比较结果,就可以决出下一个最大的数。
类比理解:
就像锦标赛:
- 第一轮:所有选手两两比赛,胜者进入下一轮
- 最终决出冠军(最大值)
- 把冠军移出,更新比赛结果
- 很快就能决出第二名(只需要更新冠军路径)
- 重复 k k k次,得到前 k k k名
实际例子:
锦标赛树方法示例:
输入:n = 10⁹,k = 1000
第1轮:找最大值
- 建锦标赛树:n-1次比较
- 找到最大值
第2到k轮:找第2到第k大值
- 每轮只需要更新路径:约log n次比较
- 总共:(k-1)(log n - 1)次比较
总比较次数:
(n-1) + (k-1)(⌈log n⌉ - 1) ≈ n次比较
空间复杂度:2n-1(n-1个内节点+n个叶子)
评价:时间效率最高(约n次比较),但空间开销最大(2n-1空间)
3.6 方法比较(Method Comparison):选择合适的方法
概念的本质:
不同方法适用于不同场景,需要根据 n n n和 k k k的关系来选择。
比较表:
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 重复找最大 | O(kn) | O(1) | k很小 |
| Select+比较 | O(n) | O(1) | k中等 |
| 归并排序 | O(n log n) | O(n) | 需要全部排序 |
| 最小堆 | O(n log k) | O(k) | k较小,空间受限 |
| 最大堆 | O(n + k log n) | O(n) | k较小,时间优先 |
| 锦标赛树 | O(n + k log n) | O(n) | k较小,时间优先 |
图解说明:
是 否 是 否 是 选择方法 k很小?
k << n 最小堆或
锦标赛树 k中等?
k ≈ n/2 Select算法 k很大?
k ≈ n 归并排序
💡 说明:实际应用中,根据n和k的关系以及n本身的大小来设计最佳算法。例如,从十亿个数字中找出前一千个最大的数字,锦标赛树方法最合适(约n次比较)。
类比理解:
就像选择交通工具:
- 短距离:步行(重复找最大)
- 中距离:自行车(Select算法)
- 长距离:汽车(堆方法)
- 超长距离:飞机(锦标赛树)
实际例子:
方法选择示例:
场景1:n = 10⁹,k = 1000
- 推荐:锦标赛树方法
- 理由:时间效率最高(约n次比较),虽然空间开销大,但时间更重要
场景2:n = 10⁶,k = 10
- 推荐:最小堆方法
- 理由:空间效率高(O(k)),时间效率也足够好
场景3:n = 10⁶,k = 5×10⁵
- 推荐:归并排序
- 理由:k很大,接近n,直接排序更简单
📝 本章总结
核心要点回顾:
-
基于比较的排序算法:
- 插入排序:O(n²),简单、稳定、原地
- 堆排序:O(n log n),原地但不稳定
- 归并排序:O(n log n),稳定但需空间
- 快速排序:平均O(n log n),原地但不稳定,平均性能最佳
-
不基于比较的排序算法:
- 计数排序:O(n),稳定,要求整数且范围有限
- 基数排序:O(n),稳定,要求d位整数
- 桶排序:O(n),要求数据在[0,1)范围内
-
前k个最大数问题:
- 方法1:重复找最大 → O(kn)
- 方法2:Select+比较 → O(n)
- 方法3:堆方法 → O(n log k)或O(n + k log n)
- 方法4:锦标赛树 → O(n + k log n),空间换时间
知识地图:
排序算法 基于比较
Ω(n log n)下界 不基于比较
O(n)可能 插入排序
堆排序
归并排序
快速排序 计数排序
基数排序
桶排序 前k个最大数 重复找最大 Select+比较 堆方法 锦标赛树
关键决策点:
- 选择排序算法:根据数据特性(整数/实数、范围、稳定性要求)选择
- 选择前k个最大数方法:根据n和k的关系选择,考虑时间和空间权衡
- 稳定性考虑:如果需要稳定排序,选择插入排序、归并排序、计数排序、基数排序
- 空间考虑:如果空间受限,选择原地排序算法(插入排序、堆排序、快速排序)
💡 延伸学习:排序算法是计算机科学的基础,掌握不同排序算法的原理和特性有助于我们:
- 根据实际需求选择合适的排序算法
- 理解算法设计的各种思想(贪心、分治等)
- 解决前k个最大数等实际问题
- 为后续学习更复杂的算法打下基础