核心思想
依次比较相邻的两个元素,将较大的元素“冒泡”到序列末端。
每一趟遍历,都会把当前未排序部分的最大元素放到该区域的尾。
稳定(相等元素不会改变相对次序)
伪代码
function bubbleSort(A[0..n-1]):
for i from 0 to n-2:
swapped = false // 标记本趟是否发生过交换
for j from 0 to n-2-i:
if A[j] > A[j+1]:
swap A[j], A[j+1]
swapped = true // 只要有一次交换,就标记为 true
if not swapped: // 如果本趟没有任何交换,说明已完全有序
break // 提前结束排序
return A
python实现
def bubble(arr):
l = len(arr)
for i in range(l-1):
sawp = False
for j in range(l-1-i):
if arr[j] > arr[j+1]:
arr[j], arr[j+1] = arr[j+1], arr[j]
sawp = True
if sawp == False:
break
return arr
lst = [5, 2, 9, 1, 5]
print("排序后:", bubble(lst))
过程演示:
以序列 [5,2,9,1,5](长度 n=5)为例,演示若按升序排列,如何将最大值逐步“冒泡”。
初始状态
A = [5, 2, 9, 1, 5]
第一趟(i=0,未排序区间 0..4)
比较 A[0]=5 与 A[1]=2,5>2 ⇒ 交换 ⇒ [2, 5, 9, 1, 5]
比较 A[1]=5 与 A[2]=9,5≤9 ⇒ 无交换 ⇒ [2, 5, 9, 1, 5]
比较 A[2]=9 与 A[3]=1,9>1 ⇒ 交换 ⇒ [2, 5, 1, 9, 5]
比较 A[3]=9 与 A[4]=5,9>5 ⇒ 交换 ⇒ [2, 5, 1, 5, 9]
本趟结果:最大元素 9 已移至末尾索引 4
第二趟(i=1,未排序区间 0..3)
比较 A[0]=2 与 A[1]=5,2≤5 ⇒ 无交换 ⇒ [2, 5, 1, 5, 9]
比较 A[1]=5 与 A[2]=1,5>1 ⇒ 交换 ⇒ [2, 1, 5, 5, 9]
比较 A[2]=5 与 A[3]=5,5≤5 ⇒ 无交换 ⇒ [2, 1, 5, 5, 9]
本趟结果:次大元素 5(第二个 5)已移至索引 3
第三趟(i=2,未排序区间 0..2)
比较 A[0]=2 与 A[1]=1,2>1 ⇒ 交换 ⇒ [1, 2, 5, 5, 9]
比较 A[1]=2 与 A[2]=5,2≤5 ⇒ 无交换 ⇒ [1, 2, 5, 5, 9]
本趟结果:第三大元素 5 已移至索引 2
第四趟(i=3,未排序区间 0..1)
比较 A[0]=1 与 A[1]=2,1≤2 ⇒ 无交换 ⇒ [1, 2, 5, 5, 9]
本趟结果:索引 1 处元素 2 已正确;索引 0 处元素 1 本身即为最小
核心思想
每一趟从“未排序”区间中选出最小(或最大)元素,将其放到“已排序”区间的末端(或开头),直到所有元素均归位。
数据规模较小、对稳定性不严格要求时可以直接使用。
当内存开销非常有限、只需常数级额外空间(O(1))时,选择排序可作为一种可行方案。
由于交换次数较少(最多 n−1次),在某些对交换代价敏感的场景里(比如写入操作成本高时)有一定优势。
伪代码
function selectionSort(A[0..n-1]):
for i from 0 to n-2: // 共需进行 n-1 趟
minIndex = i // 假设当前下标 i 处是“最小值“
// 在 [i+1..n-1] 范围内找真正的最小值
for j from i+1 to n-1:
if A[j] < A[minIndex]:
minIndex = j // 更新当前最小元素下标
// 若 minIndex 与 i 不同,则交换,放到已排序区尾部
if minIndex ≠ i:
swap A[i], A[minIndex]
return A
python实现
def Selection(arr):
l = len(arr)
for i in range(l-1):
min_index = i
for j in range(i,l):
if arr[j] < arr[min_index]:
arr[min_index],arr[j] = arr[j],arr[min_index]
return arr
补充 :补充双端选择排序,即每一次同时选择未排序的队列中的最小值和最大值,分别放到队列的两端。
def QuickSelection(arr):
left = 0
right = len(arr)-1
while left <= right:
min_index = left
max_index = right
for i in range(left, right+1):
if arr[i] < arr[min_index]:
min_index = i
if arr[i] > arr[max_index]:
max_index = i
arr[min_index], arr[left] = arr[left], arr[min_index]
arr[max_index], arr[right] = arr[right], arr[max_index]
left += 1
right -= 1
return arr
核心思想
将待排序的数组分为“已排序区间”和“未排序区间”,每次从未排序区间取出第一个元素,将其插入到已排序区间的正确位置,直到所有元素均插入完毕。
数据规模较小,或者待排序数组本身“近乎有序”时,插入排序效率较高。
适用于在线排序(数据不断到达,一边接收一边排序)。
由于算法简单易实现,也常用于“对部分有序序列进行最后微调”(如归并排序子段合并后,进行插入操作)。
伪代码
function insertionSort(A[0..n-1]):
for i from 1 to n-1:
key = A[i]
j = i - 1
// 在已排序区间 [0..i-1] 中,从后向前寻找合适插入位置
while j ≥ 0 and A[j] > key:
A[j + 1] = A[j] // 将 A[j] 向后移一位
j = j - 1
// 循环结束后,j == -1 或 A[j] ≤ key
A[j + 1] = key // 将 key 放到正确位置
return A
python实现
def insertion_sort1(arr):
for i in range(1,len(arr)):
key = i
j = i-1
while j >= 0 and arr[j] > arr[key]:
arr[j+1] = arr[j]
j -= 1
arr[j+1] = arr[key]
return arr
定义
快速排序(Quick Sort)是一种基于分治(Divide and Conquer)思想的高效排序算法。其核心思路是:
选取基准(pivot)。
分区(Partition):将待排序数组划分为两部分,左侧所有元素均不大于基准,右侧所有元素均不小于基准(某些实现方式略有不同)。
递归排序:对“左侧子数组”和“右侧子数组”分别递归执行快速排序,最终将整个数组排序完毕。
特点
平均时间复杂度较低,为 O(nlogn);
原地排序(in-place)——常见的分区方案仅需 O(logn)额外空间用于递归栈;
非稳定排序(unstable),相等元素的相对次序可能在交换过程中被改变;
实践中表现优异,是许多语言库(如 C++ std::sort
、Java 的基本类型排序)采用的核心算法。
适用场景
大多数随机、无序的场景下性能优秀;
内存允许时,原地实现减少空间开销;
尽管最坏时退化为O(n^2),但通过合理选取基准可极大降低退化出现概率,故在工业界被广泛使用。
伪代码
这里以Hoare 分区方案为例子
function partitionHoare(A, low, high):
pivot = A[(low + high) // 2]
i = low − 1
j = high + 1
while true:
do:
i = i + 1
while A[i] < pivot
do:
j = j − 1
while A[j] > pivot
if i ≥ j:
return j
swap A[i], A[j]
初始:
A = [8, 3, 1, 7, 0, 10, 2],
low=0, high=6 pivot = A[6] = 2 i = low−1 = −1
遍历 j=0..5:
j=0, A[0]=8 > 2 ⇒ 不交换,i remains −1
j=1, A[1]=3 > 2 ⇒ 不交换
j=2, A[2]=1 ≤ 2 ⇒ i=0, swap A[0]↔A[2] ⇒ A=[1,3,8,7,0,10,2]
j=3, A[3]=7 > 2 ⇒ 不交换
j=4, A[4]=0 ≤ 2 ⇒ i=1, swap A[1]↔A[4] ⇒ A=[1,0,8,7,3,10,2]
j=5, A[5]=10 > 2 ⇒ 不交换
最终 i=1。
归位基准
swap A[i+1]=A[2] ↔ A[6] ⇒ swap A[2]=8 与 A[6]=2 ⇒
A = [1, 0, 2, 7, 3, 10, 8]
返回 p = i+1 = 2 作为基准下标。
A[0..1] = [1,0] ≤ 2 A[2] = [2] == pivot A[3..6] = [7,3,10,8] ≥ 2
接下来分治排序 [1,0]
与 [7,3,10,8]
。
python实现
def Quick_sort(arr,low,high):
if low < high:
p = Hoare(arr,low,high)
Quick_sort(arr,low,p)
Quick_sort(arr,p+1,high)
return arr
def Hoare(arr,low,high):
pivot = arr[(low+high)//2]
i = low-1
j = high+1
while True:
i += 1 # 特别注意,当交换之后继续寻找下一个可交换点的关键
while arr[i] < pivot:
i += 1
j -= 1
while arr[j] > pivot:
j -= 1
if i >= j: # 循环while true结束的条件
return j
arr[i], arr[j] = arr[j], arr[i]
补充给出使用Lomuto 分区方案的代码
def partition_lomuto(arr, low, high):
pivot = arr[high]
i = low - 1
for j in range(low, high):
if arr[j] <= pivot:
i += 1
arr[i], arr[j] = arr[j], arr[i]
arr[i + 1], arr[high] = arr[high], arr[i + 1]
return i + 1
定义
归并排序(Merge Sort)是一种经典的分治(Divide and Conquer)排序算法。其核心思想是:
归并排序的核心在于“分治”与“合并”两部分:
分解(Divide)
给定数组 A[0..n-1]
,如果 n <= 1
,直接返回(数组已经有序)。
否则,计算中点 mid = ⌊(low + high)/2⌋
,将数组分为两半:A[low..mid]
和 A[mid+1..high]
。
递归排序
递归地对左半部分 A[low..mid]
调用归并排序。
递归地对右半部分 A[mid+1..high]
调用归并排序。
重复上述分解和合并过程,直到整个区间合并完毕,即得最终有序数组。
合并(Merge)
当左、右两个子数组分别已经排序后,将它们“归并”到一个临时数组 temp[]
中,使得 temp
也是有序的。具体做法是:
用两个指针 i = low
(指向左子数组起始)和 j = mid+1
(指向右子数组起始)。
比较 A[i]
与 A[j]
,较小的元素先放入 temp[k]
,并相应地移动 i
或 j
。
重复上述比较,直到其中一个子数组耗尽。
将另一个子数组剩余的所有元素依次复制到 temp
。
最后将 temp[low..high]
复制回 A[low..high]
,完成该区间的合并。
特点
时间复杂度 均衡且稳定,为 O(nlogn);
空间复杂度 需要额外 O(n)辅助空间(典型实现);
稳定排序(Stable),在合并过程中相等元素不会改变原有相对顺序;
可适用于外部排序(大规模数据需借助磁盘时常用多路归并);
递归实现简单直观,也可改写为自底向上的迭代实现。
适用场景
对大规模数据(在内存足够情况下)要求稳定排序时;
需要保证最坏时间复杂度为 O(nlogn)的场景;
在外部排序(外存排序)时,归并过程可扩展为多路归并;
对链表结构排序时,归并排序可做到 O(1)辅助空间并保持稳定。
伪代码
function mergeSort(A, low, high):
if low >= high:
return
mid = ⌊(low + high) / 2⌋
mergeSort(A, low, mid) // 对左半部分递归排序
mergeSort(A, mid + 1, high) // 对右半部分递归排序
merge(A, low, mid, high) // 合并两个有序子数组
function merge(A, low, mid, high):
// 准备临时数组 temp,用于存放合并结果
create array temp of size (high - low + 1)
i = low
j = mid + 1
k = 0
// 1. 合并过程:两个子数组都不为空时依次比较放入 temp
while i ≤ mid and j ≤ high:
if A[i] ≤ A[j]:
temp[k] = A[i]
i = i + 1
else:
temp[k] = A[j]
j = j + 1
k = k + 1
// 2. 如果左子数组还有剩余,全部复制到 temp
while i ≤ mid:
temp[k] = A[i]
i = i + 1
k = k + 1
// 3. 如果右子数组还有剩余,全部复制到 temp
while j ≤ high:
temp[k] = A[j]
j = j + 1
k = k + 1
// 4. 将 temp 中的元素复制回 A[low..high]
for t from 0 to (high - low):
A[low + t] = temp[t]
python实现
def merge_sort(A):
"""
归并排序(自顶向下递归实现)。
输入:
A: 待排序列表(会被修改)
返回:
无返回值,排序在 A 原地完成
"""
def _merge_sort(arr, low, high):
if low >= high:
return
mid = (low + high) // 2
# 1. 递归排序左半部分
_merge_sort(arr, low, mid)
# 2. 递归排序右半部分
_merge_sort(arr, mid + 1, high)
# 3. 合并两部分
_merge(arr, low, mid, high)
def _merge(arr, low, mid, high):
# 辅助数组 temp,用于暂存合并结果
temp = []
i, j = low, mid + 1
# 1. 当左右子数组都未遍历完,依次比较
while i <= mid and j <= high:
if arr[i] <= arr[j]:
temp.append(arr[i])
i += 1
else:
temp.append(arr[j])
j += 1
# 2. 如果左子数组有剩余,全部追加
while i <= mid:
temp.append(arr[i])
i += 1
# 3. 如果右子数组有剩余,全部追加
while j <= high:
temp.append(arr[j])
j += 1
# 4. 将 temp 中的元素复制回 arr[low..high]
for k in range(len(temp)):
arr[low + k] = temp[k]
# 调用递归函数对整个数组排序
_merge_sort(A, 0, len(A) - 1)
特点
非比较:不通过元素间两两比较来决定顺序,而是借助“计数”与“前缀和”直接定位位置。
线性时间:时间复杂度为 O(n+k),其中 n为元素个数,k为元素值域大小。
稳定排序:只要在合并输出时按照从后向前或维护计数时采用合适策略,就能保证相等元素的相对次序不变。
额外空间:需要一个大小约为 k+1的计数数组,以及一个大小约为 n的输出数组,空间复杂度为 O(n+k)。
伪代码
function countingSort(A, n, k):
// A: 待排序数组,索引范围 0..n-1,元素取值范围 0..k
// n: 数组长度;k: 元素最大值
// 1. 初始化计数数组 C[0..k] 为 0
create array C of length k+1, initialize all entries to 0
// 2. 统计频次
for i from 0 to n-1:
C[A[i]] = C[A[i]] + 1
// 3. 累加计数,计算前缀和
for i from 1 to k:
C[i] = C[i] + C[i-1]
// 4. 创建输出数组 B[0..n-1]
create array B of length n
// 5. 从后向前遍历 A,输出到 B
for i from n-1 down to 0:
v = A[i]
pos = C[v] - 1 // 最后一个值为 v 的元素应放在 pos
B[pos] = v
C[v] = C[v] - 1 // 预留下一个相同值放置位置
// 6. 若需将结果复制回 A,可执行:
for i from 0 to n-1:
A[i] = B[i]
// 返回有序结果 B(或 A)
return B
python实现
def counting_sort(A, k):
"""
计数排序(适用于非负整数)。
输入:
A: 待排序列表,元素均为整数,范围假设在 0..k。
k: 最大可能值(假定最小值为 0)。
输出:
就地修改 A,使之按升序排序;并返回排序结果列表。
"""
n = len(A)
# 1. 初始化计数数组 C[0..k] 为 0
C = [0] * (k + 1)
# 2. 统计频次
for v in A:
# 假设 v 在 0..k 范围内
C[v] += 1
# 3. 累加计数,计算前缀和
for i in range(1, k + 1):
C[i] += C[i - 1]
# 4. 创建输出数组 B[0..n-1]
B = [0] * n
# 5. 从后向前遍历 A,保证稳定性
for i in range(n - 1, -1, -1):
v = A[i]
pos = C[v] - 1 # 最后一个该值应放位置
B[pos] = v
C[v] -= 1
# 6. 将结果复制回 A
for i in range(n):
A[i] = B[i]
return A
核心思想
按位分组:先根据元素的最低位(个位)进行一次稳定的“计数排序”或“桶排序”,使得其在该位上有序。
逐位推进:再按次低位(十位)继续进行一次稳定排序,以此类推直到最高位。
稳定保证:由于每轮对各位进行的子排序均为稳定排序(如计数排序),上一轮已经排好序的低位次序得以保留,从而最终得到全局升序。
伪代码
function radixSort(A, n):
# A: 待排序的非负整数数组,长度为 n
# 1. 先找出最大值,以确定最大位数 d
max_val = max(A[0..n-1])
d = numberOfDigits(max_val) # 十进制位数,例如 max_val=356 -> d=3
# 2. 初始化基数 r = 10,用于针对各位做计数排序
r = 10
# 3. 从第 1 位(个位)到第 d 位逐位排序
for k from 1 to d:
# 3.1 创建计数数组 C[0..r-1],初始化为 0
create array C[0..r-1], initialize all to 0
# 3.2 创建输出数组 B[0..n-1]
create array B[0..n-1]
# 3.3 统计第 k 位的频次
for i from 0 to n-1:
digit = getDigit(A[i], k) # 获取 A[i] 的第 k 位数字 (1 表示个位, 2 表示十位, …)
C[digit] = C[digit] + 1
# 3.4 累加计数,计算前缀和
for i from 1 to r-1:
C[i] = C[i] + C[i - 1]
# 3.5 从后向前遍历 A,依据第 k 位将元素放到 B 中(保证稳定性)
for i from n-1 down to 0:
digit = getDigit(A[i], k)
pos = C[digit] - 1 # 该 digit 在输出中应放的位置
B[pos] = A[i]
C[digit] = C[digit] - 1
# 3.6 将 B 复制回 A,为下一位排序做准备
for i from 0 to n-1:
A[i] = B[i]
# 4. 完成所有位的排序,A 即为最终升序结果
return A
python实现
def radix_sort(A):
"""
基数排序(十进制 LSD 方式,针对非负整数列表)。
输入:
A: 待排序列表,元素均为非负整数
返回:
就地对 A 进行升序排序,并返回排序后的列表引用
"""
if not A:
return A
# 1. 找到最大值,以确定最大位数 d
max_val = max(A)
# 计算位数 d:例如 max_val=356 -> d=3
d = 0
while (10 ** d) <= max_val:
d += 1
# 2. 从第 1 位到第 d 位,依次进行计数排序
# 令 exp = 10^(k-1),每次定位到 k 位:digit = (num // exp) % 10
exp = 1 # 代表 10^(k-1),初始为 1(个位)
n = len(A)
# 临时输出数组
B = [0] * n
for _ in range(d):
# 2.1 构造计数数组 C[0..9],初始化为 0
C = [0] * 10
# 2.2 统计当前位(exp 指定的位)的频次
for num in A:
digit = (num // exp) % 10
C[digit] += 1
# 2.3 累加计数,计算前缀和
for i in range(1, 10):
C[i] += C[i - 1]
# 2.4 从后向前遍历 A,依据当前位将元素放到 B
for i in range(n - 1, -1, -1):
num = A[i]
digit = (num // exp) % 10
pos = C[digit] - 1
B[pos] = num
C[digit] -= 1
# 2.5 将 B 复制回 A,为下一位排序做准备
for i in range(n):
A[i] = B[i]
# 2.6 准备下一个更高位:从个位(exp=1)到十位(exp=10)、百位(exp=100)……
exp *= 10
return A
根据数据分布,将待排序元素均匀分布到若干个“桶”(Bucket)中。
对每个桶内部分别进行排序(通常采用插入排序或其他高效算法)。
最后按桶的顺序将各桶中元素依次合并,得到整体有序序列。
伪代码
function bucketSort(A[0..n-1], L, U, m):
# A: 待排序数组,长度 n
# L, U: 值域区间下界和上界(假设 A 中所有元素都满足 L ≤ x < U)
# m: 桶的数量
# 1. 初始化 m 个空桶,每个桶用动态数组(或链表)表示
create array buckets[0..m-1]
for i from 0 to m-1:
buckets[i] = empty list
# 2. 将元素分发到各自桶
Δ = (U - L) / m # 每个桶负责的区间跨度
for i from 0 to n-1:
x = A[i]
idx = floor((x - L) / Δ)
if idx == m: # 如果 x 恰好等于 U(边界情况)
idx = m - 1
buckets[idx].append(x)
# 3. 对每个桶内部进行排序(使用稳定的插入排序或其他)
for i from 0 to m-1:
insertionSort(buckets[i]) # 假设 insertionSort 是稳定的
# 4. 合并所有桶内容到输出数组 B
create array B[0..n-1]
pos = 0
for i from 0 to m-1:
for each element x in buckets[i]:
B[pos] = x
pos = pos + 1
# 5. (可选)将 B 复制回 A
for i from 0 to n-1:
A[i] = B[i]
return A # 或直接返回 B
python实现
def bucket_sort(A):
"""
桶排序(适用于浮点数,假设所有元素都在 [0, 1) 区间内)。
输入:
A: 待排序列表,元素均为 float,0 <= x < 1
返回:
就地对 A 进行升序排序,并返回排序后的列表引用
"""
n = len(A)
if n <= 1:
return A
# 1. 创建 n 个空桶(列表嵌套列表)
buckets = [[] for _ in range(n)]
# 2. 将元素分发到各自桶:桶索引为 floor(x * n)
for x in A:
idx = int(x * n) # idx in [0..n-1],当 x==1 时要特殊处理,但此处假设 x < 1
buckets[idx].append(x)
# 3. 对每个桶内部使用插入排序进行排序
for i in range(n):
insertion_sort(buckets[i]) # 使用下文定义的插入排序函数
# 4. 合并所有桶到 A
pos = 0
for i in range(n):
for x in buckets[i]:
A[pos] = x
pos += 1
return A
def insertion_sort(lst):
"""
插入排序,对列表 lst 原地升序排序(保证稳定性)。
"""
for i in range(1, len(lst)):
key = lst[i]
j = i - 1
# 向左移动大于 key 的元素
while j >= 0 and lst[j] > key:
lst[j + 1] = lst[j]
j -= 1
lst[j + 1] = key
return lst
算法 | 最坏时间复杂度 | 平均时间复杂度 | 最好时间复杂度 | 空间复杂度 | 稳定性 | 额外说明 |
---|---|---|---|---|---|---|
冒泡排序 | O(n²) | O(n²) | O(n) | O(1) | 稳定 | 简单易懂、性能差,只适合小数据或近乎有序场景 |
选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 | 交换次数少(最少为 n−1 次),但比较次数固定;不稳定 |
插入排序 | O(n²) | O(n²) | O(n) | O(1) | 稳定 | 对近乎有序效果好,可用于小规模排序或部分有序数据的最终“微调” |
希尔排序 | O(n·(log n)²)〜O(n^(1.5)) (视 gap 序列而定) | – | – | O(1) | 不稳定 | 介于 O(n²) 与 O(n log n) 之间,性能取决于增量序列;常用于中等规模数组 |
归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | 稳定 | 分治思想,性能稳定;需要线性额外空间;可做外部归并排序 |
快速排序 | O(n²) | O(n log n) | O(n log n) | O(log n) (递归栈) | 不稳定 | 在随机化或“三数取中”等优化下,实际性能很高;若无优化,最坏可能退化;不稳定 |
堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | 不稳定 | 原地排序,无需额外线性空间,但不稳定;常用于需要 O(n log n) 且空间受限场景 |
计数排序 | O(n + k) | O(n + k) | O(n + k) | O(n + k) | 稳定 | 适用于整数且 k 较小;线性时间;非比较排序 |
桶排序 | 平均 O(n + m) | – | – | O(n + m) | 视桶内排序算法而定 | 适合均匀分布数据;最坏 O(n²);空间按桶数 m 而定 |
基数排序 | O(d·(n + k)) | – | – | O(n + k) | 稳定 | 适合定长整数或字符串排序;当 d、k 为常数时近似线性;依赖稳定子过程 |
Timsort | O(n log n) | O(n)(部分有序) | O(n)(部分有序) | O(n) | 稳定 | 结合插入与归并,针对“部分有序”优化;Python、Java 等语言默认内置 |
小规模或近乎有序数据
可优先考虑 插入排序 或 冒泡排序 (带有“有序检测”优化)。
若追求低常数开销,可选 插入排序(本地缓存命中率好)。
中等规模数据
快速排序(随机化版)也是不错选择。
大规模数据且内存足够
快速排序(在绝大多数编程语言标准库中都有高效实现);
归并排序(若对稳定性有要求或计划做外部归并)。
对稳定性有严格要求时
归并排序、计数排序、基数排序、插入排序(理想场景)。
数据范围已知且元素为整数时
计数排序、基数排序、或者桶排序(特别是浮点数分布已知场景)。