归并排序详细思路与插入排序的对比

一、基本概念

1.归并概念:将两个有序数列合并成一个有序数列,我们称之为“归并”。

2. 归并排序(Merge Sort)概念

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

归并排序有多路归并排序、两路归并排序,可用与内排序,也可用于外排序。

3.算法思路及实现

设两个有序的子序列(相当于输入序列)放在同一序列中相邻的位置上:array[L..m],array[m + 1..R],先将它们合并到一个局部的暂存序列 temp (相当于输出序列)中,待合并完成后将 temp 复制回 array[L..R]中,从而完成排序。

在具体的合并过程中,设置 i,j 和 p 三个指针,其初值分别指向这三个记录区的起始位置。合并时依次比较 array[i] 和 array[j] 的关键字,取关键字较小(或较大)的记录复制到 temp[p] 中,然后将被复制记录的指针 i 或 j 加 1,以及指向复制位置的指针 p 加 1。重复这一过程直至两个输入的子序列有一个已全部复制完毕(不妨称其为空),此时将另一非空的子序列中剩余记录依次复制到 array 中即可。

4.具体步骤:

  • 分解 -- 将当前区间一分为二,即求分裂点 mid = (L + R)/2;
  • 求解 -- 递归地对两个子区间a[L...mid] 和 a[mid+1...R]进行归并排序。递归的终结条件是子区间长度为1。
  • 合并 -- 将已排序的两个子区间a[L...mid]和 a[mid+1...R]归并为一个有序的区间a[L...R]。

二、(1)图解实现:

归并排序(Merge Sort)

当我们要排序数组的时候,使用归并排序法,将数组分成两半。如图:

然后把左边的数组和右边的数组排序,最后一起归并。当我们对左边的数组和右边数组进行排序的时候,再分别将左边的数组和右边的数组分成一半,然后对每一个部分先排序,再归并。(也就是一次次递归进行分组排序,归并,再分组再排序再归并过程)如图:

对于上面的每一个部分呢,我们依然是先将他们分半,再归并,如图:

分到一定细度的时候,每一个部分就只有一个元素了,那么我们此时不用排序,对他们进行一次简单的归并就好了。如图:

直至最后归并完成。

 

归并的细节:

两个已经排序好的数组,如何归并成一个数组?

我们可以开辟一个临时数组来辅助我们的归并。也就是说他比我们插入排序也好,选择排序也好多使用了存储的空间,也就是说他需要o(n)的额外空间来完成这个排序。只不过现在计算机中时间的效率要比空间的效率重要的多。

整体来讲我们要使用三个索引来在数组内进行追踪。
 

2. 排序稳定性

所谓排序稳定性,是指如果在排序的序列中,存在两个相等的两个元素,排序前和排序后他们的相对位置不发生变化的话,我们就说这个排序算法是稳定的。

排序算法是稳定的算法。

 蓝色的箭头表示最终选择的位置,而红色的箭头表示两个数组当前要比较的元素,比如当前是2与1比较,1比2小,所以1放到蓝色的箭头中,蓝色的箭头后移,1的箭头后移。

然后2与4比较,2比4小那么2到蓝色的箭头中,蓝色箭头后移,2后移,继续比较.......

 二、(2)代码实现:

版本一:

#include<iostream>
#include<algorithm>
//#include "SortTestHelper.h"
//#include "SelectionSort.h"
#include<stdio.h>
using namespace std;
//归并排序
template<typename T>//泛型
//归并操作

//将arr[l,mid]和[mid+1,r]两部分进行归并操作
void __merge(T arr[],int l,int mid,int r){
	T aux[r-l+1];//临时存放的数组,开辟的空间
	for(int i=l;i<=r;i++)
		aux[i-l] = aux[i]; //有一个l的偏移量,并且完成临时空间
	//首先我设置两个索引已经排好序的两部分子数组
	int i = l,j = mid+=1;
	for(int k=l;k<=r;k++){
		//判断数组索引越界问题,当i>mid后面还没有进行完操作、
		if(i>mid){
			arr[k] = aux[j-l];
			j++;
		}
		else if(j>r){
			arr[k] = aux[i-l];
			i++;
		}
		else if(arr[i-l]<arr[j-l]){
			aux[k] = aux[i-l];
			i++;//索引到下一个位置
		}else{
			aux[k] = aux[j-l];
			j++;//索引到下一个位置
		}
	}
}


