归并排序来啦

目录

前言:

归并排序:

归并图解:(递归版本)

归并代码:(递归版本)

归并图解:(非递归版本)

归并代码:(非递归版本)

归并代码特性总结:

总结:


前言:

学习了那么多的排序方法,比较受欢迎的快排(Hoare版本)其实就是分治的思想。也就是把一个大问题一步步的分装成小问题,逐一解决。这样做的好处是一般能省略不少的不必要的排序操作,或是简化部分麻烦的排序操作。

而今天的主角归并排序撇开空间复杂度不讲其实是一个非常优秀的排序方法。基本思想就是把数组等分为左右两个小数组,假设在一系列操作下这两个数组有序了,那么最后有序合并这两个数组就行了。

归并排序:

定义:将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

归并图解:(递归版本)

归并代码:(递归版本)

void _MergeSort(int* a, int left, int right, int* tmp)
{
    //递归结束条件,也就是分到只有一个元素时停止拆分数组
	if (left >= right)
		return;


    //递归主体
	int mid = (left + right) / 2;
	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid+1, right, tmp);

    //把两个数组进行排序与合并
	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;
    
    //临时数组的下标index是从当前递归位置left算起的
	int index = left;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[index++] = a[begin1++];
		}
		else
		{
			tmp[index++] = a[begin2++];
		}
	}
    
    //把没放完的元素放在临时数组尾部,完成收尾工作
	while (begin1 <= end1)
	{
		tmp[index++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[index++] = a[begin2++];
	}

    //把归并好的元素放进原数组中,保证递归返回时符合归并的条件
    //特别注意这里在调用memcpy函数时,里面的数组地址是从当前递归的位置算起
    //千万别写成memcpy(a , tmp , sizeof(int) * (right - left + 1));这是错误的写法

	memcpy(a + left, tmp + left, sizeof(int) * (right - left + 1));
}

void MergeSort(int* a, int n)
{
    //开辟临时数组,用来储存排好序的元素并重置递归时原数组与之对应的的元素
	int* tmp = (int*)malloc(sizeof(int) * n);
	assert(tmp);

    //调用子函数来实现,避免重复开辟数组
	_MergeSort(a, 0, n - 1, tmp);

    //释放开辟空间,避免内存泄漏
	free(tmp);
}

归并图解:(非递归版本)

归并代码:(非递归版本)

相应解析放在代码中解释,结合上图方便给位读者老爷理解。

void MergeSortNonR(int* a, int n)
{
    //开辟临时数组
	int* tmp = (int*)malloc(sizeof(int) * n);
	assert(tmp);

    //gap表示的是初始时归并的每两个数组元素个数为都为1
	int gap = 1;
	while (gap < n) //gap大于n时表示只有一组数组了,就不需要归并了
	{
        //每次i的自增量为归并的两个子数组的长度之和
		for (int i = 0; i < n; i += gap*2)
		{
			int begin1 = i, end1 = i + gap - 1; //子数组1的开头和结尾
			int begin2 = i + gap, end2 = i + 2 * gap - 1;  //子数组2的开头和结尾
			int index = i;     //临时数组的下标index从i算起,从而保持与原数组的对应关系

            //下面的三个判断语句是对上面图解中一般情况的处理            

            //对子数组1结尾下标越界的处理
			if (end1 >= n)
			{
				end1 = n - 1;
			}
            //子数组2的开头越界时,说明子数组2不存在,直接令其首尾下标矛盾,
            //方便后面代码的识别其不存在
			if (begin2 >= n)
			{
				begin2 = n;
				end2 = n - 1;
			}

            //当代码走进下面的判断语句时,说明子数组2存在且尾部越界。
            //子数组2存在的原因就是因为如果不存在,前面的处理会使 end2<n,就不会进入判断语句了
			if (end2 >= n)
			{
				end2 = n - 1;
			}

            //把两个数组进行排序与合并
  			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[index++] = a[begin1++];
				}
				else
				{
					tmp[index++] = a[begin2++];
				}
			}

            //把没放完的元素放在临时数组尾部,完成收尾工作
			while (begin1 <= end1)
			{
				tmp[index++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[index++] = a[begin2++];
			}
		}

        //完成一趟排序后,把临时数组的元素拷贝到原数组,满足下一轮的排序条件
		memcpy(a, tmp, sizeof(int) * n);
		gap *= 2;  //每次归并的子数组元素个数都会翻一倍
	}

    //释放开辟空间,避免内存泄漏
	free(tmp);
}

归并代码特性总结:

1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。

2. 时间复杂度:O(N*logN)   :每层要进行N次排序,共有logN层

3. 空间复杂度:O(N)

4. 稳定性:稳定

总结:

希望这篇文章能对各位读者有所帮助,再见啦!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值