归并排序:Python语言实现

38 阅读4分钟

归并排序:Python语言实现

归并排序是一种递归算法,每次将一个列表一分为二。如果列表为空或只有一个元素,那么从定义上来说它就是有序的(基本情况)。如果列表不止一个元素,就将列表一分为二,并对两部分都递归调用归并排序。当两部分都有序后,就进行归并这一基本操作。归并是指将两个较小的有序列表归并为一个有序列表的过程。

下图展示了示例列表被拆分后的情况。

merge_sort1.jpg

下图给出了归并后的有序列表。

merge_sort2.jpg

在下面代码中,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 时,能切分 log2nlog_{2} n 次。第二个处理过程是归并。列表中的每个元素最终都得到处理,并被放到有序列表中。所以,得到长度为 n 的列表需要进行 n 次操作。由此可知,需要进行 lognlogn 次拆分,每一次需要进行 n 次操作,所以一共是 nlognn logn 次操作。 也就是说,归并排序算法的时间复杂度是 O(nlogn)O(n logn)

你应该记得,切片操作的时间复杂度是 O(k)O(k) ,其中 k 是切片的大小。为了保证 merge_sort函数的时间复杂度是 O(nlogn)O(n logn) ,需要去除切片运算符。在进行递归调用时,传入头和尾的下标即可做到这一点。

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 合并排序