template<typename T>//泛型
//递归使用归并排序,对arr[l,r]的范围进行排序
void __mergeSort(T arr[] ,int l,int r){
	if(l>=r){//这是一个不可能的情况,等于时候,也就是说没有数据需要处理
		return ;
	}
	//定义中间的数
	int mid = (l+r)/2;
	//开始对分开的左右两个部分分别进行归并排序
	__mergeSort(arr,l,mid);//对左边进行归并排序

	__mergeSort(arr,mid+1,r);//对右边就行归并排序
	__merge(arr,l,mid,r);//将两段进行merge,归并或者是融合操作
}


template<typename T>//泛型

void mergeSort(T arr[], int n){
	__mergeSort(arr,0,n-1);
}
int main(){

int a[105],n,i;
	scanf("%d",&n);
	for(i=0;i<n;i++)
	scanf("%d",&a[i]);
    
    mergeSort(a,n);
    sort(a,a+n);
	for(i=0;i<n;i++)
	printf("%d ",a[i]);

	return 0;
}

结果:

测试对比插入排序与归并排序的时间的复杂度代码:

main.cpp:

#include <iostream>
#include "SortTestHelper.h"
#include "InsertionSort.h"

using namespace std;
// 将arr[l...mid]和arr[mid+1...r]两部分进行归并
template<typename  T>
void __merge(T arr[], int l, int mid, int r){

    // 经测试,传递aux数组的性能效果并不好
    T aux[r-l+1];
    for( int i = l ; i <= r; i ++ )
        aux[i-l] = arr[i];

    int i = l, j = mid+1;
    for( int k = l ; k <= r; k ++ ){

        if( i > mid )   { arr[k] = aux[j-l]; j ++;}
        else if( j > r ){ arr[k] = aux[i-l]; i ++;}
        else if( aux[i-l] < aux[j-l] ){ arr[k] = aux[i-l]; i ++;}
        else                          { arr[k] = aux[j-l]; j ++;}
    }
}

// 递归使用归并排序,对arr[l...r]的范围进行排序
template<typename T>
void __mergeSort(T arr[], int l, int r){

    if( l >= r )
        return;

    int mid = (l+r)/2;
    __mergeSort(arr, l, mid);
    __mergeSort(arr, mid+1, r);
    __merge(arr, l, mid, r);
}

template<typename T>
void mergeSort(T arr[], int n){

    __mergeSort( arr , 0 , n-1 );
}


int main() {

    int n = 50000;

    // 测试1 一般性测试
    cout<<"Test for Random Array, size = "<<n<<", random range [0, "<<n<<"]"<<endl;
    int* arr1 = SortTestHelper::generateRandomArray(n,0,n);
    int* arr2 = SortTestHelper::copyIntArray(arr1, n);

    SortTestHelper::testSort("Insertion Sort", insertionSort, arr1, n);
    SortTestHelper::testSort("Merge Sort",     mergeSort,     arr2, n);

    delete[] arr1;
    delete[] arr2;

    cout<<endl;


    // 测试2 测试近乎有序的数组
    int swapTimes = 100;
    cout<<"Test for Random Nearly Ordered Array, size = "<<n<<", swap time = "<<swapTimes<<endl;
    arr1 = SortTestHelper::generateNearlyOrderedArray(n,swapTimes);
    arr2 = SortTestHelper::copyIntArray(arr1, n);

    SortTestHelper::testSort("Insertion Sort", insertionSort, arr1, n);
    SortTestHelper::testSort("Merge Sort",     mergeSort,     arr2, n);

    delete(arr1);
    delete(arr2);

    return 0;
}

SortTestHelper.h:

#ifndef INC_04_INSERTION_SORT_SORTTESTHELPER_H
#define INC_04_INSERTION_SORT_SORTTESTHELPER_H
#include <iostream>
#include <algorithm>
#include <string>
#include <ctime>
#include <cassert>

using namespace std;


namespace SortTestHelper {

    int *generateRandomArray(int n, int range_l, int range_r) {

        int *arr = new int[n];

        srand(time(NULL));
        for (int i = 0; i < n; i++)
            arr[i] = rand() % (range_r - range_l + 1) + range_l;
        return arr;
    }

    int *generateNearlyOrderedArray(int n, int swapTimes){

        int *arr = new int[n];
        for(int i = 0 ; i < n ; i ++ )
            arr[i] = i;

        srand(time(NULL));
        for( int i = 0 ; i < swapTimes ; i ++ ){
            int posx = rand()%n;
            int posy = rand()%n;
            swap( arr[posx] , arr[posy] );
        }

        return arr;
    }

