zoukankan      html  css  js  c++  java
  • 排序算法之归并排序

            前面几篇介绍的选择排序、插入排序、冒泡排序等都是非常简单非常基础的排序算法,都是用了两个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++];
            }
        }

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

            自顶向下归并

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

            自顶向下归并完整代码:

        //归并排序(递归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

            工作、学习、交流或有任何疑问,请联系邮箱:yy1340128046@163.com

  • 相关阅读:
    permission 文档 翻译 运行时权限
    TabLayout ViewPager Fragment 简介 案例 MD
    Log 日志工具类 保存到文件 MD
    OkHttp 官方wiki 翻译 MD
    Okhttp 简介 示例 MD
    OkHttp 官方Wiki之【使用案例】
    DialogPlus
    倒计时 总结 Timer Handler CountDownTimer RxJava MD
    RecyclerView 判断滑到底部 顶部 预加载 更多 分页 MD
    CSS3的媒体查询(Media Queries)与移动设备显示尺寸大全
  • 原文地址:https://www.cnblogs.com/Y-oung/p/8964964.html
Copyright © 2011-2022 走看看