基本排序算法

从本章开始讲常见的基于比较的排序算法,先讲三个简单的但是时间复杂度却不太理想的排序算法,包括冒泡排序、选择排序和插入排序。

冒泡排序

bubble sort 可以说是最简单的一种排序算法了,它的思想如下。对一个数组进行 n-1 轮迭代,每次比较相邻两个元素, 如果相邻的元素前者大于后者,就交换它们。因为直接在元素上操作而不是返回新的数组,所以是一个 inplace 的操作。 这里冒泡的意思其实就是每一轮冒泡一个最大的元素就会通过不断比较和交换相邻元素使它转移到最右边。

你可以想象假如有 10 个小盆友从左到右站成一排,个头不等。老师想让他们按照个头从低到高站好,于是他开始喊口号。 每喊一次,从第一个小盆友开始,相邻的小朋友如果身高不是正序就会两两调换,就这样第一轮个头最高的排到了最右边。(冒泡到最右边) 第二轮依次这么来,从第一个小朋友开始两两交换,这样次高的小盆友又排到了倒数第二个位置。依次类推。

我们在视频里手动模拟下它的过程。

import random


def bubble_sort(seq):  # O(n^2), n(n-1)/2 = 1/2(n^2 + n)
    n = len(seq)
    for i in range(n-1):
        print(seq)    # 我打印出来让你看清楚每一轮最高、次高、次次高...的小朋友会冒泡到右边
        for j in range(n-1-i):  # 这里之所以 n-1 还需要 减去 i 是因为每一轮冒泡最大的元素都会冒泡到最后,无需再比较
            if seq[j] > seq[j+1]:
                seq[j], seq[j+1] = seq[j+1], seq[j]
    print(seq)


def test_bubble_sort():
    seq = list(range(10))  # 注意 python3 返回迭代器,所以我都用 list 强转了,python2 range 返回的就是 list
    random.shuffle(seq)   # shuffle inplace 操作,打乱数组
    bubble_sort(seq)
    assert seq == sorted(seq)  # 注意呦,内置的 sorted 就不是 inplace 的,它返回一个新的数组,不影响传入的参数

""" 我打印出来让你看到每次从最高到次高的小盆友就这么排好序了,因为是随机数,你第一个没有排序的数组应该和我的不一样
[3, 4, 5, 0, 9, 1, 7, 8, 6, 2]
[3, 4, 0, 5, 1, 7, 8, 6, 2, 9]
[3, 0, 4, 1, 5, 7, 6, 2, 8, 9]
[0, 3, 1, 4, 5, 6, 2, 7, 8, 9]
[0, 1, 3, 4, 5, 2, 6, 7, 8, 9]
[0, 1, 3, 4, 2, 5, 6, 7, 8, 9]
[0, 1, 3, 2, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
"""

选择排序

刚才看到冒泡是每轮迭代中,如果相邻的两个元素前者大于后者了就交换两个相邻元素(假设正序排序)。其实还有一种思路就是, 每次我们找到最小的元素插入迭代的起始位置,这样每个位置从它自己的位置开始它就是最小的了,一圈下来数组就有序了。 选择可以理解为 一个 0 到 n-1 的迭代,每次向后查找选择一个最小的元素。

同样小盆友又来啦,这次我们从第一个开始,从头到尾找一个个头最小的小盆友,然后把它和第一个小盆友交换。 然后从第二个小盆友开始采取同样的策略,这样一圈下来小盆友就有序了。

def select_sort(seq):
    n = len(seq)
    for i in range(n-1):
        min_idx = i    # 我们假设当前下标的元素是最小的
        for j in range(i+1, n):    # 从 i 的后边开始找到最小的元素,得到它的下标
            if seq[j] < seq[min_idx]:
                min_idx = j    # 一个 j 循环下来之后就找到了最小的元素它的下标
        if min_idx != i:    # swap
            seq[i], seq[min_idx] = seq[min_idx], seq[i]


def test_select_sort():
    seq = list(range(10))
    random.shuffle(seq)
    select_sort(seq)
    assert seq == sorted(seq)

"""
[4, 7, 5, 3, 6, 0, 2, 9, 8, 1]
[0, 7, 5, 3, 6, 4, 2, 9, 8, 1]
[0, 1, 5, 3, 6, 4, 2, 9, 8, 7]
[0, 1, 2, 3, 6, 4, 5, 9, 8, 7]
[0, 1, 2, 3, 6, 4, 5, 9, 8, 7]
[0, 1, 2, 3, 4, 6, 5, 9, 8, 7]
[0, 1, 2, 3, 4, 5, 6, 9, 8, 7]
[0, 1, 2, 3, 4, 5, 6, 9, 8, 7]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

"""

插入排序

插入排序很多教科书都是用扑克牌的例子讲的,想象你手里有一些扑克牌,它们顺序是散乱的,现在需要你把它们整理成有序的,你会怎么做呢? 首先拿最顶上的一张,然后拿第二张,第二张点数大,你就把第二张放在第一张的下边,否则放在第一张上边。 当你拿第三张的时候,你同样会找到适合它大小的位置插入进去。

换成小朋友一样,第一个小盆友只有一个人我们假设是有序的,然后第二个小盆友会跟第一个比,如果第一个高就交换位置。 接下来第三个小盆友从第二个位置开始比较,如果没第二个高就交换位置,然后没第一个高也交换位置,保持前边三个小盆友身高有序就好。 依次类推,等到最后一个小盆友也转移到合适的位置,整个队列就是有序的了。

插入排序就是这个道理, 每次挑选下一个元素插入已经排序的数组中,初始时已排序数组只有一个元素。我们就直接上代码吧。

def insertion_sort(seq):
    """ 每次挑选下一个元素插入已经排序的数组中,初始时已排序数组只有一个元素"""
    n = len(seq)
    print(seq)
    for i in range(1, n):
        value = seq[i]    # 保存当前位置的值,因为转移的过程中它的位置可能被覆盖
        # 找到这个值的合适位置,使得前边的数组有序 [0,i] 有序
        pos = i
        while pos > 0 and value < seq[pos-1]:
            seq[pos] = seq[pos-1]  # 如果前边的元素比它大,就让它一直前移
            pos -= 1
        seq[pos] = value    # 找到了合适的位置赋值就好
        print(seq)


""" 不断把新元素放到已经有序的数组中
[1, 7, 3, 0, 9, 4, 8, 2, 6, 5]
[1, 7, 3, 0, 9, 4, 8, 2, 6, 5]
[1, 3, 7, 0, 9, 4, 8, 2, 6, 5]
[0, 1, 3, 7, 9, 4, 8, 2, 6, 5]
[0, 1, 3, 7, 9, 4, 8, 2, 6, 5]
[0, 1, 3, 4, 7, 9, 8, 2, 6, 5]
[0, 1, 3, 4, 7, 8, 9, 2, 6, 5]
[0, 1, 2, 3, 4, 7, 8, 9, 6, 5]
[0, 1, 2, 3, 4, 6, 7, 8, 9, 5]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
"""

思考题

  • 本章介绍的几个排序算法平均时间复杂度是多少?
  • 请你补充插入排序的单元测试代码

延伸阅读

  • 《Data Structures and Algorithms in Python》第5章