我很少会花一整篇的篇幅去复盘一个“听上去很简单”的排序算法,但归并排序确实值得。原因很简单:这个算法我前前后后写了不下十遍,每次以为自己稳了,结果一跑就是段错误、乱序、栈溢出,甚至无声无息地死循环。尤其是在调试器不够顺手的C语言环境里,排查起来相当折磨。而等到算法真正跑通,又会有新的问题冒出来——性能不够、内存占用过高、代码结构乱到不敢提交到开源仓库。
这篇文章就把我从头到尾的折腾过程完整记下来,包括C语言实现归并排序时最隐秘的边界条件、调试方法论、四个方向的优化手段(附带实测数据),以及最后把项目整理并推送到Gitee开源的整个流程。你如果也在写归并排序,或者正准备把自己第一个算法项目挂到Gitee上,这篇文章应该能帮你省下不少时间。
1. 为什么归并排序总在边界条件上翻车
先说结论:归并排序的核心思想并不难,难的是把“区间”和“索引”这件事搞对。
1.1 递归分治的直觉与真实执行逻辑
初学时最容易产生的直觉是:把一个数组分成两半,各自排序,再合并。这个理解在宏观上完全正确,但落到C语言的数组下标上,就会出现一连串的歧义。
比如你有一个数组 int arr[10],要对 arr[0] 到 arr[9] 排序。你可能会很自然地写一个递归函数:
c复制void merge_sort(int arr[], int left, int right) {
if (left >= right) return;
int mid = (left + right) / 2;
merge_sort(arr, left, mid);
merge_sort(arr, mid + 1, right);
merge(arr, left, mid, right);
}
这套“左闭右闭”写法看起来正常,但里面藏着好几个容易出问题的地方。
第一个问题是 if (left >= right) 这个终止条件。用左闭右闭区间时,只有一个元素的区间是 left == right,你写 >= 其实也没错,但新手很容易写成 if (left == right),然后当区间非法时(比如 left > right)递归无限进行下去。这种情况在 mid + 1 超过 right 时就会发生,表现为栈溢出。
第二个问题是 mid 的计算。(left + right) / 2 在 left 和 right 都很大时可能溢出(int 上限),标准的做法是 left + (right - left) / 2。虽然排序算法一般不会遇到这么大的数组,但养成习惯总没坏处。
第三个问题,也是我踩得最久的坑,是合并时对“左右两个子数组”的边界界定。你必须非常明确:左子数组是 arr[left] 到 arr[mid],右子数组是 arr[mid+1] 到 arr[right]。在实现 merge 函数时,新手很容易把 mid 这个位置算错,导致元素被覆盖或重复。
1.2 区间开闭约定:一切混乱的根源
我后来才意识到,归并排序里几乎所有的 bug 都源于区间开闭不统一。如果你在递归函数里用的是左闭右闭,那么合并函数里也必须使用同一套约定;如果你在分治时用了 [left, mid) 和 [mid, right) 这种左闭右开区间,那递归终止条件和合并逻辑又完全是另一套写法。
我自己最推荐的是“左闭右开”表示法。原因有三点:
- 空区间容易表示:
[left, left)就是空,不用单独考虑 left > right 的情况。 - 遍历数组时更自然:
for (int i = left; i < right; i++)不用纠结<=还是<。 - C++ STL 的
sort系列、Go 的sort.Slice底层都采用类似的半开区间理念,写习惯了跨语言迁移也轻松。
用左闭右开重写上面的递归:
c复制void merge_sort(int arr[], int left, int right) {
if (right - left <= 1) return;
int mid = left + (right - left) / 2;
merge_sort(arr, left, mid);
merge_sort(arr, mid, right);
merge(arr, left, mid, right);
}
这样,left 到 mid 是左子数组,mid 到 right 是右子数组,两个区间互不重叠,合并时也容易确定各部分的长度。
1.3 合并时的索引漂移
有了统一区间约定,合并函数写起来就清晰很多:
c复制void merge(int arr[], int left, int mid, int right) {
int n1 = mid - left;
int n2 = right - mid;
int L[n1], R[n2];
for (int i = 0; i < n1; i++) L[i] = arr[left + i];
for (int j = 0; j < n2; j++) R[j] = arr[mid + j];
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
