[算法]归并排序

前言

归并排序使用了分治思想,本篇文章将会通过这种排序让读者大概体会什么是分治思想。

归并排序

归并排序是完全符合分治思想的一种排序方法。

首先,对于待排序的数组 A[ n ],将其分为两个子列 A[ 0..n/2 ],A[ n/2+1..n-1 ];

然后,对这两个子列分别进行归并排序,然后再根据大小顺序合并在一起。

这两个过程一个是“分”,一个是“治”。

看个例子:

若是对上面这个数组进行排序,首先将其 “分” 为两个子序列:

 对这两个子序列排序,又需要将它们分别分解,逐层分解,最后得到这样的一张图:

    每一行都是上一行的子问题,当分解为单个元素时,这已经是最小子问题了,这个时候才开始 “治”,也就是逐层合并子列。

如果你看懂下图,那基本上就明白归并排序的大致思路了。

就像上面这个过程一样,当子列被拆分到单个元素时开始合并子列, 并逐层向上按顺序合并子列。

下面一个问题是,如何实现合并两个排好序的子列?这里我们以递增子列为例。

这里我们可以用两个指针来解决这个问题。两个指针分别指向两个子列的首项,然后进行比较,每次取较小的那个元素,然后所在子列的指针后移,如果其中一个子列排完,那么剩下的直接加入到数组当中。

 对于合并子列,我们可以写出这样的伪代码:

MERGE( A, p, q, r )        //A为数组,p为左子列的首项,r为右子列的末尾,q为左子列的末尾

1        n1 = p - q + 1        n2 = r - q        //获取两个子列的长度

2        let L[ 0..n1 ] and R[ 0..n2 ] be new Arrays        //创建两个子列空间

3        for i = 0 to n1-1        L[ i ] = A[ p + i ]        //拷贝赋值

4        for i = 0 to n2-1        R[ i ] = A[ q + i + 1 ]

5        L[ n1 ] = R[ n2 ] = ∞        //设置哨兵

6        i = j = 0        //设置指针

7        for k = p to r

8                if L[ i ] <= R[ j ]        A[ k ] = L[ i ],  i++

9                else        A[ k ] = R[ j ],  j++

在伪代码中,我们发现,在创建左右子列时,分别多创建了一个位置,并在第5行代码中设置为无穷大。我们称这两个位置的元素为哨兵

为什么要设置哨兵呢?

如果没有哨兵的话,在比较左右子列的每个元素时,就要判断哪个子列的元素首先全部纳入数组中,再把另外子列的剩余元素加入队列完成排序。在两个子列的尾部都加入哨兵的话,就会简单很多,因为哨兵的值为无穷大,两个子列的指针永远不会越界,并且当一个子列全部排入数组后,另一个子列的所有元素肯定小于哨兵的值,就可以顺序加入数组,从而减少了指针判定的操作。


我们已经给出了归并子列的算法,再加上前面讲述的分治的思想,你应该能够自己设计出归并排序的伪代码了:

MERGE-SORT( A, p, r )        //A为数组,p为首项,r为末尾

1        if q < r

2                q = ( p + r ) / 2

3                MERGE-SORT( A, p, q )

4                MERGE-SORT( A, q + 1, r )

5                MERGE( A, p, q, r)

 我们用递归的办法体现了分治思想,当排序数组 A[ n ] 时,我们调用 MERGE-SORT( A, 0, n-1 ) 进行排序,代码的第3、4行又将其拆分为两个子问题,递归地求解,当子问题全部排好序后,再用MERGE进行归并。

C/C++代码如下:

void Merge(int* a, int p, int q, int r) {
	                                             //求两个子序列的长度
	int n1 = q - p + 1;                          //n1为a[p]到a[q]
	int n2 = r - q;                              //n2为a[q+1]到a[r]

	int* L, * R;                                 //构造两个子序列
	L = (int*)malloc(sizeof(int) * (n1+1));      //分配空间
	R = (int*)malloc(sizeof(int) * (n2+1));      //这里+1是给最后一位留空

	L[n1] = INT_MAX;                             //最后一位设为最大值
	R[n2] = INT_MAX;

	int i, j;
	for (i = 0; i < n1; i++) {                   //分别拷贝两个序列信息
		L[i] = a[p+i];                           //从a[p]到a[q]
	}
	for (i = 0; i < n2; i++) {
		R[i] = a[q+i+1];                         //从a[q+1]到a[r]
	}

	i = 0;                                       //从两个子列的第一项开始
	j = 0;

	for (int k = p; k <= r; k++) {               //重新排序
		if (L[i] <= R[j]) {                      //选择小的放
			a[k] = L[i];
			i = i + 1;
		}
		else {                                   //这里体现最后一位INT_MAX的作用
			a[k] = R[j];                         //当一个子列全部排完后,就把另一个子列复制过来
			j = j + 1;
		}
	}

	return;

}

void Merge_Sorting(int* a, int p, int r) {              //利用递归方法进行排序
	int q;                                              //创建中间项
	if (p < r) {
		q = (p + r) / 2;
		Merge_Sorting(a, p, q);                         //归并左子列
		Merge_Sorting(a, q + 1, r);                     //归并右子列
		Merge(a, p, q, r);                              //将排好序的左右子列归并
	}
	return;
}

本文是《算法导论》的学习笔记,如有错误,希望大佬在评论区指正!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值