排序算法学习——归并排序

前言

上一篇文章提到了几个时间复杂度为O(N^2)的排序算法,它们实现简单,在特定场合效果也不错,但是当数据量大的时候单纯用那几个算法效果就不是很理想了。这时我们时间复杂度为O(NlogN)的算法就有了用武之地,虽然实现相对而言比较困难,但是它的效率在数据量大的情况下是O(N^2)排序算法的数百甚至上千倍,因此在实际生活中是很重要的。

归并排序

原理

归并排序(Merge Sort),采用分治思想,将排序的过程分成一个个子过程,当所有子过程完成时,排序也最终完成了。而归并排序是将数组不断地二分,进而形成一个个子过程。
子过程,即归并的过程如下图演示,需要额外开辟一片空间进行复制一份原数组,然后两边逐一进行比较后进行归并。
至于它的时间复杂度,我们可以看到它的树的高度应该是

l o g 2 N

这时候我们使用时间复杂度为O(N)的算法进行排序即可完成排序。
归并过程

代码实现

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

    //开辟临时空间
    T tem[R - L + 1];
    //存储原数值
    for(int i = L; i <= R; i++) {
        tem[i - L] = arr[i];
    }
    //初始化索引位置,i为左侧索引,j为右侧
    int i = L, j = mid + 1;
    //k为应该插入arr的位置
    for(int k = L; k <= R; k++) {

        //前面两个先判断索引合法性,
        if(i > mid) {
        //左侧排完了
            arr[k] = tem[j - L];
            j++;
        } else if(j > R) {
        //右侧排完了
            arr[k] = tem[i - L];
            i++;
        } else if(tem[i - L] < tem[j - L]){
            arr[k] = tem[i - L];
            i++;
        } else {
            arr[k] = tem[j - L];
            j++;
        }
    }
}


/**
递归使用归并排序,对arr[L,R]范围进行排序
L,R为闭区间,R表示最后一个而非最后一个的后面一个
*/
template<typename T>
void __mergeSort(T arr[], int L, int R) {

    //跳出条件
//    if(L >= R)
//        return;

    //优化代码2,数组较小的时候用插入代替归并,15的取值有待商榷
    if( R - L <= 15){
        insertionSort(arr, L, R);
        return;
    }

    //注意L+R可能会溢出
    int mid = (L + R)/2;
    //递归二分
    __mergeSort(arr, L, mid);
    __mergeSort(arr, mid + 1, R);

    //优化代码1
    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);
}

优化

上面给出了归并排序的实现,同时优化已经包含在内。

优化1

    //优化代码1
    if(arr[mid] > arr[mid + 1])
        //归并左右两部分
        __merge(arr, L, mid, R);

优化代码1在归并处,如果左半边的最右(左半边最大的数)小于右半边的最左(右半边最小的数),那么是不是已经是有序的呢?因此不需要再进行不必要的归并,减少了很多冗余的工作。加了一行代码就完成了针对近乎有序数组的优化。

优化2

    //跳出条件
    if(L >= R)
        return;

这是原来的跳出条件,也是根据定义,当二分到只有一个数的时候应该跳出循环。但是我们可以在这里使用插入排序进行一点优化,这基于两点原因:

  1. 在数据量较小的情况下,近乎有序的概率较高
  2. 虽然插入排序的时间复杂度为O(N^2)级别的,但是前面的系数较小,当数据量小的时候比归并排序更快
    //优化代码2,数组较小的时候用插入代替归并
    if( R - L <= 15){
        insertionSort(arr, L, R);
        return;
    }

这里的数值我本来想做一下实验的,不过受限于编译器,我使用的是codeblocks,时间只显示了四位小数,因此数值很小的时候都显示为0s……因此就使用了15这个数了,我是抱着学习原理的态度来学的,这里也没花太多时间,有兴趣大家可以实验一下,到底哪个数最优。

自底而上的归并排序

上面我们是从上到下,不断地递归分层,然后归并。我们同样可以从下而上,先两个两个归并,然后四个四个,不断增加步长完成归并。演示如下图所示:

自底而上

代码修改

/**
自底而上的归并排序
没有使用arr的索引,可用于链表的排序
*/
template <typename T>
void mergeSortBU(T arr[], int n) {

    for( int sz = 1; sz <= n; sz += sz) {
        //i+sz<n保证存在第二部分,min解决第二部分数量不足问题
        for(int i = 0; i + sz < n; i += sz * 2) {
            //对arr[i...i+sz-1]和arr[i+sz...i+2*sz-1]进行归并
            __merge( arr, i, i + sz -1, min(i + sz * 2 -1, n - 1) );
        }
    }
}

归并仍然使用__merge方法,步长sz从1开始不断*2,最终长度大于等于整个数组长度时停止。

自底而上的归并排序结论

这里我们并没有针对近乎有序的情况进行优化,所以速度稍微慢了一点。不过自底而上的归并排序有一个优势,它在实现的过程中没有使用数组下标,因此在迁移至链表的排序时优势会比较大。


图片引用百度图片
代码实现参照liuyubobobo慕课网教程

猜你喜欢

转载自blog.csdn.net/blueblueskyz/article/details/79326656