当前位置: 首页 > news >正文

数据结构:冒泡排序 (Bubble Sort)

目录

从最简单的操作开始

如何利用这个原子操作实现一个具体的小目标?

我们来手动模拟一下:

如何从一个小目标扩展到最终目标?

代码的逐步完善

第一阶段:定义函数框架和我们需要的“原子操作”

第二阶段:实现“多趟”的逻辑(外层循环)

第三阶段:实现每一趟的“比较交换”逻辑(内层循环)

思考与优化


从最简单的操作开始

📌我们的目标: 将一个无序的数组,例如 [5, 1, 4, 2, 8],变成有序的 [1, 2, 4, 5, 8]

面对这个任务,最直接、最“笨”的思考方式是什么?不是去想什么宏大的整体策略,而是思考:

我能做的最小的、能让数组变得“更”有序一点点的操作是什么?

这个最小的操作就是:只看相邻的两个元素

比如,我们先看 [5, 1]。在最终排好序的数组里,1 肯定在 5 的前面。而现在它们的顺序是错的。怎么办?很简单:把它们换过来!✅

[5, 1, 4, 2, 8] -> 交换 51 -> [1, 5, 4, 2, 8]

这个 “比较相邻元素,如果顺序错了就交换” 的操作,就是冒泡排序算法最核心的原子操作。它非常简单,但通过重复这个简单的操作,我们就能完成整个排序的宏伟目标。


如何利用这个原子操作实现一个具体的小目标?

我们不能漫无目的地到处交换。我们需要一个策略。与其想着如何把整个数组排好序,不如先定一个更小、更容易实现的目标:

“我们能否通过这个原子操作,把数组中最大的那个元素,放到它最终应该在的位置上?”

答案是可以的。最大的元素最终应该在数组的最右边。我们怎么把它“弄”过去呢?

我们可以从数组的左边开始,一路向右,不停地执行我们的“原子操作”。

我们来手动模拟一下:

假设数组是 arr = [5, 1, 4, 2, 8],长度 n = 5

1. 比较 arr[0]arr[1]:

  • [ **5, 1** , 4, 2, 8]

  • 5 > 1,顺序错了,交换。

  • 数组变为: [ **1, 5** , 4, 2, 8]

2. 比较 arr[1]arr[2]:

  • [1, **5, 4** , 2, 8]

  • 5 > 4,顺序错了,交换。

  • 数组变为: [1, **4, 5** , 2, 8]

3. 比较 arr[2]arr[3]:

  • [1, 4, **5, 2** , 8]

  • 5 > 2,顺序错了,交换。

  • 数组变为: [1, 4, **2, 5** , 8]

4. 比较 arr[3]arr[4]:

  • [1, 4, 2, **5, 8** ]

  • 5 < 8,顺序是正确的,什么都不做。

  • 数组保持: [1, 4, 2, 5, 8]

经过这样从左到右的一整轮操作,我们观察结果 [1, 4, 2, 5, 8]。我们成功了吗?

是的!最大的元素 8 已经被我们“护送”到了数组的最末端,也就是它最终应该在的位置。

这个过程就像水中的气泡,最大的那个总是最先“咕噜咕噜”地冒到水面。这就是“冒泡排序”这个名字的由来。我们把这样一整轮从头到尾的比较交换过程,称为一趟 (Pass)


如何从一个小目标扩展到最终目标?

我们已经完成了一趟,成功地将n个元素中最大的一个归位了。现在数组是 [1, 4, 2, 5, | 8] (用 | 把已归位的元素隔开)。

接下来怎么办?

很简单,我们把已经归位的 8 忽略掉,把前面的 [1, 4, 2, 5] 看作是一个规模更小的新问题

我们只需要对这个新的、长度为 n-1 的数组,重复刚才一模一样的操作

👉 我们来模拟第二趟:

  1. 比较 arr[0]arr[1]: [ **1, 4** , 2, 5, | 8] -> 1 < 4,不交换。

  2. 比较 arr[1]arr[2]: [1, **4, 2** , 5, | 8] -> 4 > 2,交换 -> [1, 2, 4, 5, | 8]

  3. 比较 arr[2]arr[3]: [1, 2, **4, 5** , | 8] -> 4 < 5,不交换。

