复杂度分析

  同一个问题可以使用不同的算法解决,那么不同的算法孰优孰劣如何区分呢?因此我们需要一个表示方法来代表每个程序的效率。

 

  衡量一个程序好坏的标准,一般是运行时间与占用内存两个指标。

  不过我们在写代码的时候肯定无法去估量程序的执行时间,因为真实的执行时间受到多方面因素的影响,比如同样一段程序,放在高配服务器上跑和放在低配服务器上跑完全是两个表现效果,比如遍历一个数组的函数,执行时间完全取决于调用函数传入的数组大小。

 

  如何在不运行程序的情况下,判断出代码的执行时间呢?显然是不可能的。

 

  不过我们虽然无法预估代码的绝对执行时间,但是我们可以预估代码基本的执行次数。

  一段代码的执行时间如果有变化,则一定是受到外部输入的数据所影响,我们将代码中所有不变的因素,表示为大O,将变化的因素作为基数n,表示为:O(n),大O的意思是忽略重要项以外的内容,我们常以这种大O表示法来判断比较各种算法的执行效率。

 

  接下来我会介绍几种常用的复杂度表示方法。

 

  PS:专业的解释必然全篇都是数学证明,未免太过于复杂,让学者更加迷茫,我这里写的并不是教材,而是最直白的理解。

 

时间复杂度

  本节中例举的各种时间复杂度以好到差依次排序。

常数时间 O(1)

  先看下这个函数:

    private static void test(int n)
    {
        int a = 2;
        int b = 3;
        int c = a+b;
        System.out.println(c);
    }

  一共4行代码,CPU要将a的值写入内存,b的值写入内存,a和b进行计算,将计算结果写入c,最后将c输出到控制台。

  尽管计算机内部要做这么多事情,这段代码的时间复杂度依然是O(1),原因是这几行代码所做的操作是固定的,是不变的因素。

 

  再看下这个函数:

    private static void test(int n)
    {
        for (int i=0;i<100000;i++){
            System.out.println(i);
        }
    }

  循环10W次,可能你觉得功耗可能有点大,不过它的时间复杂度仍然是O(1)。

 

  我们可以这么固定的认为:无论接收的参数怎么变化,只要代码执行次数是无变化的,则用1来表示。 凡是O(1)复杂度的代码,通常代表着它是效率最优的方案。

 

对数时间 O(log n)

  普遍性的说法是复杂度减半,就像纸张对折。

  示例代码:

    private static void test(int n)
    {
        for (int j=1;j<=n;j=j*2){
            System.out.println(j);
        }
    }

  这段代码的执行效果并非是一次折半,它是次次折半,以2为底,不断的进行幂运算,实际上只要有幂指数关系的,不管你的底数是几,只要能够对原复杂度进行求幂逆运算我们都可以称之为O(log n)

  比如:

    private static void test(int n)
    {
        for (int j=1;j<=n;j=j*3){
            System.out.println(j);
        }
    }

  在忽略系数、常数、底数之后,最后都可以表示为O(log n),只不过我们遇到的算法几乎不会出现一些极端例外情况,对数时间的所在地常见以二分查找为代表。

 

线性时间 O(n)

  我们将test方法稍稍修改一下:

    private static void test(int n)
    {
        for (int i=0;i<n;i++){
            System.out.println(i);
        }
    }

  修改之后这次不是执行10W次,而是执行n次,n是由参数传入的一个未知值,在没有真实运行的时候我们无法判断这个n到底是多少?因为它可以是任意int型数字,你可以这么认为:在理想的情况下,它的复杂度是O(1),在恶劣的情况下,它的复杂度是无限大。完全取决于方法调用方。

  直白的说,for循环就是循环n次,因此这段代码的时间复杂度为O(n),这种复杂度常常表现为线性查找。

 

线性对数时间 O(n log n)

  线性对数时间也就是线性时间嵌套对数时间:

    private static void t(int n){
        for (int i=0;i<n;i++){
            test(n);
        }
    }
    private static void test(int n)
    {
        for (int j=1;j<=n;j=j*2){
            System.out.println(j);
        }
    }

  t这个方法的时间复杂度就是O(n log n)

 

平方时间 O(n^2)

  平方时间就是执行程序需要的步骤数是输入参数的平方,最常见的是嵌套循环:

    private static void test(int n)
    {
        for (int i=0;i<n;i++){
            for (int j=n;j>0;j--){
                System.out.println(j);
            }
        }
    }

 

其他时间

  比O(n^2)还要慢的自然有立方级O(n^3)

  比O(n^3)更慢慢的还有指数级O(2^n)

  慢到运行一次程序要绕地球三百圈的有O(n!)

 

  正常情况下我们不会接触到这些类型的算法。

 

空间复杂度

  所谓空间,就是程序运行占用的内存空间,空间复杂度指的就是执行算法的空间成本。

 

  这里我们抛一道题来做例子:在一个数组中找出有重复的值,如数组[3,8,13,7,15,8,6,6] 找出8和6

 

  解法:

    private static void test(int[] arr)
    {
        for (int i=0;i<arr.length;i++){
            for(int j=0;j<i;j++){
                if(arr[j] == arr[i]){
                    System.out.println("找到了:"+arr[i]);
                }
            }
        }
    }

  很显然:时间复杂度为O(n^2)。

 

  那我们还可以使用一种更优的解法:

    private static void test(int[] arr)
    {
        HashSet hashSet = new HashSet();
        for (int i=0;i<arr.length;i++){
            if(hashSet.contains(arr[i])){
                System.out.println("找到了:"+arr[i]);
            }
            hashSet.add(arr[i]);
        }
    }

  也许你会惊讶的发现,时间复杂度被优化成了O(n)。

 

  虽然时间复杂度降低成了O(n),但是付出的代价是空间复杂度变成了O(n),因为新的解法使用了一个HashSet来存储数据,存储数据自然要占用内存空间,而占用的空间大小完全取决于传入数组大小。

  我们之所以说第二种解法更优,其实是一种常规思想,因为现实中绝大部分情况,时间复杂度显然比空间复杂度更为重要,我们宁愿多分配一些存储空间作为代价,来提升程序的执行速度。

 

  总而言之,比较两个算法优劣的指标有两个,时间复杂度与空间复杂度,优先比较时间复杂度,时间复杂度相同的情况下比较空间复杂度。

 

  最后:感谢阅读。

posted @ 2019-05-24 14:32 不该相遇在秋天 阅读(...) 评论(...) 编辑 收藏