☰
C++冒泡排序求最值:原理、优化与实战解析
2026/10/18 11:46:30 网站建设 项目流程

1. 为什么"最值问题"值得拉出来单独讲一遍

很多初学者看到"C++冒泡排序,求最值问题"这种题目,第一反应是:冒泡排序不就是排序吗?求最值直接遍历一次不就完事了,为什么要把这两件事放在一起?

其实这个题目背后有几个很实在的应用场景。比如某些嵌入式或单片机环境里,排序只是为了拿到最大或最小的那一个数,并不需要把整个序列都排得整整齐齐;又比如学生成绩管理、传感器数据过滤这类场景,你可能只想取前几个极值,而不是全部重排。这时候用冒泡排序的一次完整排序拿全局最大值,效率上是有冗余的。但反过来讲,如果数据量不大,冒泡排序那点开销完全无所谓,代码还最好写、最好读、最不容易出错。

我在实际项目中就遇到过类似情况:一个采集端的程序,每秒从传感器读一批数据,我需要挑出其中温度最高的三个点做告警判断。最开始图省事直接std::sort排了整个数组,后来发现性能没毛病但代码被同事吐槽"杀鸡用牛刀",而且当时项目的编译环境还老,STL支持不完整。后来干脆手写了几行冒泡逻辑,前几趟只把最大的冒到末尾,然后再取末尾元素——又直观又好解释。

所以这篇文章我就按照"原理拆解 → 完整实现 → 最值求解对比 → 复杂度与优化 → 边界与错误"这个顺序,把C++冒泡排序和求最值这两件事掰开揉碎讲清楚。适合刚学C++的朋友,也适合那些想把这部分知识讲给别人的同学参考。

2. 冒泡排序的机制:先看懂它为什么叫"冒泡"

2.1 相邻交换的直觉逻辑

冒泡排序的核心操作就是一句话:从左到右,两两比较相邻元素,如果顺序不对就交换。一趟下来,最大的数会被"顶"到数组的最右边。然后再从头开始做第二趟,但第二趟就无需再理会已经到位的最后一个元素了。

为什么叫"冒泡"?你可以想象水池里有大小不一的气泡,最大的气泡上升最快,最先浮出水面;次大的接着浮上来。数组里每做一趟完整扫描,未排序部分中最大的元素就像气泡一样"浮"到当前范围的末尾,这个末尾就是它最终该待的位置。

用生活化的例子说:假设班级里同学按身高站成一排,老师要从矮到高重新排。老师从左往右看,如果发现左边同学比右边同学高,就让两人交换位置。一趟走完,最高的同学必然被"挤"到了队伍最右端。下一趟老师只看前n-1个人,重复同样操作。这就是冒泡排序。

2.2 一趟排序到底做了什么

看一个具体的例子。假设数组是:

{5, 1, 4, 2, 8}

第一趟过程如下:

  • 比较5和1,5 > 1,交换 →{1, 5, 4, 2, 8}
  • 比较5和4,5 > 4,交换 →{1, 4, 5, 2, 8}
  • 比较5和2,5 > 2,交换 →{1, 4, 2, 5, 8}
  • 比较5和8,5 < 8,不交换 →{1, 4, 2, 5, 8}

最大数8到达末尾。第二趟只处理前4个元素:

  • 比较1和4,不交换
  • 比较4和2,4 > 2,交换 →{1, 2, 4, 5, 8}
  • 比较4和5,不交换

此时次大数5到位。第三趟继续,直到整个数组有序。

注意一个细节:如果某趟没有任何交换发生,说明数组已经完全有序,可以提前结束。这个优化在实际数据"接近有序"时非常有用,后面我会专门展开。

3. 从零手写C++冒泡排序:教科书版本到可维护版本

3.1 最基础的实现

先上一个最典型的实现,保证每个初学者都能看懂:

#include <iostream> void bubbleSort(int arr[], int n) { // 外层循环:控制排序的趟数 for (int i = 0; i < n - 1; i++) { // 内层循环:进行相邻元素比较和交换 for (int j = 0; j < n - 1 - i; j++) { if (arr[j] > arr[j + 1]) { // 交换位置 int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } } int main() { int arr[] = {5, 1, 4, 2, 8}; int n = sizeof(arr) / sizeof(arr[0]); bubbleSort(arr, n); for (int i = 0; i < n; i++) { std::cout << arr[i] << " "; } std::cout << std::endl; return 0; }

