【算法设计与分析】归并(合并)排序(分治算法经典问题)

本文主要代码及思路来源于:【算法设计与分析(第五版)】【王晓东】

1、基本思想

待排序元素分成大小大致相同的2个子集合,

分别对2个子集合进行排序,

最终将排好序的子集合合并成为所要求的排序

//递归描述
void MergeSort(Type a[ ], int left, int right)
{
      if (left<right) {//至少有2个元素
      int i=(left+right)/2;  //取中点
      MergeSort(a, left, i);
      MergeSort(a, i+1, right);
      Merge(a, b, left, i, right);  //合并到数组b
      Copy(a, b, left, right);    //复制回数组a
      }

 

2、改进——消除递归

对于算法MergeSort,还可以从多方面对它进行改进。例如,从分治策略的机制入手,容易消除算法中的递归

事实上,算法MergeSort的递归过程只是将待排序集合一分为二,直至待排序集合只剩下一个元素为止,然后不断合并两个排好序的数组段

此机制,可以首先将数组a中相邻元素两两配对。用合并算法将它们排序,构成n/2组长度为2的排好序的子数组段,然后再将它们排序成长度为4的排好序的子数组段,如此继续下去,直至整个数组排好序。

//消去递归后的合并排序算法
void MergeSort(int *a,int n)
{
	int b[n];
	int s=1;//子数组长度
	while(s<n)
	{
		//将长度为s的相邻子数组两两合并(合并成后的新数组长度为子数组的两倍且内部有序)
		//并将这样的结果保存到数组b
		MergePass(a,b,s,n);
		//子数组的长度增加一倍
		s+=s; 
		//同样使相邻的两个(内部有序)子数组合并成一个长度为"2s"的内部有序新数组 
		MergePass(b,a,s,n);
		s+=s; 
	} 
}

 

 

3、长长的代码们即将来袭

1、书上代码较完整示例(见书P23(第五版))

#include <iostream>
#include <stdio.h>
using namespace std;

template <class Type>
void Merge(Type c[], Type d[], int l, int m, int r)
// 合并c[l:m]和c[m+1:r]到d[l:r]
{
	int i=l;   // 左子数组的当前处理索引 
	int j=m+1; // 右子数组的当前处理索引
	int k=l;   // 合并结果索引
	
	while (i<=m && j<=r) 
	{
		if (c[i]<=c[j]) d[k++]=c[i++];
		else d[k++]=c[j++];
	}
	
	if (i>m) for(int q=j; q<=r; q++) d[k++]=c[q];
	else for(int q=i; q<=m; q++) d[k++]=c[q];
}

template <class Type>
void MergePass(Type x[], Type y[], int s, int n)
// 将x中长度为s的相邻子数组合并,结果存入y中
// n: 数组元素数量 
{
	int i=0; // 子数组起始位置
	
	// 合并度为s的相邻子数组 
	while (i+s+s<=n) 
	{
		Merge(x, y, i, i+s-1, i+s+s-1);
		i+=s+s;
	}
	
	// 剩下元素数量少于2s 
	if (i+s<n)	
	{
		// 剩下元素数量多于s 
		Merge(x, y, i, i+s-1, n-1);
	}
	else
	{
		// 剩下元素数量不多于s 
		for (int j=i; j<n; j++)
			y[j]=x[j];
	}
}

template <class Type>
void MergeSort(Type a[], int n)
{
	Type *b=new Type [n];
	int s=1; // 子数组长度
	while (s<n) 
	{
		MergePass(a, b, s, n);
		s+=s;
		MergePass(b, a, s, n);
		s+=s;
	}
	delete [] b;
}

template <class Type>
void OutputArray(Type elements[], int num_elements)
{
	cout<<elements[0];
	for (int i=1; i<num_elements; i++)
	{
		cout<<','<<elements[i];
	}
	cout<<endl;
}

int main(int argc, char *argv[]) {
	int a[]={13, 15, 8, 9, 11, 5, 4, 3};
	int n=sizeof(a)/sizeof(int);

	OutputArray(a, n);

	MergeSort(a, n);

	OutputArray(a, n);

	getchar();
		
	return 0;
}

2、结合书上代码复写版

#include <bits/stdc++.h>
using namespace std;

void Merge(int *c,int *d,int l,int m,int r)
// 合并c[l:m]和c[m+1:r]到d[l:r]
{
	int i=l,j=m+1,k=l;//左右子数组的当前处理索引,合并后的索引
	while(i<=m && j<=r) {
		if(c[i]<=c[j])  d[k++]=c[i++];
		else d[k++]=c[j++];
	}
	if(i>m)//左子数组全部并入数组d,同时右子数组还有值未并入数组d
		for(int q=j; q<=r; q++)
			d[k++]=c[q];
	else
		for(int q=i; q<=m; q++)
			d[k++]=c[q];
}

void MergePass(int *x,int *y,int s,int n)
// 将x中长度为s的相邻子数组合并,结果存入y中
// n: 数组元素数量
{
	int i=0;  //之数组起始位置
	while(i+2*s<=n) {
		Merge(x,y,i,i+s-1,i+2*s-1);
		i+=2*s;
	}
	if(i+s<n) { //剩下的元素少于2s
		//剩下的元素多于s
		Merge(x,y,i,i+s-1,n-1);
	} else {
		//剩下的元素不多于s
		for(int j=i; j<n; j++)
			y[j]=x[j];
	}

}

void MergeSort(int *a,int n)
{
	int b[n];
	int s=1;//子数组长度
	while(s<n) {
		//将长度为s的相邻子数组两两合并(合并成的新数组内部有序)
		//并将这样的结果保存到数组b
		MergePass(a,b,s,n);
		//子数组的长度增加一倍
		s+=s;
		//同样使相邻的两个(内部有序)子数组合并成一个长度为"2s"的内部有序新数组
		MergePass(b,a,s,n);
		s+=s;
	}
}

void Print(int *a,int n)
{
	for(int i=0; i<n; i++)
		cout<<a[i]<<" ";
	cout<<endl;
}

int main()
{
	int a[]= {13, 15, 8, 9, 11, 5, 4, 3};
	int n=sizeof(a)/sizeof(int);
	Print(a,n);
	MergeSort(a,n);
	Print(a,n);
	return 0;
}

3、书上代码一些细节优化后的参考版本

//二路归并排序算法
#include <stdio.h>
#include <malloc.h>

void disp(int a[], int n)			//输出a中所有元素
{
    int i;
    for(i = 0; i < n; i++)
        printf("%d ", a[i]);
    printf("\n");
}

void Merge(int a[], int low, int mid, int high)
//将a[low..mid]和a[mid+1..high]两个相邻的有序子序列归并为一个有序子序列a[low..high]
{
    int *tmpa;
    int i = low, j = mid + 1, k = 0;		//k是tmpa的下标,i、j分别为两个子表的下标

    tmpa = (int *)malloc((high - low + 1) * sizeof(int)); //动态分配空间

    while(i <= mid && j <= high)	//在第1子表和第2子表均未扫描完时循环
        if(a[i] <= a[j]) {	//将第1子表中的元素放入tmpa中
            tmpa[k] = a[i];
            i++;
            k++;
        }
        else {				//将第2子表中的元素放入tmpa中
            tmpa[k] = a[j];
            j++;
            k++;
        }
    while(i <= mid) {		//将第1子表余下部分复制到tmpa
        tmpa[k] = a[i];
        i++;
        k++;
    }
    while(j <= high) {		//将第2子表余下部分复制到tmpa
        tmpa[k] = a[j];
        j++;
        k++;
    }
    for(k = 0, i = low; i <= high; k++, i++) 		//将tmpa复制回a中
        a[i] = tmpa[k];
    free(tmpa);						//释放tmpa所占内存空间
}

void MergePass(int a[], int length, int n)	//一趟二路归并排序;合并排好序的相邻数组段
{
    int i;
    for(i = 0; i + 2 * length - 1 < n; i = i + 2 * length)	//归并length长的两相邻子表
        Merge(a, i, i + length - 1, i + 2 * length - 1);
    if(i + length - 1 < n)					//余下两个子表,后者长度小于length
        Merge(a, i, i + length - 1, n - 1);		//归并这两个子表
}

void MergeSort(int a[], int n)			//二路归并算法
{
    int length;
    for(length = 1; length < n; length = 2 * length)
        MergePass(a, length, n);
}

int main()
{
    int n = 11;
    int a[] = {2, 5, 11, 7, 10, 6, 9, 4, 3, 8, 1};
    printf("排序前: ");
    disp(a, n);
    MergeSort(a, n);
    printf("排序后: ");
    disp(a, n);
}

4、改进——自然合并

自然合并排序是合并排序算法MergeSort的一个变形

上述合并排序算法中,第一步合并相邻长度为1的子数组段,这是因为长度为1的子数组段是已排好序的。事实上,对于初始给定的数组a,通常存在多个长度大于1的已自然排好序的子数组段

例如,若数组a元素{48371562},则自然排好序的子数组段有{48}{37}{156}{2}。用1次对数组a的线性扫描就足以找出所有这些排好序的子数组段。然后将相邻的排好序的子数组段两两合并,构成更大的排好序的子数组段。对上面的例子,经一次合并后可得到2个合并后的子数组段{3478}{1256}。继续合并相邻排好序的子数组段,直至整个数组已排好序。这2个数组段再合并后就得到{12345678}

//自然合并排序
#include <iostream>
#include <stdio.h>

using namespace std;

/* run this program using the console pauser or add your own getch, system("pause") or input loop */

template <class Type>
void Merge(Type c[], Type d[], int l, int m, int r)
// 合并c[l:m]和c[m+1:r]到d[l:r]
{
	int i=l;   // 左子数组的当前处理索引 
	int j=m+1; // 右子数组的当前处理索引
	int k=l;   // 合并结果索引
	
	while (i<=m && j<=r) 
	{
		if (c[i]<=c[j]) d[k++]=c[i++];
		else d[k++]=c[j++];
	}
	
	if (i>m) for(int q=j; q<=r; q++) d[k++]=c[q];
	else for(int q=i; q<=m; q++) d[k++]=c[q];
}

template <class Type>
void NaturalMergePass(
Type x[], Type y[], int seg_i0[], int s, int nseg)
// 将x中长度为s的相邻子段组合并,结果存入y中
// seg_i0: 每个子段的起始索引 
// nseg: 子段数量 
{
	int i=0; // 子段组起始位置
	
	// 合并长度为s的相邻子段组 
	while (i+s+s<=nseg) 
	{
		Merge(x, y, seg_i0[i], seg_i0[i+s]-1, seg_i0[i+s+s]-1);
		i+=s+s;
	}
	
	// 剩下子段数量少于2s 
	if (i+s<nseg)	
	{
		// 剩下子段数量多于s 
		Merge(x, y, seg_i0[i], seg_i0[i+s]-1, seg_i0[nseg]-1);
	}
	else
	{
		// 剩下子段数量不多于s 
		for (int j=seg_i0[i]; j<seg_i0[nseg]; j++)
			y[j]=x[j];
	}
}

template <class Type>
void NaturalMergeSort(Type a[], int n)
{
	// 寻找子段起始位置和子段数量 
	int *seg_i0=new int [n+1];
	seg_i0[0]=0;
	int nseg=1;
	for (int i=1; i<n; i++)
	{
		if (a[i]<a[i-1])
			seg_i0[nseg++]=i;
	}
	seg_i0[nseg]=n;
	
	Type *b=new Type [n];
	int s=1; // 子数组长度
	while (s<nseg) 
	{
		NaturalMergePass(a, b, seg_i0, s, nseg);
		s+=s;
		NaturalMergePass(b, a, seg_i0, s, nseg);
		s+=s;
	}
	delete [] b;
	delete [] seg_i0;
}

template <class Type>
void OutputArray(Type elements[], int num_elements)
{
	cout<<elements[0];
	for (int i=1; i<num_elements; i++)
	{
		cout<<','<<elements[i];
	}
	cout<<endl;
}

int main(int argc, char *argv[]) {
	int a[]={13, 15, 8, 9, 11, 5, 4, 3};
	int n=sizeof(a)/sizeof(int);

	OutputArray(a, n);

	NaturalMergeSort(a, n);

	OutputArray(a, n);

	getchar();
		
	return 0;
}

 

5、简单分析

最坏时间复杂度:O(nlogn)

平均时间复杂度:O(nlogn)

辅助空间:O(n)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值