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

  • 相关阅读:
    System.Web.HtppRuntime.cs
    Code:Tree
    Code-Helper-Web:CacheHelper.cs
    Code-Helper-Web:CookieHelper.cs
    ORACLE数据库常用查询二
    涂抹Oracle笔记2:数据库的连接-启动-关闭
    windows下常用的操作命令及dos命令
    涂抹Oracle笔记1-创建数据库及配置监听程序
    表空间满处理方法
    SQL内连接-外连接join,left join,right join,full join
  • 原文地址:https://www.cnblogs.com/Y-oung/p/8964964.html
Copyright © 2011-2022 走看看