zoukankan      html  css  js  c++  java
  • 归并排序(Merge Sort)

    标签

    稳定排序、非原地排序、比较排序

    基本思想

    归并排序属于比较类非线性时间排序,号称比较类排序中性能最佳,在数据中应用中较广。

    归并排序是分治策略的一个典型的应用。所谓分治策略(Divide and Conquer),即将问题(divide)成一些小的问题以期递归求解,而治(conquer)的阶段则将分的阶段得到的各答案“融合”在一起,即分而治之。落实到归并排序中,就是将整个序列先递归划分为子序列(子序列起始长度为1),然后将相邻的子序列两两合并,合并的过程即是有序化的过程。如此重复,子序列间不断合并得到更长的有序子序列,直到整个序列合并完成,则此时序列完全有序。

    拆分阶段

    拆分可以用递归实现,也可以用迭代。

    递归的话,自上而下。先拆分,大事化小的思想。目的是将长度为$n$的待排序序列:$a_0a_1dots a_{n - 1}$,先一分为二,拆成$a_0a_1dots a_{frac{n - 1}{2} - 1}$和$a_{frac{n - 1}{2}}dots a_{n - 1}$两个子序列,再分别将这两个子序列继续一分为二,如此直到子序列长度为1,即完成拆分,进入合并阶段。

    而迭代的话,自下而上。直接从最小子序列(长度为1)开始有序化合并,逐步往上,直至整个序列合并完成有序化。

    递归 vs. 迭代

    这两种归并算法虽然实现方式不同,但也有共同之处
    1. 无论是基于递归还是迭代的归并排序, 它们调用的核心方法Merge都是相同的:完成一次合并。即两个已经有序的数组序列合并成一个更大的有序数组序列;
    2. 趋势一致,合并序列的长度都是从小(一个元素)到大(整个序列)增长的。
     

    合并阶段

    显然,合并阶段需要申请额外的临时存储空间(数组)

    如上图所示,每次合并的过程即是对两个已有序子序列进行合并再排序的过程。

    通常我们使用两个游标分别指示两个子序列当前待排序元素的位置,另有一个游标指示临时数组的当前元素存储位置。三个游标的初始位置均在头部。实际操作时,每次对两个子序列中游标指示位置的元素进行比较,将较小者(升序排列)存入临时数组中(游标指示位置),存储元素对应的两个游标均后移一位。如此操作,直至子序列之一的游标到达末部,则将另一个子序列中剩余元素直接依次全部存入临时数组中,即可。

    如此,临时数组中存储的即是两个子序列合并后的有序序列,再将其复制目标数组中,一次合并排序就完成了。

    算法描述

    • 步骤1:把长度为$n$的输入序列分成两个长度为$n/2$的子序列;
    • 步骤2:对这两个子序列分别采用归并排序;
    • 步骤3:将两个排序好的子序列合并成一个最终的排序序列。

    动图演示

    时间复杂度

    归并排序一共需要进行$O(log n)$层归并操作,每层的归并操作的总时间复杂度是$O(n)$,因此总体的时间复杂度为$O(nlog n)$。

    最坏,最好和平均时间复杂度都是$Θ(nlog n)$。

    空间复杂度

    和其他排序有所不同,为了实现归并操作,每次合并都需要开辟额外的空间来临时保存合并后的排序结果,总共需要开辟$n$个元素的空间,所以归并排序的空间复杂度为$O(n)$

    算法示例(C)

     根据实现思路不同,总的来说,归并排序的代码实现也分为递归和迭代两类。递归式自顶向下,迭代是自底向上。

    下面先讲讲两类方法的共同部分,也是核心调用——有序化合并函数Merge

    Merge函数

    背景

    Merge的前提是两个子序列已有序。

    Merge函数的输入为一个待排序数组a[low...high],则它实际上由两个已有序的子序列:a[low...mid]和a[mid+1...high]组成。为方便描述,我们分别称之为a1和a2,则目标是a1,a2的合并有序化。

    实现步骤

    1. 申请一个和原数组a长度相同的拷贝数组aux,并将原数组a中的所有元素依次拷贝进数组aux中,即aux[low...high]和a[low...high]一模一样;
    2. 与原数组中的a1和a2相对应,我们从aux1和aux2的头部元素开始,比较两个子序列的元素大小。较小的元素放入原数组a中(若a[0]已被占则放在a[1]...依次类推),并取得较小元素所在子序列的下一个元素, 继续同另一个子序列中之前那个较大的元素比较。因为前提是aux1和aux2都是有序的,所以通过这种方法我们能得到更长的有序序列;
    3. 如果aux的两个子序列的其中之一的所有元素都已比较完了, 则可直接将另一个子序列中剩下的元素,全部放入原数组a的剩余位置。
     
    步骤2和3的实现方法
    • 设置两个游标用于元素比较(在aux中进行):变量 i 和 j ,分别代表左游标和右游标,开始时分别指向aux[low]和aux[mid + 1];
    • 设置游标变量 k 用于指示在a中放置元素的位置(在a中进行),k在开始时候指向a[low];
    • 总体上来说 i, j, k 的趋势都是从左向右移动的;
    步骤2和3的图示解说
    结合上面的步骤2,比较 i 和 j 当前指示的aux中的元素的大小,取得其中比较大的那个元素(例如上图中的 i ),将其放入数组a中,此时 i++ ,左游标右移。同时 k++ , k 游标也向右移动。
    结合上面的步骤4,在 i 和 j 都向右移动的过程中。如上图这种情况,因为 j 当前指示的元素(图中位置)大于左半边即a[low...mid]的所有元素,导致 i 不断增加(右移)且越过了边界(mid), 所以这时候就不需要比较了,只要把 j 当前指示位置到high的元素都搬到原数组中,填满原数组中剩下的位置,单次合并就完成了,在这一段过程中 j 连续加1, j 游标连续右移。同时 k 也连续加1, k 游标也连续右移,直到 j == high && k == high 为止。
     
    基于上面的表述, 总结出单次合并算法Merge中最关键的4个条件判断情形:
    1. 左半边用尽(取右半边的元素)
    2. 右半边用尽(取左半边的元素)
    3. 左半边元素小于等于右半边(取左半边的元素)
    4. 右半边元素大于左半边(取右半边的元素)

    代码实现(C)

    void my_merge(int *a, int low, int mid, int high) {
        for (int p = low; p <= high; p++)
            aux[p] = a[p]; //aux为全局变量
        int i = low, j = mid + 1, k = low;
        while (i <= mid && j <= high) {
            if (aux[i] <= aux[j]) {
                a[k++] = aux[i++];
            } else {
                a[k++] = aux[j++];
            }
        }
        while (i <= mid) {
            a[k++] = aux[i++];
        }
        while (j <= high) {
            a[k++] = aux[j++];
        }
        return;
    }

    递归方法(自顶向下)

    图解Merge函数调用

    归并排序中,递归方法形成了一系列有层次、有先后调用顺序的Merge,  如下图左边的写入编号的Merge函数列表(这里是按照字母排序,A最小,Z最大):
     

    从上到下,是各个Merge的先后调用顺序。1最先调用,15最后调用。

    从右到左, 递归栈由深到浅。例如1, 2, 4, 5的递归深度是相同的, 而3比它们浅一个层次。
    merge_sort_up_to_down(a, low, mid); //1
    merge_sort_up_to_down(a, mid + 1, high); //2
    my_merge(a, low, mid, high); //3

    首先,在第一层递归的时候,先进入的是第一行的merge_sort方法,然后紧接着又进入了第二层递归的第一行merge_sort方法, 如此继续,由(a, low,mid)的参数列表可知其递归的趋势是一直向左移动的,直到最后一层递归,所以最先执行第一行merge_sort的对象是a[0]和a[1](上图编号1),然后执行的是最后一层递归的第二行代码,这时候merge_sort的对象是a[2]和a[3](上图编号2),再然后执行第三行,对已经有序的a[0]、a[1]和a[2]、a[3]进行my_merge(上图编号3)。之后返回上一层执行第二行,同样的递归向下再返回,再执行第三行,再往上返回一层。如此,递归的深度不断变浅, 直到最后对整个数组的左右两半进行my_merge,完成最终的有序化合并。

    完整代码(C)

    #include <stdio.h>
    #include <stdlib.h>
    
    int *aux;
    
    void my_merge(int *a, int low, int mid, int high) {
        for (int p = low; p <= high; p++)
            aux[p] = a[p];
        int i = low, j = mid + 1, k = low;
        while (i <= mid && j <= high) {
            if (aux[i] <= aux[j]) {
                    a[k++] = aux[i++];
            } else {
                a[k++] = aux[j++];
            }
        }
        while (i <= mid) {
            a[k++] = aux[i++];
        }
        while (j <= high) {
            a[k++] = aux[j++];
        }
        return;
    }
    
    void merge_sort_up_to_down(int *a, int low, int high) {
        if (low >= high) return;
        int mid = low + (high - low) / 2;
        merge_sort_up_to_down(a, low, mid);
        merge_sort_up_to_down(a, mid + 1, high);
        my_merge(a, low, mid, high);
        return;
    }
    
    void merge_sort(int *a, int len) {
        aux = (int *)malloc(sizeof(int) * len);
        merge_sort_up_to_down(a, 0, len - 1);
        free(aux);
        return ;
    }
    
    int main() {
        /* 归并排序(升序)*/
        int num[10] = {5, 1, 8, 4, 7, 2, 3, 9, 0, 6};
        int length = sizeof(num) / sizeof(num[0]);
        merge_sort(num, length);
        for (int i = 0; i < length; i++) 
            printf("%d ", num[i]);
        return 0;
    }

    递归的三种优化思路

    优化一. 小规模数组使用插入排序

    用不同的方法处理小规模问题能改进大多数递归算法的性能,因为递归会使小规模问题中方法调用太过频繁,所以改进对它们的处理方法就能改进整个算法。 因为插入排序非常简单, 因此一般来说在小数组上比归并排序更快。 这种优化能使归并排序的运行时间缩短10%到15%;

    快速排序的递归优化思路与之类似,对于小规模数组,在归并和快速排序算法中都可以替换成插入排序,以提升效率。

    具体做法就是把递归停止的判断语句 if (low >= high) return; 改成

    if (low + M>=high) { // 数组长度小于M的时候
        insert_sort(a, low, high); // 切换到插入排序
        return;
    }

    优化二:Merge函数可跳过情况判断

    Merge函数的功能是对a[low...high]中的a[low...mid]和a[mid...high]进行有序化合并。因为a[low...mid]和a[mid...high]本来就是有序的,而如果判断出a[mid]<=a[mid+1]的话, 那么直接就可以确保a[low]<a[low+1]...<a[mid]<=a[mid+1]<a[mid+2]...< a[high]已经有序,从而可跳过接下来的Merge过程。
    void merge_sort_up_to_down(int *a, int low, int high) {
        // 终止递归的条件
        if (low >= high) return;
        int mid = low + (high - low) / 2;
        merge_sort_up_to_down(a, low, mid);
        merge_sort_up_to_down(a, mid + 1, high);
        if(a[mid] <= a[mid+1]) return; // 避免不必要的合并调用
        merge(a, low, mid, high);  // 单次合并
      }

    优化三:去除原数组序列到辅助数组的拷贝

    每次调用Merge方法时候,我们都把a对应的序列拷贝到辅助数组aux中来:

    for (int p = low; p <= high; p++)
        aux[p] = a[p];
     
    实际上,这个拷贝的过程可以去掉。实现方式是,在递归调用的每个层次都进行输入数组和输出数组的角色交换,从而确保输入数组的有序性(即子序列分别有序)
    void merge_sort_up_to_down(int *a, int *aux, int low, int high) {
        if (low >= high) return;
        int mid = low + (high - low) / 2;
        merge_sort_up_to_down(aux, a, low, mid); //每层进行角色交换
        merge_sort_up_to_down(aux, a, mid + 1, high);
        my_merge(a, aux, low, mid, high);
        return;
    }
    
    void merge_sort(int *a, int len) {
        int *aux = (int *)malloc(sizeof(int) * len);
        for (int p = 0; p < len; p++) //只拷贝递归前的这一次
            aux[p] = a[p];
        merge_sort_up_to_down(a, aux, 0, len - 1);
        free(aux);
        return ;
    }

    【注意:因为最外部的 merge_sort 和 my_merge 的参数顺序是相同的, 所以,无论递归过程中拷贝数组和原数组的角色如何替换,对最后一次调用的my_merge而言(将整个数组左右半边合为有序的操作),最终被排为有序的一定是原数组,而不是拷贝数组!】 

    完整优化代码(C)

    #include <stdio.h>
    #include <stdlib.h>
    
    int *aux;
    
    void my_merge(int *a, int *aux, int low, int mid, int high) {
        int i = low, j = mid + 1, k = low;
        while (i <= mid && j <= high) {
            if (aux[i] <= aux[j]) {
                a[k++] = aux[i++];
            } else {
                a[k++] = aux[j++];
            }
        }
        while (i <= mid) {
            a[k++] = aux[i++];
        }
        while (j <= high) {
            a[k++] = aux[j++];
        }
        return;
    }
    
    void merge_sort_up_to_down(int *a, int *aux, int low, int high) {
        if (low + M>=high) { // 优化一:数组长度小于M的时候
            insert_sort(a, low, high); // 切换到插入排序
            return;
        }
        int mid = low + (high - low) / 2;
        merge_sort_up_to_down(aux, a, low, mid); //优化三:去掉递归内拷贝
        merge_sort_up_to_down(aux, a, mid + 1, high);
        if(a[mid] <= a[mid+1]) return; // 优化二:避免不必要的合并调用
        my_merge(a, aux, low, mid, high);  // 单次合并
        return;
    }
    
    void merge_sort(int *a, int len) {
        aux = (int *)malloc(sizeof(int) * len);
        for (int p = 0; p < len; p++)
            aux[p] = a[p];
        merge_sort_up_to_down(a, aux, 0, len - 1);
        free(aux);
        return ;
    }
    
    
    int main() {
        /* 归并排序(升序) */
        int num[10] = {5, 1, 8, 4, 7, 2, 3, 9, 0, 6};
        int length = sizeof(num) / sizeof(num[0]);
        merge_sort(num, length);
        for (int i = 0; i < length; i++)
            printf("%d ", num[i]);
        return 0;
    }
    归并排序(递归优化)

    迭代循环(自底向上)

    思路比较简单:

    完整代码(C)

    #include <stdio.h>
    #include <stdlib.h>
    #include <algorithm>
    
    int *aux;
    
    void my_merge(int *a, int low, int mid, int high) {
        int i = low, j = mid + 1, k = low;
        for (int p = low; p <= high; p++)
            aux[p] = a[p];
        while (i <= mid && j <= high) {
            if (aux[i] <= aux[j]) {
                a[k++] = aux[i++];
            } else {
                a[k++] = aux[j++];
            }
        }
        while (i <= mid) {
            a[k++] = aux[i++];
        }
        while (j <= high) {
            a[k++] = aux[j++];
        }
        return;
    }
    
    void merge_sort_down_to_up(int *a, int length) {
        aux = (int *)malloc(sizeof(int) * length);
        for (int size =1; size < length; size = size+size){
            for(int low = 0; low< length - size; low += size+size) {
                my_merge(a, low, low + size - 1, std::min(low + size + size - 1, length - 1)); //最后一个子序列可能不够一个size
            }
        }
        return;
    }
    
    int main() {
        /* 归并排序(升序) */
        int num[10] = {5, 1, 8, 4, 7, 2, 3, 9, 0, 6};
        int length = sizeof(num) / sizeof(num[0]);
        merge_sort_down_to_up(num, length);
        for (int i = 0; i < length; i++)
            printf("%d ", num[i]);
        return 0;
    }

    (整理自网络)

    参考资料:

    https://www.cnblogs.com/chengxiao/p/6194356.html

    Min是清明的茗
  • 相关阅读:
    那些年,学swift踩过的坑
    JAVA经BigDecimal圆角的解决方案及注意事项
    Jquery简介选择的
    Oracle性能优化顺序表名称来选择最有效的学习笔记
    jQuery Validate插入 reomte使用详细的说明
    公钥私人 ssh避password登陆
    Telnet,SSH1,SSH2,Telnet/SSL,Rlogin,Serial,TAPI,RAW
    mysqlbackup 还原特定的表
    OpenJDK 阅读源代码 Java 实现字节流输入类
    Android Application Thread CPU GC Operatiing and OOM Question 0603-随手笔记
  • 原文地址:https://www.cnblogs.com/MinPage/p/14103986.html
Copyright © 2011-2022 走看看