《算法导论》排序算法的分析和实现

来源:互联网 发布:linux操作系统原理pdf 编辑:程序博客网 时间:2024/05/21 11:25

排序算法分析

常见(内)排序算法分类:
- 比较排序
- 插入排序/选择排序/冒泡排序
- 归并排序
- 快速排序
- 堆排序
- Shell排序

  • 非比较排序
    • 计数排序
    • 基数排序
    • 桶排序

排序的稳定性&复杂度分析

稳定性是针对相同大小的元素,如果排序算法不改变相同大小的元素原来的顺序,则算法是稳定的。也就是说,稳定的排序算法会让原本有相同键值的记录维持相同的次序

时间复杂度:执行时间取决于比较次数交换次数

空间复杂度:取决于消耗的额外内存空间(auxiliary space,相对于in-place来讲)
- 使用堆栈、记录表
- 使用链表(指针)、数组(索引)来访问元素
- 排序元素的副本

稳定排序:
- 插入/冒泡,O(n2)
- 归并,O(nlogn),需要O(n)额外空间
- 原地归并,O(n2)
- 计数,O(n+k),需要O(K)额外空间
- 桶,O(n),需要O(k)额外空间
- 二叉树排序,期望O(nlogn),最坏O(n2),需要O(n)额外空间

不稳定排序:
- 选择,O(n2)
- Shell排序
- 堆排序,O(nlogn)
- 快排,期望O(nlogn),最坏O(n2)

sort_comparison

说明:如无例外,以下的算法说明和实现均以升序排序为例。


插入排序

插入排序可以类比打牌:每次摸到一张牌,会和左手上排好序的牌,从后往前逐一比较大小,直到找到合适的位置(第一个大于的元素之后)插好。

主要步骤

  1. 循环遍历元素A[i]
  2. 将A[i]和其之前的元素A[j]循环逐一比较大小,只要当前A[j]大于A[i],A[j]就往后移动一位(覆盖下一个值),并继续比较A[i]和再前一个A[j]的大下
  3. 直到第一个A[j]小于A[i],跳出循环
  4. 此时A[j]的后一位,就是A[i]要放的位置
"""    Insert Sort    @author: Shangru     @date: 2015/03/11"""def insert_sort(A):    for i in range(len(A)):        cur = A[i]        j = i - 1  # j之前的已排好        while j >= 0 and A[j] > cur:          # 当前数小于排好的,排好的后移一位,空出位置            A[j + 1] = A[j]            j = j - 1        A[j + 1] = cur    return A

复杂度分析

对于小规模数据,插入排序是快速的原地排序算法。

  • 时间:最好O(n),最坏O(n2),平均O(n2)
  • 空间:O(1)
  • 稳定排序

选择排序

选择排序也可以类比为打扑克牌:每次从未排序的牌里选择抽出最小的那张牌,插到左边已排好的牌末尾。

主要步骤

  1. 循环遍历每一个元素A[i]
  2. 在每次循环内,逐个访问未排序的元素A[j],比较A[i]和未排序元素A[j]的大小
  3. 如果当前元素A[i]大于A[j],则交换两个元素。比较完这一趟后,未排序元素中的最小值会放到A[i],成为已排好的元素。
  4. 继续访问下一个A[i],对比未排序的A[j]
def select_sort(A):    n = len(A)    for i in range(n - 1):        for j in range(i + 1, n):            if A[j] < A[i]:                A[i], A[j] = A[j], A[i]    return A

复杂度分析

  • 时间:最好O(n2),最坏O(n2),平均O(n2)
  • 空间:O(1)
  • 不稳定排序

冒泡排序

主要原理

每次比较相邻两个元素,如果存在逆序则互换两个元素的位置,使得小数在前,大数在后。类似于冒泡,每次遍历完后,较大的元素都会往后移(“沉”)。重复n次可以让数组有序。

主要步骤

def bubble_sort(A):    n = len(A)    for i in xrange(n):        for j in xrange(n - 1, i, -1):            if A[j] < A[j - 1]:                A[j], A[j - 1] = A[j - 1], A[j]    return Aprint bubble_sort([9, 8, 7, 6, 5, 4])

复杂度分析

  • 时间:最好O(n),最坏O(n2),平均O(n2)
  • 空间:O(1)
  • 稳定排序

对于原始的算法,有2种优化方法:
1. 如果某一趟遍历,没有发生数据交换,则不需要再循环访问。设flag标识,跳出循环。
2. 记录某次遍历时最后发生数据交换的位置,这个位置之后的数据有序,不用再排序。记录这个位置,可以确定下次循环的范围。

实现方法参考:https://github.com/wuchong/Algorithm-Interview/blob/master/Sort/python/BubbleSort.py


希尔排序

Shell排序(Donald Shell, 1959)本质上是分组插入排序,但却是非稳定排序

主要步骤

将输入待排序元素分成多个组(相隔gap个数的元素组成),分别对各组进行插入排序后,按原来对应的方式拼接回来。然后依次减小gap,再进行排序。直到整个序列基本有序,gap足够小,为1时就变成插入排序,这可以保证数据一定会被排序。

