数据结构与算法:二路归并排序(合并排序)

数据结构与算法:二路归并排序/合并排序

概述

  • 二路归并排序,又称合并排序。
  • 假设我们有这样一两个数组,A[1,4,5],B[2,3,7]。这两个数组是有序的。我们要将这两个数组,合并成为一个有序的数组,我们可以这样,将A B的第一个元素拿出来比较。小的元素提出来,也就是1,然后将剩下来的元素A[4,5],B[2,3,7],继续按照刚刚的方法,提出2,新数组就是[1,2]剩下A[4,5],B[3,7]。继续执行这样的操作。最后得出来了一个有序的数组 [1,2,3,4,5,7]。我们暂且叫他卡牌算法(因为在讲这个的时候,算法老师举了一个有两堆有序的卡牌,如何合并为一个有序卡牌的例子)
  • 因此,我们可以得知,我们只要有两个有序数组,我们就可以在时间复杂度为O(n)的情况下,把他们排序成一个有序的数组。
  • 所以说,在对于任意一个数组而言,我们只要找到他最小的有序部分,然后两两合并,两两合并,直到得到一个完全有序的新数组。那么,对于一个任意的数组,他的最小的有序部分是什么呢。
  • 因为数组是任意的,所以我们只敢肯定的说,最小的有序部分,肯定是单一一个元素,他肯定是有序的,因为只有他一个。比如A[4,1,2],他的最小有序部分有三份,[4],[1],[2]。

图解

  • 假设我们有这样一组数据 A[9] = {4,3,6,7,9,1,2},他在内存中这样排序
    数组A
    在这里插入图片描述

  • 我们首先将数组分为如图的部分。将他们划分为最小的部分,最小的部分我们称他为一个数组(大小为1),而不是一个元素,所以,图中划分完后有7个数组,且每个数组是有序的(因为只有一个元素)。分别为A[0…0],A[1…1]。。。

  • 也就是说,这7个小数组,大小都是1,然后,我们将相邻的数组,两两进行有序的合并,使得合并后的数组,仍然有序。
    在这里插入图片描述

  • 在对两个有序数组合并的时候,采用的是卡牌算法。

  • 一轮排序后,继续重复执行上述操作:将相邻的数组,两两进行有序的合并,使得合并后的数组,仍然有序。

在这里插入图片描述

  • 两轮合并结束后,得到了如图的结果。
  • 整个过程如下
    -在这里插入图片描述

设计代码

  • 第一步、首先要将数组进行划分,设待排序的数组是A[SIZE]。从图可知,划分形式如同一棵二叉树。我们每次从数组中间开始划分。
    • 设待划分的数组是A{ low…high }(是A中的一小段)。划分点也就是他的中点,mid = (low+high)/2
    • 若数组段只剩一个元素,比如A[5…5],划分出来也是(5+5)/2 = 5 ,A[5…5]也是他本身。
    • 若数组段是奇数项。比如A[3…5],(3+5)/2 = 4 ,划分为了A[3…4] A[5…5]
    • 若数组段是偶数项。比如A[2…5],(2+5)/2 = 3(因为是int),划分为了A[2…3]、A[4…5],均分
  • 第二步、划分必定是一个递归的操作。因此设计一个类似于二叉树遍历的递归代码。
    • 函数名为mergeSort( A[] ,int low , int high),每次对A[low…high]进行划分,
      划分为A[low…mid]、A[mid+1…high],然后再对这两段数组进行递归的划分。
    • 划分到单一元素的时候,进行合并操作。
  • 第三步、合并操作。对于任意两个有序的数组,我们要将他们合并,利用概述中的卡牌算法。
    • 在合并的时候,比如我们对A[2,4,6,1,3,5]合并,在中间划分,分成了两个小数组,这时候,在原址上合并,会造成数据丢失,因此,我们需要一个辅助数组B,和A一模一样,在B的基础上进行判断操作,在A的基础上进行排序操作。
    • 情况1 两个有序数组,刚好全部排序好。
    • 情况2 其中一个数组一个元素都没有了,但是另一个数组里,还有很多元素,这种情况下,这个数组里剩余的元素肯定都是大于已经排序好的那一部分(默认从小到大)。
      比如 A[1,2,4] B[3,5,6]
      当排序到[1,2,3,4]的情况下,A已经没了,但是B还有[5,6]这两个元素,这两个元素肯定是比所有的已排序的大的,直接接到已排序的后面就好。

