目录
排序算法
排序算法用于将一组无序数据按照指定规则(如升序或降序)重新排列,是数据结构与算法中的基础内容。在分析和选择排序算法时,除了时间复杂度和空间复杂度外,还需要重点关注两个重要特性:
- 排序稳定性(Stability)
- 是否为原地排序(In-place Sort)
这两个概念在工程实践中具有直接的业务影响。
排序算法的稳定性
1. 什么是稳定排序
如果在排序前,两个或多个相等关键字的元素在排序后仍保持原有的相对顺序,则称该排序算法是稳定的。
示例
排序前(按成绩排序):
A(90), B(85), C(90)
排序后:B(85), A(90), C(90)
若 A 仍然排在 C 前面,则该排序是稳定的;
若变为:B(85), C(90), A(90),则该排序是不稳定的。
2. 稳定性的重要性
稳定排序在以下场景中非常重要:
- 多关键字排序
- 先按次要字段排序
- 再按主要字段排序
- 业务数据展示
- 需要保持原始录入顺序
- 日志、账单、流水等数据处理
例如:
先按时间排序,再按用户分组,如果排序不稳定,时间顺序可能被破坏。
原地排序(In-place Sorting)
1. 什么是原地排序
如果排序算法在执行过程中 只使用常数级额外空间(O(1)),而不依赖额外的辅助数组,则称该算法为原地排序。递归调用栈通常不计入额外空间。
2. 原地排序的意义
原地排序的优势主要体现在:
- 内存占用低
- 缓存友好
- 适合大规模数据排序
- 更适合嵌入式或内存受限环境
为什么不能只看时间复杂度
在实际工程中:
- 稳定性影响业务正确性
- 是否原地影响系统内存消耗
- 时间复杂度影响性能上限
因此,排序算法的选择往往是多维度权衡的结果。
冒泡排序
基础概念
冒泡排序是一种基于相邻元素比较、通过不断交换让最大(或最小)元素逐步“浮”到一端
的排序算法。
基本思想
冒泡排序的核心思想是:
- 从左到右依次比较相邻元素
- 若顺序错误则交换
- 每一轮都会把当前最大(或最小)元素放到正确位置
- 重复上述过程,直到序列有序
执行过程
以数组为例:
[5, 3, 8, 4]
| 第一轮 |
比较 5 和 3 → 交换,[3, 5, 8, 4] |
比较 5 和 8 → 不交换,[3, 5, 8, 4] |
比较 8 和 4 → 交换,[3, 5, 4, 8] |
第一轮结束,8 已就位。 |
| 第二轮 |
比较 3 和 5 → 不交换 |
比较 5 和 4 → 交换,[3, 4, 5, 8] |
||
| 第三轮 |
比较 3 和 5 → 不交换 |
算法特点
| 特性 |
描述 |
| 排序思想 |
交换 |
| 是否比较排序 |
是 |
| 是否稳定 |
稳定 |
| 是否原地排序 |
是 |
| 最好时间复杂度 |
O(n) |
| 最坏时间复杂度 |
O(n²) |
| 平均时间复杂度 |
O(n²) |
| 空间复杂度 |
O(1) |
实现方式
基础版本
def bubble_sort(arr):
n = len(arr)
for i in range(n):
for j in range(0, n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
优化版本(提前结束)
def bubble_sort_optimized(arr):
n = len(arr)
for i in range(n):
swapped = 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]
swapped = True
if not swapped:
break
时间复杂度分析
最坏情况(完全逆序)
比较次数:(n-1) + (n-2) + ... + 1 = n(n-1)/2 → O(n²)
最好情况(已排序,带优化)
一轮比较无交换即可结束 →O(n)
空间复杂度分析
仅使用常数级临时变量
空间复杂度:O(1)
优缺点
| 优点 |
|
| 缺点 |
|
适用场景
- 排序算法教学
- 数据规模极小
- 数据几乎有序(带优化版本)
快速排序
基础概念
快速排序是一种基于分治思想(Divide & Conquer),通过选取基准值(Pivot)将序列划分为小于基准/大于基准两部分,再对左右子序列递归排序的高效排序算法。
基本思想
快速排序包含三个关键步骤:
- 选择基准(Pivot)
从数组中选取一个元素作为基准值。
- 划分(Partition)
将数组重新排列,使:
左侧元素 ≤ pivot
右侧元素 ≥ pivot
- 递归排序
对基准左右两部分分别进行快速排序。
执行过程
以数组为例:
[7, 3, 9, 4, 6]
第 1 次划分
选 pivot = 7
重排后:
[3, 4, 6, 7, 9]
此时:
7 已在最终位置
左侧 [3,4,6]
右侧 [9]
第 2 次递归(左区)
pivot = 3:
[3, 4, 6]
3 已就位,继续递归 [4,6]
排序完成
[3, 4, 6, 7, 9]
算法特点
| 特性 |
描述 |
| 排序思想 |
分治 |
| 是否比较排序 |
是 |
| 是否稳定 |
不稳定 |
| 是否原地排序 |
是 |
| 平均时间复杂度 |
O(n log n) |
| 最坏时间复杂度 |
O(n²) |
| 最好时间复杂度 |
O(n log n) |
| 空间复杂度 |
O(log n) |
实现方式
Lomuto 分区(简单、好理解)
def partition(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
Hoare 分区(更高效,工业常用)
def partition(arr, low, high):
pivot = arr[(low + high) // 2]
i, j = low - 1, high + 1
while True:
i += 1
while arr[i] < pivot:
i += 1
j -= 1
while arr[j] > pivot:
j -= 1
if i >= j:
return j
arr[i], arr[j] = arr[j], arr[i]
工程化技巧
随机化基准
import random
pivot_index = random.randint(low, high)
避免最坏情况
三数取中(median-of-three)
取 arr[low]、arr[mid]、arr[high] 的中位数
实际工程中非常常用
小数组切换插入排序
当子数组规模 < 16
使用插入排序更快
时间复杂度分析
| 最好情况 |
每次正好平分数组,时间复杂度:O(n log n)。 |
| 平均情况 |
每次划分较均匀,递归深度约为 log n,T(n) = 2T(n/2) + O(n) → O(n log n)。 |
| 最坏情况 |
每次选择最小或最大元素作为 pivot,退化为链式递归,T(n) = T(n-1) + O(n)→ O(n²)。 |
空间复杂度分析
原地排序
递归栈空间:
平均:O(log n)
最坏:O(n)
优缺点
| 优点 |
|
| 缺点 |
|
适用场景
- 内存排序
- 对性能要求极高
- 可接受不稳定排序
归并排序
基础概念
归并排序是一种基于分治思想(Divide & Conquer)将序列不断二分拆分,在回溯阶段有序合并的稳定、高效排序算法。
基本思想
归并排序遵循三个步骤(分治策略):
分(Divide)
将数组递归地拆分成两个子数组,直到每个子数组只剩1个元素。
治(Conquer)
单个元素天然有序,无需处理。
合(Merge)
将两个有序子数组合并成一个新的有序数组。
执行过程
以数组为例:
[8, 3, 5, 4, 7, 6, 1, 2]
拆分阶段
[8,3,5,4] [7,6,1,2]
[8,3] [5,4] [7,6] [1,2]
[8] [3] [5] [4] [7] [6] [1] [2]
合并阶段
[8] + [3] → [3,8]
[5] + [4] → [4,5]
[7] + [6] → [6,7]
[1] + [2] → [1,2]
[3,8] + [4,5] → [3,4,5,8]
[6,7] + [1,2] → [1,2,6,7]
最终
[1,2,3,4,5,6,7,8]
算法特点
| 特性 |
描述 |
| 排序思想 |
分治 |
| 是否比较排序 |
是 |
| 是否稳定 |
稳定 |
| 是否原地排序 |
否 |
| 时间复杂度 |
始终 O(n log n) |
| 空间复杂度 |
O(n) |
实现方式
递归实现
def merge_sort(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
return merge(left, right)
def merge(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
迭代(自底向上)实现
将数组视为 n 个长度为 1 的子数组
两两合并成长度为 2 的子数组
再合并为 4、8... 直到完成
常用于工程优化,避免递归栈溢出
def merge_sort_bottom_up(arr):
n = len(arr)
temp = [0] * n # 辅助数组
size = 1 # 当前子数组长度
while size < n:
# 每次合并相邻的两个 size 长度子数组
for left in range(0, n, 2 * size):
&nb

最低0.47元/天 解锁文章
1730

被折叠的 条评论
为什么被折叠?



