[算法]归并排序(C语言实现)

一、归并排序的定义      

          归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。 

二、归并排序的算法原理

        归并排序的算法可以用递归法和非递归法来实现,在理解的角度来看,归并排序就是一种递归排序。其将一个数组分成均匀的两份小的数组,然后将其分成的两份各自再分,得到四份小的数组,如此重复,直到所分成的小数组没有元素或者只有一个元素为止,这就是分而治之的。当们把数组分好后,再依次进行归并(将两个有序的小数组归并成一个有序数组),只有一个元素的小数组视为有序,第一次归并后,每个有序数组的元素个数就从一变成了二,再次归并有序数组,每个有序数组的个数就从二变成了四,如此重复,直到有序的数组个数等于原数组的个数,此时整个数组完全有序,完成了排序的工作。

下面是归并排序分而治之的演示图:

        上面这个图长得非常像一个二叉树,其实就是回溯这个二叉树进行归并的过程。而归并两个有序数组的方法非常简单,这里就不进行赘述。下面我来使用递归的方法和非递归的方法来实现递归排序。 

归并排序的动态示意图:

三、归并排序的递归法实现

        递归的方法实现归并排序很简单,就是不断递归左子树和右子树进行归并排序,当左右子树都有序后,就对左子树和右子树进行归并排序即可。为了简单起见,排序整数数组。

具体代码如下:

//归并排序递归法
void _MergeSort(int* arr,int* pTempArr,int leftIndex,int rightIndex)
{
	//如果区间内没有元素,停止递归
	//如果区间内只有一个元素,则视为有序,停止递归
	if (leftIndex >= rightIndex)
		return;

	//将当前区间均分为两个区间 
	//[leftIndex,midIndex]和[midIndex + 1,rightIndex]
	int midIndex = (leftIndex + rightIndex) / 2;

	//递归左右子树使左右子树有序,当左右子树有序时进行递归排序
	_MergeSort(arr, pTempArr, leftIndex, midIndex);//递归左子树
	_MergeSort(arr, pTempArr, midIndex + 1, rightIndex);//递归右子树

	//到了这里,说明左右子树有序了
	//归并两个有序数组[leftIndex,midIndex]和[midIndex + 1,rightIndex]
	int key = leftIndex;
	int index1 = leftIndex;
	int index2 = midIndex + 1;

	while(index1 <= midIndex && index2 <= rightIndex)
	{
		if (arr[index1] <= arr[index2])
			pTempArr[key++] = arr[index1++];
		else
			pTempArr[key++] = arr[index2++];
	}

	//归并剩余的元素
	while(index1 <= midIndex)
	{
		pTempArr[key++] = arr[index1++];
	}

	while (index2 <= rightIndex)
	{
		pTempArr[key++] = arr[index2++];
	}

	//将归并好的有序数据转移到待排序的数组中
	memcpy(arr + leftIndex, pTempArr + leftIndex, sizeof(int) * (rightIndex - leftIndex + 1));
}

//归并排序递归法实现
void MergeSort(int* arr,int nums)//传入数组和数组的大小
{
	//为归并两个有序数组临时开辟所需要的空间
	int* pTempArr = (int*)malloc(sizeof(int) * nums);

	//对数组进行归并排序
	_MergeSort(arr, pTempArr, 0, nums - 1);

	//释放临时数组的空间
	free(pTempArr);
}

四、归并排序的非递归方法实现 

        归并排序推荐使用非递归的方法来实现的,因为递归会出现栈溢出的问题,而非递归的方法就不用在意这个问题,迭代不需要像递归那样开辟大量栈空间。但是非递归的方法实现起来有一点的困难。

在上面的分而治之的图中:

        迭代的方式不需要分而治之,实现归并排序可以跳过的过程,直接,我们从图中可以看到,第一层有序数组进行归并排序时,每个有序数组的元素个数为1;当第二层有序数组进行归并排序时,每个有序数组的元素个数为2;当第三层进行归并排序时,每个有序数组的元素个数为4,我们发现,每次归并排序完成后,其每个将要归并的有序数组的元素个数是上一层的两倍,于是我们可以使用迭代的方式进行递归。

下面我将画图来演示这个过程: 

        归并排序的非递归法需要注意的是数组最后的几组有序数组元素的归并,视情况进行特殊处理。

其情况有以下几种:

1、倒数第二组有序数组和倒数第一组有序数组匹配进行归并。

此时又分为两种情况:

倒数第一组有序数组的个数和倒数第二组的有序数组的个数相同,归并的数组大小是对称的。

倒数第一组有序数组的个数比倒数第二组有序数组的个数少,归并的数组大小不是对称的

2、倒数第二组有序数组和倒数第三组有序数组匹配进行归并,而倒数第一组有序数组没有可以匹配归并的有序数组,此时倒数第一组有序数组不需要进行归并操作。

具体代码如下: 

