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

排序算法分析

常见(内)排序算法分类:
- 比较排序
- 插入排序/选择排序/冒泡排序
- 归并排序
- 快速排序
- 堆排序
- 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 A

print 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 82
25 59 94 65 23
45 27 73 25 39
10

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

10 14 73 25 23
13 27 94 33 39
25 59 94 65 82
45

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

10 14 73
25 23 13
27 94 33
39 25 59
94 65 82
45

排序后得到:

10 14 13
25 23 33
27 25 59
39 65 73
45 94 82
94

再以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 A

def 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 A

if __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 A

def 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 random
def 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:
        return

def 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)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值