总结
本文主要介绍常见排序及其python实现。试图用尽可能短的描述解释清楚几种排序算法,权当方便自己理解记忆。
排序方法 | 平均时间复杂度 | 最坏时间复杂度 | 最好时间复杂度 | 稳定性 |
---|---|---|---|---|
选择排序 | 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 ( n 2 ) O(n^2) O(n2) | 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 ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | 稳定 |
希尔排序 | O ( n 1.3 ) O(n^{1.3}) O(n1.3) | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | 不稳定 |
归并排序 | O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) | O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) | O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) | 稳定 |
快速排序 | O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) | O ( n 2 ) O(n^2) O(n2) | O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) | 不稳定 |
堆排序 | O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) | O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) | O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) | 不稳定 |
选择排序
将元素分为两部分,左边是已排序区,右边是待排序区,每次从待排序部分中选择最小的数与第一个待排序元素(也即已排序区后面的第一个元素)交换。
不管数组原先是否有序,时间复杂度都是
O
(
n
2
)
O(n^2)
O(n2)。
不稳定举例:
(7) 2 5 9 3 4 [7] 1
利用选择排序算法进行排序时候,(7)和1调换,(7)就跑到了[7]的后面了,原来的次序改变了,因此不稳定。
def selection_sort(arr):
l = len(arr)
for i in range(l-1): # 左边已排序边界
min_index = i
for j in range(i+1, l): # 对于右边待排序元素,选出最小与最左交换
if arr[j] < arr[min_index]:
min_index = j
arr[i], arr[min_index] = arr[min_index], arr[i]
return arr
冒泡排序
将元素分为两部分,左边是待排序区,右边是已排序区。每次两两比较相邻元素,如果顺序不对,交换,这样一轮时候最大的数字被交换到最右。
不管数组原先是否有序,时间复杂度都是
O
(
n
2
)
O(n^2)
O(n2)。
相同元素不交换能保证稳定。
def bubble_sort(arr):
l = len(arr)
for i in range(l-1, -1, -1): # 已排序区边界
for j in range(i): # 对于待排序元素依次比较交换
if arr[j] > arr[j + 1]: # >保证稳定;如果是>=,则不稳定
arr[j], arr[j + 1] = arr[j + 1], arr[j]
return arr
插入排序
左边是已排序区,右边是待排序区,对于每个未排序元素,将其与前面已排序元素依次对比(从右到左)之后插入到正确的位置。
当原数组有序时,是最佳情况,时间复杂度为
O
(
n
)
O(n)
O(n)。
将元素插入到排序区中时,如果遇到相等元素,结束交换,能保证稳定性。
def insert_sort(arr):
l = len(arr)
for i in range(1, l): # 对于右边待排序区
num = arr[i]
j = i - 1
while j >= 0 and arr[j] > num: # 如果待排序元素比排序区中元素小,应该查到排序区里面。 >保证稳定;如果是>=,则不稳定
arr[j+1] = arr[j] # 将已排序元素往后挪一位,给num腾位置
j -= 1
arr[j+1] = num
return arr
希尔排序
先将整个序列分割成为若干子序列分别进行插入排序,待整个序列中的记录“基本有序”时,再对全体元素依次插入排序。
def shell_sort(arr):
count = len(arr)
step = 2
group = count // step
while group > 0:
for i in range(group):
j = i + group
while j < count:
k = j - group
key = arr[j]
while k >= 0:
if arr[k] > key:
arr[k + group] = arr[k]
arr[k] = key
k -= group
j += group
group //= step
return arr
归并排序
将待排序数组分为两段,递归排序。使用辅助数组helper,需要额外空间,空间复杂度O(n)。
merge时,如果遇到两个元素相等,先拷贝左边的,能保证稳定性。
def merge(arr, L, mid, R): # 对L~mid, mid+1~R合并
i, j = L, mid + 1
helper = [] # 辅助数组大小在所有过程加起来长度=len(arr),因此空间复杂度O(n),不过这里每次使用完都“销毁”
while i <= mid and j <= R:
if arr[i] <= arr[j]: # 如果两个元素相等,先拷贝左边的,因此能保证稳定性
helper.append(arr[i])
i += 1
else:
helper.append(arr[j])
j += 1
helper += arr[i:mid+1]
helper += arr[j:R+1]
arr[L:R+1] = helper
def merge_sort(arr, L, R):
# 归并排序,递归操作
if L == R:
return
mid = (L + R) // 2
merge_sort(arr, L, mid) # 对左侧排序
merge_sort(arr, mid+1, R) # 对右侧排序
merge(arr, L, mid, R) # 左右两侧合并
def merge_sort_main(arr):
L = 0
R = len(arr) - 1
merge_sort(arr, L, R)
print(arr)
merge_sort_main([4,3,7,1,7,8,5])
快速排序
主要思想:
- partition操作:选取pivot,将要数组分成两部分,左边区域<=x,右边区域>x;
- 对这两部分数据分别重复步骤1。
主要操作:对数组arr从L到R位置进行partition操作——返回pivot最终的位置
我们指定最右边元素arr[R]为pivot,进行如下操作:
- 左边区域为<=区域,初始边界为L-1;
- 使用指针 i 初始指向起点L,比较arr[i]与arr[R]:
如果arr[i] <= arr[R]:arr[i]与<=区域后面元素交换,<=区域扩大一位;
每次比较完指针 i 右移,直到 i 到达R,结束; - 最后,将arr[R]与<=区域后面元素交换,并返回arr[R]的位置。
图解partition:
注意,使用快排空间复杂度是O(logn)。
def partition(arr, L, R):
less = L - 1 # 小于等于区域的右边界
for i in range(L, R): # 遍历数组的指针i
if arr[i] <= arr[R]:
arr[i], arr[less+1] = arr[less+1], arr[i]
less += 1
arr[less+1], arr[R] = arr[R], arr[less+1]
return less+1
def quick_sort(arr, L, R):
if L < R:
pivot = partition(arr, L, R)
quick_sort(arr, L, pivot - 1)
quick_sort(arr, pivot + 1, R)
def quick_sort_main(arr):
l = len(arr)
return quick_sort(arr, 0, l-1)
arr = [4, 1, 8, 10, 7, 3, 13, 9, 2]
quick_sort_main(arr)
最坏情况:数组已经排好序且每次pivot选取最右边元素。
解决:pivot随机选取,在quick_sort中添加两行index = random.randint(L, R), arr[index], arr[R] = arr[R], arr[index]
即可。
三路快排:
问题:
如果重复的元素比较多,使用上面的快排方法会比较慢,因为对于等于pivot的元素需要重复比较。
解决:
partition操作改为将数组分为三个区域:小于区域,等于区域,大于区域。
改进后代码如下:
import random
def partition3(arr, L, R):
less = L - 1
more = R
while L < more:
if arr[L] < arr[R]:
arr[L], arr[less + 1] = arr[less + 1], arr[L]
less += 1
L += 1
elif arr[L] > arr[R]:
arr[L], arr[more - 1] = arr[more - 1], arr[L]
more -= 1
else:
L += 1
arr[R], arr[more] = arr[more], arr[R]
return [less + 1, more]
def quick_sort(arr, L, R):
if L < R:
index = random.randint(L, R)
arr[index], arr[R] = arr[R], arr[index]
pivot1, pivot2 = partition3(arr, L, R)
quick_sort(arr, L, pivot1 - 1)
quick_sort(arr, pivot2 + 1, R)
def quick_sort_main(arr):
return quick_sort(arr, 0, len(arr) - 1)
arr = [4, 1, 8, 10, 7, 3, 13, 9, 2]
quick_sort_main(arr)
堆排序
建立最大堆后,每次把根节点换到未排序的节点,因此越往后是越大的,用来升序排序;
同理,建立最小堆,用来降序排序。
对于用数组表示的二叉树中的节点i,且父节点下标为(i-1)//2,左孩子为2i+1,右孩子为2i+2。
通过建立最大堆得到升序排序:
- 建最大堆:从倒数第二行末尾(下标为n//2)开始,判断其是否大于左右孩子,如不满足,需要交换,最后得到最大堆;
- 排序:认为堆的末尾为已排序区域,将根节点(未排序区域最大值)与排序区前一个元素交换,排序区扩大一位,交换后需调整堆使其继续满足最大堆,重复该操作直到排序区域扩展到根节点,说明已经完成排序。
注意,在建立最大堆的过程,就可能将相等元素原先的相对位置破坏,因此不稳定。
def adjust_heap(arr, i, up):
# i:需要调整的节点
# up:只调整arr[0, up-1],如果调整整个列表是arr[0,n-1]
while 2 * i + 1 < up: # 如果有左子节点
son = 2 * i + 1
if son + 1 < up and arr[son + 1] > arr[son]: # 先把son指向最大的儿子
son += 1
if arr[i] < arr[son]: # 判断是否需要交换
arr[i], arr[son] = arr[son], arr[i]
i = son
else:
break
# 建最大堆操作
def build_heap(arr):
n = len(arr)
for i in range(n // 2)[::-1]:
adjust_heap(arr, i, n)
def heap_sort(arr):
n = len(arr)
build_heap(arr)
for i in range(n)[::-1]:
arr[i], arr[0] = arr[0], arr[i] # 交换根节点和待排序的最后一个节点
adjust_heap(arr, 0, i) # 交换完后调整树,注意已排序的不需要调整了
print(arr[::-1]) # 从大到小排序
print(arr) # 从小到大排序
heap_sort([23, 1, 5, 3, 2, 6, 26])
拓展
下面是排序算法的一些拓展应用题目: