各种常用排序算法

参考资料:

  1. 菜鸟教程
排序方法时间复杂度(平均情况,最好情况,最坏情况)空间复杂度稳定性
冒泡排序 O ( n 2 ) , O ( n ) , O ( n 2 ) O(n^2),O(n),O(n^2) O(n2),O(n),O(n2) O ( 1 ) O(1) O(1)稳定
选择排序 O ( n 2 ) , O ( n 2 ) , O ( n 2 ) O(n^2),O(n^2),O(n^2) O(n2),O(n2),O(n2) O ( 1 ) O(1) O(1)不稳定
插入排序 O ( n 2 ) , O ( n ) , O ( n 2 ) O(n^2),O(n),O(n^2) O(n2),O(n),O(n2) O ( 1 ) O(1) O(1)稳定
shell排序 O ( n 1.3 ) , O ( n ) , O ( n 2 ) O(n^{1.3}),O(n),O(n^2) O(n1.3),O(n),O(n2) O ( 1 ) O(1) O(1)不稳定
归并排序 O ( n l o g 2 n ) , O ( n l o g 2 n ) , O ( n l o g 2 n ) O(nlog2n),O(nlog2n),O(nlog2n) O(nlog2n),O(nlog2n),O(nlog2n) O ( n ) O(n) O(n)稳定
快速排序 O ( n l o g 2 n ) , O ( n l o g 2 n ) , O ( n 2 ) O(nlog2n),O(nlog2n),O(n^2) O(nlog2n),O(nlog2n),O(n2) O ( n l o g 2 n ) O(nlog2n) O(nlog2n)不稳定
堆排序 O ( n l o g 2 n ) , O ( n l o g 2 n ) , O ( n l o g 2 n ) O(nlog2n),O(nlog2n),O(nlog2n) O(nlog2n),O(nlog2n),O(nlog2n) O ( 1 ) O(1) O(1)不稳定
基数排序 O ( d ( r + n ) ) , O ( d ( n + r d ) ) , O ( d ( r + n ) ) O(d(r+n)),O(d(n+rd)),O(d(r+n)) O(d(r+n)),O(d(n+rd)),O(d(r+n)) O ( r d + n ) O(rd+n) O(rd+n)稳定

注:

  1. 基数排序的复杂度中,r代表关键字的基数,d代表长度,n代表关键字的个数
  2. 排序算法稳定性:排序序列中存在多个相同的值,经过排序后,相对次数保持不变。
  3. 理解每种排序算法原理后,就能看懂菜鸟教程中的动画演示

冒泡排序

  1. 遍历数组,前后俩俩依次比较,如果前者比后者大,则交换两数;一轮结束后,数组中末尾数最大;类似鱼吐泡泡,故名冒泡
  2. 重复上述步骤,直至交换次数为0,排序结束

注:最多遍历n-1轮,第i轮比较n-i次,最后一轮比较 1 次即可

import numpy as np
arr = np.array([22, 34, 3, 32, 82, 55, 89, 50, 37, 5, 64, 35, 9, 70])