这里解释几个关键点:

  • 外层循环i从0到n-2,一共执行n-1趟。因为最后一个元素在第一趟之后只剩一个时无需再排。
  • 内层循环j的上限是n - 1 - i,每完成一趟,末尾就有一个元素归位,所以内层扫描长度可以减少i。
  • 交换采用一个临时变量temp,这是C风格写法,C++里可以用std::swap更简洁。

上面的代码每次判断arr[j] > arr[j+1],用的是"升序"逻辑。如果要降序,把>改成<即可。

3.2 加一点工程味道:用std::swap和模板

教科书版本虽然好懂,但实际写代码时,建议至少优化成这样:

#include <iostream> #include <algorithm> // for std::swap void bubbleSort(int arr[], int n) { for (int i = 0; i < n - 1; i++) { for (int j = 0; j < n - 1 - i; j++) { if (arr[j] > arr[j + 1]) { std::swap(arr[j], arr[j + 1]); } } } }

std::swap内部通常比手写临时变量交换更高效,而且代码可读性更好。编译器在优化下两者几乎没有性能差异,但std::swap不会写错变量名,这是实际开发中很重要的优势。

再进一步,可以写成函数模板,让不同类型的数组都能使用:

template <typename T> void bubbleSort(T arr[], int n) { for (int i = 0; i < n - 1; i++) { for (int j = 0; j < n - 1 - i; j++) { if (arr[j] > arr[j + 1]) { std::swap(arr[j], arr[j + 1]); } } } }

模板版本支持int[]、double[]、float[],只要类型支持>运算符即可。这个细节在工程上很实用,尤其是当你要对double或自定义结构体排序时,不需要再写一份重复代码。

3.3 降序排列只需要改一个符号

很多初学者会忽略的一点:冒泡排序的"升序"和"降序"只差一个比较符号。升序用的是arr[j] > arr[j+1],含义是"如果左边比右边大,就交换",从而把大的往后挪;降序则改为arr[j] < arr[j+1],把小的往后挪。

我建议把比较逻辑单独抽出来,这样代码意图更清晰:

template <typename T> void bubbleSort(T arr[], int n, bool ascending = true) { for (int i = 0; i < n - 1; i++) { for (int j = 0; j < n - 1 - i; j++) { if (ascending ? (arr[j] > arr[j + 1]) : (arr[j] < arr[j + 1])) { std::swap(arr[j], arr[j + 1]); } } } }

这种设计在后续扩展比较器时也很方便,比如想按照自定义结构体的某个字段排序,直接把arr[j] > arr[j+1]换成对字段的比较即可。

4. 求最值问题:用冒泡排序之前先想清楚这几个方案

4.1 最直接的办法:一次遍历

如果是单纯求最大值或最小值,其实不需要排序,更不需要冒泡排序。一次线性遍历就足够了:

int findMax(int arr[], int n) { int maxVal = arr[0]; for (int i = 1; i < n; i++) { if (arr[i] > maxVal) { maxVal = arr[i]; } } return maxVal; } int findMin(int arr[], int n) { int minVal = arr[0]; for (int i = 1; i < n; i++) { if (arr[i] < minVal) { minVal = arr[i]; } } return minVal; }

时间复杂度是 O(n),空间复杂度 O(1)。这是最理想的做法。为什么还要提冒泡排序?因为有些场景下你不仅要最大值,还要"第二大""第三大",或者"最小的三个数",甚至需要这些值与它们所在位置的关系。此时一次遍历虽然也可以写,但代码会绕很多。

4.2 只做一次冒泡,快速拿到最大值

如果确定要用冒泡排序的思路求最大值,有个"轻量版"技巧:只执行一趟完整冒泡,最大值自然就出现在数组末尾。相比完整排序,一趟冒泡的时间是 O(n),不过内部交换次数可能很多,常数比单纯遍历大,但在可接受范围内。

