经典排序算法总结与Python实现(上)

本篇博客中的有序都是指的升序,降序情况稍微改改就出来了。

排序算法时间复杂度(最好)时间复杂度(最差)时间复杂度(平均)空间复杂度稳定性
插入排序 O ( n ) O(n) O(n) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)稳定
冒泡排序 O ( n 2 ) O(n^2) O(n2) / 及时终止 O ( n ) O(n) O(n) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)稳定
选择排序 O ( n 2 ) O(n^2) O(n2) / 及时终止 O ( n ) O(n) O(n) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)不稳定
快速排序 O ( n log ⁡ n ) O(n\log{n}) O(nlogn) O ( n 2 ) O(n^2) O(n2) O ( n log ⁡ n ) O(n\log{n}) O(nlogn) O ( log ⁡ n ) O(\log{n}) O(logn)不稳定
谢尔排序 O ( n log ⁡ n ) O(n\log{n}) O(nlogn) O ( n 2 ) O(n^2) O(n2) O ( n log ⁡ n ) − O ( n 2 ) O(n\log{n})-O(n^2) O(nlogn)O(n2) O ( 1 ) O(1) O(1)不稳定

1. 插入排序

从左往右依次建立有序数组,a[0:1]就一个元素,就是一个有序数组,考虑a[0:i]已经排好序是一个有序数组,下一步把a[i]插入这个有序数组得到a[0:i+1]的有序数组,这种插入是从最右边开始,连续与各个元素比较,把比a[i]大的元素右移一个位置,直到不满足条件即视为找到插入位置,再进行下一个a[i+1]的插入,直至a[n-1]。

def insertionSort(a):
    n = len(a)
    for i in range(1, n):
        # 把a[i]插入a[0:i]
        t = a[i]
        j = i-1
        while (j >= 0 and a[j] > t):
            # 比较i之前的元素a[j]
            a[j+1] = a[j]
            j -= 1
        a[j+1] = t
    return a

性能分析

  • 最好情况:(数组升序排列时)比较次数为 n − 1 n-1 n1,所以时间复杂度为 O ( n ) O(n) O(n)

  • 最坏情况:(数组降序排列时)比较次数为 1 + 2 + ⋯ + n − 1 = n ( n − 1 ) / 2 1+2+\cdots+n-1=n(n-1)/2 1+2++n1=n(n1)/2,所以时间复杂度为 O ( n 2 ) O(n^2) O(n2)

  • 平均情况:a[i]可能插入的位置索引有 0 , 1 , … , i 0,1,\dots,i 0,1,,i,且位于每个位置的概率相等,均为 1 / i 1/ i 1/i,若a[i]插入 i i i,则比较1次,插入 i − 1 i-1 i1,则比较2次,……插入 0 0 0,则比较 i − 1 i-1 i1次,所以平均a[i]的平均比较次数为 1 i [ 1 + 2 + ⋯ + ( i − 1 ) ] = i − 1 2 {1\over i}[1+2+\cdots+(i-1)]={i-1\over 2} i1[1+2++(i1)]=2i1。所以n个元素一共比较了 ∑ i = 1 n i − 1 2 = n 2 − n 4 \sum_{i=1}^n{i-1\over 2}={n^2-n\over 4} i=1n2i1=4n2n次,所以平均时间复杂度为 O ( n 2 ) O(n^2) O(n2)

  • 稳定性:由于插入排序只是将比较大的元素顺序移动,并不会改变原本大小相等的元素的相对位置,所以是稳定的。

  • 空间复杂度:只需一个额外空间给t存放当前需要插入的元素,因此空间复杂度为 O ( 1 ) O(1) O(1)

2. 冒泡排序

通过一次或者多次冒泡过程依次将左边未排序的子数组中最大值通过冒泡操作放到该子数组最右边,即比较相邻两个元素,若左边较大则互换位置,反之不变,最终得到一个有序数组。

def bubbleSort(a):
    n = len(a)
    for i in range(n-1):
        # 下面遍历未排序的子数组
        for j in range(n-1-i):
            if a[j] > a[j+1]:
                a[j], a[j+1] = a[j+1], a[j]
    return a

若未排序子数组已经有序,那就不需要再进行之后的遍历,可以直接输出有序数组,这样可以及时终止程序,下面是及时终止的冒泡排序:

def bubbleSort(a):
    n = len(a)
    for i in range(n-1):
        swapped = False
        # 未排序的子数组
        for j in range(n-1-i):
            if a[j] > a[j+1]:
                a[j], a[j+1] = a[j+1], a[j]
                swapped = True
        # 如果本次遍历子数组都没有发生交换,说明该子数字已经有序,不用再继续
        if swapped==False:
            break
    return a

