归并排序的非递归实现

归并排序的非递归实现 merge sort

归并排序也称为合并排序,本文详细介绍归并非递归的实现。

问题描述

有一串乱序的数字,将它们(利用合并排序的思想)排列成有序的。

通常使用一个数组来保存这个串无序的序列,输出也用一个数组来表示

输入:乱序的数组A,数组的长度n

输出:有序的数组A

 

特殊情形(元素个数为2i)

基本思路:看作是一颗倒过来的满二叉树,两两成对

这张图叙述了合并排序算法的基本思路,类似一棵倒放的二叉树。

图中标记的解释:state0代表初始的状态,经过第一趟合并(merge1)之后得到的状态为State1,以此类推。

归并排序的基本思路

  1. 在State0初始状态时,两两合并,合并用到的算法是“合并有序的数组 merge sorted array”。即每次合并得到的都是有序的数组。

    两两合并的规则是:将两个相同序列长度的序列进行合并,合并后的序列长度double。

    第一趟合并(merge 1)调用了4次merge sorted array,得到了4个有序的数组:"5, 8","3, 9","6, 4","1, 4"(每个合并后的序列长度为2)

    第二趟合并(merge 2)调用了2次merge sorted array,得到了2个有序的数组:"3, 5, 8, 9","1, 4, 6, 11''(每个合并后的序列长度为4)

  2. 按步骤1的思想以此类推,经过多次合并最终得到有序的数组,也就是State3。

    可以看出经过一共3趟合并,最终得到有序的数组。

    可以看出每趟要执行的合并次数不同,第一趟合并执行4次,第二趟合并执行2次,第三趟合并行1次。

归并排序的循环体设计思路

看了上述的算法思想,可以知道算法可以设计成两层循环

  1. 外层循环遍历趟数
  2. 内层循环遍历合并次数

下面的伪码描述了两层循环体的实现: 

1 merge_sort(A[], n) {
2     while (趟数条件) {    // 外层循环:合并的大趟数 
3         while (执行合并的条件) {    // 内层循环:每趟合并的次数
4         // 进行两两合并
5         }
6     }
7 }

 

