6-1 Iterative Mergesort
How would you implement mergesort without using recursion?
The idea of iterative mergesort is to start from N sorted sublists of length 1, and each time to merge a pair of adjacent sublists until one sorted list is obtained. You are supposed to implement the key function of merging.
Format of functions:
void merge_pass( ElementType list\[\], ElementType sorted\[\], int N, int length );
The function merge_pass performs one pass of the merge sort that merges adjacent pairs of sublists from list into sorted. N is the number of elements in the list and length is the length of the sublists.
Sample program of judge:
cs
#include <stdio.h>
#define ElementType int
#define MAXN 100
void merge_pass( ElementType list[], ElementType sorted[], int N, int length );
void output( ElementType list[], int N )
{
int i;
for (i=0; i<N; i++) printf("%d ", list[i]);
printf("\n");
}
void merge_sort( ElementType list[], int N )
{
ElementType extra[MAXN]; /* the extra space required */
int length = 1; /* current length of sublist being merged */
while( length < N ) {
merge_pass( list, extra, N, length ); /* merge list into extra */
output( extra, N );
length *= 2;
merge_pass( extra, list, N, length ); /* merge extra back to list */
output( list, N );
length *= 2;
}
}
int main()
{
int N, i;
ElementType A[MAXN];
scanf("%d", &N);
for (i=0; i<N; i++) scanf("%d", &A[i]);
merge_sort(A, N);
output(A, N);
return 0;
}
/* Your function will be put here */
Sample Input:
10
8 7 9 2 3 5 1 6 4 0
Sample Output:
7 8 2 9 3 5 1 6 0 4
2 7 8 9 1 3 5 6 0 4
1 2 3 5 6 7 8 9 0 4
0 1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 7 8 9
cs
void merge_pass(ElementType list[], ElementType sorted[], int N, int length) {
int i = 0;
//主循环:每次处理两个相邻的长度为length的子序列
//i+2*length-1 < N保证了数组中至少还有两个完整的子序列可以进行合并
while (i + 2 * length - 1 < N) {
int l = i; // 第一个子序列的起始位置
int m = i + length - 1; // 第一个子序列的结束位置(也是中间位置)
int r = i + 2 * length - 1; // 第二个子序列的结束位置
int p1 = l, p2 = m + 1, k = l;
//归并排序的核心逻辑,双指针比较两个有序子序列的元素,较小的先放入 sorted 数组
while (p1 <= m && p2 <= r) {
if (list[p1] <= list[p2]) {
sorted[k++] = list[p1++];
} else {
sorted[k++] = list[p2++];
}
}
//如果第一个子序列还有剩余元素,直接复制到 sorted 中
while (p1 <= m) {
sorted[k++] = list[p1++];
}
//如果第二个子序列还有剩余元素,直接复制到 sorted 中
while (p2 <= r) {
sorted[k++] = list[p2++];
}
//移动到下一对子序列的起始位置
i += 2 * length;
}
//处理数组末尾剩下的元素(不足两个完整的子序列)
//情况1:剩下的元素个数大于 length,说明还有一个完整的子序列和一个不完整的子序列需要合并
if (i + length < N) {
int l = i;
int m = i + length - 1;
int r = N - 1;
int p1 = l, p2 = m + 1, k = l;
while (p1<=m && p2<=r) {
if (list[p1] <= list[p2]) {
sorted[k++] = list[p1++];
} else {
sorted[k++] = list[p2++];
}
}
while (p1 <= m) {
sorted[k++] = list[p1++];
}
while (p2 <= r) {
sorted[k++] = list[p2++];
}
} else {
// 情况2:剩下的元素不足一个完整的 length(也就是"落单"的元素)
// 无法配对,直接原封不动地复制到 sorted 数组的对应位置即可
while (i < N) {
sorted[i] = list[i];
i++;
}
}
}
这道题要你做的事情很明确:不是让你完整写归并排序,而是只补全一次"归并趟"的函数 merge_pass。
也就是说,判题程序已经帮你写好了 main() 和 merge_sort(),你只需要实现:
void merge_pass( ElementType list[], ElementType sorted[], int N, int length );
一、这道题到底在考什么?
归并排序通常是递归写法:
先把数组不断拆成两半,再合并。
但这道题问的是:
不用递归,怎么实现归并排序?
非递归归并排序的思路是:
一开始,把每个元素都看成一个已经有序的小序列。
例如原数组:
8 7 9 2 3 5 1 6 4 0
一开始可以看成 10 个长度为 1 的有序子序列:
[8] [7] [9] [2] [3] [5] [1] [6] [4] [0]
第一趟,合并相邻两个长度为 1 的子序列:
[8] 和 [7] -> [7 8]
[9] 和 [2] -> [2 9]
[3] 和 [5] -> [3 5]
[1] 和 [6] -> [1 6]
[4] 和 [0] -> [0 4]
所以得到:
7 8 2 9 3 5 1 6 0 4
这正是样例输出第一行。
二、merge_pass 的作用
函数声明是:
void merge_pass(ElementType list[], ElementType sorted[], int N, int length);
含义如下:
| 参数 | 含义 |
|---|---|
list[] |
原数组,也就是本趟归并前的数据 |
sorted[] |
目标数组,把本趟归并结果存进去 |
N |
数组元素总数 |
length |
当前每个有序子序列的长度 |
所以 merge_pass 要做的是:
把
list[]中相邻的、长度为length的两个有序子序列合并,结果放到sorted[]对应位置。
例如当:
length = 2
说明现在数组应该被看作:
[7 8] [2 9] [3 5] [1 6] [0 4]
然后合并:
[7 8] 和 [2 9] -> [2 7 8 9]
[3 5] 和 [1 6] -> [1 3 5 6]
[0 4] 没有配对对象,直接保留
得到:
2 7 8 9 1 3 5 6 0 4
这就是样例输出第二行。
三、判题程序怎么调用你的函数?
题目给出的 merge_sort() 是:
void merge_sort(ElementType list[], int N)
{
ElementType extra[MAXN];
int length = 1;
while (length < N) {
merge_pass(list, extra, N, length);
output(extra, N);
length *= 2;
merge_pass(extra, list, N, length);
output(list, N);
length *= 2;
}
}
这个地方很重要。
它会反复调用 merge_pass:
第一次:
merge_pass(list, extra, N, 1);
把长度为 1 的小段两两合并,结果放到 extra。
第二次:
merge_pass(extra, list, N, 2);
把长度为 2 的小段两两合并,结果放回 list。
第三次:
merge_pass(list, extra, N, 4);
把长度为 4 的小段两两合并。
第四次:
merge_pass(extra, list, N, 8);
把长度为 8 的小段和剩余部分合并。
所以 list 和 extra 是来回倒腾的。
四、你的代码整体结构
你的代码可以分成两大部分:
while (i + 2 * length - 1 < N) {
// 处理两个完整的长度为 length 的子序列
}
这一部分处理正常情况:数组中还有两个完整子序列可以合并。
然后:
if (i + length < N) {
// 剩下一个完整子序列 + 一个不完整子序列
} else {
// 剩下不足一个完整子序列,直接复制
}
这一部分处理数组末尾的特殊情况。
因为数组长度 N 不一定刚好是 2 * length 的倍数,所以最后可能会剩下一些元素。
五、主循环详细解释
代码:
int i = 0;
i 表示当前要处理的一对子序列的起始位置。
例如 length = 2 时:
7 8 2 9 3 5 1 6 0 4
第一组从 i = 0 开始:
[7 8] [2 9]
第二组从 i = 4 开始:
[3 5] [1 6]
第三组从 i = 8 开始:
[0 4]
主循环条件:
while (i + 2 * length - 1 < N)
这个条件的意思是:
从位置
i开始,后面至少还有2 * length个元素。
也就是有两个完整子序列可以合并。
例如 N = 10, length = 2:
当 i = 0:
i + 2 * length - 1 = 0 + 4 - 1 = 3 < 10
说明 [0,1] 和 [2,3] 可以合并。
当 i = 4:
4 + 4 - 1 = 7 < 10
说明 [4,5] 和 [6,7] 可以合并。
当 i = 8:
8 + 4 - 1 = 11 < 10
不成立。
说明从第 8 个位置开始,已经不够两个完整长度为 2 的子序列了。
六、每一对子序列的边界
代码:
int l = i;
int m = i + length - 1;
int r = i + 2 * length - 1;
这里定义了三个关键下标:
| 变量 | 含义 |
|---|---|
l |
第一个子序列的起点 |
m |
第一个子序列的终点 |
m + 1 |
第二个子序列的起点 |
r |
第二个子序列的终点 |
也就是说,两个子序列分别是:
list[l ... m]
list[m+1 ... r]
例如:
length = 2
i = 0
则:
l = 0
m = 1
r = 3
两个子序列是:
list[0 ... 1] = [7 8]
list[2 ... 3] = [2 9]
七、双指针归并逻辑
代码:
int p1 = l, p2 = m + 1, k = l;
三个指针含义:
| 指针 | 作用 |
|---|---|
p1 |
指向第一个子序列当前元素 |
p2 |
指向第二个子序列当前元素 |
k |
指向 sorted[] 中要写入的位置 |
核心归并:
while (p1 <= m && p2 <= r) {
if (list[p1] <= list[p2]) {
sorted[k++] = list[p1++];
} else {
sorted[k++] = list[p2++];
}
}
意思是:
两个子序列都是有序的,所以每次只需要比较两个当前元素,把较小的放入 sorted[]。
例如合并:
[7 8] 和 [2 9]
过程是:
7 和 2 比,2 小,放 2
7 和 9 比,7 小,放 7
8 和 9 比,8 小,放 8
第一个序列用完,剩下 9 直接放进去
得到:
[2 7 8 9]
八、为什么要复制剩余元素?
代码:
while (p1 <= m) {
sorted[k++] = list[p1++];
}
如果第一个子序列还有元素,就全部复制进去。
代码:
while (p2 <= r) {
sorted[k++] = list[p2++];
}
如果第二个子序列还有元素,就全部复制进去。
原因是:
归并过程中,只要有一个子序列先用完,另一个子序列剩下的元素本身已经有序,所以不用再比较,直接接到后面即可。
九、处理完一组后移动 i
代码:
i += 2 * length;
因为当前已经处理了两个长度为 length 的子序列,也就是一共处理了:
2 * length
个元素。
所以 i 移动到下一组的起点。
十、数组末尾特殊情况
主循环只能处理两个完整子序列。
但最后可能剩下:
情况一:一个完整子序列 + 一个不完整子序列
例如:
N = 10
length = 8
数组可以看成:
[1 2 3 5 6 7 8 9] [0 4]
第一个子序列长度是 8,第二个子序列长度只有 2。
虽然第二个不完整,但仍然可以合并。
所以代码写:
if (i + length < N)
表示:
从
i开始,至少还有超过length个元素。
也就是说,有第一段完整的长度为 length 的子序列,并且后面还有一些元素可以作为第二段。
于是:
int l = i;
int m = i + length - 1;
int r = N - 1;
这时第二个子序列的终点不再是:
i + 2 * length - 1
而是数组最后一个元素:
N - 1
然后照常归并。
情况二:只剩一个不足 length 的子序列
代码:
else {
while (i < N) {
sorted[i] = list[i];
i++;
}
}
这说明最后剩下的元素连一个完整的长度为 length 的子序列都不够。
它没有配对对象,所以不需要合并,直接复制到 sorted[] 对应位置即可。
例如 length = 2 时,最后剩下:
[0 4]
其实它已经是一个有序段了,但没有另一个长度为 2 的段和它合并,所以直接复制。
十一、用样例完整走一遍
输入:
10
8 7 9 2 3 5 1 6 4 0
第 1 趟:length = 1
原数组:
[8] [7] [9] [2] [3] [5] [1] [6] [4] [0]
两两合并:
[8] [7] -> [7 8]
[9] [2] -> [2 9]
[3] [5] -> [3 5]
[1] [6] -> [1 6]
[4] [0] -> [0 4]
输出:
7 8 2 9 3 5 1 6 0 4
第 2 趟:length = 2
当前数组:
[7 8] [2 9] [3 5] [1 6] [0 4]
合并:
[7 8] [2 9] -> [2 7 8 9]
[3 5] [1 6] -> [1 3 5 6]
[0 4] 没有配对对象,直接复制
输出:
2 7 8 9 1 3 5 6 0 4
第 3 趟:length = 4
当前数组:
[2 7 8 9] [1 3 5 6] [0 4]
合并:
[2 7 8 9] [1 3 5 6] -> [1 2 3 5 6 7 8 9]
[0 4] 直接复制
输出:
1 2 3 5 6 7 8 9 0 4
第 4 趟:length = 8
当前数组:
[1 2 3 5 6 7 8 9] [0 4]
合并:
[1 2 3 5 6 7 8 9] [0 4]
得到:
0 1 2 3 4 5 6 7 8 9
输出:
0 1 2 3 4 5 6 7 8 9
最后 merge_sort() 结束后又输出一次最终数组,所以样例最后两行一样。
十二、这份代码的核心思想总结
这份代码做了三件事:
第一,找到每一对相邻的有序子序列。
list[l ... m]
list[m+1 ... r]
第二,用双指针把它们归并到 sorted[]。
p1 指向左半段
p2 指向右半段
k 指向结果数组
第三,处理数组末尾不够两个完整子序列的情况。
一个完整 + 一个不完整:继续合并
只剩一小段:直接复制
十三、这道题最容易错的地方
最容易错的是最后剩余部分。
很多同学只写主循环:
while (i + 2 * length - 1 < N)
这样只能处理两个完整子序列。
但如果数组末尾剩下元素,就会漏掉。
例如 N = 10, length = 4 时:
[2 7 8 9] [1 3 5 6] [0 4]
最后 [0 4] 如果不复制到 sorted[],结果数组后两个位置就是错误的。
所以你的代码专门写了:
if (i + length < N)
和:
else {
while (i < N) {
sorted[i] = list[i];
i++;
}
}
这是非常关键的。
十四、可以记住的答题模板
这道题本质上就是:
非递归归并排序中的一趟归并。每趟将数组中相邻两个长度为
length的有序子序列合并到辅助数组中,若末尾不足两个完整子序列,则根据剩余元素数量决定继续合并或直接复制。
你的答案代码逻辑是正确的。核心是:双指针归并 + 末尾特殊处理 。