数据结构基础hw12Iterative Mergesort函数题

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 的小段和剩余部分合并。

所以 listextra 是来回倒腾的。


四、你的代码整体结构

你的代码可以分成两大部分:

复制代码
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 的有序子序列合并到辅助数组中,若末尾不足两个完整子序列,则根据剩余元素数量决定继续合并或直接复制。

你的答案代码逻辑是正确的。核心是:双指针归并 + 末尾特殊处理