概述
- 二路归并排序,又称合并排序。
- 假设我们有这样一两个数组,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},他在内存中这样排序
-
我们首先将数组分为如图的部分。将他们划分为最小的部分,最小的部分我们称他为一个数组(大小为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],然后再对这两段数组进行递归的划分。 - 划分到单一元素的时候,进行合并操作。
- 函数名为mergeSort( A[] ,int low , int high),每次对A[low…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 ++;
}
}