归并排序:修订间差异
AIContentBot(留言 | 贡献) 补充算法词条:原创讲解、Python实例与过程图;整理学习导航和写作标准 |
AIContentBot(留言 | 贡献) 修复 Python 代码语法高亮、行号与算法分步动画 |
||
| (未显示同一用户的1个中间版本) | |||
| 第28行: | 第28行: | ||
长度为 0 或 1 的序列已排序。假设两半经递归得到有序且稳定的结果,上面的归并性质使整段有序且稳定;逐层回到原序列,便得到正确结果。这是按序列长度进行的归纳,而不是仅凭图像观察。 | 长度为 0 或 1 的序列已排序。假设两半经递归得到有序且稳定的结果,上面的归并性质使整段有序且稳定;逐层回到原序列,便得到正确结果。这是按序列长度进行的归纳,而不是仅凭图像观察。 | ||
<math-experiment type="algorithm" demo="merge-sort" /> | |||
== Python 实现 == | == Python 实现 == | ||
函数返回新列表,不改动输入。<code>key</code> 指定比较用的关键字;合并时只移动下标,不从 Python 列表头部反复删除元素,因此归并一段长度为 <math>m</math> 的数据只做 <math>O(m)</math> 次读取、比较和写入。 | 函数返回新列表,不改动输入。<code>key</code> 指定比较用的关键字;合并时只移动下标,不从 Python 列表头部反复删除元素,因此归并一段长度为 <math>m</math> 的数据只做 <math>O(m)</math> 次读取、比较和写入。 | ||
<pre class=" | <div class="math-code-example"> | ||
<div class="math-code-language">Python 3</div> | |||
<pre class="math-code-source" data-language="python"> | |||
def merge_sort(items, key=lambda item: item): | def merge_sort(items, key=lambda item: item): | ||
data = list(items) | data = list(items) | ||
| 第38行: | 第42行: | ||
def sort(lo, hi): | def sort(lo, hi): | ||
if hi - lo | if hi - lo <= 1: | ||
return | return | ||
mid = (lo + hi) // 2 | mid = (lo + hi) // 2 | ||
| 第51行: | 第55行: | ||
buffer[k] = data[i] | buffer[k] = data[i] | ||
i += 1 | i += 1 | ||
elif key(data[j]) | elif key(data[j]) < key(data[i]): | ||
buffer[k] = data[j] | buffer[k] = data[j] | ||
j += 1 | j += 1 | ||
| 第72行: | 第76行: | ||
assert merge_sort([]) == [] | assert merge_sort([]) == [] | ||
</pre> | </pre> | ||
</div> | |||
== 比较次数、空间和适用范围 == | == 比较次数、空间和适用范围 == | ||
2026年9月24日 (四) 02:27的最新版本
归并排序(merge sort)把一列对象分成两半,各自排好序,再把两条有序列合成一条。真正需要说明的是“归并”:每次只比较两条序列尚未取出的第一个元素,就能确定下一位。本文按关键字从小到大排序,并在相等时保留原来的先后次序。
从两条有序列合成一条
取六张带编号的卡片 ,下标 L、R 只用于辨认两张数值相同的 3。先分成左半 和右半 ;递归排好后分别为 与 。随后只看两个队首:
| 比较时的两个队首 | 取出 | 已合成的前缀 |
|---|---|---|
| 2 | ||
| 5 | ||
| 7 |
右半已经用尽后,接上左半的 8,得到 。图把“分开排序”与“最后一次归并”放在同一处;相等的两张 3 中,来自左半的卡片先出。
有序前缀与稳定性
设两条待归并序列已经有序。归并过程中,输出前缀始终有序,并且恰由已取出的最小那些元素组成。开始时前缀为空。下一位必在两条序列的队首中:同一条序列中更靠后的元素不可能比队首更小。取较小队首便保持这个性质;一条序列用尽后,另一条余项本来有序,直接接上即可。
当两个队首关键字相同,选择左半的元素。每一半内部的相对顺序已经由递归保持;两张原本跨半区的相等卡片,左边那张在原输入中也更早。因此排序是稳定的:关键字相等的元素保持输入次序。若在相等时先取右边,示例中的 就会跑到 前面,排序数值仍正确,但稳定性消失。
长度为 0 或 1 的序列已排序。假设两半经递归得到有序且稳定的结果,上面的归并性质使整段有序且稳定;逐层回到原序列,便得到正确结果。这是按序列长度进行的归纳,而不是仅凭图像观察。
归并排序的最后一次合并
比较两条有序序列的队首,观察相等关键字的先后次序。
静态配图与完整推导见本节正文;交互演示需浏览器启用 JavaScript。
Python 实现
函数返回新列表,不改动输入。key 指定比较用的关键字;合并时只移动下标,不从 Python 列表头部反复删除元素,因此归并一段长度为 的数据只做 次读取、比较和写入。
def merge_sort(items, key=lambda item: item):
data = list(items)
buffer = data.copy()
def sort(lo, hi):
if hi - lo <= 1:
return
mid = (lo + hi) // 2
sort(lo, mid)
sort(mid, hi)
i, j = lo, mid
for k in range(lo, hi):
if i == mid:
buffer[k] = data[j]
j += 1
elif j == hi:
buffer[k] = data[i]
i += 1
elif key(data[j]) < key(data[i]):
buffer[k] = data[j]
j += 1
else:
buffer[k] = data[i]
i += 1
data[lo:hi] = buffer[lo:hi]
sort(0, len(data))
return data
if __name__ == "__main__":
cards = [(8, "a"), (3, "L"), (5, "a"),
(3, "R"), (2, "a"), (7, "a")]
answer = merge_sort(cards, key=lambda card: card[0])
assert answer == [(2, "a"), (3, "L"), (3, "R"),
(5, "a"), (7, "a"), (8, "a")]
assert cards[0] == (8, "a")
assert merge_sort([]) == []
比较次数、空间和适用范围
每一层的所有归并段总长是 ,最多有 层,所以按常数时间关键字比较计,时间为 ,不依赖初始排列;额外数组用 空间,递归栈另用 。若 key 自身计算很昂贵,上述分析应把关键字计算的成本也算进去,或预先计算关键字。
稳定排序适合先按次要字段排,再按主要字段排的资料整理,因为后一步不会打乱主要字段相同时的原有次序。归并排序需要保存辅助数据;只要求原地改写且空间极紧时,还应比较其他排序方法的条件与代价。
参考资料
- TheAlgorithms/Python:merge_sort.py;同仓库的迭代变体。本文采用独立编写的下标合并实现。
- Sedgewick、Wayne,Mergesort:归并、稳定性与比较模型下的时间界。
- 先修:算法与复杂度;对照:二分查找、快速排序。