基本排序算法 之六 ——归并排序

 


基本排序算法总结与对比 之六  ——归并排序 


1、归并排序  递归版本

template<typename T>
void mergeSort(T arr[], int lo, int hi)
{
    if(hi - lo < 2) return;
    int mi = (lo + hi) >> 1;
    mergeSort(arr, lo, mi);  mergeSort(arr, mi, hi);  merge(arr, lo, mi, hi);
}

template<typename T>
void merge(T arr[], int lo, int mi, int hi)
{
    T* C = arr + lo;    //C 为待排序数列的起点

    T* A = new T[mi - lo];    
    for(int i = 0; i < mi - lo; A[i] = C[i++]);    //拷贝arr[]中区间[lo,mi)的元素到A

    T* B = arr + mi;    //arr[]中区间[mi,hi)的元素不用单独拷贝出来,排序过程中后半段元素是安全的

    for(int a = 0, b = 0, c = 0; a < mi - lo || b < hi - mi; )    //按升序排列
    {
        if(a < mi - lo && (!(b < hi - mi) || A[a] <= B[b])) C[c++] = A[a++];
        if(b < hi - mi && (!(a < mi - lo) || A[a] >  B[b])) C[c++] = B[b++];
    }
    delete [] A;
}

       不难得出,上述代码中归并排序的时间复杂度最好,最坏,平均均为O(nlogn)。(证明过程参考有关书籍)

       实际上略微修改上段代码,即可提高算法效率: 

       ① 频繁的new和delete时间成本极高,可以在函数运行初分配好空间。

       ② merge(arr, lo, mi, hi)仅仅需要在arr[mid - 1] > arr[mid]才有必要执行,若arr[mid - 1] ≤ arr[mid]则其本身就是有序的。

       ③ 归并函数中引入了较为复杂的控制逻辑,目的是为了统一对不同情况的处理。尽管如此可使代码在形式上更为简洁,但同时也会在一定程度上造成运行效率的下降。

       基于以上几点,对上算法略做改进。

 2、归并排序 递归版本 的改进A

template<typename T>
void mergeSort(T arr[], int lo, int hi, bool init = true, T* A = nullptr, int len = 0)
{
    if (hi - lo < 2) { return; }
    if (init) { A = new T[hi - lo]; len = hi - lo; }  //先准备好存储[lo,mi)数据的空间A
                                                      //并且记录下数组长度len
    int mi = (lo + hi) >> 1;
    mergeSort(arr, lo, mi, false, A, len);    mergeSort(arr, mi, hi, false, A, len);
    if (arr[mi - 1] > arr[mi]) merge(arr, lo, mi, hi, A, len); //仅在arr[mid - 1] >         
                                                               //arr[mid]时才执行归并
}

template<typename T>
void merge(T arr[], int lo, int mi, int hi, T* A, int len)
{
    T* C = arr + lo;
    for (int i = 0; i < mi - lo; A[i] = C[i++]);
    T* B = arr + mi;

    //归并过程中可能会出现4种情况
    //1、A,B中数的均没有放完(放进C,我们先这么认为 因为B与C的后半段是重叠的)
    //2、A没有放完,B放完了
    //3、A放完了,B没有放完
    //4、A,B均放完了
    //情况1:a < mi - lo && b < hi - mi,需要对A,B中的数比较
    //情况2:a < mi - lo && b >= hi - mi,只需要把A中剩余的数拷贝到C后边即可
    //情况3:a > mi - lo && b < hi - mi,只需要把B中剩余的数放到C后边即可,
           //但是B与C的后半段是重叠的,即B中剩余的元素就是C的后边部分,所以不用任何操作!
    //情况4:均放完了,也不用任何操作
    //--综上:需要操作的情形只有情况1和2,也就是a < mi - lo
    //--然后只需要 b < hi - mi时比较元素 和 b >= hi - mi时拷贝元素
    //--因此,归并的逻辑控制可以简化如下:

    for (int a = 0, b = 0, c = 0; a < mi - lo; )  //只有a < mi - lo时才需要操作
    {
        if (b < hi - mi){                         //b < hi - mi时比较元素
            if(A[a] <= B[b])  C[c++] = A[a++];
            else C[c++] = B[b++];
        }
        else                                      //b >= hi - mi时拷贝元素
            C[c++] = A[a++];
    }
    if (len == hi - lo) delete[] A; 
}

        上述函数的参数列表可能显得有些冗余,但为了统一函数入口,使其简单易用,而不得不做出一些牺牲。 实际测试表明,改进的归并排序A 在对大量数据排序时 所耗时间 为 未改进的排序方法 的一半左右。

        但是,以上改进仍然有不足之处: 

        ① 递归方法不是尾递归形式,最高栈高为log2n,虽然不至于栈满,但空间利用还可以压榨。                                                        

