参考资料:
排序方法 | 时间复杂度(平均情况,最好情况,最坏情况) | 空间复杂度 | 稳定性 |
---|---|---|---|
冒泡排序 | 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) | 稳定 |
注:
- 基数排序的复杂度中,r代表关键字的基数,d代表长度,n代表关键字的个数
- 排序算法稳定性:排序序列中存在多个相同的值,经过排序后,相对次数保持不变。
- 理解每种排序算法原理后,就能看懂菜鸟教程中的动画演示
冒泡排序
- 遍历数组,前后俩俩依次比较,如果前者比后者大,则交换两数;一轮结束后,数组中末尾数最大;类似鱼吐泡泡,故名冒泡
- 重复上述步骤,直至交换次数为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])
结论
- 数组长度为14,需遍历13轮,在第11轮交换次数为0,提前结束
- 时间复杂度
- 最好的情况,原数组有序,只需遍历一边数组,比较 n − 1 n-1 n−1次,移动 0 0 0次即可,时间复杂度 O ( n ) O(n) O(n);
- 最坏的情况,原数组倒序,需要遍历 n − 1 n-1 n−1轮数组,每轮比较 n − i , i = 1 , . . . , n − 1 n-i,i=1,...,n-1 n−i,i=1,...,n−1次,移动 n − i , i = 1 , . . . , n − 1 n-i,i=1,...,n-1 n−i,i=1,...,n−1次,累计比较 n ( n − 1 ) 2 = O ( n 2 ) \frac{n(n-1)}{2}=O(n^2) 2n(n−1)=O(n2)次,移动 n ( n − 1 ) 2 = O ( n 2 ) \frac{n(n-1)}{2}=O(n^2) 2n(n−1)=O(n2)次,时间复杂度为 O ( n 2 ) O(n^2) O(n2);
- 平均情况,原数组一半有序一半无序,每轮平均比较 n − i 2 , i = 1 , . . . , n − 1 \frac{n-i}{2},i=1,...,n-1 2n−i,i=1,...,n−1次,平均移动 n − i 2 , i = 1 , . . . , n − 1 \frac{n-i}{2},i=1,...,n-1 2n−i,i=1,...,n−1次,时间复杂度为 O ( n 2 ) O(n^2) O(n2);
- 空间复杂度:原地排序,占用空间为 O ( 1 ) O(1) O(1)
- 稳定性:数组元素相等不交换位置,故冒泡排序为稳定性排序
选择排序
思想:选择待排序序列中最小值,放入已排序序列末尾。原地排序,不稳定
- 待排序序列(每次少一个元素)中寻找最小元素下标 minIndex
- 交换 待排序序列第一个元素 与 最小元素。(当交换的值与后续元素相等,则稳定性破坏)
- 重复 n − 1 n-1 n−1次步骤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])
结论
- 数组长度为n,需遍历 n − 1 n-1 n−1轮,无法提前结束
- 时间复杂度:无论原数组是否有序,均需遍历
n
−
1
n-1
n−1轮数组
- 最好的情况,原数组有序,每轮比较 n − i , i = 1 , . . . , n − 1 n-i,i=1,...,n-1 n−i,i=1,...,n−1次,移动 0 0 0次;累计比较 n ( n − 1 ) 2 = O ( n 2 ) \frac{n(n-1)}{2}=O(n^2) 2n(n−1)=O(n2)次,时间复杂度 O ( n 2 ) O(n^2) O(n2);
- 最坏的情况,原数组倒序,每轮比较 n − i , i = 1 , . . . , n − 1 n-i,i=1,...,n-1 n−i,i=1,...,n−1次,移动 n − i , i = 1 , . . . , n − 1 n-i,i=1,...,n-1 n−i,i=1,...,n−1次,累计比较 n ( n − 1 ) 2 = O ( n 2 ) \frac{n(n-1)}{2}=O(n^2) 2n(n−1)=O(n2)次,移动 n ( n − 1 ) 2 = O ( n 2 ) \frac{n(n-1)}{2}=O(n^2) 2n(n−1)=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,5,3,1]
第一次交换结果为[1,5,3,3]
;我们发现原序列的第一个3排在了第二个3的后面,因此选择排序不稳定
插入排序
- 将数组第一个元素置于已排序数组中(一个元素自成有序列)
- 从待排序序列中选出任意一个元素(一般取第一个)
- 依次从后向前遍历已排序序列,直到 选出元素>=某个已排序元素,将选出元素置于其后
- 重复 n − 1 n-1 n−1次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])
结论
- 数组长度为n,需遍历 n − 1 n-1 n−1轮,无法提前结束
- 时间复杂度:无论原数组是否有序,均需遍历
n
−
1
n-1
n−1轮数组
- 最好的情况,原数组有序,每轮比较 1 1 1次,移动 0 0 0次;累计比较 n − 1 n-1 n−1次,时间复杂度 O ( n ) O(n) O(n);
- 最坏的情况,原数组倒序,每轮比较 i , i = 1 , . . . , n − 1 i,i=1,...,n-1 i,i=1,...,n−1次,移动 i , i = 1 , . . . , n − 1 i,i=1,...,n-1 i,i=1,...,n−1次,累计比较 n ( n − 1 ) 2 = O ( n 2 ) \frac{n(n-1)}{2}=O(n^2) 2n(n−1)=O(n2)次,移动 n ( n − 1 ) 2 = O ( n 2 ) \frac{n(n-1)}{2}=O(n^2) 2n(n−1)=O(n2)次,时间复杂度为 O ( n 2 ) O(n^2) O(n2);
- 平均时间复杂度为 O ( n 2 ) O(n^2) O(n2);
- 空间复杂度:原地排序,占用空间为 O ( 1 ) O(1) O(1)
- 稳定性:序列中相同的值,会按顺序选中插入新的序列,故稳定
快速排序
- 思想:分而治之,冒泡排序基础上的递归分治法
- 选择数列中第一个元素,作为比较基准
- 小于基准值的元素放在基准前面,其余元素放在基准的后面。此时,基准处于数列的中间位置,称为分区(partition)操作;
- 递归地(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]
结论
- 快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
归并排序
- 原理:二分法,分而治之的思想,类似二叉树。不断把数据集划分成左右两份,直至最小元素为 1,然后往树的根部排序合并
- 原理图示:
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]
结论
- 归并不是原地排序算法
shell 排序
- 选择一个递减增量序列 k n = 3 ∗ n + 1 , n = 0 , 1 , . . . k_n=3*n+1,n=0,1,... kn=3∗n+1,n=0,1,...,即 1 , 4 , 13 , . . . 1,4,13,... 1,4,13,...;其中 3 ∗ n + 1 3*n+1 3∗n+1中的3代表首次分组,每组元素不超过3
- 将待排序序列分割成 k n k_n kn个子序列,对每个子序列进行插入排序
- 重复步骤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])
结论
- shell排序是插入排序的一种更高效的改进版本,因为插入排序对已经排好序的数组排序时,可以达到线性排序的效率;shell排序将数组分割成若干个子序列分别进行插入排序,待序列中的数“基本有序”时,再对全体记录进行插入排序。
- 时间复杂度:
- 最好的情况,原数组有序,时间复杂度 O ( n ) O(n) O(n);
- 最坏的情况,原数组倒序,时间复杂度为 O ( n 2 ) O(n^2) O(n2);
- 平均时间复杂度为 O ( n 1.3 ) O(n^{1.3}) O(n1.3);计算较复杂
- 空间复杂度:原地排序,占用空间为 O ( 1 ) O(1) O(1)
- 稳定性:当相同元素被分在不同的组中,位置会发生改变,故不稳定