性能分析

  • 最好情况:(数组升序排列时)对于及时终止的冒泡排序只用比较 n − 1 n-1 n1次,没有发生交换所以直接退出循环,所以时间复杂度为 O ( n ) O(n) O(n);而不是及时终止的始终会执行外层循环,因此这时时间复杂度和最坏情况一样都是 O ( n 2 ) O(n^2) O(n2)

  • 最坏情况:(数组降序排列时)比较次数为 ( n − 1 ) + ( n − 2 ) + ⋯ + 1 = n ( n − 1 ) / 2 (n-1)+(n-2)+\cdots+1=n(n-1)/2 (n1)+(n2)++1=n(n1)/2,所以时间复杂度为 O ( n 2 ) O(n^2) O(n2)

  • 平均情况:对于不及时终止的最好最坏情况都一样,所以平均来说也是一样的 O ( n 2 ) O(n^2) O(n2);及时终止的分析比较麻烦,但平均时间复杂度也是 O ( n 2 ) O(n^2) O(n2)

  • 稳定性:只比较相邻元素,前一个比后一个大才会交换,因此若原本相等的元素并不会改变他们的相对顺序,所以是稳定的。

  • 空间复杂度:其实是需要一个临时变量来存放交换时的元素,会占一个内存空间,因此空间复杂度为 O ( 1 ) O(1) O(1)

3. 选择排序

在未排序的数组中找到最大元素然后与该数组的最末位交换,然后在余下的未排序数组中又找最大的元素再与该数组的最末位交换,重复操作,直到数组有序。

def selectionSort(a):
    n = len(a)
    for i in range(n-1):
        # 记录最大元素位置
        t = 0
        for j in range(1, n-i):
            # 遍历未排序数组
            if a[j] >= a[t]:
                t = j
        # 与未排序数组的最末位交换
        a[t], a[n-i-1] = a[n-i-1], a[t]
    return a

假如数组在第二次排序之后就已经是有序数组,但是上面这个程序依旧会把外层 n − 1 n-1 n1次循环都执行完。为了在数组有序时及时终止程序,可以进行下面的优化:

def selectionSort(a):
    n = len(a)
    for i in range(n-1):
        # 默认数组有序
        flag = True
        t = 0
        for j in range(1, n-i):
            if a[j] >= a[t]:
                t = j
            else:
                # 如果没有赋值,说明左大右小,数组无序
                flag = False
        if flag==True:
            break
        a[t], a[n-i-1] = a[n-i-1], a[t]
    return a

性能分析

  • 最好情况:(数组升序排列时)对于及时终止的选择排序只用比较 n − 1 n-1 n1次,外层循环只执行一次就直接退出循环,所以时间复杂度为 O ( n ) O(n) O(n);而不是及时终止的始终会执行外层循环,因此这时时间复杂度和最坏情况一样都是 O ( n 2 ) O(n^2) O(n2)

  • 最坏情况:(数组降序排列时)比较次数为 ( n − 1 ) + ( n − 2 ) + ⋯ + 1 = n ( n − 1 ) / 2 (n-1)+(n-2)+\cdots+1=n(n-1)/2 (n1)+(n2)++1=n(n1)/2,所以时间复杂度为 O ( n 2 ) O(n^2) O(n2)

  • 平均情况:对于不及时终止的最好最坏情况都一样,所以平均来说也是一样的 O ( n 2 ) O(n^2) O(n2);及时终止的分析比较麻烦,但平均时间复杂度也是 O ( n 2 ) O(n^2) O(n2)

  • 稳定性:不稳定,在选择出当前最大的进行交换位置后可能会改变原本相同的元素的相对位置。例如数组[2 7 3 5 5],第一次遍历会选择最大值7与最后的5交换,那这样原本在后面的5就跑到前面去了,所以原本相等的元素相对位置变化了,因此选择排序是不稳定的。

  • 空间复杂度:需要一个临时变量来存放每次最大元素的位置,因此空间复杂度为 O ( 1 ) O(1) O(1)

4. 快速排序

快速排序就是选择一个基准,逐个元素与这个基准元素进行比较,小于基准的放在左边,大于基准的放在右边,由此找到基准的位置,在对两部分递归进行这样的排序,直到最后只剩下一个元素即为有序。

按照这个思路算法实现如下:

def quickSort(a):
	# 递归结束条件
    if len(a) <= 1:
        return a
    # 选取基准,并分别储存小于基准和大于基准的元素
    base = a.pop()
    left = []
    right = []
    for i in a:
        if i < base:
            left.append(i)
        else:
            right.append(i)
    # 递归调用
    return quickSort(left) + [base] + quickSort(right)

上述代码需要额外的left和right空间来储存,而实际上只需要一个额外的存储空间即可完成排序(除开递归所需要的栈空间外)。

实现的思路是利用双指针i、j,排序开始的时候:i=0,j=n-1。把数组的第一个元素当做基准(也可以随机取一个)并储存起来,这时a[i]相当于是一个空位;从j开始从右往左搜索(j–),找到第一个小于基准的值a[j],将a[j]放到刚刚的空位a[i]去,这时a[j]相当于是一个空位;然后i开始从左往右搜索(i++),找到第一个大于基准的值a[i],将a[i]放到刚刚的空位a[j]去,a[i]就有空出来了;重复这种移动和交换过程直到i=j,然后把基准值填入a[i],这样就完成了一趟快排。接下来对基准两边的分别进行快排即可。

def quickSort(a):
    return quickSortHelper(a, 0, len(a)-1)

