【C++】【排序算法】归并排序;并的优化、应用插入排序的优化、额外空间的优化、迭代的归并【手撕排序】


一、归并排序

1. 相关问题

merge操作的同时,可以解决 “逆序对问题”;

2.优劣

如果包含大量重复元素,paitition会分的不平衡,最差情况退化为O(n2);所以把等于标定点的值均匀分到两边。

相对稳定;即:相同的元素,排之后的次序和排之前的次序一致。

需要额外空间;即:在merge 的过程中,需要使用额外的空间。

3.复杂度分析

空间复杂度为O(n);

时间复杂度:

因为归并排序递归的将其二分,于是递归的层级就有logn层。具体的是以2为底n的对数。long2 n;

每层递归中,都将n个数据都扫描了一次。例如:第二层递归中,堆left-mid 和mid+1 -right 都扫描了一遍。

最终时间复杂度就为O(nlogn)

二、代码

1.normal

下面是没有经过优化的完全体:

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

    T *aux = new T[r-l+1];

    for( int i = l ; i <= r; i ++ )
        aux[i-l] = arr[i];

    // 初始化,i指向左半部分的起始索引位置l;j指向右半部分起始索引位置mid+1
    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 ++;
        }
    }

    delete[] aux;
}

// 递归使用归并排序,对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 );
}

2.merge优化+插入排序优化

区别在于

  1. merge之前如果已经发现两段是有序的,就不需要mege了
  2. 对于递归到小数量级的,使用插入排序,速度更佳。这是因为插入排序虽然是0(n2),但是其常数更小,在n并不大且近乎有序的情况下,比归并更快。

下面也将查部分的插入排序加入代码段

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

    T *aux = new T[r-l+1];

    for( int i = l ; i <= r; i ++ )
        aux[i-l] = arr[i];

    // 初始化,i指向左半部分的起始索引位置l;j指向右半部分起始索引位置mid+1
    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 ++;
        }
    }

    delete[] aux;
}

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

    // 对于小规模数组, 使用插入排序
    if( r - l <= 15 ){
        insertionSort(arr, l, r);
        return;
    }

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

    // 对于arr[mid] <= arr[mid+1]的情况,不进行merge
    // 对于近乎有序的数组非常有效,但是对于一般情况,有一定的性能损失
    if( arr[mid] > arr[mid+1] )
        __merge(arr, l, mid, r);
}

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

    __mergeSort( arr , 0 , n-1 );
}
// 对arr[l...r]范围的数组进行插入排序
template<typename T>
void insertionSort(T arr[], int l, int r){

    for( int i = l+1 ; i <= r ; i ++ ) {

        T e = arr[i];
        int j;
        for (j = i; j > l && arr[j-1] > e; j--)
            arr[j] = arr[j-1];
        arr[j] = e;
    }

    return;
}

3.额外空间的优化。

对于额外空间的优化其实很简单,只是将频繁的申请释放内存过程,直接在一开始阶段就申请好。

这样避免了频繁的申请、释放内存,浪费时间的同时,有可能使得空间申请失败。

于是我们就在一开始申请和原数组一样大的内存空间,每次merge 的时候只需要先cpoy一下。

这样空间复杂度虽然没有变化,但是时间性能还是有显著提高。


// 将arr[l...mid]和arr[mid+1...r]两部分进行归并
// 其中aux为完成merge过程所需要的辅助空间
template<typename  T>
void __merge2(T arr[], T aux[], int l, int mid, int r){

    // 由于aux的大小和arr一样, 所以我们也不需要处理aux索引的偏移量
    // 进一步节省了计算量:)
    for( int i = l ; i <= r; i ++ )
        aux[i] = arr[i];

    // 初始化,i指向左半部分的起始索引位置l;j指向右半部分起始索引位置mid+1
    int i = l, j = mid+1;
    for( int k = l ; k <= r; k ++ ){

        if( i > mid ){  // 如果左半部分元素已经全部处理完毕
            arr[k] = aux[j]; j ++;
        }
        else if( j > r ){  // 如果右半部分元素已经全部处理完毕
            arr[k] = aux[i]; i ++;
        }
        else if( aux[i] < aux[j] ) {  // 左半部分所指元素 < 右半部分所指元素
            arr[k] = aux[i]; i ++;
        }
        else{  // 左半部分所指元素 >= 右半部分所指元素
            arr[k] = aux[j]; j ++;
        }
    }

}

