一、冒泡排序的核心原理
冒泡排序的名字源于其排序过程像 “水中气泡上浮”——每一轮遍历中,最大(或最小)的元素会像气泡一样,通过相邻元素的比较与交换,逐步 “浮” 到数组的末尾(或开头)。
以 “升序排序” 为例,核心逻辑可概括为:
- 从数组的第一个元素开始,依次比较相邻的两个元素(如第 1 个和第 2 个、第 2 个和第 3 个……);
- 若前一个元素大于后一个元素,就交换它们的位置(确保较大的元素向后移动);
- 完成一轮遍历后,数组中最大的元素会 “浮” 到数组的最后一位(无需再参与后续比较);
- 排除已 “归位” 的元素,对剩余未排序部分重复步骤 1-3,直到所有元素都归位。
1.1 直观示例:数组 [3,1,4,1,5] 的冒泡过程
为了更清晰理解,我们以升序排序数组[3,1,4,1,5]为例,拆解每一轮的操作(括号内为已归位的元素):
| 轮次 | 初始数组 | 比较交换过程 | 本轮结果 | 归位元素 |
|---|---|---|---|---|
| 1 | [3,1,4,1,5] | 3>1→交换→[1,3,4,1,5];3<4→不交换;4>1→交换→[1,3,1,4,5];4<5→不交换 | [1,3,1,4,5] | 5 |
| 2 | [1,3,1,4,(5)] | 1<3→不交换;3>1→交换→[1,1,3,4,(5)];3<4→不交换 | [1,1,3,4,(5)] | 4 |
| 3 | [1,1,3,(4,5)] | 1=1→不交换;1<3→不交换 | [1,1,3,(4,5)] | 3 |
| 4 | [1,1,(3,4,5)] | 1=1→不交换 | [1,1,(3,4,5)] | 无(已排序) |
1.2 时间复杂度与空间复杂度
冒泡排序是原地排序算法(无需额外数组存储数据),也是稳定排序算法(相等元素的相对位置不会改变),其性能分析如下:
| 场景 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 最坏情况 | O(n²) | O(1) | 数组完全逆序(如 [5,4,3,2,1]),每一轮需交换 n-i 次(i 为轮次),总操作数为 n (n-1)/2 |
| 最好情况 | O(n) | O(1) | 数组已完全有序(如 [1,2,3,4,5]),若加 “提前终止” 优化,一轮遍历后即可结束 |
| 平均情况 | O(n²) | O(1) | 随机数组,平均需遍历 n/2 轮,每轮需交换 n/2 次,总操作数接近 n²/4 |
为什么时间复杂度是 O (n²)?冒泡排序的核心是 “双重循环”:外层循环控制 “轮次”(最多 n-1 轮),内层循环控制 “每轮的比较交换”(最多 n-i 次),总操作次数与 n² 成正比,因此时间复杂度为平方级。
二、基础实现
原始版冒泡排序严格遵循 “每轮浮起一个最大元素” 的逻辑,代码简洁,适合入门学习。
2.1 代码实现(升序排序)
def bubble_sort_basic(nums):
"""
原始版冒泡排序(升序)
:param nums: 待排序数组(列表)
:return: 排序后的数组
"""
n = len(nums)
# 外层循环:控制轮次(最多n-1轮,因为最后1个元素无需比较)
for i in range(n - 1):
# 内层循环:控制每轮比较(每轮排除已归位的i个元素,比较到n-1-i为止)
for j in range(n - 1 - i):
# 前一个元素大于后一个元素,交换位置
if nums[j] > nums[j + 1]:
nums[j], nums[j + 1] = nums[j + 1], nums[j]
return nums
# 测试案例
if __name__ == "__main__":
test_nums1 = [3, 1, 4, 1, 5, 9, 2, 6] # 随机数组
test_nums2 = [5, 4, 3, 2, 1] # 逆序数组
test_nums3 = [1, 2, 3, 4, 5] # 有序数组
print("随机数组排序结果:", bubble_sort_basic(test_nums1)) # [1, 1, 2, 3, 4, 5, 6, 9]
print("逆序数组排序结果:", bubble_sort_basic(test_nums2)) # [1, 2, 3, 4, 5]
print("有序数组排序结果:", bubble_sort_basic(test_nums3)) # [1, 2, 3, 4, 5]
2.2 代码解析
- 外层循环:
for i in range(n-1),最多执行 n-1 轮(例如 n=5 的数组,4 轮即可完成排序); - 内层循环:
for j in range(n-1-i),每轮比上一轮少比较 i 次(因为已有 i 个最大元素归位到末尾); - 交换逻辑:
if nums[j] > nums[j+1],仅当相邻元素逆序时才交换,确保较大元素向后移动。
三、冒泡排序的三重优化
原始版冒泡排序存在明显缺陷:即使数组已提前有序,仍会执行完所有轮次的循环,造成性能浪费。通过以下三重优化,可大幅提升其在特定场景下的效率,甚至让 “最好情况” 时间复杂度降至 O (n)。
3.1 优化 1:提前终止(解决 “有序数组” 浪费问题)
问题根源:原始版冒泡排序对已完全有序的数组(如 [1,2,3,4,5]),仍会执行 n-1 轮循环,而实际上 1 轮遍历后即可确定数组有序,无需继续。优化方案:增加一个 “是否交换” 的标志位swapped,每轮遍历前初始化swapped=False;若本轮发生过交换,则设swapped=True;若本轮未发生任何交换(swapped=False),说明数组已完全有序,直接终止循环。
优化后代码
def bubble_sort_optim1(nums):
"""
冒泡排序优化1:提前终止(检测到有序则退出)
"""
n = len(nums)
for i in range(n - 1):
swapped = False # 标志位:本轮是否发生交换
for j in range(n - 1 - i):
if nums[j] > nums[j + 1]:
nums[j], nums[j + 1] = nums[j + 1], nums[j]
swapped = True # 发生交换,设为True
# 若本轮无交换,说明数组已有序,直接退出
if not swapped:
break
return nums
# 测试有序数组(优化效果最明显)
if __name__ == "__main__":
sorted_nums = [1, 2, 3, 4, 5, 6]
bubble_sort_optim1(sorted_nums)
print("有序数组排序轮次:", i + 1) # 输出1(仅执行1轮)
优化效果
- 最好情况(完全有序数组):仅需 1 轮遍历,时间复杂度从 O (n²) 降至O(n);
- 最坏情况(完全逆序数组):无额外开销,仍为 O (n²);
- 平均情况:减少不必要的轮次,性能小幅提升。
3.2 优化 2:记录最后一次交换位置(减少无效比较)
问题根源:即使数组未完全有序,每轮遍历中 “最后一次交换的位置” 之后的元素也已完全有序(因为这些元素未参与交换,说明它们比前面的元素都小)。原始版仍会比较到n-1-i,存在无效比较。优化方案:记录每轮 “最后一次交换的位置”last_swap_idx,下一轮遍历仅需比较到last_swap_idx(而非n-1-i),减少后续的无效比较。
优化后代码
def bubble_sort_optim2(nums):
"""
冒泡排序优化2:记录最后一次交换位置(减少无效比较)
"""
n = len(nums)
last_swap_idx = n - 1 # 初始化为数组末尾(表示需比较到最后一个元素)
for i in range(n - 1):
swapped = False
current_last_swap = 0 # 记录本轮最后一次交换的位置
for j in range(last_swap_idx): # 仅比较到上一轮最后一次交换的位置
if nums[j] > nums[j + 1]:
nums[j], nums[j + 1] = nums[j + 1], nums[j]
swapped = True
current_last_swap = j # 更新本轮最后一次交换的位置
# 更新下一轮的比较边界
last_swap_idx = current_last_swap
# 提前终止(无交换则有序)
if not swapped:
break
return nums
# 测试案例:数组尾部已有部分有序
if __name__ == "__main__":
test_nums = [3, 1, 4, 2, 5, 6, 7] # 尾部[5,6,7]已有序
bubble_sort_optim2(test_nums)
print("优化后排序结果:", test_nums) # [1, 2, 3, 4, 5, 6, 7]
优化效果
- 针对 “局部有序” 的数组(如尾部有序),每轮比较次数从
n-1-i减少到last_swap_idx,无效比较大幅减少; - 例如数组
[3,1,4,2,5,6,7],第一轮最后一次交换位置是j=3(交换 2 和 4),下一轮仅需比较到j=3,无需再比较j=4,5(尾部已有序)。
3.3 优化 3:双向冒泡(解决 “小元素沉底慢” 问题)
问题根源:原始冒泡排序每轮仅能将 “最大元素” 浮到末尾,但 “最小元素” 需要多轮才能沉到开头(如数组[4,3,2,1,0],最小元素 0 需 5 轮才能到开头),效率低下。优化方案:双向冒泡(也叫 “鸡尾酒排序”)—— 每轮分为 “正向遍历” 和 “反向遍历”:
- 正向遍历:将最大元素浮到末尾;
- 反向遍历:将最小元素沉到开头;
- 交替进行,直到数组有序。
优化后代码
def bubble_sort_optim3(nums):
"""
冒泡排序优化3:双向冒泡(鸡尾酒排序)
"""
n = len(nums)
left = 0 # 左边界(反向遍历的起点)
right = n - 1 # 右边界(正向遍历的终点)
while left < right:
# 1. 正向遍历:将最大元素浮到right位置
swapped = False
for j in range(left, right):
if nums[j] > nums[j + 1]:
nums[j], nums[j + 1] = nums[j + 1], nums[j]
swapped = True
right -= 1 # 右边界左移(最大元素已归位)
if not swapped:
break
# 2. 反向遍历:将最小元素沉到left位置
swapped = False
for j in range(right, left, -1):
if nums[j] < nums[j - 1]:
nums[j], nums[j - 1] = nums[j - 1], nums[j]
swapped = True
left += 1 # 左边界右移(最小元素已归位)
if not swapped:
break
return nums
# 测试案例:最小元素在末尾(优化效果最明显)
if __name__ == "__main__":
test_nums = [4, 3, 2, 1, 0] # 最小元素0在末尾
bubble_sort_optim3(test_nums)
print("双向冒泡排序结果:", test_nums) # [0, 1, 2, 3, 4]
优化效果
- 针对 “最小元素在末尾” 或 “两端无序、中间有序” 的数组(如
[2,3,4,5,1]),效率提升显著; - 例如数组
[4,3,2,1,0],原始冒泡需 5 轮,双向冒泡仅需 3 轮(正向 2 轮、反向 1 轮)。
四、冒泡排序的应用场景与局限性
4.1 适用场景
冒泡排序虽然时间复杂度是 O (n²),但在某些场景下仍有其价值:
- 小规模数据排序:当数据量 n≤100 时,O (n²) 的性能开销可忽略,且冒泡排序代码简单、易于实现和调试;
- 近乎有序的数据排序:若数据已接近有序(如仅少数元素逆序),加 “提前终止” 优化后,冒泡排序可接近 O (n) 的效率,甚至比快速排序(有递归开销)更快;
- 教学与面试场景:作为排序算法的入门案例,冒泡排序能帮助理解 “比较 - 交换” 的核心逻辑,也是面试中常考的 “手写排序” 题目(尤其是优化方案);
- 需要稳定排序且空间有限的场景:冒泡排序是稳定排序,且原地排序(空间 O (1)),适合内存受限的嵌入式系统或简单场景。
4.2 局限性
- 大规模数据效率低:当 n>1000 时,O (n²) 的时间复杂度会导致排序耗时急剧增加(如 n=10000 时,需约 1 亿次操作),远不如 O (n log n) 的快速排序、归并排序;
- 比较交换次数多:即使是优化版,每轮仍需多次相邻元素比较,相比 “选择排序”(每轮仅 1 次交换),交换操作的开销更大(交换需 3 次赋值,比较仅 1 次判断)。
39万+

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



