1.冒泡排序
from typing import *
T = TypeVar('T')
def bubble_sort(arr: list[T], cmp_func: Callable[[T, T], bool]):
"""
+ 假设数组`arr`长度为n,冒泡排序算法流程如下:
1) 对n个元素执行“冒泡”,将最大(最小)元素交换至正确位置
2) 对n-1个元素执行“冒泡”,将最大(最小)元素交换至正确位置
3) 重复以上过程,经过n-1轮“冒泡”后,数组前n-1大(小)元素已正确排序
4) 仅剩的一个元素必然是最小(最大),无需排序,因此整个数组排序完成
+ 算法特性:
1) 时间复杂度:O(n^2)
2) 空间复杂度:O(1),原地排序,无需额外空间
3) 稳定性排序,交换过程不会改变相同元素在数组中的相对位置
"""
if len(arr) <= 1:
return
n = len(arr)
# 外层循环,未排序区间[0, i]
for i in range(n-1, 0, -1):
flag = False # 标记本轮冒泡过程是否发生交换
# 内层循环;将未排序区间的最大(最小)元素交换到区间最右侧
for j in range(i):
if cmp_func(arr[j+1], arr[j]):
arr[j+1], arr[j] = arr[j], arr[j+1]
flag = True
if not flag: # 本轮未发生交换,则直接跳出
break
2.插入排序
from typing import *
T = TypeVar('T')
def insertion_sort(arr: list[T], cmp_func: Callable[[T, T], bool]):
"""
+ 假设数组`arr`大小为n,选择排序算法流程如下:
1) 初始状态下,数组的第一个元素已经有序
2) 选取数组的第2个元素作为base,将其插入到合适位置,此时数组前两个元素有序
3) 选取数组的第3个元素作为base,将其插入到合适位置,此时数组前三个元素有序
4) 重复以上过程,在最后一轮中,选取最后一个元素作为base,将其插入到合适位置后,数组整体有序
+ 算法特性:
1) 时间复杂度:O(n^2)
2) 空间复杂度:O(1),无需额外空间
3) 稳定性排序,交换过程不会改变相同元素在数组中的相对位置
4) 尽管插入排序的时间复杂度较高,但在数据量较小的情况下,插入排序通常更快。
"""
if len(arr) <= 1:
return
n = len(arr)
# 外层循环,已排序区间[0, i-1]
for i in range(1, n):
base = arr[i]
j = i - 1
# 内层循环,将base插入到已排序区间[0, i-1]中的合适位置
while j >= 0 and cmp_func(base, arr[j]):
arr[j+1] = arr[j] # 将第j个元素向后移一位
j -= 1
arr[j+1] = base # 将base赋值到正确位置
3.选择排序
from typing import *
T = TypeVar('T')
def selection_sort(arr: list[T], cmp_func: Callable[[T, T], bool]):
"""
+ 设数组`arr`长度为n,选择排序算法流程如下:
1) 初始状态下,所有元素均为排序,即未排序区间为[0, n-1]
2) 选取区间[0, n-1]中最小(最大)的元素,将其与第0个元素交换,完成后数组前1个数据有序
3) 选取区间[1, n-1]中最小(最大)的元素,将其与第1个元素交换,完成后数组前2个数据有序
4) 重复以上过程,经过n-1轮选择和交换后,数组的前n-1个元素有序
5) 仅剩的最后一个元素必然是整个数组的最小(最大)元素,无需排序,因此数组排序完成
+ 算法特性:
1) 时间复杂度: O(n^2)
2) 空间复杂度: O(1),原地排序,无需额外空间
3) 非稳定性排序,交换过程中相同元素在数组中的相对位置可能会发生改变
"""
n = len(arr)
# 外层循环n-1次,未排序区间[i, n-1]
for i in range(n - 1):
k = i
# 内层循环找到未排序区间最小(最大)值索引
for j in range(i+1, n):
if cmp_func(arr[j], arr[k]):
k = j # 选择未排序区间最小(最大)值索引
# 与区间首个元素交换
arr[i], arr[k] = arr[k], arr[i]
4. 归并排序
def merge(arr: list[int], left: int, mid: int, right: int):
# 合并左子数组[left, mid]和右子数组[mid+1, right]
tmp_arr = [0] * (right - left + 1) # 临时数组存储排序结果
# 每次取两个子数组中较小的数放入临时数组中
i, j, k = left, mid + 1, 0
while i <= mid and j <= right:
if arr[i] < arr[j]:
tmp_arr[k] = arr[i]
i += 1
else:
tmp_arr[k] = arr[j]
j += 1
k += 1
# 将左子数组和右子数组剩余的元素放入临时数组中
while i <= mid:
tmp_arr[k] = arr[i]
i += 1
k += 1
while j <= right:
tmp_arr[k] = arr[j]
j += 1
k += 1
# 将已经排好序的临时数组元素复制回原数组对应的区间
for i in range(len(tmp_arr)):
arr[left + i] = tmp_arr[i]
def merge_sort(arr: list[int], left: int, right: int):
"""
+ 归并排序算法分为“递归”和“合并”两个阶段:
1) 递归阶段:通过递归不断地将数组从中点处分开,将长数组的排序问题转换为短数组的排序问题
2) 合并阶段:当子数组长度为 1 时终止划分,开始合并,持续地将左右两个较短的有序数组合并为
一个较长的有序数组,直至结束。
+ 算法特性:
1) 时间复杂度:O(NlogN)
2) 空间复杂度:O(N),非原地排序,合并过程需要借助辅助数组实现,需要使用O(N)大小的额外空间
3) 稳定排序:合并过程中相同元素的次序不变
"""
if left >= right:
return
mid = left + (right - left)//2
merge_sort(arr, left, mid)
merge_sort(arr, mid+1, right)
merge(arr, left, mid, right)
5. 快速排序
from typing import *
def median_three(arr: list[int], left: int, mid: int, right: int) -> int:
"""三数取中优化"""
l, m, r = arr[left], arr[mid], arr[right]
if (l <= m <= r) or (r <= m <= l):
return mid
if (r <= l <= m) or (m <= l <= r):
return left
return right
def partition(arr: list[int], left: int, right: int) -> int:
"""
+ 哨兵划分算法流程如下:
1) 选取数组最左端元素arr[left]为基准(或采取三数取中),初始化两个指针i和j分别指向数组两端
2) 设置一个循环,每轮循环中使用i(j)寻找第一个比基准大(小)的位置,然后交换这两个位置上的元素
3) 循环执行步骤2),直到两个指针相遇时停止,将基准元素与两个子数组分界位置元素交换,此时基准元素已处于正确位置
4) 返回基准元素的索引
"""
median = median_three(arr, left, left+(right-left)//2, right)
# 将中位数与left交换
arr[left], arr[median] = arr[median], arr[left]
pivot = arr[left]
i, j = left, right
while i < j:
while i < j and arr[j] >= pivot:
j -= 1 # 从右向左找第一个小于基准的元素
while i < j and arr[i] <= pivot:
i += 1 # 从左到右找第一个大于基准的元素
arr[i], arr[j] = arr[j], arr[i] # 交换元素
# 将基准元素交换至两子数组分界位置
arr[i], arr[left] = arr[left], arr[i]
# 返回基准元素位置索引
return i
def quick_sort(arr: list[int], left: int, right: int):
"""
+ 快速排序算法流程如下:
1) 首先对原数组执行一次"哨兵划分",得到未排序的左子数组和右子数组
2) 然后,对左子数组和右子数组分别递归执行"哨兵划分"过程
3) 持续递归,直到子数组长度为1,此时整个数组排序完成
+ 算法特性:
1) 时间复杂度:O(NlogN),最坏情况时间复杂度O(N^2)
2) 空间复杂度:O(N),原地排序, 尾递归优化:O(logN)
3) 非稳定性排序,在哨兵划分的最后一步,基准数可能会被交换至相等元素的右侧。
"""
# 尾递归优化
# 子数组长度为1时终止
while left < right:
# 哨兵划分操作
pivot = partition(arr, left, right)
# 对两个子数组中较短的那个执行队递归
if pivot - left < right - pivot:
quick_sort(arr, left, pivot-1) # 递归排序左数组
left = pivot + 1 # 未排序区间[pivot+1, right]
else:
quick_sort(arr, pivot+1, right) # 递归排序右数组
right = pivot - 1 # 未排序区间[left, pivot-1]
6.堆排序
def heap_sort(arr: list[int]):
"""
+ 堆排序算法流程:
1) 输入数组并建立大顶堆。完成后,最大元素位于堆顶
2) 将堆顶元素(第一个元素)与堆底元素(最后一个元素)交换。完成交换后,堆的长度减1,
已排序元素数量加1
3) 从堆顶元素开始,从顶到底执行堆化操作(sift down)。完成堆化后,堆的性质得到修复。
4) 循环执行第(2)步和第(3)步。循环`n-1`轮后,即可完成数组排序。
+ 算法特性:
1) 时间复杂度:O(NlogN)
2) 空间复杂度:O(1),原地排序,交换元素和堆化操作都是在原数组上进行的
3) 非稳定性排序:交换堆顶和堆底元素时,相同元素的相对位置可能会发生改变
"""
# 建大顶堆:堆化除叶节点以外的所有子节点
for i in range(len(arr)//2, -1, -1):
_adjust_down(arr, i, len(arr))
# 从堆中提取最大元素(堆顶元素),循环n-1轮
for i in range(len(arr)-1, 0, -1):
# 交换堆顶与堆底
arr[0], arr[i] = arr[i], arr[0]
# 从根节点开始向下调整
_adjust_down(arr, 0, i)
def _adjust_down(arr: list[int], i: int, n: int):
# 大顶堆调整算法
# n: 数组arr有效长度; i: 待调整的节点索引
while True:
# 从节点i及其左右子节点中找出最大值节点
pa = i
left = 2 * i + 1
right = 2 * i + 2
if left < n and arr[left] > arr[pa]: #
pa = left
if right < n and arr[right] > arr[pa]:
pa = right
if pa == i: # 不需要调整
break
# 将最大值交换到父节点i
arr[i], arr[pa] = arr[pa], arr[i]
# 循环向下调整
i = pa
7. 桶排序
def bucket_sort(arr: list[int], bucket_nums: int = 5):
"""
+ 桶排序算法流程如下:
1) 初始化k个桶,将n个元素分配到k个桶中。
2) 对每个桶分别执行排序
3) 按照桶从小到大的顺序合并结果。
+ 算法特性:
1) 桶排序适用于处理体量很大的数据。
2) 时间复杂度:O(N + K)
3) 自适应排序:在最差情况下,所有数据被分配到一个桶中,且排序该桶使用O(N^2)时间
4) 空间复杂度:O(N + K),非原地排序
5) 桶排序是否稳定取决于排序桶内元素的算法是否稳定。
"""
if len(arr) <= 1:
return
buckets = [[] for _ in range(bucket_nums)] # 初始化bucket_nums个桶
min_val, max_val = float("inf"), -float("inf")
for num in arr:
if num < min_val:
min_val = num
if num > max_val:
max_val = num
# 将数组元素分配到各个桶内
for num in arr:
i = int((num-min_val)*(bucket_nums-1)/max_val)
buckets[i].append(num)
# 对各个桶分别执行排序
i = 0
for bucket in buckets:
if len(bucket) > 0:
insertion_sort(bucket, lambda x1, x2 : x1 < x2) # 调用插入排序算法对桶内元素排序
for num in bucket:
arr[i] = num
i += 1
8. 计数排序
def counting_sort(arr: list[int]):
"""
+ 计数排序算法完整流程如下:
1) 遍历数组,找出其中的最大数字,记为`m`,然后创建一个长度为`m+1`的辅助数组`counter`
2) 借助`counter`统计`arr`中各数字的出现次数,其中`counter[num]`对应数字`num`的出现次数
3) 计算`counter`的前缀和数组`prefix`,前缀和数组的含义:`prefix[num]-1`表示元素`num`在
结果数组`res`中最后一次出现的索引
4) 倒序遍历原数组`arr`中的每个元素`num`,将`num`填入数组`res`的索引`prefix[num]-1`处,令
前缀和`prefix[num]`减小1,得到下次放置`num`的索引
5) 遍历完成后,数组`res`中存放的就是排序好的结果,最后将其覆盖原数组`arr`
+ 算法特性:
1) 时间复杂度:O(N + M)
2) 空间复杂度:O(N + M),非原地排序
3) 稳定性排序:由于向`res`中填充元素的顺序是“从右向左”的,因此倒序遍历`arr`可以避免改变相等
元素之间的相对位置
"""
if len(arr) <= 1:
return
# 1. 统计原数组最大值
max_val = -float("inf")
for num in arr:
max_val = max(max_val, num)
# 2. 统计原数组每个元素出现的次数
counter = [0 for _ in range(max_val + 1)]
for num in arr:
counter[num] +=1
# 3. 计算counter的前缀和
for i in range(len(counter)-1):
counter[i+1] += counter[i]
# 4.倒序遍历arr
res = [0 for _ in range(len(arr))]
for i in range(len(arr)-1, -1, -1):
num = arr[i]
res[counter[num] - 1] = num
counter[num] -= 1
# 5. 用res覆盖arr
for i in range(len(arr)):
arr[i] = res[i]
9. 基数排序
def _digit(num: int, exp: int):
# 获取num第k位上的数字,其中,exp=10^(k-1)
return (num // exp) % 10
def _counting_sort_digit(arr: list[int], exp: int):
# 对arr中的元素按照第k位上的数字进行计数排序
counter = [0] * 10
for num in arr:
d = _digit(num, exp)
counter[d] += 1
for i in range(1, 10):
counter[i] += counter[i-1]
res = [0] * len(arr)
for i in range(len(arr)-1, -1, -1):
num = arr[i]
d = _digit(num, exp)
idx = counter[d] - 1
res[idx] = num
counter[d] -= 1
for i in range(len(arr)):
arr[i] = res[i]
def radix_sort(arr: list[int]):
"""
+ 假设数字的最低位是第1位,基数排序算法流程如下:
1) 初始化k=1
2) 对数字的第k位进行“计数排序”,完成后,数据会按照第k位从小到大排序
3) 将k增加1,返回步骤2),继续迭代,直到所有位都排序后结束
+ 算法特性:
1) 相较于计数排序,基数排序适用于数值范围较大的情况,但前提是数据必须可以
表示为固定位数的格式,且位数不能过大
2) 时间复杂度:O(nk), n为数据量,k为数据最大位数
3) 空间复杂度:O(n + d),d为进制数
4)排序稳定性取决于内部计数排序算法的稳定性
"""
m = max(arr)
# 按照低位到高位的顺序遍历
exp = 1
while exp <= m:
# 对数组元素的第k位进行计数排序
_counting_sort_digit(arr, exp)
exp *= 10 # 循环遍历下一位
参考:Hello 算法