归并排序:Python语言实现
归并排序是一种递归算法,每次将一个列表一分为二。如果列表为空或只有一个元素,那么从定义上来说它就是有序的(基本情况)。如果列表不止一个元素,就将列表一分为二,并对两部分都递归调用归并排序。当两部分都有序后,就进行归并这一基本操作。归并是指将两个较小的有序列表归并为一个有序列表的过程。
下图展示了示例列表被拆分后的情况。
下图给出了归并后的有序列表。
在下面代码中,merge_sort 函数以处理基本情况开始。如果列表的长度小于或等于 1,说明它已经是有序列表,因此不需要做额外的处理。如果长度大于 1,则通过 Python 的切片操作得到左半部分和右半部分。要注意,列表所含元素的个数可能不是偶数。这并没有关系,因为左右子列表的长度最多相差 1。
def merge_sort(alist):
if len(alist) > 1:
mid = len(alist) // 2
lefthalf = alist[:mid]
righthalf = alist[mid:]
merge_sort(lefthalf)
merge_sort(righthalf)
i = 0
j = 0
k = 0
while i < len(lefthalf) and j < len(righthalf):
if lefthalf[i] < righthalf[j]:
alist[k] = lefthalf[i]
i = i + 1
else:
alist[k] = righthalf[j]
j = j + 1
k = k + 1
while i < len(lefthalf):
alist[k] = lefthalf[i]
i = i + 1
k = k + 1
while j < len(righthalf):
alist[k] = righthalf[j]
j = j + 1
k = k + 1
函数首先将原列表分成两个子列表(通过复制),然后在对左右子列表调用 merge_sort 函数后,就假设它们已经排好序了。后面的三个 while 循环负责将两个小的有序列表归并为一个大的有序列表。注意,归并操作每次从有序列表中取出最小值,放回初始列表(alist)。
测试代码:
a_list = [54, 26, 93, 17, 77, 31, 44, 55, 20]
print(f"before sorting: {a_list}")
merge_sort(a_list)
print(f"after sorting: {a_list}")
assert a_list == sorted(a_list)
输出:
before sorting: [54, 26, 93, 17, 77, 31, 44, 55, 20]
after sorting: [17, 20, 26, 31, 44, 54, 55, 77, 93]
性能测试:
from random import randint, seed
seed(13)
lst_to_sort = [randint(100, 999) for _ in range(1000)]
%timeit merge_sort(lst_to_sort)
结果:
697 μs ± 18.6 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
分析 merge_sort 函数时,要考虑它的两个独立的构成部分。首先,列表被一分为二。当列表的长度为 n 时,能切分 次。第二个处理过程是归并。列表中的每个元素最终都得到处理,并被放到有序列表中。所以,得到长度为 n 的列表需要进行 n 次操作。由此可知,需要进行 次拆分,每一次需要进行 n 次操作,所以一共是 次操作。 也就是说,归并排序算法的时间复杂度是 。
你应该记得,切片操作的时间复杂度是 ,其中 k 是切片的大小。为了保证 merge_sort函数的时间复杂度是 ,需要去除切片运算符。在进行递归调用时,传入头和尾的下标即可做到这一点。
def merge_sort(alist):
copybuffer = alist[:]
merge_sort_helper(alist, copybuffer, 0, len(alist) - 1)
def merge_sort_helper(alist, copybuffer, low, high):
# alist: list being sorted
# copybuffer: temp space needed during merge
# low, high: bounds of sublist, [low, high]
# middle: midpoint of sublist
if low < high:
middle = (low + high) // 2
merge_sort_helper(alist, copybuffer, low, middle)
merge_sort_helper(alist, copybuffer, middle+1, high)
merge(alist, copybuffer, low, middle, high)
def merge(alist, copybuffer, low, middle, high):
# alist: list that is being sorted
# copybuffer: temp space needed during the merge process
# low: beginning of first sorted sublist
# middle: end of first sorted sublist
# middle + 1: beginning of second sorted sublist
# high: end of second sorted sublist
i = low
j = middle+1
k = low
while i <= middle and j <= high:
if alist[i] < alist[j]:
copybuffer[k] = alist[i]
i = i + 1
else:
copybuffer[k] = alist[j]
j = j + 1
k = k + 1
while i <= middle:
copybuffer[k] = alist[i]
i = i + 1
k = k + 1
while j <= high:
copybuffer[k] = alist[j]
j = j + 1
k = k + 1
for i in range(low, high + 1):
alist[i] = copybuffer[i]
测试代码:
a_list = [54, 26, 93, 17, 77, 31, 44, 55, 20]
print(f"before sorting: {a_list}")
merge_sort(a_list)
print(f"after sorting: {a_list}")
assert a_list == sorted(a_list)
输出:
before sorting: [54, 26, 93, 17, 77, 31, 44, 55, 20]
after sorting: [17, 20, 26, 31, 44, 54, 55, 77, 93]
性能测试:
from random import randint, seed
seed(13)
lst_to_sort = [randint(100, 999) for _ in range(1000)]
%timeit merge_sort(lst_to_sort)
结果:
882 μs ± 27.6 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
有一点要注意:merge_sort 函数需要额外的空间来存储切片操作得到的两半部分。当列表较大时,使用额外的空间可能会使排序出现问题。
参考文档
- 《Python数据结构与算法分析(第2版)》:5.3.5 归并排序
- 《数据结构Python语言描述》:3.5.2 合并排序