计算之魂 关于排序的讨论

1. 关于排序的讨论

排序算法是最基础的、在程序中使用频率最高的算法之一,通过排序算法,可以培养我们看出算法中做无用功的门道。

排序算法根据时间复杂度可分为O(nlogn)、O(n2),本质上O(nlogn)的算法就是移除了O(n2)算法中的无用项。

要理解它们,关键要掌握两个计算机科学的精髓——递归和分治

1.1 直观的排序算法

选择排序:冒泡排序,比较相邻元素,保证大的在后面,时间复杂度为O(n^2)

def selection_sort(a):
    for i in range(len(a) - 1, -1, -1):
        for j in range(0, i):
            if a[j] > a[j + 1]:
                a[j], a[j + 1] = a[j + 1], a[j]
    return a

插入排序:从后往前扫描,找到相应的位置插入,时间复杂度为O(n^2)

def insertion_sort(a):
    for i in range(1, len(a)):
        temp = a[i]
        j = i
        while j > 0 and a[j - 1] > temp:
            a[j] = a[j - 1]
            j -= 1
        a[j] = temp
    return a

上述两种排序说明效率不高的算法的主要问题是存在大量的,甚至重复的无用功,而提高算法效率,就需要分析哪些计算是不可或缺的、哪些是无用功。

1.2 有效的排序算法

归并排序、快速排序、堆排序

这里给出归并排序的代码:

def merge(left_arr, right_arr):
    arr = []
    while left_arr and right_arr:
        if left_arr[0] <= right_arr[0]:
            arr.append(left_arr.pop(0))
        else:
            arr.append(right_arr.pop(0))
            
    while left_arr:
        arr.append(left_arr.pop(0))
        
   	while right_arr:
        arr.append(right_arr.pop(0))
        
    return arr


def merge_sort(a):
    size = len(a)
    if size < 2:
        return a
    mid = len(a) // 2
    left_a, right_a = a[0: mid], a[mid:]
    return merge(merge_sort(left_a), merge_sort(right_a))

以上三种排序算法的对比:

算法 平均时间复杂度 最坏时间复杂度 额外空间复杂度 稳定性
归并排序 O(nlogn) O(nlogn) O(n) True
堆排序 O(nlogn) O(nlogn) O(1) False
快速排序 O(nlogn) O(n^2) O(logn) False

为什么归并排序要比选择排序和插入排序要快呢?因为在归并排序的合并过程中,获得当前最小的元素只需要让两个可能的最小元素进行一次比较,而在选择排序中,则需要让一个元素和几乎所有元素比较。

计算机科学领域做事的两个原则:

  • 要尽可能地避免那些做了大量无用功的方法
  • 接近理论最佳值地算法可能有很多种,除了单纯靠量计算时间外,可能还有很多考量地维度

1.3 针对特殊情况,是否有更好的答案

归并排序、桶排序、快速排序都有各自的优缺点,特别是快速排序,由于不能满足稳定性,在处理多列表格时比较麻烦。因此,今天人们对排序算法的改进大多是结合多种排序算法地思想,形成混合排序算法(Hybrid Sorting Algorithm),比如将快速排序和堆排序结合起来的内省排序(Introspective Sort,大多数标准函数库中地排序使用的算法),还有蒂姆排序(Timsort,是Java和安卓操作系统内部使用的排序算法,也是Python默认使用的排序算法)

Timsort灵活地利用了插入排序简单直观及归并排序效率高的特点,并且找到了归并排序的一些可以进一步提高的地方,即归并过程中过多地一对一比较大小。

能够在解决实际问题时自觉应用Timsort的那些原则,就有了成为3-2.5级工程师的潜力。

1.4 思考题

  • 赛跑问题(GS)

假定有25名短跑选手比赛争夺前三名,赛场上有五条赛道,一次可以有五名选手同时比赛。比赛并不计时,只看相应的名次。加入选手的发挥是稳定的,即A比B跑得快,B比C跑得快,A一定比C跑得快,最少需要几组比赛能决出前三名?

分析:

  • 要想得出前三名,一定要把所有人都跑一遍,25名选手,一次五组,因此一定要至少跑五次,不妨设为五组,每组前面的人快于后面的人,如下:

A 1 , A 2 , A 3 , A 4 , A 5 B 1 , B 2 , B 3 , B 4 , B 5 C 1 , C 2 , C 3 , C 4 , C 5 D 1 , D 2 , D 3 , D 4 , D 5 E 1 , E 2 , E 3 , E 4 , E 5 A_1, A_2, A_3, A_4, A_5 \\ B_1, B_2, B_3, B_4, B_5 \\ C_1, C_2, C_3, C_4, C_5 \\ D_1, D_2, D_3, D_4, D_5 \\ E_1, E_2, E_3, E_4, E_5 A1,A2,A3,A4,A5B1,B2,B3,B4,B5C1,C2,C3,C4,C5D1,D2,D3,D4,D5E1,E2,E3,E4,E5
因为要得出前三名,显然每组的后两个选手一定不会进入前三名,因此排除了共10个选手

  • 然后将 A 1 , B 1 , C 1 , D 1 , E 1 A_1, B_1, C_1, D_1, E_1 A1,B1,C1,D1,E1放在一组,设 A 1 > B 1 > C 1 > D 1 > E 1 A_1 > B_1 > C_1 > D_1 > E_1 A1>B1>C1>D1>E1,因此得出 A 1 A_1 A1为第一名,因为要得出前三名,显然可以排除 D 1 , E 1 D_1, E_1 D1,E1,排除 D 1 D_1 D1 E 1 E_1 E1之后就可以排除D和E两组( D 1 D_1 D1是D组的第一名, E 1 E_1 E1是E组的第一名)
  • C 1 > C 2 > C 3 C_1 > C_2 > C_3 C1>C2>C3,结合 A 1 > B 1 > C 1 A_1 > B_1 > C_1 A1>B1>C1,得出 A 1 > B 1 > C 1 > C 2 > C 3 A_1 > B_1 > C_1 > C_2 > C_3 A1>B1>C1>C2>C3,因此排除 C 2 , C 3 C_2, C_3 C2,C3
  • B 1 > B 2 > B 3 B_1 > B_2 > B_3 B1>B2>B3,结合 A 1 > B 1 A_1 > B_1 A1>B1,得出 A 1 > B 1 > B 2 > B 3 A_1 > B_1 > B_2 > B_3 A1>B1>B2>B3,排除 B 3 B_3 B3

