zoukankan      html  css  js  c++  java
  • 另辟蹊径的归并排序复杂度分析


    笔者,特别地对归并排序的 复杂度 进行了分析

    看了好多博客,只是简单的说了下结论,结论怎么来的,根本不去分析,写博客跟没写似的,,更气人的是,还有抄书的,书上写啥,博客就写啥,浪费时间,这种博客,写的人、看的人,时间都被浪费了;


    目录


    归并排序

    先了解下归并排序中归并的意思;

    归并:即将两个 有序 的数组,合并为一个更大的 有序 的数组 ;

    思想:要想将一个大数组排序,我们可以将大数组分为两个小的数组,对这个小的数组进行排序,然后将两个小的有序的数组,归并 到一个大的数组中;递归 这个过程,直到小的数组中元素 只有一个,然后进行归并;


    缺点(使用‘原地归并’解决)

    从上面的 思想 里面,我们可以知道,归并排序 是一个使用 递归 的排序算法;而说到递归,就会想到每次递归需要的 空间

    就像我们上面说的一样,每次归并两个小数组的时候,都创建一个新的数组(申请新的内存空间),将归并的结果放到里面;这样做,当需要进行排序的数组的长度,很小的时候,没有什么问题;

    但是,需要排序的数组的长度很大的时候,我们需要递归的次数,也就随之变多,这样一个递归排序需要的空间,就急剧增加,是我们不能忍受的 ;

    这里我们可以将使用的空间降低到最低,就是每次递归,进行归并两个小的数组的时候,我们并不创建新的数组,来存储归并的结果 ;而是在递归开始的时候,创建一个跟需要排序的数组一样大的数组,在每次递归的时候,先把两个小数组里面的元素,复制进这个大数组里面,而将归并的结果,放回小数组里面这样,就避免了,每次递归都创建数组的缺点

    但是,还是避免不了最少使用 N 个额外的数组空间 ;这里的原地,并不是真正的原地 ;还是需要最少 N 个额外空间 ;

    上述思想是一种原地排序 的思想;


    java代码实现

    归并排序有两种实现方式,这是其一:自顶向下的归并排序
    
    package cn.yaz.study.sort._2_2;
    
    import cn.yaz.study.sort._2_1.Example;
    import org.junit.Test;
    
    /**
     * 归并排序(自顶向下的归并排序)
     */
    public class Merge {
    
        @Test
        public void test(){
            Integer[] a = {3, 2, 1, 6, 34, 2, 12, 55, 334, 56, 2, 78,12,45,25};
            sort(a);
            Example.show(a);
        }
    
    
        //    辅助数组。所有归并需要的暂用空间,由它提供
        public Comparable[] c;
    
        public void sort(Comparable[] comparable) {
    //        创建辅助数组
            c = new Comparable[comparable.length];
    
    //        进行归并排序
            sort(comparable, 0, comparable.length - 1);
        }
    
        /**
         * 归并排序的递归代码
         */
        public void sort(Comparable[] comparables, int lo, int hi) {
    //        先判断小数组的长度
            if (lo >= hi) {
                return;
            }
    
    //        继续递归,将每一个小数组,再次一分为二
    //        将左半边数组排序
    
            sort(comparables, lo, (lo + hi) / 2);
    
    //        将右半边数组排序
            sort(comparables, (lo + hi) / 2 + 1, hi);
    
    //        小数组各自排序完毕。进行归并
            merge(comparables, lo, hi);
    
        }
    
        /**
         * 归并代码
         */
        public void merge(Comparable[] comparables, int lo, int hi) {
    //        先将需要归并的小数组,复制到辅助数组里面
            for (int i = lo; i <= hi; i++) {
                c[i] = comparables[i];
            }
    
    //        右半边数组的起始角标
            int rStart = (lo + hi) / 2 + 1;
    //        记录下这个其实角标
            int mid = rStart;
    //        因为下面我们需要单独操作数组c、comparables ;因此,我们再准备一套角标
    //        lo、hi 操作 comparables数组; i、j 操作 c 数组
            int i = lo;
            int j = hi;
    
    
    //        进行归并
            while (lo <= hi) {
    
    //            需要重点注意的事情:下面的if语句的顺序,是不可以随便写的
    //            也就是判断小数组是个归并结束的代码,要放在数组元素比较之前;
    
    //            表示左半个数组,归并完事了,可以将右边数组剩下元素,直接插入了
               if (i == mid) {
                    comparables[lo++] = c[rStart++];
    //                剩下的最后一种情况,就是右半个数组,归并完事了,可以将左边数组剩下元素,直接插入了
                } else if (rStart == j + 1) {
                    comparables[lo++] = c[i++];
                }
    //          如果 c[i] < c[rStart],小的元素,归并进 comparables 数组中
                else if (Example.less(c[i], c[rStart])) {
    //                赋值的同时,完成 i 指向下一个数组元素
                    comparables[lo++] = c[i++];
                }
    //          如果 c[i] > c[rStart],小的元素,归并进 comparables 数组中
                else if (!Example.less(c[i], c[rStart])) {
    //                赋值的同时,完成 i 指向下一个数组元素
                    comparables[lo++] = c[rStart++];
                }
    
            }
    
        }
    
    }

    Example类,请见 ;


    复杂度分析

    我看了好多博客,根本不对复杂度进行分析,只是直接写出结论,没意思;
    

    先分析 比较次数

    递归树

    这里写图片描述

    (K从0开始计数)第K层,有 2k 个子数组 ;每个子数组有 2nk 个元素,对于有 2nk 个元素的数组,最多需要比较 2nk 次;第 K 层,需要 2k x 2nk = 2n 次比较;( n = log2N N是数组元素的总数,因此,这里也就是 N 次比较 ;)

    这里说的,都是书上的话,看了也不定记得!反正笔者是没记住

    擦!正在我苦苦思索,怎么用语言把比较次数直白的简单的容易记住的 描述出来的时候,我突然发现,递归树,每层元素都是一样多的,都是N个元素,而我们为N个元素进行排序的时候,最多需要N次比较;so,只要知道递归树有多少层就可以求出一共比较了多少次了;很容易推导出,N 个元素的数组的 二分递归树 一共有 log2N 层(根节点除外,学过高中数学就可以推导出

    因此一共比较了 N x log2N = Nlog2N 次;

    上述说的比较次数是最坏情况下,因为,数组的个数是2的幂,此时递归树是一个完全二叉树

    假如,数组的不是2的幂,则 递归树,将不是 一个完全二叉树,最后一层的元素,就将比 N 少,因此,最后一层的比较次数,比 N 少;

    因此,总体上是 小于 Nlog2N


    再分析 访问数组次数

    我另辟蹊径还是从,递归树,每层元素都是一样多的,都是N个元素,这个角度入手

    访问数组的操作,只发生在 merge( ) 方法里面,而递归树的每层的每一个元素,都是要经过 merge( ) 方法的,在 merge( ) 方法里面,N 个元素,需要 N 次 复制,N 次赋值,最多需要 N 次比较,我们在 merge( ) ,一共有 两个数组 ,每次访问都是一起访问,因此,访问数组 3N x 2 = 6N 次,而我们一共有 log2N 层;

    因此,一共访问 6Nlog2N 次数组 ;


    master定理

    对于这种递归的复杂度,找不到合理的方法,干推,太闹心了,如果不想推导,可以直接使用 master定理

    • master定理

      递归关系: T(n) = a*T(n/b)+c*n^k ;T(1) = c ;

      有这样的结论:

      if (a > b^k) —— T(n) = O(n^(logb(a)))

      if (a = b^k) —— T(n) = O(n^k*logn)

      if (a < b^k) —— T(n) = O(n^k)


            sort(comparables, lo, (lo + hi) / 2);   //
    
            sort(comparables, (lo + hi) / 2 + 1, hi);
    
            merge(comparables, lo, hi);
    

    我们写的代码的 递归状态方程:这里又需要写出 递归状态方程 。。。。。不写了!(感兴趣的同学,自己去看专门介绍 master定理 的博客,这里我还是重点讲 归并排序 的)


    改进

    算法第四版的课后作业,有下面的题,我会把代码放到我写的《算法第四版-课后习题答案》,里面,需要的同学,可以去查看下 ;

    我们可以对上述的代码,进行改进,提高归并排序的性能 ;

    • 对小规模数组使用 插入排序

      理由:当我们进行递归二数组的时候,如果当小数组的长度,已经不是很大的时候,依然使用递归继续二分的话,那么,我们将频繁的调用 merge() 方法;

      这也是递归固有的通病:当 子问题 规模很小的时候,,递归会使得调用过于频繁 ;

      因此,我们可以考虑,当小数组的长度小于一定长度(书上建议为15)的时候,使用 插入排序,来对小数组进行排序,可以提高 插入排序 的性能 ;

      因为 插入排序小规模乱序数组满足插入排序适用场景之一:每个元素距离它最终的位置都不远; 排序很快;

    • 每次归并之前,测试两个小数组,是否有序

      理由:如果两个小数组满足 [mid] < [mid+1] ,也就是说,左边的小数组的最后一个元素,小于右边小数组的第一个元素;那么这两个小数组,是可以直接归并进大数组里面的,这样就省却了每次都比较的开销 ;

    • 每次归并的时候,不将数组复制到辅助数组里面

      理由: 我们可以不用把数组复制到辅助数组里面,再排序回原数组中 ;其实,可以让辅助数组、原数组的角色,来回交替变化,达到完成排序,而不复制的效果 ;

    上述改动,均可见代码:


    归并排序的意义

    我们前面学过的排序算法,复杂度都是指数级别的,今天学的归并排序,将复杂度降低到了,对数级别;这是一个很有意义的算法,让我们在对非常大多的数据量进行排序,变为可能;

    但是,归并排序并不是完美的,它有一个缺点,就是需要 N 个额外空间的辅助数组,来辅助排序,并不是原地排序 ;

    下次讲的 快速排序,则可以克服掉 归并排序 的 缺点 ;

  • 相关阅读:
    让程序调用运行linux shell命令
    纯C的字符串问题
    Linux的打包和解压缩命令
    ubuntu安装mosquitto-1.4.5
    无Teamview授权,使用Teamview方式
    有效利用家用宽带,动态域名服务(DDNS)
    pfx格式证书转成nginx可用的证书
    iis文件上传限制
    vue脚手架使用
    netcore中执行linux命令
  • 原文地址:https://www.cnblogs.com/young-youth/p/11665725.html
Copyright © 2011-2022 走看看