def quickSortHelper(a, left, right):
    # 递归结束条件,只有一个元素或者数组为空时
    if left >= right:
        return a
    else:
        i = left
        j = right
        base = a[i]
    # 一趟排序
    while i < j:
        # 从右往左找第一个小于基准的值
        while a[j] >= base and i < j:
            j -= 1
        # 找到之后放空位去
        if i < j:
            a[i] = a[j]
        # 从左往右找第一个大于基准的值
        while a[i] <= base and i < j:
            i += 1
        # 找到之后放空位去
        if i < j:
            a[j] = a[i]
    # 填入基准值
    a[i] = base
    # 递归
    quickSortHelper(a, left, i-1)
    quickSortHelper(a, i+1, right)
    return a
    

性能分析

  • 最好情况:每次选择的基准恰好可以均匀二分数组,这样总共需要 O ( log ⁡ n ) O(\log{n}) O(logn)次划分,而每次放空位移动都需要与基准进行比较,复杂度为 O ( n ) O(n) O(n),因此这种情况下时间复杂度为 O ( n log ⁡ n ) O(n\log{n}) O(nlogn)

  • 最坏情况:每次选择的基准都是最大或者最小的情况,那划分就没有意义,这时就类似选择排序,时间复杂度为 O ( n 2 ) O(n^2) O(n2)

  • 平均情况:评价情况下时间复杂度为 O ( n log ⁡ n ) O(n\log{n}) O(nlogn)

  • 稳定性:不稳定

  • 空间复杂度:只需要一个额外空间来储存基准,所以主要空间还是递归造成的栈空间,最好情况即每次都能均匀二分,栈的最大深度为 log ⁡ ( n + 1 ) \log{(n+1)} log(n+1),这样空间复杂度为 O ( log ⁡ n ) O(\log{n}) O(logn),最差情况栈的最大深度为 n n n,平均情况下空间复杂度为 O ( log ⁡ n ) O(\log{n}) O(logn)

5. 谢尔排序

谢尔排序(希尔排序)就是对无序序列等间隔的划分为好几个子序列,对子序列分别进行插入排序。例如序列 [ 4 , 1 , 5 , 8 , 2 , 3 , 7 , 9 ] [4,1,5,8,2,3,7,9] [4,1,5,8,2,3,7,9]先划分为间隔为 8 / / 2 = 4 8//2=4 8//2=4的子序列 [ 4 , 2 ] [4,2] [4,2] [ 1 , 3 ] [1,3] [1,3] [ 5 , 7 ] [5,7] [5,7] [ 8 , 9 ] [8,9] [8,9]分别进行插入排序,第一次排序后序列就变成 [ 2 , 1 , 5 , 8 , 4 , 3 , 7 , 9 ] [2,1,5,8,4,3,7,9] [2,1,5,8,4,3,7,9];然后间隔递减为 4 / / 2 = 2 4//2=2 4//2=2,再分别对子序列 [ 2 , 5 , 4 , 7 ] [2,5,4,7] [2,5,4,7] [ 1 , 8 , 3 , 9 ] [1,8,3,9] [1,8,3,9]分别进行插入排序,第二次排序后序列变成 [ 2 , 1 , 4 , 3 , 5 , 8 , 7 , 9 ] [2,1,4,3,5,8,7,9] [2,1,4,3,5,8,7,9];最后间隔递减为 2 / / 2 = 1 2//2=1 2//2=1,即随着间隔的缩小,子序列越来越长,最后直到间隔为1时子序列就是原序列,再进行一次插入排序即可获得有序序列。

因为列表越接近有序,插入排序的比对次数就越少,在最后一次的插入排序前先尽量把序列弄得有序些,减少了很多无效的比较。

def shellSort(a):
    # 设定间隔
    gap = len(a) // 2
    while gap > 0:
        # 对间隔子序列做插入排序
        for start in range(gap):
            # 带间隔的插入排序(当最后gap=1时子序列就是原序列,即正常的插入排序)
            for i in range(start+gap, len(a), gap):
                t = a[i]
                j = i - gap
                while j >= 0 and a[j] > t:
                    a[j+gap] = a[j]
                    j -= gap
                a[j+gap] = t
        # 缩小间隔,使得子序列越来越长
        gap = gap // 2
    return a
    

性能分析

  • 时间复杂度:谢尔排序的详尽分析比较复杂,还未找到比较官方的材料和证明,百度百科上说下界是 O ( n log ⁡ n ) O(n\log{n}) O(nlogn),总之大致是介于 O ( n log ⁡ n ) − O ( n 2 ) O(n\log{n})-O(n^2) O(nlogn)O(n2)

  • 稳定性:间隔的插入排序是跳跃式的,因此不稳定

  • 空间复杂度:空间复杂度为 O ( 1 ) O(1) O(1)

参考资料

1、数据结构、算法与应用 C++语言描述 原书第2版
2、MOOC:数据结构与算法Python版(北大 陈斌老师)
3、参考博客1:十大经典排序算法(动图演示)
4、参考博客2:排序算法总结(Python版)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值