int maxByOneBubblePass(int arr[], int n) { for (int j = 0; j < n - 1; j++) { if (arr[j] > arr[j + 1]) { std::swap(arr[j], arr[j + 1]); } } return arr[n - 1]; // 一趟结束后,最后一个元素是最大值 }

这个写法在某些教学场景里很讨巧:你一边演示冒泡过程,一边顺手就把最大值求出来了。尤其是当题目要求"写出冒泡排序代码"又"求最大最小值"时,这种做法能被老师一眼看出你理解了两者之间的关系。

同理,最小值可以通过一趟"反向冒泡"得到:从左往右比较,如果arr[j] < arr[j+1]就交换,这样最小值被一路"沉"到末尾。或者更简单点,一趟从左到右的普通扫描里把最小值也记下来。

4.3 通过部分排序求前K个极值

需要前K个最大值时,最实用的方案是执行K趟冒泡。每趟会把当前剩余元素中的最大值冒到末尾,K趟之后末尾K个元素就是最大的K个值,而且它们之间已经排好序了。

void topKByBubble(int arr[], int n, int k) { for (int i = 0; i < k; i++) { for (int j = 0; j < n - 1 - i; j++) { if (arr[j] > arr[j + 1]) { std::swap(arr[j], arr[j + 1]); } } } // 此时 arr[n-1] 到 arr[n-k] 是前k个最大值(升序排列) }

时间复杂度 O(k * n)。当 K 远小于 n 时,这比完整冒泡 O(n^2) 快得多,而且不依赖额外数据结构。如果你用最小堆也是可以的,但代码复杂度会上升,对小规模数据来说冒泡局部排序反而是最优解。

举个例子:数组有10000个元素,只要找最大的5个。完整冒泡需要约5000万次比较;只做5趟冒泡只需约5万次比较。这个差距在实时性要求高的场景里非常明显。

4.4 三种方案对比总结

需求推荐方案时间复杂度代码复杂度备注
只求最大值或最小值单次线性遍历O(n)低空间 O(1)
求前K个最大最小值K趟部分冒泡O(K*n)低K小的时候表现好
需要全序列有序完整冒泡O(n^2)低数据量小时可接受
大数据量全排序改用快排/堆排/排序库O(n log n)中不要用冒泡硬扛

注意 如果数据量超过几千且要求全排序,别纠结冒泡排序,直接上std::sort。冒泡排序更适合教学、小规模数据、以及"顺便求极值"这类场景。

5. 冒泡排序的性能账:O(n^2) 到底意味着什么

5.1 比较次数和交换次数

标准的冒泡排序,无论数据初始状态如何,比较次数是固定的。总共要比:

(n-1) + (n-2) + ... + 1 = n(n-1)/2

所以比较次数始终是 O(n^2)。但交换次数取决于数据的初始顺序:

  • 最好情况:数组已经完全有序,交换次数为0。
  • 最坏情况:数组完全逆序,每次比较都需要交换,交换次数也是 n(n-1)/2。
  • 平均情况:大约是 n(n-1)/4,仍然属于 O(n^2)。

所以冒泡排序特别依赖"是否能在内层循环中提前退出"。如果加了提前退出标志,最好的情况时间复杂度可以降到 O(n),这一点在下一节详细说明。

不妨算一笔实际账:处理100个元素的数组,完整冒泡约需要4950次比较,现代CPU一纳秒内就能完成,完全感觉不到;处理1万个元素,约5000万次比较,也只需要几十毫秒;可一旦到100万个元素,那就是约5000亿次比较,运行时间直奔分钟级。这就是 O(n^2) 算法在大规模数据上水土不服的根本原因。

5.2 稳定性与空间复杂度

冒泡排序是稳定排序。所谓稳定,是指如果两个元素值相等,排序后它们的相对顺序不会改变。这在某些场景下很重要,比如你先按学号排了序,再按成绩排序,希望同分情况下仍然保持学号顺序,此时稳定排序的价值就体现出来了。

空间复杂度上,冒泡排序是原地排序,只需要一个临时变量(std::swap内部也可能只用一个临时空间),即 O(1) 的额外空间。相比归并排序需要 O(n) 的辅助空间,冒泡在内存紧张的嵌入式环境里是个优点。

6. 工程级优化:提前退出、双向冒泡与比较器

6.1 最有效的优化:加一个交换标志

经典优化方式是在内层循环里设置一个swapped标志。如果某一趟从头到尾都没有发生交换,说明所有元素已经到位,直接退出外层循环。

template <typename T> void bubbleSortOptimized(T arr[], int n) { bool swapped; for (int i = 0; i < n - 1; i++) { swapped = false; for (int j = 0; j < n - 1 - i; j++) { if (arr[j] > arr[j + 1]) { std::swap(arr[j], arr[j + 1]); swapped = true; } } if (!swapped) break; // 提前结束 } }

这个优化有多大价值?考虑一个极端情况:数组已经按升序排好,但你的代码没有提前退出标志。此时外层要跑 n-1 趟,每一趟都要做完整的 n-1-i 次比较,总共仍要近5000次比较(对于n=100),虽然不会交换元素,但白白浪费时间。加了标志后,第一趟扫描发现没有任何交换,立即结束,总共只需99次比较。两者相差50倍。对于"近似有序"的真实数据,这种优化能把冒泡性能从 O(n^2) 拉回接近 O(n),这是冒泡排序为数不多能打的场景。

6.2 鸡尾酒排序(双向冒泡)

鸡尾酒排序是冒泡的一种变体,思路是:第一趟从左往右把最大元素冒到末尾,第二趟从右往左把最小元素冒到开头,如此交替进行。

template <typename T> void cocktailSort(T arr[], int n) { int left = 0, right = n - 1; bool swapped = true; while (swapped) { swapped = false; for (int i = left; i < right; i++) { if (arr[i] > arr[i + 1]) { std::swap(arr[i], arr[i + 1]); swapped = true; } } right--; if (!swapped) break; swapped = false; for (int i = right; i > left; i--) { if (arr[i] < arr[i - 1]) { std::swap(arr[i], arr[i - 1]); swapped = true; } } left++; } }

鸡尾酒排序在数据分布"前段特别大、后段特别小"的情况下明显更快,因为单向冒泡每一趟只能把一个极值送到一侧,而双向冒泡一趟能同时处理两端。缺点是代码量增加,且整体时间复杂度依然是 O(n^2)。

6.3 把比较逻辑泛化

如果需要排序自定义对象,比如按年龄、按价格字段,把比较逻辑封装成可调用对象是最灵活的方案:

#include <functional> template <typename T, typename Compare> void bubbleSort(T arr[], int n, Compare comp) { bool swapped; for (int i = 0; i < n - 1; i++) { swapped = false; for (int j = 0; j < n - 1 - i; j++) { if (comp(arr[j], arr[j + 1])) { // comp(a, b) 为 true 表示 a 应该在 b 之前 std::swap(arr[j], arr[j + 1]); swapped = true; } } if (!swapped) break; } }

调用方式:

struct Student { int id; double score; }; int main() { Student stu[] = {{3, 89.5}, {1, 92.0}, {2, 76.5}}; int n = 3; bubbleSort(stu, n, [](const Student& a, const Student& b) { return a.score > b.score; // 按成绩降序 }); for (int i = 0; i < n; i++) { std::cout << stu[i].id << " -> " << stu[i].score << std::endl; } return 0; }

这种设计的优点在于:排序函数本身不关心比较逻辑,调用处通过 lambda 灵活指定。日后如果排序规则变了,只需要改动 lambda 内部的一行代码,不需要动排序函数。

7. 边界情况排查:数组越界、空数组、并列极值

7.1 数组越界是初学者最容易踩的坑

冒泡排序最经典的错误就在内层循环的边界上。如果写成j < n - 1也能跑,但意味着每一趟都会多扫描一个已经归位的元素,虽然不算越界,却增加了无意义比较。更危险的是写成了j < n,循环体内访问arr[j + 1]时,最后一轮j = n-1,访问arr[n],直接越界。

在C++中,越界访问不会像Java那样抛异常,而是把栈上相邻内存里的值读出来参与比较,结果完全不可预测。我来演示一个常见的错误写法:

// 错误示范:内层循环条件写错 void wrongBubbleSort(int arr[], int n) { for (int i = 0; i < n - 1; i++) { for (int j = 0; j < n; j++) { // bug! if (arr[j] > arr[j + 1]) { // j = n-1 时 arr[n] 越界 std::swap(arr[j], arr[j + 1]); } } } }

这种错误在测试小数据时可能不显眼,因为越界读到的恰好是一个不影响结果的值;一旦数据变多或相邻内存布局改变,就会突然冒出一堆莫名其妙的数字。排查方式:写单元测试,用随机数组验证排序结果;或者用 Valgrind、AddressSanitizer 这类工具检查内存访问。

7.2 空数组和只有一个元素

空数组:n = 0时,外层循环i < n - 1即i < -1不成立,直接跳过,函数安全返回。但要注意调用方是否真的传入了n = 0,有的代码用sizeof(arr)/sizeof(arr[0])来推导,空数组用这种方式推导会得到0,需要确认不会拿这个空数组去做arr[0]取值——那才是真正的问题。

只有一个元素:n = 1时,外层循环同样不执行,函数安全返回。此时如果调用findMax,arr[0]是唯一的合法取值。

这两种边界场景建议在代码里显式处理,哪怕只是加一行注释,也能防止后续接手的同事踩坑:

if (n <= 1) return; // 空数组或单元素无需排序

7.3 并列极值:最大值出现多次怎么处理

冒泡排序对重复元素的表现很稳定。以求最大值为例,如果最大值出现了多次,排序完成后它们会连续排列在数组末尾。一次遍历求最大值的方法则会返回第一个遇到的最大值。两者都不会报错,但要注意:如果你同时要求"最大值出现的位置",一次遍历法拿到的位置是第一个最大值的位置,冒泡法因为交换次数多,同一个最大值的原始位置已经变了,要追踪位置必须额外记录。

举个例子:

arr = {3, 5, 2, 5, 1}

一次遍历求最大值,i=1时遇到5记录位置1,之后i=3再次遇到5,如果条件是>而不是>=,位置保持为1;条件写成>=,位置会更新为3。程序行为差别很细微,要在注释中明确写清楚"返回第一个最大值位置"。

如果需要同时返回最大值和最小值,一次遍历可以同时维护两个变量:

struct MinMax { int minVal; int maxVal; }; MinMax findMinMax(int arr[], int n) { if (n <= 0) return {0, 0}; MinMax result = {arr[0], arr[0]}; for (int i = 1; i < n; i++) { if (arr[i] < result.minVal) result.minVal = arr[i]; if (arr[i] > result.maxVal) result.maxVal = arr[i]; } return result; }

这种写法比"先找最小值、再找最大值"的两趟遍历在数据量巨大时略有优势,但实际上两趟也是O(n),差别只在常数。如果数据能并行处理,两趟还更容易向量化,所以常量级别优化不要过度纠结。

7.4 数组全相等的情况

如果数组所有元素相等,比如{7, 7, 7, 7, 7},无论升序还是降序,冒泡排序都会完整跑完 n-1 趟吗?这取决于你用的是不是带有swapped标志的优化版。

  • 无优化版本:全部比较完成,每趟都会扫描,但一次交换都不会发生。比较次数依然是 n(n-1)/2。
  • 带优化版本:第一趟扫描发现没有任何交换,立即退出,只比较 n-1 次。

这也是我在前面强调提前退出标志重要的原因之一。真实工程中,数据"几乎有序"或"存在大量重复值"比"完全随机"常见得多,优化标志的收益远高于你的直觉。

8. 实战案例:把找最大/最小应用到具体场景

8.1 案例A:传感器数据中的最大温差

假设某监控系统每5分钟记录一组温度数据,我需要找出一天中最高温和最低温,并计算温差。用readTemperatures函数读入数据(具体实现取决于硬件接口),然后调用上面的findMinMax:

#include <iostream> #include <vector> double findMax(const std::vector<double>& data) { double maxVal = data[0]; for (size_t i = 1; i < data.size(); i++) { if (data[i] > maxVal) maxVal = data[i]; } return maxVal; } double findMin(const std::vector<double>& data) { double minVal = data[0]; for (size_t i = 1; i < data.size(); i++) { if (data[i] < minVal) minVal = data[i]; } return minVal; } int main() { std::vector<double> temps = {21.5, 22.1, 20.8, 19.4, 25.3, 26.8, 24.2, 23.6}; double maxTemp = findMax(temps); double minTemp = findMin(temps); std::cout << "Max: " << maxTemp << ", Min: " << minTemp << ", Diff: " << (maxTemp - minTemp) << std::endl; return 0; }

这个场景用std::vector而不是原始数组,因为数据量不确定。注意空vector的处理:直接访问data[0]会崩溃,应该先检查data.empty()。

8.2 案例B:求N个学生成绩的前三名

假设有100个学生,我需要找出成绩前三名的分数。用部分冒泡:

void topK(int scores[], int n, int k) { for (int i = 0; i < k; i++) { for (int j = 0; j < n - 1 - i; j++) { if (scores[j] > scores[j + 1]) { std::swap(scores[j], scores[j + 1]); } } } } int main() { int scores[] = {72, 85, 91, 68, 95, 88, 79, 93, 84, 90}; int n = 10; int k = 3; topK(scores, n, k); std::cout << "Top 3: "; for (int i = n - 1; i >= n - k; i--) { std::cout << scores[i] << " "; } std::cout << std::endl; return 0; }

这段代码执行3趟之后,数组末尾3个元素就是最大的三个,而且按升序排列:scores[n-1]最大,scores[n-2]次之。

如果您还想知道前三个分数对应的学生是谁,可以并行维护一个下标数组:

int idx[] = {0, 1, 2, ..., n-1}; // 在交换时同时交换 idx 数组 if (scores[j] > scores[j + 1]) { std::swap(scores[j], scores[j + 1]); std::swap(idx[j], idx[j + 1]); }

这样排序结束后,idx[n-1]就是最高分学生的原始编号。这个"并行数组"技巧在很多场合(比如保持原数据顺序、只返回位置索引)都能用到。

9. 我在实际项目中总结的几个冒泡排序使用心得

9.1 小数据量别看不起冒泡

曾经有个同事在一个线程里排序几十个结构体数据,为了追求"规范"直接用std::sort。结果排查性能问题时发现,该线程调用频率极高(每秒上千次),每次创建迭代器、函数调用栈的额外开销叠加起来,比手写冒泡还慢。换成冒泡后代码虽然"土"了一点,但整体消耗反而下降了。

这里不是说std::sort不如冒泡,而是在数据量极小且调用频繁时,简单算法的常数优势可能盖过渐进复杂度优势。n=20时,冒泡190次比较,std::sort也会做若干递归划分,函数回调开销不小。所以当你的数据规模很小时,心想 "O(n^2)一定比O(n log n)慢" 是不严谨的,要结合实际情况实测验证。

9.2 写排序前先问一句:真的要排序吗

在需求评审阶段,如果发现只需要最大最小值或者前K个值,而产品经理说要"排序",一定要确认清楚。很多场景下,用户看到列表排序背后真正想要的是"最大的几个""最小的几个""超过阈值的有哪些"。用部分冒泡或者一次遍历就能满足,没必要付出全排序的成本。

即便最后需要展示完整列表,如果数据量不大且基本静态,冒泡完全可接受;如果数据量大或频繁刷新,就应该使用更合适的排序方案。正确的技术选型不是"别人用什么我选什么",而是"这个数据规模和访问模式最适合什么"。

9.3 写算法题和写工程代码的边界不同

在算法面试或作业中,冒泡排序是为了考察你理解基本排序原理;在工程中,冒泡通常不是最优选择。要让代码表达出"我理解复杂度,也知道权衡",可以在注释里写明:

// 数据量小(n <= 64),使用冒泡排序避免排序库调用开销; // 数据量增大时应切换到std::sort。

这种注释会让人觉得你是真的在工程思考,而不是抄了一段算法代码。

10. 最后再分享一个调试冒泡排序的小技巧

写冒泡排序代码时,最容易出问题的不是算法本身,而是"如何验证我写的排序到底对不对"。我推荐一个非常实用的调试思路:先用少量的完全逆序数组做肉眼验证,再用大量随机数组做自动化验证。

具体做法是在测试代码里写一个isSorted函数:

bool isSorted(int arr[], int n) { for (int i = 0; i < n - 1; i++) { if (arr[i] > arr[i + 1]) { return false; } } return true; }

然后循环测试多组随机数据,每组排序后调用isSorted,同时检查排序前后元素集合是否一致(通过求和或另存一份拷贝做排序后逐元素对比)。如果发现问题,先用小数组(比如5个元素的逆序数组)手动断点跟踪每一趟交换。

另外分享一个经验:如果要调试"求前三名"这类部分排序代码,建议在每趟结束后打印当前数组状态,观察末尾元素是否正确依次归位。打印语句虽土,但在这种基础算法上比任何调试器都直观。

冒泡排序看似简单,但在"求最值"这个用法上,仍有不少值得琢磨的细节:一趟冒泡拿最大值的技巧、K趟局部排序求TopK、稳定性与边界情况、以及工程中使用场景的取舍。希望这篇文章能帮你把冒泡排序这棵"小树"的根扎扎实实地种下去,后面学快排、归并排序时,对比会更深刻。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询