第二趟结束后,数组变为 [1, 2, 4, | 5, 8]。看,现在次大的元素 5 也归位了!

📈 这个逻辑可以一直重复下去。

  • 第三趟,对 [1, 2, 4] 操作,会把 4 归位。

  • 第四趟,对 [1, 2] 操作,会把 2 归位。

  • 当只剩下 1 的时候,它自然就在正确的位置了。

我们需要多少趟呢?对于一个有n个元素的数组,最多需要 n-1 趟,就能把所有元素都排好序。

这个“重复执行”的思路,在编程里天然就对应着循环✅。


代码的逐步完善

现在,我们把上面的推导翻译成代码。

第一阶段:定义函数框架和我们需要的“原子操作”

我们知道肯定需要一个排序函数,并且排序过程中必然要用到“交换”这个原子操作。

// C/C++ 中,要在函数内部修改外部变量的值,需要使用指针
void swap(int* a, int* b) {int temp = *a;*a = *b;*b = temp;
}// 冒泡排序函数的整体框架
// arr 是要排序的数组,n 是数组的长度
void bubbleSort(int arr[], int n) {// 排序逻辑将在这里实现
}

第二阶段:实现“多趟”的逻辑(外层循环)

根据我们的推导,需要重复执行 n-1 趟排序。所以我们需要一个循环来控制这个“趟数”。

void bubbleSort(int arr[], int n) {// 这个外层循环控制总共需要多少“趟” (Pass)// i 表示已经有多少个元素在数组末尾归位了for (int i = 0; i < n - 1; ++i) {// 在这里实现每一趟具体的比较和交换逻辑}
}

第三阶段:实现每一趟的“比较交换”逻辑(内层循环)

在每一趟中,我们从数组的第一个元素开始,向后两两比较。 关键点:比较到哪里为止?

  • 第一趟 (i=0),n个元素都未排序,要比较 n-1 次,检查到 arr[n-2]arr[n-1] 为止。

  • 第二趟 (i=1),最后1个元素已归位,只需要处理前面 n-1 个元素,比较 n-2 次,检查到 arr[n-3]arr[n-2] 为止。

  • i 趟,最后 i 个元素已归位,比较范围是 n-1-i 次。

这个逻辑正好可以用另一个循环,也就是内层循环来实现。

#include <iostream> // 为了方便打印结果void swap(int* a, int* b) {int temp = *a;*a = *b;*b = temp;
}void bubbleSort(int arr[], int n) {// 外层循环,控制“趟数”for (int i = 0; i < n - 1; ++i) {// 内层循环,控制每一趟中的“比较与交换”// 范围是 n - 1 - i,因为每过一趟,末尾就多一个排好的数for (int j = 0; j < n - 1 - i; ++j) {// 这就是我们的“原子操作”if (arr[j] > arr[j+1]) {swap(&arr[j], &arr[j+1]);}}// 我们可以加一句打印,来观察每一趟之后的结果std::cout << "第 " << i + 1 << " 趟排序后: ";for(int k=0; k<n; ++k) std::cout << arr[k] << " ";std::cout << std::endl;}
}

至此,一个可以正确工作的冒泡排序算法就完成了。它完美地复现了我们从第一性原理出发的推导过程。

复杂度分析

最好情况(数组有序):只需要一趟扫描,比较 n−1 次,没有交换 → 时间复杂度: O(n)

最坏情况(数组逆序):每次都需要比较并交换 → 时间复杂度: O(n^2)

空间复杂度:冒泡排序是 原地排序,只需常数个辅助空间 → O(1)

稳定性

冒泡排序是 稳定的

  • 原因:只有在 a[j] > a[j+1] 时才交换,如果相等,不动。

  • 因此相同元素的前后顺序不会被破坏。


思考与优化

我们的算法已经能用了,但它是不是最聪明的?有没有什么情况下它做了“无用功”?