实现

  • 划分函数
/*这个函数的作用就是对数组进行一个递归
*把数组划分为最小块 然后进行
合并排序->合并排序->合并排序。
*/ 
void mergeSort(ElemType A[],int low,int high){
    
    
	/*递归的边界条件是,原数组已经被划分为一个一个单独的数了。
	*也就是low = high的情况。 就会跳出递归。
	*/ 
	if(low < high){
    
    	
		//划分规则 中点 
		int mid = (low + high)/2; 
		mergeSort(A,low,mid);
		mergeSort(A,mid+1,high);
		//一次划分 一次合并
		merge(A,low,mid,high); 		 
	} 
} 
  • 合并函数
/*这个函数的作用是:
*将A[low..mid] 和 A[mid+1...high]
*这两段数据 进行合并排序 (卡牌算法) 
*这里需要一个临时数组 来存放 A[] 
*/
void merge(ElemType A[],int low,int mid,int high){
    
    
	//B里暂存A的数据 
	for(int k = low ; k < high + 1 ; k++){
    
    
		B[k] = A[k]; 
	} 
	/*这里对即将合并的两个数组 
	*A[low..mid] 头元素 A[i]和 A[mid+1...high] 头元素  A[j] 
	*进行一个头部的标记, 分别表示为数组片段的第一个元素 
	*k 是目前插入位置。 
	*/ 
	int i = low , j = mid + 1 , k = low; 	
	//只有在这种情况下 才不会越界 
	while(i < mid + 1 && j < high + 1) {
    
    
		//A的元素暂存在B里,因为不能再A上原地操作,会打乱数据
		//这也是为什么二路归并排序(合并排序)空间复杂度是O(n)的原因 
		//我们这里把值小的放在前面,最后排序结果就是从小到大 
		if(B[i] > B[j]){
    
    
			A[k++] = B[j++]; 
		}else{
    
    
			A[k++] = B[i++]; 
		} 	 
	} 
	//循环结束后,会有一个没有遍历结束的数组段。处理上文的情况2
	while(i < mid + 1) 
		A[k++] = B[i++]; 
	while(j < high + 1) 
		A[k++] = B[j++]; 
}
  • 这样就完成了mergeSort 合并排序。

复杂度分析

  • 时间复杂度
    • 我们每次合并,都花费O(n)的时间,因为每次合并,都要遍历一下整个数组。
    • 一共画合并的次数,是树的高度。 对于长度是n的数组,划分成数,高度是logn
      设高度是h ,2^h = n ,h = logn(底数2省略了),所以一共合并h = logn次
    • 时间复杂度 O(nlogn)
  • 空间复杂度
    • 因为合并操作不能原地进行,所以需要一个辅助数组B 大小和A一样
    • 空间复杂度O(n)
  • 在测试数据为10万的情况下。
数据量100000
O(nlogn)归并排序花费时间---------------------0.031000
O(n^2)选择排序花费时间---------------------14.135000
普通排序花费时间是合并排序的455.967742--------------------------------
Process exited after 14.21 seconds with return value 0
请按任意键继续. . .

学好算法 人人有责。

全部代码

  • 这里设置了随机数,只要在宏上修改就行。和普通的排序进行了比较
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define SIZE 100000 
typedef int ElemType; 

void merge(ElemType A[],int low,int mid,int high); 
void mergeSort(ElemType A[],int low,int high); 
void selectSort(ElemType A[],int low,int high);

ElemType *B = (ElemType*)malloc(sizeof(ElemType)*SIZE); 