    int *copyIntArray(int a[], int n){

        int *arr = new int[n];
        copy(a, a+n, arr);
        return arr;
    }

    template<typename T>
    void printArray(T arr[], int n) {

        for (int i = 0; i < n; i++)
            cout << arr[i] << " ";
        cout << endl;

        return;
    }

    template<typename T>
    bool isSorted(T arr[], int n) {

        for (int i = 0; i < n - 1; i++)
            if (arr[i] > arr[i + 1])
                return false;

        return true;
    }

    template<typename T>
    void testSort(const string &sortName, void (*sort)(T[], int), T arr[], int n) {

        clock_t startTime = clock();
        sort(arr, n);
        clock_t endTime = clock();
        cout << sortName << " : " << double(endTime - startTime) / CLOCKS_PER_SEC << " s"<<endl;

        assert(isSorted(arr, n));

        return;
    }

};
#endif //INC_04_INSERTION_SORT_SORTTESTHELPER_H

InsertionSort.h:

#include <iostream>
#include "SortTestHelper.h"
#include "InsertionSort.h"

using namespace std;
// 将arr[l...mid]和arr[mid+1...r]两部分进行归并
template<typename  T>
void __merge(T arr[], int l, int mid, int r){

    // 经测试,传递aux数组的性能效果并不好
    T aux[r-l+1];
    for( int i = l ; i <= r; i ++ )
        aux[i-l] = arr[i];

    int i = l, j = mid+1;
    for( int k = l ; k <= r; k ++ ){

        if( i > mid )   { arr[k] = aux[j-l]; j ++;}
        else if( j > r ){ arr[k] = aux[i-l]; i ++;}
        else if( aux[i-l] < aux[j-l] ){ arr[k] = aux[i-l]; i ++;}
        else                          { arr[k] = aux[j-l]; j ++;}
    }
}

// 递归使用归并排序,对arr[l...r]的范围进行排序
template<typename T>
void __mergeSort(T arr[], int l, int r){

    if( l >= r )
        return;

    int mid = (l+r)/2;
    __mergeSort(arr, l, mid);
    __mergeSort(arr, mid+1, r);
    __merge(arr, l, mid, r);
}

template<typename T>
void mergeSort(T arr[], int n){

    __mergeSort( arr , 0 , n-1 );
}


int main() {

    int n = 50000;

    // 测试1 一般性测试
    cout<<"Test for Random Array, size = "<<n<<", random range [0, "<<n<<"]"<<endl;
    int* arr1 = SortTestHelper::generateRandomArray(n,0,n);
    int* arr2 = SortTestHelper::copyIntArray(arr1, n);

    SortTestHelper::testSort("Insertion Sort", insertionSort, arr1, n);
    SortTestHelper::testSort("Merge Sort",     mergeSort,     arr2, n);

    delete[] arr1;
    delete[] arr2;

    cout<<endl;


    // 测试2 测试近乎有序的数组
    int swapTimes = 100;
    cout<<"Test for Random Nearly Ordered Array, size = "<<n<<", swap time = "<<swapTimes<<endl;
    arr1 = SortTestHelper::generateNearlyOrderedArray(n,swapTimes);
    arr2 = SortTestHelper::copyIntArray(arr1, n);

    SortTestHelper::testSort("Insertion Sort", insertionSort, arr1, n);
    SortTestHelper::testSort("Merge Sort",     mergeSort,     arr2, n);

    delete(arr1);
    delete(arr2);

    return 0;
}

 测试结果:

三、时间复杂度及稳定性

1. 时间复杂度

长度为n的序列需要进行logn次二路归并才能完成排序(归并排序的形式其实就是一棵二叉树,需要遍历的次数就是二叉树的深度),而每趟归并的时间复杂度为O(n),因此归并排序的时间复杂度为O(nlogn)

算法复杂度:O(nlogn)

也许有很多同学说,原来也学过很多O(n^2)或者O(n^3)的排序算法,有的可能优化一下能到O(n)的时间复杂度,但是在计算机中都是很快的执行完了,没有看出来算法优化的步骤,那么我想说有可能是你当时使用的测试用例太小了,我们可以简单的做一下比较

当数据量很大的时候 nlogn的优势将会比n^2越来越大,当n=10^5的时候,nlogn的算法要比n^2的算法快6000倍,那么6000倍是什么概念呢,就是如果我们要处理一个数据集,用nlogn的算法要处理一天的话,用n^2的算法将要处理6020天。这就基本相当于是15年。

 

 

 

 

 

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值