【C语言】算法学习·归并排序

目录

归并排序

算法描述

自顶向下和自底向上

实例 

代码

递归实现

迭代实现 

进阶·原地归并


归并排序

算法描述

归并排序是 分治思想 的应用,即将原待排数组 递归或迭代地 分为左右两半,直到数组长度为1,然后对左右数组进行合并(merge),在合并中完成排序。详细过程需结合代码理解,如下动图展示了{4,6,2,1,7,9,5,8,3}的归并排序过程(自顶向下非原地)。合并过程采用 非原地 合并方法,即依次比较两部分已排序数组,将比较结果依次写入 新空间 中。后续会介绍一种称作 原地(in-place) 归并排序的改进,使得空间复杂度达到 常数级 (自底向上时,O(1))。

如下树状图中的橙色线表示递归的轨迹(自顶向下递归归并排序)。

稳定性:稳定。

合并时的此判断中的等号if(left[l_next] <= right[r_next]),保证了出现相等元素时,居左的元素总会被放在左侧,稳定性不受影响。

自顶向下和自底向上

可以通过 自顶向下(top-down) 或 自底向上(bottom-up) 的方式实现归并排序。

自顶向下(top-down):从输入数组出发,不断二分该数组,直到数组长度为1,再执行合并。适合用 递归 实现。

自底向上(bottom-up):从输入数组的单个元素出发,一一合并,二二合并,四四合并直到数组有序。适合用 迭代 实现。

实例 

a[]={1,3,5,7,2,4,6,8};


对于这个数组,利用归并的思想排序,就是把它分成两个部分,从中间截开,分成两组数:1,3,5,7和2,4,6,8;我们可以发现两组数都是从小到大排序的,我们可以定义两个变量一个指向前一串数的第一个数字,另一个变量指向第二组数的第一个变量,分别比较这两个数,将小的那个放进一个新数组,然后变量往后移,逐个比较,最终就有了一个新数组,这个新数组就是排序好的数组。
但是如果分成两组数之后,两边的数字并不是有序的该怎么办?这时候说明把数组分开一次不够,就要继续再分,如果还不是有序的?再分,直到把它们分为一个一个的数,然后再用归并的思想把它们重新排回原来的数组,整个数组就变得有序了。

代码

递归实现

void merge_sort(int a[],int left,int right){
    if(left<right){
        int mid = (left + right) / 2;//从中间截开
        merge_sort(a,left, mid);//把左边沿中间截开
        merge_sort(a, mid + 1, right);//把右边沿中间截开
        merge(a, left, right, mid);//合并
    }
}
//接下来这个函数是合并的过程。

void merge(int a[],int left,int right,int mid) {
    int s[100];//一个新数组用来存储排序好的数组
    int i = left, j = mid + 1;//两个变量分别指向左边和右边两组数的第一个数
    int sor = left;
    while (i <= mid && j <= right) {
        if (a[i] < a[j]) {//归并的过程
            s[sor++] = a[i++];
        }
        else {
            s[sor++] = a[j++];
        }
    }
    while (i <= mid) s[sor++] = a[i++];//当一组数已经全部排进去之后,再将另外一组数的全部数字都排进去
    while (j <= right)  s[sor++] = a[j++];
    sor = left;
    while (sor <= right) {//把排好序的新数组全部放回原数组里
        a[sor] = s[sor];
        sor++;
    }
}

迭代实现 