int main(){
    
    
	
	printf("数据量%d",SIZE); 
	srand(time(NULL));
	ElemType *A = (ElemType*)malloc(sizeof(ElemType)*SIZE); 
	ElemType *A1 = (ElemType*)malloc(sizeof(ElemType)*SIZE); 
	ElemType *A2 = (ElemType*)malloc(sizeof(ElemType)*SIZE);  
    //随机数产生的范围是0~19999 随机了1W个数据; 
    for(int i = 0; i < SIZE; i++)
    	A[i] = rand()%SIZE*2;
	
	for(int i = 0; i < SIZE; i++) {
    
    
		A1[i] = A[i];
		A2[i] = A[i]; 
	}
	/*	
	printf("原数组---------------------\n"); 
	for(int i = 0 ; i < SIZE ;i++){
		if(i%20 == 0)
			printf("\n"); 
		printf("%5d ",A[i]);
	}*/ 
   	free(A);	
   	printf("\n"); 	
   		
	clock_t start,end; 
	double total1,total2; 
	start = clock();
	mergeSort(A1,0,SIZE-1);
	end = clock();
	total1 = (double)(end - start) / CLOCKS_PER_SEC;
	printf("O(nlogn)归并排序花费时间---------------------%f\n",total1);
	/*for(int i = 0 ; i < SIZE ;i++){
		if(i%20 == 0)
			printf("\n"); 
		printf("%5d ",A1[i]);
	}*/	 	
	free(A1);	 

	printf("\n"); 

	start = clock();
	selectSort(A2,0,SIZE-1);
	end = clock();
	total2 = (double)(end - start) / CLOCKS_PER_SEC;
	printf("O(n^2)选择排序花费时间---------------------%f\n",total2);
	
	printf("普通排序花费时间是合并排序的%f倍",total2/total1); 
	/*	
	for(int i = 0 ; i < SIZE ;i++){
		if(i%20 == 0)
			printf("\n"); 
		printf("%5d ",A2[i]);
	}	*/ 
	free(A2);	
	return 0; 
} 

/*这个函数的作用是:
*将A[low..mid] 和 A[mid+1...high]
*这两段数据 进行合并排序 (卡牌算法) 
*这里需要一个临时数组 来存放 A[] 
*/
void merge(ElemType A[],int low,int mid,int high){
    
    
	//B里暂存A的数据 
	for(int k = low ; k < high + 1 ; k++){
    
    
		B[k] = A[k]; 
	} 
	/*这里对即将合并的两个数组 
	*A[low..mid] 头元素 A[i]和 A[mid+1...high] 头元素  A[j] 
	*进行一个头部的标记, 分别表示为数组片段的第一个元素 
	*k 是目前插入位置。 
	*/ 
	int i = low , j = mid + 1 , k = low; 
	
	//只有在这种情况下 才不会越界 
	while(i < mid + 1 && j < high + 1) {
    
    
		//A的元素暂存在B里,因为不能再A上原地操作,会打乱数据
		//这也是为什么二路归并排序(合并排序)空间复杂度是O(n)的原因 
		//我们这里把值小的放在前面,最后排序结果就是从小到大 
		if(B[i] > B[j]){
    
    
			A[k++] = B[j++]; 
		}else{
    
    
			A[k++] = B[i++]; 
		} 
		 
	} 
	
	//循环结束后,会有一个没有遍历结束的数组段。
	while(i < mid + 1) 
		A[k++] = B[i++]; 
	while(j < high + 1) 
		A[k++] = B[j++]; 
}

/*这个函数的作用就是对数组进行一个递归
*把数组划分为最小块 然后进行
合并排序->合并排序->合并排序。
*/ 
void mergeSort(ElemType A[],int low,int high){
    
    
	/*递归的边界条件是,原数组已经被划分为一个一个单独的数了。
	*也就是low = high的情况。 就会跳出递归。
	*/ 
	if(low < high){
    
    	
		//划分规则 中点 
		int mid = (low + high)/2; 
		mergeSort(A,low,mid);
		mergeSort(A,mid+1,high);
		//一次划分 一次合并
		merge(A,low,mid,high); 		 
	} 
}
void selectSort(ElemType A[],int low,int high){
    
    
	//分割点 
	int i = low; 
	while(i < high + 1){
    
    
		int min = A[i];
		int k = i; 
		for(int j = i ; j < high + 1 ; j++){
    
    
			if(A[j] < min){
    
    
				min = A[j]; 
				k = j;
			}
		} 
		A[k] = A[i]; 
		A[i] = min;
		i ++;
	} 
} 















Guess you like

Origin blog.csdn.net/weixin_42585456/article/details/106059519