快速排序:Python语言实现

1,396 阅读6分钟

快速排序:Python语言实现

快速排序采用分治策略,但不使用额外的存储空间。不过,代价是列表可能不会被一分为二。出现这种情况时,算法的效率会有所下降。

快速排序算法首先选出一个基准值。尽管有很多种选法,但为简单起见,选取列表中的第一个元素作为基准值。基准值的作用是帮助切分列表。在最终的有序列表中,基准值的位置通常被称作分割点,算法在分割点切分列表,以进行对快速排序的子调用。

在下图中,元素 54 将作为第一个基准值。下一步是分区操作。它会找到分割点,同时将其他元素放到正确的一边——要么大于基准值,要么小于基准值。

quick_sort1.jpg

分区操作首先找到两个坐标——leftmark 和 rightmark——它们分别位于列表剩余元素的开头和末尾,如下图所示。分区的目的是根据待排序元素与基准值的相对大小将它们放到正确的一边,同时逐渐逼近分割点。下图展示了为元素 54 寻找正确位置的过程。

quick_sort2.jpg

首先加大 leftmark,直到遇到一个大于基准值的元素。然后减小 rightmark,直到遇到一个小于基准值的元素。这样一来,就找到两个与最终的分割点错序的元素。本例中,这两个元 素就是 93 和 20。互换这两个元素的位置,然后重复上述过程。

当 rightmark 小于 leftmark 时,过程终止。此时,rightmark 的位置就是分割点。将基准值与当前位于分割点的元素互换,即可使基准值位于正确位置,如下图所示。分割点左边的所有元素都小于基准值,右边的所有元素都大于基准值。因此,可以在分割点处将列表一分为二,并针对左右两部分递归调用快速排序函数。

quick_sort3.jpg

在下面代码中,快速排序函数 quick_sort 调用了递归函数 quick_sort_helper。quick_sort_helper 首先处理和归并排序相同的基本情况。如果列表的长度小于或等于 1,说明 它已经是有序列表;如果长度大于 1,则进行分区操作并递归地排序。分区函数 partition 实现了前面描述的过程。

def quick_sort(alist):
    quick_sort_helper(alist, 0, len(alist)-1)

def quick_sort_helper(alist, first, last):
    if first < last:
        splitpoint = partition(alist, first, last)

        quick_sort_helper(alist, first, splitpoint-1)
        quick_sort_helper(alist, splitpoint+1, last)

def partition(alist, first, last):
    pivotvalue = alist[first]

    leftmark = first + 1
    rightmark = last

    done = False
    while not done:

        while leftmark <= rightmark and alist[leftmark] <= pivotvalue:
            leftmark = leftmark + 1

        while alist[rightmark] >= pivotvalue and rightmark >= leftmark:
            rightmark = rightmark - 1

        if rightmark < leftmark:
            done = True
        else:
            temp = alist[leftmark]
            alist[leftmark] = alist[rightmark]
            alist[rightmark] = temp

    temp = alist[first]
    alist[first] = alist[rightmark]
    alist[rightmark] = temp

    return rightmark

测试代码:

a_list = [54, 26, 93, 17, 77, 31, 44, 55, 20]
print(f"before sorting: {a_list}")
quick_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 quick_sort(lst_to_sort)

结果:

8.71 ms ± 331 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)

在分析 quick_sort 函数时要注意,对于长度为 n 的列表,如果分区操作总是发生在列表的中部,就会切分 lognlogn 次。为了找到分割点,n 个元素都要与基准值比较。所以,时间复杂度是 O(nlogn)O(n logn) 。另外,快速排序算法不需要像归并排序算法那样使用额外的存储空间。

不幸的是,最坏情况下,分割点不在列表的中部,而是偏向某一端,这会导致切分不均匀。在这种情况下,含有 n 个元素的列表可能被分成一个不含元素的列表与一个含有 n–1 个元素的列 表。然后,含有 n–1 个元素的列表可能会被分成不含元素的列表与一个含有 n–2 个元素的列表,依此类推。这会导致时间复杂度变为 O(n2)O(n^2) ,因为还要加上递归的开销。

前面提过,有多种选择基准值的方法。可以尝试使用三数取中法避免切分不均匀,即在选择基准值时考虑列表的头元素、中间元素与尾元素。本例中,先选取元素 54、77 和 20,然后取 中间值 54 作为基准值(当然,它也是之前选择的基准值)。这种方法的思路是,如果头元素的正确位置不在列表中部附近,那么三元素的中间值将更靠近中部。当原始列表的起始部分已经有 序时,这一招尤其管用。

下面代码增加median3函数,实现了三数取中法,可以通过性能测试看出性能的提升。

def quick_sort(alist):
    quick_sort_helper(alist, 0, len(alist)-1)

def quick_sort_helper(alist, first, last):
    if first < last:
        splitpoint = partition(alist, first, last)

        quick_sort_helper(alist, first, splitpoint-1)
        quick_sort_helper(alist, splitpoint+1, last)

def partition(alist, first, last):
    median3(alist, first, last)
    pivotvalue = alist[first]

    leftmark = first + 1
    rightmark = last

    done = False
    while not done:

        while leftmark <= rightmark and alist[leftmark] <= pivotvalue:
            leftmark = leftmark + 1

        while alist[rightmark] >= pivotvalue and rightmark >= leftmark:
            rightmark = rightmark - 1

        if rightmark < leftmark:
            done = True
        else:
            temp = alist[leftmark]
            alist[leftmark] = alist[rightmark]
            alist[rightmark] = temp

    temp = alist[first]
    alist[first] = alist[rightmark]
    alist[rightmark] = temp

    return rightmark

def median3(alist, first, last):
    center = (first + last) // 2
    medianIndex = first

    if alist[first] < alist[last]:
        if alist[center] < alist[first]:
            medianIndex = first
        elif alist[last] < alist[center]:
            medianIndex = last
        else:
            medianIndex = center
    else:
        if alist[center] < alist[last]:
            medianIndex = last
        elif alist[first] < alist[center]:
            medianIndex = first
        else:
            medianIndex = center

    if medianIndex != first:
        temp = alist[first]
        alist[first] = alist[medianIndex]
        alist[medianIndex] = temp

测试代码:

a_list = [54, 26, 93, 17, 77, 31, 44, 55, 20]
print(f"before sorting: {a_list}")
quick_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 quick_sort(lst_to_sort)

性能测试:

490 μs ± 14.3 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)

参考文档

《Python数据结构与算法分析(第2版)》:5.3.6 快速排序