void mergesort(int num[],int len){
	//对数组num归并排序迭代实现,len为数组长度,从小到大排序,O(nlog2^n),稳定
	/*核心思想,i表示步长,也就是左右两组各几个元素比较
		,从第一轮左右每组1个开始,每轮步长增大一倍
		,比较后从小到大存入temp,再对剩余元素进行处理
		,最后将排好序的temp返回num数组
		*/

	//分别为步长、temp下标、左边起始下标、左边终点下标、右边起始下标、右边终止下标
	int i,next,left_min,left_max,right_min,right_max;
	//新建一个temp数组,长度等于初始数组长度
	int *temp = (int*)malloc(len * sizeof(int));

	//每轮比较左右两个步长i长度的区间,每轮后i*=2
	for(i=1; i<len; i*=2){

		//从数组0号开始,下一组的起始位置等于上一组的终止位置,如果下一组左边步长都不够就不比了
		for(left_min=0; left_min < len-i; left_min = right_max){
			//右边起始位置=左边终止位置=左边起始加步长i
			right_min = left_max = left_min + i;
			//右边终止位置=右边起始位置加步长i
			right_max = right_min + i;
			next = 0;//temp的下标

			if(right_max > len){//如果右边越界
				right_max = len;//右边终止位置最大值只能为len
			}

			while(left_min < left_max && right_min < right_max){//左右都没到尽头
				if(num[left_min] < num[right_min]){//左小右大,左边存入temp
					temp[next++] = num[left_min++];
				}else{//右小左大,右边存入temp
					temp[next++] = num[right_min++];
				}
			}

			/*左边还有一组剩余元素,右边已到终止位置
				,说明左边剩余元素最大,将剩余元素移到右边最后
				,如果是右边有剩余,则不需要移了已经在最后*/
			while(left_min < left_max){
				num[--right_min] = num[--left_max];
			}

			while(next > 0){//把排好序的temp部分返回num
				 num[--right_min] = temp[--next];
			}
		}
	}
}

进阶·原地归并

前述归并排序,每一次合并都是将两部分待合并数组的比较结果写入一个与arr等大小的临时数组tmpArr中,写入后再将tmpArr中的合并结果写回到arr中。于是tmpArr的空间开销即为该实现的空间复杂度,为 O(n)。实际上,通过一种 原地旋转交换 的方法(俗称手摇算法/内存反转算法/三重反转算法),则只需要 O(1) 的辅助空间(由于递归空间为O(logn),其总的空间复杂度仍为 O(logn))。以下介绍旋转交换的实现方法。

以 456123 为例,欲将 456 和 123 交换位置转换为 123456,只需要执行三次旋转即可:

旋转 456,得到 654
旋转 123,得到 321
旋转 654321 得到 123456。
应用上述「手摇算法」对两个排序序列的「原地归并」过程如下。

  1. 记左数组第一个数下标为i,记右数组第一个数下标为j。
  2. 找到左数组中第一个 大于 右数组第一个数字的数,记其下标为i。
  3. 以index暂存右数组第一个元素的下标index = j。
  4. 找到右数组中第一个 大于等于 arr[i]的数,记其下标为j。此时必有 [i, index - 1]下标范围序列大于 [index, j - 1] 下标范围序列。
  5. 通过三次翻转交换 [i, index-1] 和 [index, j - 1] 序列 (指下标范围),即依次翻转[i, index-1],翻转[index, j - 1],翻转[i, j - 1]。
  6. 重复上述过程直到不满足(i < j && j <= rightEnd)

※ 第4步如果找「大于」而不是「大于等于」,对于数字数组排序,结果正确,但将 破坏稳定性。建议动手画一下。

以{1, 2, 4, 6, 7}与{3, 5, 8, 9} 两个已排序序列的合并为例,观察借助手摇算法实现原地归并的过程。

在{1, 2, 4, 6, 7}中找到第一个大于3的数4,其下标为2,i = 2。index = j = 5。在{3, 5, 8, 9}中找到第一个大于arr[i] = arr[2] = 4的数5,其下标为6,j = 6。
如上操作使得[0, i - 1]必是最小序列,[index, j - 1]必小于arr[i]。因此交换[i, index - 1]和[index, j - 1](采用三次旋转完成交换),使得这部分序列在整个数组中有序。
交换后,继续执行上述过程,直到不满足该条件 :i < j && j <= rightEnd。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值