大 O 复杂度表示法

算法是啥?

很多人一看到算法两个字就头疼,觉得算法是高深莫测的,觉得算法是非常难的,实际不然,算法其实就是解决一定的问题的程序代码,这里有段非常简单的代码,求 1,2,3…n 的累加和 :


int cal(int n) {
    int sum = 0;
    int i = 1;
    for (; i <= n; ++i) {
        sum = sum + i;
    }
    return sum;
}

那么,以上的一段程序代码我们也可以称为一个算法,这是一个非常简单的算法,当然,算法也是可以非常复杂的,比如以后我们会碰到的一些动态规划算法、还有机器学习的算法,这些算法相对比较复杂的。

所以说,算法是有简单的,也有复杂的。还是那句话:算法其实就是解决一定的问题的程序代码。

算法的执行效率
既然算法是解决一定问题的程序代码,那么在执行这个程序代码时就需要时间,通过执行这个程序代码的执行时间可以评估对应算法的执行效率。

我们还是以上面的求 1,2,3…n 的累加和这个算法来举例:

int cal(int n) {
    int sum = 0;
    int i = 1;
    for (; i <= n; ++i) {
        sum = sum + i;
    }
    return sum;
}

我们可以通过下面的方法来计算执行上面的算法所花的时间:

long startTime = System.currentTimeMillis();
cal(100);
System.out.println("花的时间:" + (startTime - System.currentTimeMillis()));

执行上面程序,打印出来的就是使用 cal 这个算法计算出 1 到 100 的累加值所花的时间。

虽然我们可以通过上面的方式来计算一个算法的执行时间,但是上面的方式有两个缺点:

在不同机器环境中执行上面的算法花的时间是不同的,比如在我的机器上执行 cal(100) 可能只需要话几毫秒,可能在其他电脑机器上执行的话就需要几十毫秒了。所以说上面计算执行时间非常的依赖测试环境。
假设在我的机器上,计算 cal(100) 可能需要花几毫秒,但是计算 cal(10000) 就需要几十毫秒,而计算 cal(100000) 需要的时间可能就更多了。所以说上面计算执行时间受数据规模的影响很大。
所以说,通过上面的方式来分析一个算法的执行效率的话,既非常的依赖测试环境,又受数据规模的影响很大。而且这种方式还被我们称为事后分析法,就是说写完了代码再通过跑一遍代码来确定算法的执行时间,这种方式是正确的,但是有很大的局限性(上面的两大缺点)。

所以,我们需要一个不用具体的测试数据来测试,就可以粗略地估计一个算法的执行效率的方法,这就是我们接下来要讲的时间、空间复杂度分析方法。

大 O 复杂度表示法
算法的执行效率,粗略的讲,就是算法代码执行的时间。但是,我们怎么样在不运行代码的情况下,用 “肉眼” 得到一段代码的执行时间呢?

扫描二维码关注公众号,回复: 9619060 查看本文章

我们还是以上面的求 1,2,3…n 的累加和这个算法来举例:


int cal(int n) {
    int sum = 0;            // (1)
    int i = 1;              // (2)
    for (; i <= n; ++i) {   // (3)
        sum = sum + i;      // (4)
    }                       
    return sum;             // (5)
}

从 CPU 的角度来看,这段代码的每一行都执行着类似的操作:读数据 - 运算 - 写数据。尽管每行代码对应的 CPU 执行的个数、执行的时间都不一样,但是,我们这里只是粗略估计,所以我们可以假设每行代码执行的时间都一样,为一个单元时间(unit_time)。那么在这个假设的基础之上,这段代码总执行时间是多少呢?

第 (1)、(2) 行代码分别需要 1 个 unit_time 的执行时间,第 (4)、(5) 行都需要运行可 n 遍,所以需要 2n*unit_time 的执行时间,所以这段代码总的执行时间就是 (2n+2)*unit_time。所以,所有代码的执行时间与每行代码的执行次数成正比。

假设,上面所有代码的执行时间使用 T(n) 表示,那么对于上面的程序代码执行的时间就会有如下的一个公式:

T(n) = (2n+2)*unit_time
按照上面的思路,我们再来看一段简单的代码:


int cal(int n) {
    int sum = 0;                // (1)
    int i = 1;                  // (2)
    int j = 1;                  // (3)
    for (; i <= n; ++i) {       // (4)
        j = 1;                  // (5)
        for (; j <= n; ++j) {   // (6)
            sum = sum + i * j;  // (7)
        }
    }
    return sum;                 // (8)
}

还是假设么个语句的执行时间是 unit_time,那么这段代码的总执行时间 T(n) 是多少呢?

第 (1)、(2)、(3) 行代码都分别需要 1 个 unit_time 的执行时间,第 (4)、(5) 行代码循环执行了 n 次,所以需要 2n * unit_time 的执行时间。第 (6)、(7) 行代码循环执行了 n^2 次,所以需要 2(n^2) * unit_time 的执行时间。所以,整段代码总的执行时间 T(n) = (2(n^2) + 2n + 3) * unit_time。

注意:n^2 表示 n 的 2 次方

尽管我们不知道 unit_time 的具体值,但是通过这两段代码执行时间的推导过程,我们可以得到一个非常重要的规律,那就是:所有代码的执行时间 T(n) 与每行代码的执行次数 n 成正比。

我们可以将这个规律使用大 O 来表示:

T(n) = O(f(n))
我们对上面的公式做一个解释。其中,T(n) 表示代码执行的时间;n 表示数据规模的大小;f(n) 表示每行代码执行的次数总和,因为它是一个公式,所以用 f(n) 来表示。公式中的 O,表示代码的执行时间 T(n) 与 f(n) 表达式成正比。

所以,以上:

第一个例子中的 T(n) = O(2n + 2)
第二个例子中的 T(n) = O(2(n^2) + 2n + 3)
这就是大 O 时间复杂度表示法。大 O 时间复杂度实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势,所以,也叫做渐进时间复杂度,简称时间复杂度

当 n 很大时,你可以把它想象成 10000、1000000。而公式中的低阶、常量、系数三部分并不左右增长趋势,所以都可以忽略。我们只需要记录一个最大量级就可以了,如果用大 O 表示法表示刚讲的那两段代码的时间复杂度,就可以记为:T(n) = O(n);T(n) = O(n^2)。

比如一个表达式:2(n^2) + 2n + 3,其中 2n 是低阶,3 是常量、2(n^2) 前面的 2 就是系数了。

> 來自日常的学习记录总结

发布了4 篇原创文章 · 获赞 4 · 访问量 230

猜你喜欢

转载自blog.csdn.net/qq_38326500/article/details/104692621
今日推荐