最终只剩下 A 2 , A 3 , B 1 , B 2 , C 1 A_2, A_3, B_1, B_2, C_1 A2,A3,B1,B2,C1共5个未确定名次,再进行一组即可得出最终的前三名,因此一共需要七组比赛。

  • 区间排序

如果有N个区间 [ l 1 , r 1 ] , [ l 2 , r 2 ] , . . . , [ l N , r N ] [l_1, r_1], [l_2, r_2], ...,[l_N, r_N] [l1,r1],[l2,r2],...,[lN,rN],只要满足下面的条件我们就说这些区间是有序的:存在 x i ∈ [ l i , r i ] x_i \in [l_i, r_i] xi[li,ri]其中 i = 1 , 2 , . . . , N i=1, 2, ..., N i=1,2,...,N,满足 x 1 < x 2 < . . . < x N x_1 < x_2 < ... < x_N x1<x2<...<xN

比如,[1, 4]、[2, 3]和[1.5, 2.5]是有序的,因为我们可以从这三个区间中选择1.1、2.1和2.2三个数。同时[2, 3]、[1, 4]和[1.5, 2.5]也是有序的,因为我们可以选择2.1、2.2和2.4。但是[1, 2]、[2.7, 3.5]和[1.5, 2.5]不是有序的。

对于任意一组区间,如何将它们进行排序?

易得,要想保证区间是有序的,只需要保证 l i < = r j , i < j l_i <= r_j, i < j li<=rj,i<j即可,因此针对两个区间 [ l 1 , r 1 ] , [ l 2 , r 2 ] [l_1, r_1], [l_2, r_2] [l1,r1],[l2,r2]判断是否有序只需要保证 l 1 < r 2 l_1 < r_2 l1<r2即可保证两个区间是有序的,其实这里对应着六种情况(想象一下两个区间在数轴的情况,对应六种情况):

  • l 2 < r 2 < l 1 < r 1 l_2 < r_2 < l_1 < r_1 l2<r2<l1<r1,此时一定逆序,需要交换两区间
  • l 2 < l 1 < r 2 < r 1 l_2 < l_1 < r_2 < r_1 l2<l1<r2<r1,此时两区间是有序的,可以随意放置
  • l 2 < l 1 < r 1 < l 2 l_2 < l_1 < r_1 < l_2 l2<l1<r1<l2,此时两区间是有序的
  • l 1 < l 2 < r 2 < r 1 l_1 < l_2 < r_2 < r_1 l1<l2<r2<r1,此时能确保两区间是有序的
  • l 1 < l 2 < r 1 < r 2 l_1 < l_2 < r_1 < r_2 l1<l2<r1<r2,能确保两区间是有序的
  • l 1 < r 1 < l 2 < r 2 l_1 < r_1 < l_2 < r_2 l1<r1<l2<r2,能确保两区间是有序的

因此只有为第一种情况时,才需要交换两区间顺序。综上,针对整个区间排序可以采用归并排序的思想,只需要修改merge的部分即可,对应伪代码如下:

def interval_merge(left_interval: list, right_interval: list) -> list:
    arr = []
    while left_interval and right_interval:
        if left_interval[0][0] > right_interval[0][1]:
            arr.append(right_interval.pop(0))
        else:
            arr.append(left_interval.pop(0))

    while left_interval:
        arr.append(left_interval.pop(0))

    while right_interval:
        arr.append(right_interval.pop(0))

    return arr


def interval_merge_sort(arr):
    if len(arr) <= 1:
        return arr
    left = 0
    right = len(arr)
    mid = (left + right) // 2
    return interval_merge(interval_merge_sort(arr[left: mid]), interval_merge_sort(arr[mid: right]))

2. 排序算法时间复杂度的下界证明

已知条件如下:

  • 对长度为N的序列进行排序,本质上就是在所有可能的序列( N ! N! N!)中找出最小的序列,因此排序转化成了从长度为N的序列中找出最小的序列;
  • 现在看比较长度为N的序列需要进行多少次元素的比较,显然,比较k次,最多能区分 2 k 2^k 2k种不同序列的大小;
  • 有M种序列,需要比较logM次;
  • 根据斯特林公式可得, l n N ! = N l n N − N + O ( l n N ) lnN! = NlnN - N + O(lnN) lnN!=NlnNN+O(lnN)

根据以上四个条件,可得 l o g ( N ! ) = O ( N l o g N ) log(N!) = O(NlogN) log(N!)=O(NlogN)

References

  1. 计算之魂思考题1.4

你可能感兴趣的:(计算之魂,排序算法,算法,数据结构)