冒泡排序¶
概述¶
冒泡排序(Bubble Sort)是最简单直观的排序算法,其基本思想是:通过重复交换相邻的逆序元素,使最大(或最小)元素逐渐"冒泡"到数组末尾。
- 稳定排序:相等元素的相对顺序保持不变
- 原地排序:空间复杂度 O(1)
- 比较排序:通过比较确定元素顺序
- 简单易懂:适合教学和入门
生活类比
想象水中的气泡:轻的气泡会逐渐上浮到水面。在冒泡排序中,每轮比较让较大的元素"上浮"到数组末尾,就像气泡从水底浮到水面一样。
算法原理¶
基本思想¶
flowchart TB
subgraph "冒泡排序核心思想"
A["从左到右遍历数组"] --> B["比较相邻元素"]
B --> C{"arr[j] > arr[j+1]?"}
C -->|是| D["交换两元素"]
C -->|否| E["保持不变"]
D --> F["继续下一对"]
E --> F
F --> G{"还有元素?"}
G -->|是| B
G -->|否| H["一轮完成<br>最大元素到达末尾"]
end
style D fill:#FFF3E0,stroke:#FF9800
style H fill:#E8F5E9,stroke:#4CAF50
排序过程可视化¶
算法状态转移图¶
stateDiagram-v2
[*] --> 第一趟开始
第一趟开始 --> 比较0: 开始
比较0 --> 比较1: 继续遍历
比较1 --> 比较2: 继续遍历
比较2 --> 比较3: 继续遍历
比较3 --> 第一趟结束: 遍历完成
第一趟结束 --> 第二趟开始: 继续下一趟
第二趟开始 --> 第二趟比较: 开始
第二趟比较 --> 第二趟结束: 遍历完成
第二趟结束 --> 第三趟开始: 继续下一趟
第三趟开始 --> 第三趟比较: 开始
第三趟比较 --> 第三趟结束: 遍历完成
第三趟结束 --> 第四趟开始: 继续下一趟
第四趟开始 --> 第四趟比较: 开始
第四趟比较 --> 排序完成: 遍历完成
排序完成 --> [*]
基本实现¶
多语言实现¶
优化版本¶
优化一:提前终止(检测是否已有序)¶
flowchart TB
A["开始一轮遍历"] --> B["swapped = false"]
B --> C["比较相邻元素"]
C --> D{"需要交换?"}
D -->|是| E["执行交换<br>swapped = true"]
D -->|否| F["继续下一对"]
E --> F
F --> G{"本轮结束?"}
G -->|否| C
G -->|是| H{"swapped == false?"}
H -->|是| I["数组已有序<br>提前终止"]
H -->|否| J["继续下一轮"]
style I fill:#E8F5E9,stroke:#4CAF50
| Rust | |
|---|---|
优化二:记录最后交换位置¶
原理: 每趟最后一次交换位置之后的所有元素已经有序
示例: [3, 2, 1, 5, 6, 7]
第1趟遍历: ┌────────────────────────────────────────────────────┐ │ [3, 2] → 交换, lastSwap=0 │ │ [3, 1] → 交换, lastSwap=1 │ │ [3, 5] → 不换 │ │ [5, 6] → 不换 │ │ [6, 7] → 不换 │ │ │ │ lastSwap=1 表示位置1之后的元素[5,6,7]已经有序 │ │ 下一趟只需遍历到位置1 │ └────────────────────────────────────────────────────┘
普通版本下一趟需要遍历到位置4 优化版本下一趟只需遍历到位置1 节省 75% 比较次数!
优化三:双向冒泡(鸡尾酒排序)¶
flowchart LR
subgraph "单向冒泡"
A["从左到右"] --> B["最大值到右边"]
end
subgraph "双向冒泡(鸡尾酒排序)"
C["从左到右"] --> D["最大值到右边"]
D --> E["从右到左"]
E --> F["最小值到左边"]
end
style F fill:#E8F5E9,stroke:#4CAF50
优化效果对比¶
复杂度分析¶
时间复杂度¶
graph TB
subgraph "时间复杂度分析"
A["最好情况 O(n)"] --> A1["数组已有序<br>提前终止优化后只需一轮"]
B["平均情况 O(n²)"] --> B1["随机数据<br>约 n²/2 次比较"]
C["最坏情况 O(n²)"] --> C1["完全逆序<br>每次比较都交换"]
end
style A fill:#E8F5E9,stroke:#4CAF50
style C fill:#FFF3E0,stroke:#FF9800
详细推导¶
第 i 趟需要比较 n-i 次(i 从 0 开始)
总比较次数 = (n-1) + (n-2) + ... + 1 = n(n-1)/2
时间复杂度 = O(n²)
最坏情况(完全逆序):每次比较都交换
交换次数 = n(n-1)/2
最好情况(已有序):0 次交换
空间复杂度¶
| 变量 | 空间 | 说明 |
|---|---|---|
| 临时变量 temp | O(1) | 交换时使用 |
| 循环变量 i, j | O(1) | 控制循环 |
| 标记变量 swapped | O(1) | 优化使用 |
| 总空间 | O(1) | 原地排序 |
逆序对分析¶
稳定性分析¶
为什么冒泡排序是稳定的¶
保持稳定性的关键¶
| C | |
|---|---|
与其他排序算法对比¶
graph TB
subgraph "排序算法对比"
A["冒泡排序<br>O(n²)"]
B["选择排序<br>O(n²)"]
C["插入排序<br>O(n²)"]
D["快速排序<br>O(n log n)"]
E["归并排序<br>O(n log n)"]
end
A --> A1["✓ 稳定<br>✓ 简单<br>✗ 效率低"]
B --> B1["✗ 不稳定<br>✓ 简单<br>✗ 效率低"]
C --> C1["✓ 稳定<br>✓ 小数据高效<br>✗ 大数据效率低"]
D --> D1["× 不稳定<br>✓ 高效<br>✓ 工程常用"]
E --> E1["✓ 稳定<br>✓ 高效<br>✗ 需要额外空间"]
style A fill:#E3F2FD,stroke:#2196F3
style D fill:#E8F5E9,stroke:#4CAF50
style E fill:#E8F5E9,stroke:#4CAF50
性能对比表¶
变体应用¶
1. 求第 K 大/小元素¶
2. 检测数组是否有序¶
| C | |
|---|---|
3. 统计逆序对数量¶
| C | |
|---|---|
4. 奇偶排序(并行冒泡排序)¶
适用场景分析¶
适用场景¶
- 教学演示:算法简单,易于理解和实现
- 小规模数据:n < 50 时效率可接受
- 基本有序数据:配合提前终止优化效果好
- 稳定性要求:需要保持相等元素原有顺序
- 资源受限环境:代码简单,内存占用极小
不适用场景¶
- 大规模数据:O(n²) 效率太低
- 性能要求高:应选择快速排序、归并排序等
- 实时系统:排序时间不可预测
- 大量数据排序:n > 1000 时效率显著下降
常见错误与陷阱¶
1. 边界条件错误¶
| C | |
|---|---|
2. 优化后忘记更新边界¶
| C | |
|---|---|
3. 双向冒泡边界处理¶
总结¶
算法特点总结¶
mindmap
root((冒泡排序))
基本特性
时间复杂度 O(n²)
空间复杂度 O(1)
稳定排序
原地排序
优点
简单易懂
代码量少
稳定性好
适合小数据
缺点
效率低下
大数据性能差
比较次数多
优化方向
提前终止
记录交换位置
双向冒泡
应用场景
教学演示
小规模排序
检测有序性
求第K大元素
学习建议¶
- 理解原理:掌握相邻交换和冒泡的核心思想
- 动手实现:从基本版本开始,逐步添加优化
- 分析复杂度:理解最好、最坏、平均情况
- 对比学习:与选择排序、插入排序对比理解
- 了解局限:认识到 O(n²) 算法的局限性
参考资料¶
- 《算法导论》第2章 - 插入排序与归并排序
- 《数据结构与算法分析》第7章 - 排序
- Bubble Sort - Wikipedia
- Sorting Algorithm Animations