💡设想一个情况:arr = [1, 2, 3, 5, 4]

  • 第一趟后,45交换,数组变为 [1, 2, 3, 4, 5]

  • 此时数组已经完全有序了。

但是,我们上面的代码并不知道这一点。它会继续傻傻地执行第二趟、第三趟、第四趟,虽然在这些趟中一次交换都不会发生。这显然是浪费。

优化的第一性原理: 如果我们发现某一趟从头到尾走下来,一次交换都没有发生,这说明了什么?

这说明数组中每一个元素都已经不大于它的后一个元素了,也就是说,整个数组已经完全有序了!

那么,我们就可以提前结束排序,而不必执行后面多余的趟数。

最终阶段:完善代码,加入优化

我们可以在每一趟开始前,设置一个标志位 swapped = false。如果在这一趟中发生了交换,就把它设为 true

一趟结束后,检查这个标志位。如果它仍然是 false,说明没发生任何交换,我们就可以直接 break 退出外层循环。

// 优化后的冒泡排序
void bubbleSortOptimized(int arr[], int n) {// 外层循环,控制“趟数”for (int i = 0; i < n - 1; ++i) {bool swapped = false; // 设立标志位// 内层循环for (int j = 0; j < n - 1 - i; ++j) {if (arr[j] > arr[j+1]) {swap(&arr[j], &arr[j+1]);swapped = true; // 只要发生一次交换,就将标志位置为true}}// 检查标志位:如果在一整趟中都没有发生交换,说明已经有序if (swapped == false) {std::cout << "在第 " << i + 1 << " 趟后提前结束。" << std::endl;break; // 退出外层循环}std::cout << "第 " << i + 1 << " 趟排序后: ";for(int k=0; k<n; ++k) std::cout << arr[k] << " ";std::cout << std::endl;}
}

这样,我们就从最基本的“交换相邻错误元素”这个原子操作出发,通过“完成小目标 -> 重复操作 -> 发现冗余 -> 加入判断”这一系列逻辑严谨的推导,最终构建出了一个完整且经过优化的冒泡排序算法。

http://www.dtcms.com/a/355592.html

相关文章:

  • Android14 USB子系统的启动以及动态切换相关的init.usb.rc详解
  • mysql主从复制GTID模式
  • Day16_【机器学习—模型拟合问题】
  • AI智能能源管理系统深度剖析:从工业节能到数据中心降耗,解锁企业降本减排新方案
  • Docker 40个自动化管理脚本-1 (20/40)
  • CVPR2019 Oral论文《Side Window Filtering》解读及算法 Python 实现
  • 网络编程3-UDP协议
  • Megatron-Energon 和 Megatron-LM
  • 解锁五大联赛数据:API技术指南
  • Python在AI与数据科学工作流中的新角色:2025年实践指南
  • X-AnyLabeling:Win10上安装使用X-AnyLabeling标注工具
  • 国内永久免费云服务器有哪些?
  • AI视频生成工具全景对比:元宝AI、即梦AI、清影AI和Vidu AI
  • Java学习笔记之——通过分页查询样例感受JDBC、Mybatis以及MybatisPlus(一)
  • CPU、进程、线程上下文切换
  • 使用Rag 命中用户feedback提升triage agent 准确率
  • 特斯拉 Tesla FSD 12.6.4,FSD14, VLA 和 华为 ADS 4.0 比较
  • 广东省省考备考(第八十九天8.28)——判断推理(第九节课)
  • 智能客服多智能体(知识库问答+情绪感知+工单路由)
  • 玄机靶场 | 第九章-blueteam 的小心思3
  • openEuler中LVM调整实现home与root分区空间平衡
  • LeetCode 3446. 按对角线进行矩阵排序
  • Linux部分底层机制
  • 【每天一个知识点】云存储(Cloud Storage)
  • 从混沌到有序:工作流设计的创作迷思与破局之道
  • AI智能农业监测系统深度解读:从大田作物管理到病虫害预警,破解传统农业增产难题
  • 大模型私有化部署
  • RAG概念被误用:AI应用落地需回归上下文工程本质
  • leetcode算法day22
  • kotlin中关于协程的使用