排序算法之归并排序

        前面几篇介绍的选择排序、插入排序、冒泡排序等都是非常简单非常基础的排序算法,都是用了两个for循环,时间复杂度是平方级别的。本篇介绍一个比前面稍微复杂一点的算法:归并排序。归并排序算法里面的归并思想和递归方法是值得我们学习的,归并的过程往往伴随着递归,其他很多地方都会用这两种方法,比如前面一篇《剑指offer题目系列三》中第12题“合并两个排序的链表”就用到这两种思想方法。

        归并的过程

        对于两个独立的数组来说,是将两个有序的数组合并到一个数组中,使合并后的数组依然有序。对于一个数组来说,可以先将其划分为两部分,先使其各部分都有序,然后合并成一个有序数组。具体操作时,先定义两个指针,分别指向两个数组中的元素,用于遍历数组,然后新建一个数组用于存储合并后的元素。

        归并排序中,假设p、q、mid分别指向数组arr[]的第一个元素、最后一个元素、中间元素的索引位置,将数组arr[]划分成两半:arr[p~mid]、arr[mid+1~q],然后将两个子数组中的元素归并。还可以将两个子数组再次划分为更小的子数组,归并更小的子数组……以此类推,直到子数组长度为1,然后依次归并。归并时,有4个判定条件:如果左半块元素遍历完毕,则直接将右半块剩余元素放入数组中;如果右半块元素遍历完毕,则直接将左半块剩余元素放入数组中;如果左半块当前元素小于右半块当前元素,则左半块当前元素放入数组;反之,右半块当前元素放入数组。

        下面以长度为8的数组为例,说明归并的具体过程。设原数组为int arr[] = {1,3,5,7,2,4,6,8};,新建一个辅助数组aux[]用于临时存储数组中的元素,先将原数组中的元素复制到辅助数组中,再把归并的结果放回原数组中。初始i、j分别指向辅助数组前半部分、后半部分子数组的第一个元素位置,然后慢慢移动遍历两个数组。红色元素代表每一趟 i、j 两个指针指向的两个子数组的元素位置,灰色元素代表已遍历完的元素,黑色加粗元素代表还未遍历的元素。

                

        归并过程的代码:

    public static void merge(int[] arr,int[] aux,int p,int mid,int q){
        for(int k=p;k<=q;k++){  //先复制到辅助数组中
            aux[k] = arr[k];
        }
        int i=p,j=mid+1;  //i、j指向辅助数组左右半块指针,从起始位置开始
        for(int k=p;k<=q;k++){  //k指向原数组arr[],根据i、j指针位置判断左右半块是否遍历完
            if(i > mid)  arr[k] = aux[j++];  //左半块遍历完
            else if(j>q) arr[k] = aux[i++];  //右半块遍历完
            else if(aux[j]>aux[i]) arr[k] = aux[i++];
            else arr[k] = aux[j++];
        }
    }

        下面介绍递归排序的两种方式:自顶向下归并排序和自底向上归并排序,两种方式都会用到上面的归并代码。

        自顶向下归并

        自顶向下归并是一种基于递归方式的归并,也是算法设计中“分治思想”的典型用法。它将一个大问题分割成一个个小问题,分别解决小问题,然后用所有小问题的答案来解决整个大问题。如果能将两个子数组排序,就能通过归并两个子数组使整个数组排序。自顶向下归并每次先将数组的左半部分排序,然后将右半部分排序,通过归并左右两部分使整个数组排序。详细过程见下面代码注释。

        自顶向下归并完整代码:

扫描二维码关注公众号,回复: 443574 查看本文章
    //归并排序(递归Recursion,自顶向下)
    public static void sort(int[] arr){  //本方法只会执行一次,下面两个方法执行多次
        if(arr == null) return;
        int[] aux = new int[arr.length];  //辅助数组
        sort(arr,aux,0,arr.length-1);
    }
    public static void sort(int[] arr,int[] aux,int p,int q){
        if(p>=q) return;
        int mid = (p+q)>>1;
        sort(arr,aux,p,mid);  //左半块归并
        sort(arr,aux,mid+1,q);  //右半块归并
        merge(arr,aux,p,mid,q);  //归并详细过程
    }
    public static void merge(int[] arr,int[] aux,int p,int mid,int q){
        for(int k=p;k<=q;k++){  //先复制到辅助数组中
            aux[k] = arr[k];
        }
        int i=p,j=mid+1;  //i、j指向辅助数组左右半块指针,从起始位置开始
        for(int k=p;k<=q;k++){  //k指向原数组arr[],根据i、j指针位置判断左右半块是否遍历完
            if(i > mid)  arr[k] = aux[j++];  //左半块遍历完
            else if(j>q) arr[k] = aux[i++];  //右半块遍历完
            else if(aux[j]>aux[i]) arr[k] = aux[i++];
            else arr[k] = aux[j++];
        }
    }

        自底向上归并

        上面自顶向下归并是一种基于递归方式的归并,解决大数组排序问题时很好用。实际上我们平时遇到的多数是小数组,所以自底向上归并是先归并那些微小数组,然后再成对归并这些小数组,以此类推,直到将整个数组归并在一起。首先我们进行的是两两归并,然后是四四归并,然后是八八归并,一直进行下去。每趟最后一次归并的第二个子数组长度可能比第一个子数组长度小,其余情况两个子数组长度应该相等,每趟子数组长度翻倍。详细过程见下面代码注释。

        自底向上归并完整代码:

    //非递归方式
    public static void sortNotRecursion(int[] arr){
        if(arr == null) return;
        int[] aux = new int[arr.length];
        for(int i=1;i<arr.length;i*=2){  //p-q+1=2*i:即子数组长度为2*i,i为子数组半长,每趟i翻倍
            for(int j=0;j<arr.length-i;j+=i*2){  //j:子数组起始位置
                int p = j;  //子数组头指针
                int q = Math.min(j+i*2-1,arr.length-1);  //子数组尾指针,取两者最小值仅仅是因为每一趟最后的子数组长度可能小于2*i,最后位置指针j+i*2-1的值可能会超过数组最大索引,此时取最大索引arr.length-1
                int mid = j+i-1;  //中间位置。注意不能用(p+q)>>1,因为每一趟最后的子数组长度可能小于2*i,q的位置可能是arr.length-1。
                merge(arr,aux,p,mid,q);  //每一趟最后一个子数组只有长度大于i时才会进行归并操作,小于或等于i则不进行,由j<arr.length-i控制
            }
        }
    }
    public static void merge(int[] arr,int[] aux,int p,int mid,int q){
        for(int k=p;k<=q;k++){  //先复制到辅助数组中
            aux[k] = arr[k];
        }
        int i=p,j=mid+1;  //i、j指向辅助数组左右半块指针,从起始位置开始
        for(int k=p;k<=q;k++){  //k指向原数组arr[],根据i、j指针位置判断左右半块是否遍历完
            if(i > mid)  arr[k] = aux[j++];  //左半块遍历完
            else if(j>q) arr[k] = aux[i++];  //右半块遍历完
            else if(aux[j]>aux[i]) arr[k] = aux[i++];
            else arr[k] = aux[j++];
        }
    }

        归并排序是一种稳定的排序算法,但它不是原地归并,而是需要一个辅助数组。归并排序的时间复杂度为O(NlogN),空间复杂度为O(N)。

        转载请注明出处 http://www.cnblogs.com/Y-oung/p/8964964.html

        工作、学习、交流或有任何疑问,请联系邮箱:[email protected]

猜你喜欢

转载自www.cnblogs.com/Y-oung/p/8964964.html