• 复杂度分析


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

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

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

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

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

      一段代码的执行时间如果有变化,则一定是受到外部输入的数据所影响,我们将代码中所有不变的因素,表示为大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来存储数据,存储数据自然要占用内存空间,而占用的空间大小完全取决于传入数组大小。

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

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

      最后:感谢阅读。

  • 相关阅读:
    BZOJ2762: [JLOI2011]不等式组
    BZOJ1452: [JSOI2009]Count
    Codeforces Round #441 (Div. 2, by Moscow Team Olympiad)
    BZOJ1635: [Usaco2007 Jan]Tallest Cow 最高的牛
    BZOJ2730: [HNOI2012]矿场搭建
    Flask实现异步非阻塞请求功能
    在flask中使用websocket-实时消息推送
    Python数据库连接池DBUtils
    flask请求上下文
    scanf与getchar
  • 原文地址:https://www.cnblogs.com/fengyumeng/p/10918037.html
Copyright © 2020-2023  润新知