def bubbleSort(arr):
    for i in range(1, len(arr)):
        trans_flag = True
        for j in range(0, len(arr)-i):
            if arr[j] > arr[j+1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
                trans_flag = False
        if trans_flag: # 不存在交换则终止循环
            break
    return arr

# 分步骤展示
def bubbleSort_view(arr):
    print('length:{}\nraw arr:{}\n'.format(len(arr),arr))
    for i in range(1, len(arr)):
        count = 0
        for j in range(0, len(arr)-i):
            if arr[j] > arr[j+1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
                count += 1
        print('round {}\tarr:{}\t本轮交换{}次'.format(i,arr,count))
        if count == 0:
            break
    return arr
%%time
arr = np.array([5,4,3,2,1])
bubbleSort_view(arr)
length:5
raw arr:[5 4 3 2 1]

round 1	arr:[4 3 2 1 5]	本轮交换4次
round 2	arr:[3 2 1 4 5]	本轮交换3次
round 3	arr:[2 1 3 4 5]	本轮交换2次
round 4	arr:[1 2 3 4 5]	本轮交换1次
CPU times: user 1.83 ms, sys: 0 ns, total: 1.83 ms
Wall time: 1.22 ms





array([1, 2, 3, 4, 5])
arr = np.array([22, 34, 3, 32, 82, 55, 89, 50, 37, 5, 64, 35, 9, 70])
bubbleSort(arr)
array([ 3,  5,  9, 22, 32, 34, 35, 37, 50, 55, 64, 70, 82, 89])

结论

  1. 数组长度为14,需遍历13轮,在第11轮交换次数为0,提前结束
  2. 时间复杂度
    1. 最好的情况,原数组有序,只需遍历一边数组,比较 n − 1 n-1 n1次,移动 0 0 0次即可,时间复杂度 O ( n ) O(n) O(n)
    2. 最坏的情况,原数组倒序,需要遍历 n − 1 n-1 n1轮数组,每轮比较 n − i , i = 1 , . . . , n − 1 n-i,i=1,...,n-1 ni,i=1,...,n1次,移动 n − i , i = 1 , . . . , n − 1 n-i,i=1,...,n-1 ni,i=1,...,n1次,累计比较 n ( n − 1 ) 2 = O ( n 2 ) \frac{n(n-1)}{2}=O(n^2) 2n(n1)=O(n2)次,移动 n ( n − 1 ) 2 = O ( n 2 ) \frac{n(n-1)}{2}=O(n^2) 2n(n1)=O(n2)次,时间复杂度为 O ( n 2 ) O(n^2) O(n2)
    3. 平均情况,原数组一半有序一半无序,每轮平均比较 n − i 2 , i = 1 , . . . , n − 1 \frac{n-i}{2},i=1,...,n-1 2ni,i=1,...,n1次,平均移动 n − i 2 , i = 1 , . . . , n − 1 \frac{n-i}{2},i=1,...,n-1 2ni,i=1,...,n1次,时间复杂度为 O ( n 2 ) O(n^2) O(n2)
  3. 空间复杂度:原地排序,占用空间为 O ( 1 ) O(1) O(1)
  4. 稳定性:数组元素相等不交换位置,故冒泡排序为稳定性排序

选择排序

思想:选择待排序序列中最小值,放入已排序序列末尾。原地排序,不稳定

  1. 待排序序列(每次少一个元素)中寻找最小元素下标 minIndex
  2. 交换 待排序序列第一个元素 与 最小元素。(当交换的值与后续元素相等,则稳定性破坏)
  3. 重复 n − 1 n-1 n1次步骤1,2;排序完毕
def selectionSort(arr):
    print('已排序序列                        待排序序列')
    for i in range(len(arr) - 1):
        # 记录最小数的索引
        minIndex = i
        for j in range(i + 1, len(arr)):
            if arr[j] < arr[minIndex]:
                minIndex = j
        # i 不是最小数时,将 i 和最小数进行交换
        if i != minIndex:
            arr[i], arr[minIndex] = arr[minIndex], arr[i]
        print(arr[:i+1],arr[i+1:])
    return arr
%%time
arr = np.array([22, 34, 3, 32, 82, 55, 89, 50, 37, 5, 64, 35, 9, 70])
# 每次选择待排序序列中最小的 加入 已排序序列的末尾
selectionSort(arr)
已排序序列                        待排序序列
[3] [34 22 32 82 55 89 50 37  5 64 35  9 70]
[3 5] [22 32 82 55 89 50 37 34 64 35  9 70]
[3 5 9] [32 82 55 89 50 37 34 64 35 22 70]
[ 3  5  9 22] [82 55 89 50 37 34 64 35 32 70]
[ 3  5  9 22 32] [55 89 50 37 34 64 35 82 70]
[ 3  5  9 22 32 34] [89 50 37 55 64 35 82 70]
[ 3  5  9 22 32 34 35] [50 37 55 64 89 82 70]
[ 3  5  9 22 32 34 35 37] [50 55 64 89 82 70]
[ 3  5  9 22 32 34 35 37 50] [55 64 89 82 70]
[ 3  5  9 22 32 34 35 37 50 55] [64 89 82 70]
[ 3  5  9 22 32 34 35 37 50 55 64] [89 82 70]
[ 3  5  9 22 32 34 35 37 50 55 64 70] [82 89]
[ 3  5  9 22 32 34 35 37 50 55 64 70 82] [89]
CPU times: user 12.9 ms, sys: 0 ns, total: 12.9 ms
Wall time: 7.01 ms





array([ 3,  5,  9, 22, 32, 34, 35, 37, 50, 55, 64, 70, 82, 89])
arr = np.array([3,5,3,1])
selectionSort(arr)
已排序序列                        待排序序列
[1] [5 3 3]
[1 3] [5 3]
[1 3 3] [5]





array([1, 3, 3, 5])

结论

  1. 数组长度为n,需遍历 n − 1 n-1 n1轮,无法提前结束
  2. 时间复杂度:无论原数组是否有序,均需遍历 n − 1 n-1 n1轮数组
    1. 最好的情况,原数组有序,每轮比较 n − i , i = 1 , . . . , n − 1 n-i,i=1,...,n-1 ni,i=1,...,n1次,移动 0 0 0次;累计比较 n ( n − 1 ) 2 = O ( n 2 ) \frac{n(n-1)}{2}=O(n^2) 2n(n1)=O(n2)次,时间复杂度 O ( n 2 ) O(n^2) O(n2)
    2. 最坏的情况,原数组倒序,每轮比较 n − i , i = 1 , . . . , n − 1 n-i,i=1,...,n-1 ni,i=1,...,n1次,移动 n − i , i = 1 , . . . , n − 1 n-i,i=1,...,n-1 ni,i=1,...,n1次,累计比较 n ( n − 1 ) 2 = O ( n 2 ) \frac{n(n-1)}{2}=O(n^2) 2n(n1)=O(n2)次,移动 n ( n − 1 ) 2 = O ( n 2 ) \frac{n(n-1)}{2}=O(n^2) 2n(n1)=O(n2)次,时间复杂度为 O ( n 2 ) O(n^2) O(n2)
    3. 因此平均时间复杂度为 O ( n 2 ) O(n^2) O(n2)
  3. 空间复杂度:原地排序,占用空间为 O ( 1 ) O(1) O(1)
  4. 稳定性:序列[3,5,3,1]第一次交换结果为[1,5,3,3];我们发现原序列的第一个3排在了第二个3的后面,因此选择排序不稳定

插入排序

  1. 将数组第一个元素置于已排序数组中(一个元素自成有序列)
  2. 从待排序序列中选出任意一个元素(一般取第一个)
  3. 依次从后向前遍历已排序序列,直到 选出元素>=某个已排序元素,将选出元素置于其后
  4. 重复 n − 1 n-1 n1次2,3,排序完毕

注:原理类似打扑克抓牌,新牌插入已排序牌的过程;

# 原地排序
def insertionSort(arr):
    for i in range(len(arr)):
        preIndex = i-1 # 已排序序列 末尾元素 位置
        current = arr[i] # 待插入元素
        # 已排序序列未遍历完 且 已排序值 大于 当前值
        while preIndex >= 0 and arr[preIndex] > current: 
            arr[preIndex+1] = arr[preIndex] # 较大元素往后挪一位
            preIndex-=1 # 依次往前遍历
        arr[preIndex+1] = current # 找到首个 current>= 已排序值,当前值置于已排序值 后面
    return arr
arr = np.array([22, 34,  3, 32, 82, 55, 89, 50, 37,  5, 64, 35,  9, 70])
insertionSort(arr)
array([ 3,  5,  9, 22, 32, 34, 35, 37, 50, 55, 64, 70, 82, 89])

结论

  1. 数组长度为n,需遍历 n − 1 n-1 n1轮,无法提前结束
  2. 时间复杂度:无论原数组是否有序,均需遍历 n − 1 n-1 n1轮数组
    1. 最好的情况,原数组有序,每轮比较 1 1 1次,移动 0 0 0次;累计比较 n − 1 n-1 n1次,时间复杂度 O ( n ) O(n) O(n)
    2. 最坏的情况,原数组倒序,每轮比较 i , i = 1 , . . . , n − 1 i,i=1,...,n-1 i,i=1,...,n1次,移动 i , i = 1 , . . . , n − 1 i,i=1,...,n-1 i,i=1,...,n1次,累计比较 n ( n − 1 ) 2 = O ( n 2 ) \frac{n(n-1)}{2}=O(n^2) 2n(n1)=O(n2)次,移动 n ( n − 1 ) 2 = O ( n 2 ) \frac{n(n-1)}{2}=O(n^2) 2n(n1)=O(n2)次,时间复杂度为 O ( n 2 ) O(n^2) O(n2)
    3. 平均时间复杂度为 O ( n 2 ) O(n^2) O(n2)
  3. 空间复杂度:原地排序,占用空间为 O ( 1 ) O(1) O(1)
  4. 稳定性:序列中相同的值,会按顺序选中插入新的序列,故稳定

快速排序

  1. 思想:分而治之,冒泡排序基础上的递归分治法
  2. 选择数列中第一个元素,作为比较基准
  3. 小于基准值的元素放在基准前面,其余元素放在基准的后面。此时,基准处于数列的中间位置,称为分区(partition)操作;
  4. 递归地(recursive)在每个分区上重复步骤2,3
def quickSort(arr, left=None, right=None):
    left = 0 if not isinstance(left,(int, float)) else left
    right = len(arr)-1 if not isinstance(right,(int, float)) else right
    if left < right:
        partitionIndex = partition(arr, left, right)
        quickSort(arr, left, partitionIndex-1)
        quickSort(arr, partitionIndex+1, right)
    return arr

def partition(arr, left, right):
    pivot = left 		# 基准0
    index = pivot+1 	# 待交换 较小分区 下标 1
    i = index 			# 遍历下标
    while  i <= right:
        if arr[i] < arr[pivot]: # 将小于基准的数据移到 较小分区,保持原顺序
            swap(arr, i, index)
            index+=1
        i+=1
    swap(arr,pivot,index-1) # 基准0 与 较小分区最大值 交换,此时基准左侧分区均小于右侧分区,破坏了较小分区的稳定性
    return index-1
# 30 40 2 19 45
# 30 2 19 40 45
def swap(arr, i, j):
    arr[i], arr[j] = arr[j], arr[i]

结论

  1. 快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。

归并排序

  1. 原理:二分法,分而治之的思想,类似二叉树。不断把数据集划分成左右两份,直至最小元素为 1,然后往树的根部排序合并
  2. 原理图示:
    在这里插入图片描述
def mergeSort(arr,n=1):
    import math
    if(len(arr)<2):
        return arr
    middle = math.floor(len(arr)/2)
    left, right = arr[0:middle], arr[middle:] 	# 数据一分为二
    left_m = mergeSort(left,n+1) 				# 递归划分
    right_m = mergeSort(right,n+1)
    print('当前树深度:{}\t左枝:{}\t右枝:{}'.format(n,left_m, right_m))
    return merge(left_m, right_m) 				# 合并

# 合并左右分支
def merge(left,right):
    result = []
    while left and right:
        if left[0] <= right[0]:
            result.append(left.pop(0))
        else:
            result.append(right.pop(0));
    while left:
        result.append(left.pop(0))
    while right:
        result.append(right.pop(0));
    return result

arr = [2,4,7,5,8,1,3,6]
mergeSort(arr)
当前树深度:3	左枝:[2]	右枝:[4]
当前树深度:3	左枝:[7]	右枝:[5]
当前树深度:2	左枝:[2, 4]	右枝:[5, 7]
当前树深度:3	左枝:[8]	右枝:[1]
当前树深度:3	左枝:[3]	右枝:[6]
当前树深度:2	左枝:[1, 8]	右枝:[3, 6]
当前树深度:1	左枝:[2, 4, 5, 7]	右枝:[1, 3, 6, 8]

[1, 2, 3, 4, 5, 6, 7, 8]

结论

  1. 归并不是原地排序算法

shell 排序

  1. 选择一个递减增量序列 k n = 3 ∗ n + 1 , n = 0 , 1 , . . . k_n=3*n+1,n=0,1,... kn=3n+1,n=0,1,...,即 1 , 4 , 13 , . . . 1,4,13,... 1,4,13,...;其中 3 ∗ n + 1 3*n+1 3n+1中的3代表首次分组,每组元素不超过3
  2. 将待排序序列分割成 k n k_n kn个子序列,对每个子序列进行插入排序
  3. 重复步骤2,直至 n = 0 n=0 n=0 k n = 1 k_n=1 kn=1
def shellSort(arr):
    import math
    gap=1
    while(gap < len(arr)/3):
        gap = gap*3+1
    while gap > 0:
        for i in range(gap,len(arr)):
            temp = arr[i]
            j = i-gap
            while j >=0 and arr[j] > temp:
                arr[j+gap]=arr[j]
                j-=gap
            arr[j+gap] = temp
        gap = math.floor(gap/3)
    return arr

def shellSort_my(arr):
    import math
    k_n = [1] # 递减增量序列 
    while(k_n[-1] < len(arr)/3):
        k_n.append(k_n[-1]*3+1)
    while k_n:
        gap = k_n[-1]
        for i in range(gap,len(arr)):
            temp,j = arr[i],i-gap # 第一组待插入元素,前一个元素下标
            while j >=0 and arr[j] > temp: # 存在前一个元素 且 前一个元素>待插入元素
                arr[j+gap]=arr[j] # 前一个元素往后挪一位
                j-=gap
            arr[j+gap] = temp # 当前位置插入新元素
        k_n.pop()
    return arr
arr = np.array([22, 34,  3, 32, 82, 55, 89, 50, 37,  5, 64, 35,  9, 70])
# shellSort(arr)
print(arr)
shellSort_my(arr)
[22 34  3 32 82 55 89 50 37  5 64 35  9 70]
13
4
1





array([ 3,  5,  9, 22, 32, 34, 35, 37, 50, 55, 64, 70, 82, 89])

结论

  1. shell排序是插入排序的一种更高效的改进版本,因为插入排序对已经排好序的数组排序时,可以达到线性排序的效率;shell排序将数组分割成若干个子序列分别进行插入排序,待序列中的数“基本有序”时,再对全体记录进行插入排序。
  2. 时间复杂度:
    1. 最好的情况,原数组有序,时间复杂度 O ( n ) O(n) O(n)
    2. 最坏的情况,原数组倒序,时间复杂度为 O ( n 2 ) O(n^2) O(n2)
    3. 平均时间复杂度为 O ( n 1.3 ) O(n^{1.3}) O(n1.3);计算较复杂
  3. 空间复杂度:原地排序,占用空间为 O ( 1 ) O(1) O(1)
  4. 稳定性:当相同元素被分在不同的组中,位置会发生改变,故不稳定

基数排序

堆排序

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值