参考Wikipedia(https://en.wikipedia.org/wiki/Shellsort)的例子:

假设有一序列[ 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 ],以步长(增量)为5,每隔5个取元素,得到5列分组:

13 14 94 33 8225 59 94 65 2345 27 73 25 3910

对每组(即每列)插入排序,得到

10 14 73 25 2313 27 94 33 3925 59 94 65 8245

将四行数字拼接一起,得到[ 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 ],然后递减步长,以3为步长排序:

10 14 7325 23 1327 94 3339 25 5994 65 8245

排序后得到:

10 14 1325 23 3327 25 5939 65 7345 94 8294

再以1为步长进行排序即可。

代码实现

def shell_sort(A):    n = len(A)    gap = int(round(n / 2)) # initial gap    while gap > 0:        for i in xrange(gap, n):            temp = A[i] # insert sort            j = i            while j >= gap and A[j - gap] > temp:                A[j] = A[j - gap]                j -= gap            A[j] = temp        gap = int(round(gap / 2)) # update gap    return A

复杂度分析

Shell排序的复杂度与gap大小有关:
- 当gap取n/2^i,最差复杂度为O(n2)
- 当gap取2^k - 1,最差为O(n1.5)
- 当gap取2^i*3^j,最差为O(nlog2n)


归并排序

归并排序是分治法的经典使用:
1. 分解:把原问题(n个元素的排序)分解为两个子问题(n/2个元素的排序),
2. 递归求解:归并法(Merge)对两个子序列递归的排序
3. 合并:再回到上层,合并这两个子问题的结果(合并两个已排序的子序列)

优势:
1. 归并对于连续存储的数据结构有优势(顺序地merge),如链表(只需要O(1)的额外空间开销),链表不适合随机访问,此时归并排序远优于快排和堆排序;而快排需要随机读取,对于RAM-based存储有优势。
2. 归并可以不用递归实现。
3. 稳定排序

劣势:
1. 和堆排序O(1)比,额外空间往往是O(n)

"""    Merge Sort    @author: Shangru """def MergeSort(A, l, r):    """        Sort A[l..r], divide into sorting A[l..mid], A[mid+1..r]    """    if l < r:        mid = (l + r) / 2        MergeSort(A, l, mid)        MergeSort(A, mid + 1, r)        Merge(A, l, mid, r)    return Adef Merge(A, l, mid, r):    """        Merge two sorted arrays into one        A[l..mid], A[mid+1..r] -> A[l..r]        Need extra space left & right    """    leftlen = mid - l + 1    rightlen = r - mid    left = A[l : mid + 1]    right = A[mid + 1: r + 1]    print left, right    i, j, k = 0, 0, l    while i < leftlen and j < rightlen:        if left[i] <= right[j]:            A[k] = left[i]            i += 1        else:            A[k] = right[j]            j += 1        k += 1    if i == leftlen:        A[k : r + 1] = right[j : rightlen]    else:        A[k : r + 1] = left[i : leftlen]    return Aif __name__ == '__main__':    print MergeSort([9,8,7,6,5,4,3,2,1], 0, 8)

复杂度分析

  • 时间:最优O(nlogn),最差O(nlogn),平均O(nlogn)
  • 空间:最差O(n)

快速排序

快排主要是划分函数partition加分治法,通过划分函数把原序列划分成两个子序列,一个全部比基准数小,另一个全部比基准数大,分别对两个子序列递归调用自身快排。

优势:
1. 实际应用中比其他O(nlogn)算法快,因为内循环可以在大多数architecture和真实数据中高效实现,可以减少O(n2)的出现概率
2. 原地排序,额外空间复杂度O(1)

劣势:
1. 递归算法
2. 不是稳定算法

"""    Quick Sort    @date: 2015/03/11""" def quick_sort(A, l, r):    if l < r:        pivot = partition(A, l, r)        quick_sort(A, l, pivot - 1)        quick_sort(A, pivot + 1, r)    else:        return Adef partition(A, l, r):    x = A[r]    pivot = l - 1    for i in xrange(l, r + 1):        if A[i] < x:            pivot += 1            A[pivot], A[i] = A[i], A[pivot]     A[pivot + 1], A[r] = A[r], A[pivot + 1]     return pivot + 1

划分函数partition的时间复杂的为Θ(n)。最佳划分是平衡划分,T(n)满足T(n)<=2T(n/2)+Θ(n),由主定理可以求得T(n)=O(nlogn)1T(n)T(n) = T(n-1) + \Theta(n)O(n^2)O(nlogn)怀\Theta(n^2)$。

import randomdef random_quick_sort(A, l, r):    if l < r:        pivot = random_partition(A, l, r)        random_quick_sort(A, l, pivot - 1)        random_quick_sort(A, pivot + 1, r)    else:        returndef random_partition(A, l, r):    rand = random.randint(l, r)    A[rand], A[r] = A[r], A[rand]    pivot = l - 1     for i in xrange(l, r + 1):        if A[i]  < A[r]:            pivot += 1            A[pivot], A[i] = A[i], A[pivot]    A[pivot + 1], A[r] = A[r], A[pivot + 1]    return pivot + 1

复杂度分析

  • 时间:最优O(nlogn),最差O(n2),平均O(nlogn)
  • 空间:O(n)O(logn)(Sedgewick 1978)
0 0