一般情形(数组的元素个数不一定是2i

如图可知,一般数组元素的个数不一定是2i个。右边多出的"10, 7, 2"子树可以视作一般情况的情形。

虽然元素个数不一定是2i个,但是任意元素个数的n,必然可以拆分成2+ m个元素的情形(数学条件不去深究)

由图可知,特殊情形思想中的两两合并的规则不能满足一般情况的合并需求

  • 图中灰色的箭头代表无需合并,因为找不到配对的元素。
  • 图中墨蓝色的箭头代表两个长度不相等的元素也需要进行合

将上述的合并需求称为长短合并,容易知道长短合并仅存在1次或0次。

下面讨论的合并前/后的序列长度特指两两合并后得到的序列长度。

 

合并趟数的循环设计

虽然一般情形特殊情形的合并规则有差别(一般情形复杂),但是可以设计一个通用的合并趟数条件。

设置变量t:记录每趟合并序列的长度(t=2k),其中k是趟次(如图中的merge1、2、3、4)

  • 通过观察发现:每次合并后序列的大小有规律,第一趟后合并的序列大小都是2,第二趟合并后的序列大小都是4,以此类推..
  • "10, 7, 2"这三个元素组合而成的序列长度虽然不满足上述的规律,但是并不影响趟数的计算。2= 16 ≥ 11,4趟后循环结束。
  • 可以设计成:if 最后合并后的序列长度≥实际元素的个数n,这时可以说明循环结束

下面的伪代码给出了合并趟数的循环设计,以及循环结束的条件。

1 merge_sort(A[], n) {
2     int t = 1;  // t:每趟合并后序列的长度
3     while (t<n) { // 合并趟数的结束条件是:最后合并后的序列长度t≥数组元素的个数n
4         t *= 2;
5         // TODO:每趟进行两两合并
6     }
7 }

 

每趟合并次数的循环设计

从上图可以看出:每趟的合并次数和元素的总数n有关,且和合并前/后的序列长度有关。

数组的元素总数n越大,自然需要合并的次数更多。

每趟合并前序列长度越长,这趟需要合并的次数更少。

设置变量s:记录合并前序列的长度

下面列出了一般情形下的合并数学规律

记m为两两合并的次数(图中每对黑色箭头代表一次两两合并)

记j为长短合并的次数(图中每对墨蓝箭头代表一次长短合并)

第一趟合并 :n=11,s=1,t=2,m=5,j=0

第二趟合并 :n=11,s=2,t=4,m=2,j=1

第三趟合并 :n=11,s=4,t=8,m=1,j=0

第四趟合并 :n=11,s=8,t=16, m=0,j=1

存在公式:m = n / t,j =(n % t > s ) ? 1: 0 可以设计如下的内层循环

其中第二个公式不太好理解,可以以merge2的合并过程作为参考:

两两合并得到了"3, 5, 8, 9","1, 4, 6, 11"两个序列后。还剩余3个元素,因为3>2=s,所以还要进行长短合并。

 1 merge_sort(A[], n) {
 2     int t = 1;
 3     int i;    // 每趟合并时第一个序列的起始位置
 4     int s;     // 合并前序列的长度
 5     while (t < n) {
 6         t *= 2;
 7         i = 0;
 8         s = t;
 9         while(i + t < n) {// 每趟进行两两合并,结束条件:
10             // TODO:两两合并操作(对两个长度相等的数组进行合并)
11             i = i + t
12         }
13         if (i + s < n) {    // 判断:还有两两长度不相同的数组需要合并
14             // TODO:长短合并操作(对于长度不相同的数组进行合并)
15         }
16     }
17 }

 

归并排序的完整代码

 1 /**
 2  * 归并排序算法
 3  * @param A 乱序数组A 
 4  * @param n 数组A的元素个数
 5  */
 6 void merge_sort(int A[], int n) {
 7     int i,s;  
 8     int t = 1;
 9     while (t < n) {        // 外层循环:合并的大趟数  
10         s = t;
11         t *=2;
12         i = 0;
13         while (i + t < n) {        // 内层循环:每趟需要合并的次数
14             merge(A, i, i+s-1, i+s*2-1, t); 
15             i = i + t;
16         }
17         if (i + s < n) {  // 判断还有剩下的元素待处理。
18             merge(A, i, i+s-1, n-1, n-i);
19         }
20     }
21 }

其中merge算法,请查看我的上一篇文章介绍:合并两个有序的数组。下面给出了实现:

 1 /**
 2  * 合并两个有序的子数组( A[p]~A[q]及A[q+l]~A[r]已按递增顺序排序 )
 3  * @param A 整数数组
 4  * @param p 第一个子数组的起始下标
 5  * @param q 第一个子数组的末尾下标
 6  * @param r 第二个字数组的末尾下标
 7  * @param n A数组的元素个数
 8  */
 9 void merge(int A[], int p, int q, int r, int n) {
10     int *B = new int[n];        // 建立缓冲区
11     int k = 0;                  // 指向B的游标,主要用于插入数据进B中
12     int i = p, j = q + 1;
13     while (i <= q && j <= r) {                  // while循环的跳出条件是:i和j只要有一个超过各种数组的界限
14         if (A[i] >= A[j]) {
15             B[k++] = A[j++];
16         } else {
17             B[k++] = A[i++];
18         }
19     }
20     if (i == q+1) {    // 说明是前半段先遍历完,把后半段的拼到数组后面
21         while (j <= r) {
22             B[k++] = A[j++];
23         }
24     } else {
25         while (i <= q) {
26             B[k++] = A[i++];
27         }
28     }
29     // 将选定的部分替换为B的数组
30     k = 0;
31     for (i = p; i <= r; i++) {
32         A[i] = B[k++];
33     }
34     delete[] B;
35 }

 

posted @ 2021-02-05 14:29  凱廸bob  阅读(2726)  评论(2编辑  收藏  举报