前两天群里有人问:学排序到底先学哪个?我说如果只选两个入手,那一定是交换排序里的冒泡排序和快速排序。这两个算法都用“交换元素”来消除逆序,但一个稳扎稳打,一个快刀斩乱麻,正好把 C 语言里数组、指针、递归、分治这些基本功全串起来了。这篇文章我准备用 C 语言把两块讲透:从最朴素的代码,到优化版本,再到 debug 和边界处理,适合刚啃数据结构的人照抄,也适合调试一半卡住的人回来翻。
1. 交换排序到底在解决什么问题
1.1 逆序对是排序的“病灶”
排序目标说白了就是让序列变成升序。那在交换排序的思路里,问题的根源是“逆序对”:一对元素 i < j,但 arr[i] > arr[j],这就叫一个逆序。排序的过程,就是在不断消灭逆序对。冒泡排序是相邻两个元素慢慢消除逆序,像气泡一样把大的或者小的顶到一头;快速排序则是跳着交换,一次操作就能把一个元素放到最终位置,同时把剩下的序列切成两半。两者的共同点是都依赖交换操作,区别在于交换的“跨度”和“策略”。
为什么要先理解逆序对?因为无论是分析稳定,还是设计优化,都离不开它。比如冒泡排序只在相邻元素间交换,一旦交换就把一个逆序对消掉,还不会跨越相同元素的相对位置,所以它是稳定的。快速排序交换经常跨很远,同值元素可能被换到另一边,所以通常情况下它不稳定。理解了这层关系,后面很多现象就不用死记。
从工程角度看,交换排序属于“原地排序”,不需要开辟额外的大块辅助数组,代码里只借助一个临时变量。这非常适合嵌入式、系统编程等内存受限的场景,也正因为它不申请额外存储,C 语言课程和面试题里才会反复考。
1.2 C语言里实现交换的关键点
C 语言实现交换排序,第一个基本功就是写对 swap 函数。很多人一上来写成的样子是这样的:
c复制void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
}
这样写并不会改变实参。因为 C 语言函数参数是值传递,a 和 b 只是实参的副本,函数执行完,外部变量还是老样子。正确做法需要传地址:
c复制void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
调用时写 swap(&arr[i], &arr[j])。这里的 & 就是取地址,让函数内部能直接修改数组元素。如果你看到代码“排序完了数组一点没变”,十有八九就是把 swap 写成了值传递,或者调用时忘了加 &。
另外一个容易忽略的点是数组传参。在 C 语言里,函数参数里写 int arr[] 和 int *arr 几乎等价,数组名传入函数后会退化成指向首元素的指针,所以函数内无法通过 sizeof(arr) / sizeof(arr[0]) 拿到元素个数。这就是为什么排序函数必须额外传一个长度 n。很多新手在主函数里 sizeof 算得挺开心,到了子函数发现长度变成 1,原因就是这个退化机制。
1.3 冒泡和快排的分工
就实际使用来说,冒泡排序更像“教学排序”:代码直观、逻辑简单,适合用来理解排序最朴素的执行流程。快速排序则是“工程级排序”:平均时间复杂度 O(n log n),相比冒泡的 O(n^2) 有数量级优势,在很多标准库里被作为默认排序方案之一。C 语言标准库里的 qsort,名字就来自 quick sort,虽然具体实现可能还会混合插入排序或堆排序,但核心思路仍然是快速排序。
所以这篇文章的主角虽然是两个算法,但读者心里可以先有个谱:冒泡是“地基”,快速排序是“目标”。前者帮你建立循环和交换的感觉,后者帮你建立分治和递归的思维。把这两套吃透,再去看别的排序,比如归并、堆排序,会发现很多概念都是相通的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 冒泡排序:先看懂最土的版本
2.1 外层和内层循环的分工
冒泡排序最经典的版本是这样:
c复制void bubble_sort(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;
}
}
}
}
外层循环的 i 控制“已经放好多少个元素到最终位置”。第一轮比较结束后,最大值一定被交换到了数组末尾;第二轮结束后,次大值一定到了倒数第二的位置。所以内层循环的右边界可以不断收缩,写成 n - 1 - i,就不需要再去碰后面已经排好的区域。
内层循环里比较的是 arr[j] 和 arr[j + 1],也就是相邻元素。如果前一个大于后一个,就交换。这轮结束之后,当前未排序区间内的最大值就像气泡一样浮到最右边。有人喜欢让小的往前浮,其实只要比较符号对称就行,核心机制完全一样。
这里有个书写细节:内层循环如果写成 j < n - 1,代码也能运行,但第一轮之后每一轮都会重复比较已经就位的尾部元素,白白浪费时间。养成 n - 1 - i 的习惯,是理解冒泡优化过程的第一步。
2.2 加flag提前结束
最原始的冒泡就算遇到一个已经有序的数组,例如 {1, 2, 3, 4, 5},依然会执行两层循环,做大量无意义的比较。解决办法是加一个“是否有交换”的标记:
c复制void bubble_sort_flag(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
int swapped = 0;
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;
swapped = 1;
}
}
if (!swapped) {
break;
