由于“代码随想录”没有讲排序算法,这一部分很基础但如果久了不用还是会忘记很多细节,在此总结
排序算法 | 最好情况 | 最坏情况 | 平均情况 | 空间复杂度 | 是否稳定 |
---|---|---|---|---|---|
冒泡排序 | O(n) | O(n²) | O(n²) | O(1) | ✅ 是 |
选择排序 | O(n²) | O(n²) | O(n²) | O(1) | ❌ 否 |
插入排序 | O(n) | O(n²) | O(n²) | O(1) | ✅ 是 |
希尔排序 | O(n log n)¹ | O(n²) | O(n^(1.3)~n^1.5)² | O(1) | ❌ 否 |
归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | ✅ 是 |
快速排序 | O(n log n) | O(n²) | O(n log n) | O(log n)³ | ❌ 否 |
堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | ❌ 否 |
冒泡排序
对于长度为n的array,进行n轮比较(或n-1轮),每轮比较结束时,都会将最大的数冒泡到array尾部
每轮比较都将相邻两个数进行比较
第一轮会比较n-1次
第二轮会比较n-2次(因为此时最大的数已经在队尾,不考虑了)
def bubble_sort(arr):
arr = arr.copy()
n = len(arr)
for i in range(n):
is_swap = False
for j in range(0, n - i - 1):
if arr[j] > arr[j+1]:
arr[j], arr[j+1] = arr[j+1], arr[j]
is_swap = True
if not is_swap:
break
return arr
选择排序
对于长度为n的array,每轮选择一个min值与前面的值交换,n轮(或n-1)后完成
def selection_sort(arr):
arr = arr.copy()
n = len(arr)
for i in range(n):
min_index = i
for j in range(i+1, n):
if arr[j] < arr[min_index]:
min_index = j
arr[i], arr[min_index] = arr[min_index], arr[i]
return arr
插入排序
对于长度为n的array,遍历到当前值时,认为之前的值都有序了,往前遍历找到插入位置(同时移动数组)
def insert_sort(arr):
arr = arr.copy()
n = len(arr)
for i in range(1, n):
key = arr[i]
j = i - 1
while j >= 0 and arr[j] > key:
arr[j+1] = arr[j]
j -= 1
arr[j+1] = key
return arr
希尔排序
将原array看作间隔为gap的多个子array,这样可以快速移动小值到前面,大值到后面
def shell_sort(arr):
arr = arr.copy()
n = len(arr)
gap = n // 2
while gap > 0:
for i in range(gap, n):
key = arr[i]
j = i - gap
while j >= 0 and arr[j] > key:
arr[j+gap] = arr[j]
j -= gap
arr[j+gap] = key
gap = gap // 2
return arr
归并排序
利用分治,当然这里是后序,在merge之前我需要保证左右子数列有序
def merge_sort(arr):
def merge(left_arr, right_arr):
res = []
i = 0
j = 0
while i < len(left_arr) and j < len(right_arr):
if left_arr[i] < right_arr[j]:
res.append(left_arr[i])
i += 1
else:
res.append(right_arr[j])
j += 1
res.extend(left_arr[i:])
res.extend(right_arr[j:])
return res
def recursion(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left_arr = recursion(arr[:mid])
right_arr = recursion(arr[mid:])
return merge(left_arr, right_arr)
arr = arr.copy()
return recursion(arr)
快速排序
思想是选取一个值pivot,比pivot小的放左边,比pivot大的放右边,递归处理。
快速排序的边界条件很难处理,我采取的区间是【】左闭右闭,选取第一个值为pivot
i永远指向比pivot大的值——i之前的值都<=pivot
j永远指向比pivot小的值——j之后的值都>=pivot
while中i与j的条件都必须带等号,不然i==j这种情况就跳出循环的话,以上两句话就可能不成立
(也就是j可能没正确指向比他小的值,而是指向>=pivot的值时刻,由于i==j跳出了循环)
导致pivot无法移动到正确的位置
def fast_sort(arr):
def recursion(arr, left, right):
if left >= right:
return
pivot = arr[left]
i = left + 1
j = right
while i <= j:
while i <= j and arr[i] <= pivot:
i += 1
while i <= j and arr[j] >= pivot:
j -= 1
#必须有的,没有的话会导致i > j的时候也做交换
if i < j:
arr[i], arr[j] = arr[j], arr[i]
arr[left], arr[j] = arr[j], arr[left]
recursion(arr, left, j - 1)
recursion(arr, j + 1, right)
return arr
arr = arr.copy()
return recursion(arr,0, len(arr)-1)
堆排序
堆是用数组实现的——底层数据结构是“完全二叉树”
我们以大顶堆为例,为什么可以用数组来存储这个堆?因为完全二叉树的性质决定了这些节点是“紧凑的”父子节点的坐标关系可以直接用下标计算,无需使用指针(也就不用链表啦)
当前节点i
左孩子left == 2 * i + 1
右孩子right == 2 * i + 2
孩子节点i
父节点 father = (i - 1 )// 2 or (i + 1) // 2 - 1
就是这点良好的性质,让我们可以很方便地去建堆、维护堆
首先实现heapify堆化函数,逻辑如下:
arr代表处理数组,n为数组长度,i为当前处理节点
首先假定,当前节点,左孩子,右孩子中最大者为当前节点,然后将三者比较,得出最大者的下标,如果i不为最大者则,交换i与其值(注意,此时交换后,可能改变该结点子树的“大顶堆性质”),那我需要递归地向下去做堆化,直到堆底部。)
然后就是建堆、排序的过程
def heap_sort(arr):
def heapify(arr, n, i):
largest_index = i
left = 2 * i + 1
right = 2 * i + 2
if left < n and arr[left] > arr[largest_index]:
largest_index = left
if right < n and arr[right] > arr[largest_index]:
largest_index = right
if largest_index != i:
arr[largest_index], arr[i] = arr[i], arr[largest_index]
#递归地向下做堆化
heapify(arr, n, largest_index)
arr = arr.copy()
n = len(arr)
#数组最后一个节点,也就是完全二叉树的最后一层的最右侧节点index为n-1,那么我们应该从他的父亲开始做堆化,
#就是(i+1) // 2 - 1
#也就是n // 2 -1
start_index = n // 2 - 1
for i in range(start_index, -1, -1):
heapify(arr, n, i)
#每次都把堆顶元素arr【0】,与arr【i】交换,也就是将最大元素放到了数列末端
for i in range(n -1, -1, -1):
arr[i], arr[0] = arr[0], arr[i]
#此时堆顶被改变,我们需要去维护堆的性质,那就从堆顶开始heapify
heapify(arr, i, 0)
return arr