3、归并排序 迭代版本

        若递归函数的尾递归形式难以直接按逻辑列出,则先考虑迭代形式。或者仍然按照普通的递归逻辑,建立一个栈 来储存参数 以实现尾递归。本文将不讨论尾递归形式的实现方法,网络上已经有一个尾递归实现的版本可以参考。

        下面列出归并排序的迭代形式。

        迭代版本的实质上是省略了递归版本的递归分解部分,从而直接进行归并。

                                       

        先对 【 n-n 2n元素归并】 做个解释:第一路n个有序元素 与 第二路n个有序元素 归并成 共2n个有序元素 的 归并操作。

        从1-1 2元素归并 → 2-2 4元素归并 → 4-4 8元素归并 →。。。 → n-n 2n元素归并。(代码中为:step-_step  step+_step元素归并  ,_step为 step 或者 hi - _lo - step 。)

        但是,在上述迭代过程中存在2种特殊情况:

         ① 在 k-k 2k元素归并时,可能 第二路 元素不足k个(因为在末尾部分),即实际应该为k-k1 k+k1元素归并(step-_step  step+_step元素归并)。

         ② 在 k-k 2k元素归并时,可能 剩余元素连 第一路 都不足,即末尾剩余的不足k个(或者刚好k个)元素 不能进行归并。

        对于情况①:仅需要把第二路元素的长度设置为 排除 第一路元素后的 剩余的 元素个数 即可。

        对于情况②:对于出现剩余元素连 第一路 都不足,那么剩余元素肯定是有序的(证明略)。因此不用考虑对剩余元素的归并,因为归并长度是2倍递增的,这些剩余元素最终会称为情况①中的第二路元素。

         基于以上分析,实现代码如下:

template<typename T>
void mergeSort(T arr[], int lo, int hi)
{
    T *A = new T[hi-lo], *B, *C;
    int _step = 1;  //储存第二路元素的长度用, 提前分配空间 避免循环中反复分配, 要是开心,后边的变量都可以提前分配好空间
    for(int step = 1; step < hi - lo; step <<= 1)  //step为第一路元素长度
    {
        for(int _lo = lo; _lo < hi - step; _lo += (step << 1))  //_lo < hi - step 为考虑情况 2
        {
            _step = _lo + step << 1 > hi ? hi - _lo - step : step;  //考虑情况 1,设置第二路元素长度
            C = arr + _lo;   B = C + step; 
            for(int i = 0; i < step; A[i] = C[i++]);  //拷贝第一路元素到A
            
            //第一路元素[_lo,_lo + step)与第二路元素[_lo + step,_lo + step +_step) 进行step-_step step+_step元素归并
            for(int a = 0,b = 0, c = 0; a < step; )
            {
                if(b < _step)
                {
                    if(A[a] <= B[b]) C[a++] = A[a++];
                    else C[c++] = B[b++];
                }
                else
                    C[a++] = A[a++];
            }
        }
    }
    delete [] A;
}

       上述代码没有考虑 只有当 arr[mid - 1]  > arr[mid] (即C[step - 1] > B[0])时才需要进行归并 这一条件,逻辑上似乎设置arr[mid - 1] > arr[mid] 的条件判断 能够避免不需要的归并操作。但是实际的随机数排序测试中加入了这一条件判断反而耗时略多。(笔者的猜测是:可能出现arr[mid - 1]  <= arr[mid] 从而不需要进行归并操作 的概率太低了,节省的时间反而不能补偿循环中进行arr[mid - 1]  > arr[mid] 条件判断的时间!)所以,代码中没有加入这一考虑。 当然,这样在最好的情况下就会更耗时!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值