// 使用优化的归并排序算法, 对arr[l...r]的范围进行排序
// 其中aux为完成merge过程所需要的辅助空间
template<typename T>
void __mergeSort2(T arr[], T aux[], int l, int r){

    // 对于小规模数组, 使用插入排序
    if( r - l <= 15 ){
        insertionSort(arr, l, r);
        return;
    }

    int mid = (l+r)/2;
    __mergeSort2(arr, aux, l, mid);
    __mergeSort2(arr, aux, mid+1, r);

    // 对于arr[mid] <= arr[mid+1]的情况,不进行merge
    // 对于近乎有序的数组非常有效,但是对于一般情况,有一定的性能损失
    if( arr[mid] > arr[mid+1] )
        __merge2(arr, aux, l, mid, r);
}


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

    // 在 mergeSort2中, 我们一次性申请aux空间,
    // 并将这个辅助空间以参数形式传递给完成归并排序的各个子函数
    T *aux = new T[n];

    __mergeSort2( arr , aux, 0 , n-1 );

    delete[] aux;   // 使用C++, new出来的空间不要忘记释放掉:)
}

三、自下而上-迭代归并

自底向上的思路,稍显复杂。但是很有实际意义。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

// 自底向上的归并排序中, merge函数并没有改变
// 将arr[l...mid]和arr[mid+1...r]两部分进行归并
template<typename  T>
void __mergeBU(T arr[], int l, int mid, int r){

    //* VS不支持动态长度数组, 即不能使用 T aux[r-l+1]的方式申请aux的空间
    //* 使用VS的同学, 请使用new的方式申请aux空间
    //* 使用new申请空间, 不要忘了在__merge函数的最后, delete掉申请的空间:)
    T aux[r-l+1];
    //T *aux = new T[r-l+1];

    for( int i = l ; i <= r; i ++ )
        aux[i-l] = arr[i];

    // 初始化,i指向左半部分的起始索引位置l;j指向右半部分起始索引位置mid+1
    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 ++;
        }
    }

    //delete[] aux;
}

// 使用自底向上的归并排序算法
template <typename T>
void mergeSortBU(T arr[], int n){

    // Merge Sort Bottom Up 无优化版本
    for( int sz = 1; sz < n ; sz += sz )
        for( int i = 0 ; i < n - sz ; i += sz+sz )
   // 对 arr[i : i+sz-1] 和 arr[i+sz : min(i+sz+sz-1,n-1)] 进行归并
           if( arr[i+sz-1] > arr[i+sz] )
            	__mergeBU(arr, i, i+sz-1, min(i+sz+sz-1,n-1) );

}

1.caution:

for( int sz = 1; sz < n ; sz += sz )

  1. int sz = 1 ; sz += sz;代表的是,第一次是将两个大小为1的合并起来。然后每次区间翻倍。
  2. 即:第一次将两个1合并为大小为2的、第二次将大小为2的合并成大小为4的。1、2、4、8、16这样下去。

for( int i = 0 ; i < n - sz ; i += sz+sz )

  1. i < n - sz ;为的是合并arr[i…i+sz-1] 和 arr[i+sz…i+2*sz-1] 时,第二个区间还存在.
  2. i += sz+sz 为的是,每次跳过合并好的两个区间。

__mergeBU(arr, i, i+sz-1, min(i+sz+sz-1,n-1) );

  1. merge 中min(i+sz+sz-1,n-1) ,为了取其最小值,防止越界

参考

模板类、模板函数设计

bobo老师的github

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值