干货版《算法导论》07:递归视角下的选择排序与归并排序
- [Bilibili 同步视频](#Bilibili 同步视频)
- [一、🌿 选择排序:每一步都「选最大」,朴素却深刻](#一、🌿 选择排序:每一步都「选最大」,朴素却深刻)
-
- [1.1 核心直觉:一句话讲懂](#1.1 核心直觉:一句话讲懂)
- [1.2 递归实现:理论版写法(非工程最优)](#1.2 递归实现:理论版写法(非工程最优))
- [① 辅助函数:prefix_max(找 0~i 最大值下标)](#① 辅助函数:prefix_max(找 0~i 最大值下标))
- [② 选择排序主递归](#② 选择排序主递归)
- [1.3 正确性证明:数学归纳法](#1.3 正确性证明:数学归纳法)
- [1.4 时间复杂度:为什么是 Θ(n²)?](#1.4 时间复杂度:为什么是 Θ(n²)?)
- [① prefix_max 复杂度](#① prefix_max 复杂度)
- [② 选择排序总复杂度](#② 选择排序总复杂度)
- [二、🚀 归并排序:分治的艺术,从 O (n²) 飞跃到 O (n log n)](#二、🚀 归并排序:分治的艺术,从 O (n²) 飞跃到 O (n log n))
-
- [2.1 核心直觉:分而治之,两两合并](#2.1 核心直觉:分而治之,两两合并)
- [2.2 双指合并(Two-Finger Algorithm)](#2.2 双指合并(Two-Finger Algorithm))
- [2.3 递归伪代码(清晰版)](#2.3 递归伪代码(清晰版))
- [2.4 复杂度证明:为什么是 Θ(n log n)?](#2.4 复杂度证明:为什么是 Θ(n log n)?)
- [三、📊 两大算法硬核对比(一看就懂)](#三、📊 两大算法硬核对比(一看就懂))
- [四、💡 结语:算法之美,在于「思想」而非「代码」](#四、💡 结语:算法之美,在于「思想」而非「代码」)
Bilibili 同步视频
在算法的世界里,排序是最朴素、最经典,也最能体现「思想之美」的基石问题。我们每天都在和有序序列打交道,而背后支撑这一切的,是一套套精巧的排序逻辑。
今天,我们不聊花哨的工程优化,只从递归 + 数学证明的视角,拆解两种极具代表性的排序:
-
🌱 选择排序(Selection Sort):直观、简单,但藏着 O (n²) 的宿命
-
⚡ 归并排序(Merge Sort):分治之神,稳定 O (n log n) 的效率王者
全文会用符号、公式、伪代码、复杂度推导,把原理讲透,把证明写清,带你看懂「为什么排序能从平方级跃迁至对数级」。
一、🌿 选择排序:每一步都「选最大」,朴素却深刻
1.1 核心直觉:一句话讲懂
把数组看成「未排序区」和「已排序区」:
-
从未排序区里找到最大值
-
把它交换到未排序区的末尾
-
未排序区缩小一位,重复直到全部有序
举个栗子🌰:
数组:[8, 2, 4, 9, 3]
-
第一轮:找最大
9↔ 末尾3交换 →[8,2,4,3, | 9] -
第二轮:左边找最大
8↔3交换 →[3,2,4, | 8,9] -
......
-
最终:
[2,3,4,8,9]
红线右侧就是已排序区 ,单元素天然有序,这就是递归的归纳基础。
1.2 递归实现:理论版写法(非工程最优)
我们用递归重写选择排序,只为正确性证明 + 复杂度分析。
① 辅助函数:prefix_max(找 0~i 最大值下标)
Plain
// 递归求 arr[0...i] 中的最大值下标
prefix_max(arr, i):
if i == 0:
return 0 // base case:只有一个元素
j = prefix_max(arr, i-1)
if arr[i] > arr[j]:
return i
else:
return j
✅ 思想:最大值要么在 i 位置,要么在 0~i-1 里------ 这就是「递归的信仰」。
② 选择排序主递归
Plain
// 递归选择排序:排序 arr[0...i]
selection_sort(arr, i):
if i <= 0:
return
// 1. 找 0~i 最大元素下标
max_idx = prefix_max(arr, i)
// 2. 交换到末尾
swap(arr[max_idx], arr[i])
// 3. 递归排序左边 0~i-1
selection_sort(arr, i-1)
工程上我们用双层 for 循环,但递归版更易证明正确性与复杂度。
1.3 正确性证明:数学归纳法
-
Base Case:i=0,单元素有序,成立。
-
Inductive Step :假设
prefix_max(arr, i-1)正确找到 0i-1 最大值,则只需再比较arr[i],即可得到 0i 最大值。 -
结论:算法总能把当前最大元素放到正确位置,递归收缩后整体有序。
1.4 时间复杂度:为什么是 Θ(n²)?
① prefix_max 复杂度
Plain
s(n) = s(n-1) + Θ(1)
s(1) = Θ(1)
展开得:s(n) = Θ(n)
② 选择排序总复杂度
Plain
T(n) = T(n-1) + Θ(n)
T(1) = Θ(1)
展开:
T (n) = Θ(n) + Θ(n-1) + ... + Θ(1) = Θ(n²)
用代入法验证 :
假设 T (n) ≤ c・n²
c・n² ≤ c・(n-1)² + Θ(n)
c・n² ≤ c・n² - 2cn + c + Θ(n)
0 ≤ -2cn + c + Θ(n)
成立 → T(n) = Θ(n²)
二、🚀 归并排序:分治的艺术,从 O (n²) 飞跃到 O (n log n)
2.1 核心直觉:分而治之,两两合并
归并排序是分治思想的完美示范:
-
分解:把数组从中间切两半
-
解决:递归排序左半、右半
-
合并:用「双指算法」把两个有序数组合成一个大有序数组
关键洞察:单个元素天然有序 → 递归到底层全是最小有序单元。
2.2 双指合并(Two-Finger Algorithm)
合并两个有序数组 A=[1,5,6,7]、B=[2,3,4,9]:
-
两指针分别指向末尾
-
每次取更大的放到结果末尾
-
指针左移,直到全部合并
优点:只遍历一遍,Θ(n) 完成合并。
2.3 递归伪代码(清晰版)
Plain
// 归并排序主函数
merge_sort(arr, l, r):
if l >= r:
return
mid = (l + r) // 2
merge_sort(arr, l, mid) // 左有序
merge_sort(arr, mid+1, r) // 右有序
merge(arr, l, mid, r) // 双指合并
// 双指合并:合并 arr[l..mid] 与 arr[mid+1..r]
merge(arr, l, mid, r):
新建临时数组 temp
i = l, j = mid+1, k = 0
while i ≤ mid && j ≤ r:
if arr[i] ≤ arr[j]:
temp[k++] = arr[i++]
else:
temp[k++] = arr[j++]
// 复制剩余部分
while i ≤ mid: temp[k++] = arr[i++]
while j ≤ r: temp[k++] = arr[j++]
// 拷回原数组
for t from 0 to r-l:
arr[l+t] = temp[t]
2.4 复杂度证明:为什么是 Θ(n log n)?
递归式:
Plain
T(n) = 2·T(n/2) + Θ(n)
T(1) = Θ(1)
代入法证明 :
假设 T (n) ≤ c・n log n
左边:c・n log n
右边:2・c・(n/2)・log (n/2) + Θ(n)
= c・n・(log n - 1) + Θ(n)
= c・n log n - c・n + Θ(n)
两边抵消 c・n log n,得:
0 ≤ -c・n + Θ(n)
成立 → T(n) = Θ(n log n)
三、📊 两大算法硬核对比(一看就懂)
| 维度 | 选择排序 Selection Sort | 归并排序 Merge Sort |
|---|---|---|
| 思想 | 贪心 + 选择极值 | 分治 + 递归合并 |
| 时间复杂度 | Θ(n²) | Θ(n log n) |
| 空间复杂度 | Θ(1) 原地排序 | Θ(n) 需临时空间 |
| 稳定性 | 不稳定 | 稳定 |
| 适用场景 | 小数据、教学演示 | 大数据、稳定排序 |
| 最优 / 最坏 | 始终 Θ(n²) | 始终 Θ(n log n) |
一句话总结:
-
选择排序:简单但慢,适合理解递归与归纳
-
归并排序:高效稳定,是真正工业级的排序基石
四、💡 结语:算法之美,在于「思想」而非「代码」
从选择排序到归并排序,我们看到的不只是效率提升,更是思维方式的跃迁:
-
选择排序:用「选择」解决问题,简单直观,却逃不开 O (n²)
-
归并排序:用「分治」拆解问题,把复杂变简单,直达 O (n log n)
算法从来不是枯燥的循环与递归,而是数学之美、逻辑之美、工程之美的结合。

下次当你面对无序数据时,不妨想
你是要「一步步选到有序」,还是「分而治之,快速归并」?