排序就是把一组数按从小到大(或从大到小)排好。比较排序靠两两比大小,平均最快大约是 O(n log n)。计数排序这类不比大小,在整数范围不大时可以做到 O(n)。
下面都按升序,输入用 [5, 2, 4, 6, 1, 3],代码不改原列表,返回新列表。
| 算法 | 平均 | 最坏 | 额外空间 | 稳定 |
|---|---|---|---|---|
| 冒泡 | O(n²) | O(n²) | O(1) | 是 |
| 选择 | O(n²) | O(n²) | O(1) | 否 |
| 插入 | O(n²) | O(n²) | O(1) | 是 |
| 归并 | O(n log n) | O(n log n) | O(n) | 是 |
| 快速 | O(n log n) | O(n²) | O(log n) 栈 | 否 |
| 堆 | O(n log n) | O(n log n) | O(1) | 否 |
| 计数 | O(n + k) | O(n + k) | O(n + k) | 是 |
k 是数值范围。稳定的意思是:相等的两个数,排序后仍保持原来的先后顺序。
冒泡排序
相邻两个数如果左边更大,就交换。每一轮都会把当前未排序部分里最大的数「冒」到末尾。某一轮没有发生交换,说明已经有序,可以提前结束。
[5, 2, 4, 1] 第一轮:
5, 2, 4, 1
2, 5, 4, 1
2, 4, 5, 1
2, 4, 1, 5
5 已经到位。再继续,直到剩下的部分也有序。
def bubble_sort(a):
a = a:
n = len(a)
for i in range(n):
swapped = False
for j in range(n - 1 - i):
if aj > aj + 1:
aj, aj + 1 = aj + 1, aj
swapped = True
if not swapped:
break
return a
选择排序
每一轮在未排序区间里找出最小的数,放到这个区间的最前面。
[5, 2, 4, 6, 1, 3]:
第 1 轮最小值 1,和 5 交换 → 1, 2, 4, 6, 5, 3
第 2 轮从下标 1 起,2 已是最小 → 1, 2, 4, 6, 5, 3
第 3 轮最小值 3,和 4 交换 → 1, 2, 3, 6, 5, 4
第 4 轮最小值 4,和 6 交换 → 1, 2, 3, 4, 5, 6
def selection_sort(a):
a = a:
for i in range(len(a)):
min_i = i
for j in range(i + 1, len(a)):
if aj < amin_i:
min_i = j
ai, amin_i = amin_i, ai
return a
插入排序
把序列看成「左手已排好、右手待插入」。每次取出右手第一个数,在左手里从右往左找位置插进去。数据几乎有序时很快,接近 O(n)。
[5, 2, 4, 6, 1, 3]:
5 | 2, 4, 6, 1, 3
2, 5 | 4, 6, 1, 3
2, 4, 5 | 6, 1, 3
2, 4, 5, 6 | 1, 3
1, 2, 4, 5, 6 | 3
1, 2, 3, 4, 5, 6
def insertion_sort(a):
a = a:
for i in range(1, len(a)):
key = ai
j = i - 1
while j >= 0 and aj > key:
aj + 1 = aj
j -= 1
aj + 1 = key
return a
归并排序
先对半拆开,各自排好,再把两个有序序列合并。合并时每次取两边当前更小的那个。拆分是 log n 层,每层合并一共看 n 个数,所以是 O(n log n)。相等时先取左边,因此是稳定的。
[5, 2, 4, 1] 的合并过程:
5, 2\] 和 \[4, 1
5\] 与 \[2\] → \[2, 5
4\] 与 \[1\] → \[1, 4
2, 5\] 与 \[1, 4\] → \[1, 2, 4, 5
def merge_sort(a):
if len(a) <= 1:
return a:
mid = len(a) // 2
left = merge_sort(a:mid)
right = merge_sort(amid:)
out = \[\]
i = j = 0
while i < len(left) and j < len(right):
if lefti <= rightj:
out.append(lefti)
i += 1
else:
out.append(rightj)
j += 1
out.extend(lefti:)
out.extend(rightj:)
return out
快速排序
选一个基准(这里用区间最后一个)。比基准小或相等的放到左边,比基准大的放到右边,基准落在最终位置,再递归排左右两段。平均 O(n log n)。如果每次基准都是当前区间的最大或最小,会退化成 O(n²)。
[5, 2, 4, 6, 1, 3],基准是 3:
扫描后:2, 1, 3, 6, 5, 4
↑ 3 已在最终位置
左边 2, 1 ,右边 6, 5, 4
def quick_sort(a):
a = a:
def sort(lo, hi):
if lo >= hi:
return
pivot = ahi
i = lo
for j in range(lo, hi):
if aj <= pivot:
ai, aj = aj, ai
i += 1
ai, ahi = ahi, ai
sort(lo, i - 1)
sort(i + 1, hi)
if a:
sort(0, len(a) - 1)
return a
堆排序
先把数组建成大顶堆:每个父节点都大于等于它的子节点,堆顶是最大值。然后反复把堆顶换到末尾,堆的大小减一,再把新的堆顶沉下去。全程 O(n log n),额外空间 O(1)。
下标 i 的左孩子是 2*i+1,右孩子是 2*i+2。
def heap_sort(a):
a = a:
def sift_down(start, end):
root = start
while True:
child = 2 * root + 1
if child >= end:
break
if child + 1 < end and achild < achild + 1:
child += 1
if aroot < achild:
aroot, achild = achild, aroot
root = child
else:
break
n = len(a)
for i in range(n // 2 - 1, -1, -1):
sift_down(i, n)
for end in range(n - 1, 0, -1):
a0, aend = aend, a0
sift_down(0, end)
return a
计数排序
适合整数,而且最大值和最小值差距不大。先统计每个值出现了几次,再按值从小到大写回。范围是 k 时,时间是 O(n + k)。下面这份对负数也适用。
[-1, 2, -1, 0] 的计数(偏移后下标对应 -1、0、1、2):
次数:2, 1, 0, 1
写回:-1, -1, 0, 2
def counting_sort(a):
if not a:
return \[\]
lo, hi = min(a), max(a)
counts = 0 * (hi - lo + 1)
for x in a:
countsx - lo += 1
out = \[\]
for value, count in enumerate(counts):
out.extend(value + lo * count)
return out
怎么选
- 数据量小,或基本有序:插入排序就够用。
- 要稳定、又要 O(n log n):用归并。
- 平均要快、可以接受最坏 O(n²):用快速排序;基准选得随机一些,最坏情况更少见。
- 要原地、又要稳定的最坏时间:用堆排序。
- 整数且范围不大:用计数排序。
Python 自带的 sorted() 和 list.sort() 用的是 Timsort:归并和插入的组合,稳定,最坏 O(n log n),对接近有序的数据更快。日常直接用它。上面这些实现适合看清每一步在做什么。
可以放在一个文件里直接跑:
data = 5, 2, 4, 6, 1, 3
print(bubble_sort(data))
print(selection_sort(data))
print(insertion_sort(data))
print(merge_sort(data))
print(quick_sort(data))
print(heap_sort(data))
print(counting_sort(data))
print(sorted(data))