//归并两个有序数组到新数组中
void MergeArray(int* pTempArr, int* arr,int leftIndex, int midIndex, int rightIndex)
{
	//归并有序数组[leftIndex,midIndex]和[midIndex + 1,rightIndex]
	int index1 = leftIndex;
	int index2 = midIndex + 1;
	int key = leftIndex;


	while (index1 <= midIndex && index2 <= rightIndex)
	{
		if (arr[index1] <= arr[index2])
			pTempArr[key++] = arr[index1++];
		else
			pTempArr[key++] = arr[index2++];
	}

	while (index1 <= midIndex)
	{
		pTempArr[key++] = arr[index1++];
	}

	while (index2 <= rightIndex)
	{
		pTempArr[key++] = arr[index2++];
	}
}

//归并排序非递归
void MergeSortNonR(int* arr,int nums)
{
	//为归并有序数组临时开辟所需要的空间
	int* pTempArr = (int*)malloc(sizeof(int) * nums);

	int gap = 1; //每组有序数组的元素个数
	while (gap < nums)
	{
		int leftIndex = 0; //归并的有序数组的起始位置(下标的左边界)

		// 每一层归并后的有序数组下标的分组
		// 0 1 2 3 4 5 6 7 8 9 10
		// [0 1] [2 3] [4 5] [6 7] [8 9] 10
		// [0 1 2 3] [4 5 6 7] [8 9 10]
		// [0 1 2 3 4 5 6 7] [8 9 10]
		// [0 1 2 3 4 5 6 7 8 9 10]

		// nums - leftIndex 得到leftIndex以及往后的元素的个数
		// nums - leftIndex >= 2 * gap 可以保证归并到的有序数组都是对称的
		while (nums - leftIndex >= 2 * gap)
		{
			int rightIndex = leftIndex + 2 * gap - 1;
			int midIndex = (leftIndex + rightIndex) / 2;

			//归并有序数组[leftIndex,midIndex]和[midIndex + 1,rightIndex]
			MergeArray(pTempArr, arr, leftIndex, midIndex, rightIndex);
			
			//将归并好的元素转移到待排序的数组中
			memcpy(arr + leftIndex, pTempArr + leftIndex, sizeof(int) * (rightIndex - leftIndex + 1));

			//归并下两组有序数组
			leftIndex += 2 * gap; 
		}

		//处理不对称的归并和没有归并的有序数组可以匹配的情况
		//如果满足 nums - leftIndex > gap 
		//说明leftIndex以及后面的元素个数大于gap个
		//需要进行归并排序,只不过归并排序的区间并不对称
		if (nums - leftIndex > gap)
		{
			//这里分成的两个区间由于不对称,所以不能使用左右下标相除的方法算出中间下标
			//归并排序[leftIndex,leftIndex + gap - 1] [leftIndex + gap,nums - 1]
			MergeArray(pTempArr, arr, leftIndex, leftIndex + gap - 1, nums - 1);
			memcpy(arr + leftIndex, pTempArr + leftIndex, sizeof(int) * (nums - leftIndex));
		}
		//如果剩余元素不足gap个,不需要进行归并排序

		//进行下一层的归并排序
		gap *= 2;
	}

	//释放临时数组的空间
	free(pTempArr);
}

五、总结 

        归并排序的时间复杂度为O(nlogn),空间复杂度为O(N),因为归并有序数组需要额外开辟空间,所以其排序的性能仅次于快排,但是归并排序稳定。

二路归并排序是一种高效的排序算法,它结合了分治策略和归并操作,将数组分成两个独立的部分分别进行排序,然后将结果合并。下面是使用C语言实现的基本步骤: 1. **划分**:将待排序的数组分为两部分,通常根据数组长度的一半进行划分。 2. **递归排序**:对每部分递归地应用二路归并排序,直到每个部分只剩一个元素(这已经是有序的)。 3. **合并**:将两个已排序的部分合并成一个有序的数组。这个过程是通过比较两个指针指向的元素,将较小的元素放入结果数组,然后移动对应的指针。 以下是一个简单的C语言版本的二路归并排序代码示例: ```c #include <stdio.h> #include <stdlib.h> // 归并两个已排序的区间 void merge(int *arr, int left, int mid, int right) { int i, j, k; int n1 = mid - left + 1; int n2 = right - mid; // 创建临时数组 int L[n1], R[n2]; for (i = 0; i < n1; i++) L[i] = arr[left + i]; for (j = 0; j < n2; j++) R[j] = arr[mid + 1 + j]; i = 0; j = 0; k = left; while (i < n1 && j < n2) { if (L[i] <= R[j]) arr[k++] = L[i++]; else arr[k++] = R[j++]; } // 将剩余未复制的元素添加到结果数组 while (i < n1) arr[k++] = L[i++]; while (j < n2) arr[k++] = R[j++]; } // 二路归并排序函数 void mergeSort(int *arr, int left, int right) { if (left < right) { int mid = left + (right - left) / 2; mergeSort(arr, left, mid); mergeSort(arr, mid + 1, right); // 合并 merge(arr, left, mid, right); } } int main() { int arr[] = {9, 7, 5, 11, 12, 2, 14, 3, 10}; int n = sizeof(arr) / sizeof(arr); printf("Before sorting:\n"); for (int i = 0; i < n; i++) { printf("%d ", arr[i]); } mergeSort(arr, 0, n - 1); printf("\nAfter sorting:\n"); for (int i = 0; i < n; i++) { printf("%d ", arr[i]); } return 0; } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值