【数据结构取经之路】归并排序

简介

归并排序是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

归并排序的思想

归并排序分为分解合并两个步骤。假设数组长度为n.

分解:将数组分割为两个数组,在分别将两个数组分别分割为两个数组,直到最后每个数组都是一个元素,这时将该单元素数组看为有序数组。

合并:将分割出的有序数组进行合并,合并为新的有序数组,如此重复,直到得到一个长度为n的有序数组。

核心操作是将数组中前后相邻的两个有序序列归并为一个有序序列。

归并排序的时间复杂度

O(N * logN)

归并排序的空间复杂度

O(N)

算法稳定性

稳定

 归并排序的递归实现

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void Print(int* a, int n)
{
	for (int i = 0; i < n; i++) printf("%d ", a[i]);
	printf("\n");
}

void _MergeSort(int* a, int* tmp, int begin, int end)
{
	//分解:当数组只有一个元素时停止
	if (begin >= end) return;    
	int mid = (begin + end) / 2;
	_MergeSort(a, tmp, begin, mid);
	_MergeSort(a, tmp, mid + 1, end);

	//合并
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	int i = begin;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2]) tmp[i++] = a[begin1++];
		else tmp[i++] = a[begin2++];
	}

	while (begin1 <= end1) tmp[i++] = a[begin1++];
	while (begin2 <= end2) tmp[i++] = a[begin2++];

	memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}

void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}

	_MergeSort(a, tmp, 0, n - 1);
}

int main()
{
	int a[] = { 5,2,8,1,9,0,3,6,7,6 };
	int n = sizeof(a) / sizeof(int);
	Print(a, n);
	MergeSort(a, n);
	Print(a, n);

	return 0;
}

先申请空间,再将归并排序的过程作为子函数调用,这样不用在每次递归过程中申请释放空间。上述代码中,_MergeSort为MergeSort的子函数。

归并排序的非递归实现

归并排序的非递归实现是一种比较复杂的算法,它不像快排那样借助栈来存储要处理的区间的范围,而是直接利用循环搞定,此方法要求我们对细节的把控要非常好,尤其是在处理数组边界这一块。

非递归实现归并排序的思想

归并排序,它利用了分治的思想,所谓分治,就是分而治之。不管是用递归来实现还是用非递归来实现,都不可能脱离算法本身的思想。在上述的递归实现过程中我们可以看到,首先是利用递归将数组分解成单个元素的数组(单个元素意味着有序),接着再一一归并,二二归并,四四归并……非递归则是省略了将数组分解为单元素数组的过程,直接引入一个gap代表每组的元素个数,如果令gap = 1,那么每个子数组不就是有序的单元素数组吗?相邻的两个单元素数组归并后,得到了一个有两个元素的有序数组,这时,在令gap = 2,让相邻的有两个元素的数组归并……这一过程和递归方法中的合并过程是一致的。

 两个数组归并,并不要求这两个数组中的元素个数相同。上图中的数字27,在第一趟归并过程中,并没有参与归并,但是随着gap的增大,27也会参与到归并当中,这里,我想说明的是,不用担心归并过程中会漏掉最后一个元素,因为随着gap的增大,它一定会参与到归并中来。

 相邻两个子数组的下标表示

因为在归并过程中,是针对相邻两个数组的,因此,我们得把控好它们的下标。用 i 表示原数组的下标,begin1、end1,begin2、end2分别表示第一个子数组和第二个子数组的首尾元素下标。begin1 = i,end1 = i + gap - 1,begin2 = i + gap, end2 = i + 2*gap - 1。

begin1 = i,end1 = i + gap - 1,begin2 = i + gap, end2 = i + 2*gap - 1(均为左闭右闭区间), i += 2*gap(跳过两个子数组),再去算begin1、end1,begin2、end2

以下是一趟归并排序的过程展开图。 

数组边界处理 

数组的边界控制是非递归方法中最为细节的一部分。当数组元素个数不是2的次方的时候就存在越界的问题。越界分为以下三种情况:

1、end1,begin2、end2全部越界

2、begin2、end2越界

3、end2越界

越界的三种情况如下图所示: 

> 第一种情况, end1,begin2、end2全部越界,这时,第二个数组中没有任何元素,而且第一个数组中的元素是有序的,这种情况下不需要归并,也就是说,本趟归并不对第一个数组做任何处理,后面, 随着gap的增大,该组中的数据会参与到归并当中。

> 第二种情况,begin2、end2越界,同第一种情况,第一个数组中有元素,第二个数组中没有元素,不需要归并。

> 第三种情况,end2越界,第一个数组和第二个数组中都有元素, 因此需要归并,考虑到end2是越界的,所以需要对end2进行修正,将end2修改为 n - 1(n为数组总元素个数),即将end2修改为最后一个元素的下标,然后再归并。

非递归实现归并排序的代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void Print(int* a, int n)
{
	for (int i = 0; i < n; i++) printf("%d ", a[i]);
	printf("\n");
}

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail\n");
		return;
	}

	int gap = 1;//gap为每个子数组的元素个数,只有一个元素就代表着有序
	while (gap < n)
	{
		int j = 0;
		for (int i = 0; i < n; i += 2 * gap)
		{
			//子数组的下标
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;

            //第一、第二种情况,第二个数组中没有元素,不需要归并
			if (end1 >= n || begin2 >= n) break;

            //第三种情况,两个子数组中都有元素,需要修正end2,然后完成归并
			if (end2 >= n) end2 = n - 1;

			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2]) tmp[j++] = a[begin1++];
				else tmp[j++] = a[begin2++];
			}

			while (begin1 <= end1) tmp[j++] = a[begin1++];
			while (begin2 <= end2) tmp[j++] = a[begin2++];

            //归并一组就拷贝一组,不是整体(n)拷贝
			memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
		}
		gap *= 2;
	}
}

int main()
{
	int a[] = { 5,2,7,1,9,3,6,7,0 };
	int n = sizeof(a) / sizeof(int);
	Print(a, n);
	MergeSortNonR(a, n);
	Print(a, n);
	return 0;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值