写在前面
FlyPython推出的《Python面试专项课程》原计划在春节假期后开始更新,考虑到后面刷题的需求可能增多,我们对题目进行了调整并于今天开始更新。
+原先的LeetCode刷题计划中的精选TOP面试题推后,我们先过一遍《程序员面试金典》这本书上的面试题,为基础薄弱的同学夯实基础。
+-
+
- 网址:https://leetcode-cn.com/problemset/lcci/ +
- 总题数: 97 +
欢迎关注我们的公众号flypython和网站flypython.com,我们将持续更新这个专项课程。
+为什么需要复杂度分析
今天是这个专项课程的第0课,我们带来算法学习最重要的一个部分:复杂度分析。
+为什么说复杂度分析是最重要呢?因为数据结构和算法其实解决的是”快”和”省”的问题。”快”就是如何让代码运行得更快,”省”就是让代码节约存储空间。怎么样来衡量你编写的算法代码执行效率呢?这里引出来今天要讲的内容:时间、空间复杂度分析。
+你可能会说,不就是执行效率么?我把代码跑一遍,增加cProfile的一些操作,不就得到了执行时间和内存占用么。
+其实你上面说的也是一种方法,叫事后统计法,在做性能优化时会大量采用。但是事后统计法有它的局限性。第一,它依赖测试环境,测试环境不一样,得到的数据会都不一样;第二,它受数据规模的影响大,比如排序,排10个数和排100万个数肯定不一样。所以我们不能以具体测试数据来表示,需要的是一个估算的执行效率的方法。这个方法就是我们要讲的大O复杂度表示法。
+大O复杂度表示法
我们从一个例子开始,下面是求1,2,3,…n的累加和的代码
+1 | 1 int cal(int n) { |
我们假设每一行代码的执行时间是相同的,为unit_time,那么这一段代码,第2,3行执行了一次,4,5行执行了n遍,第7行执行了一次,那总共执行的次数2n+3,执行时间为(2n+3)* unit_time。
+可以看出,所有代码的执行时间 T(n) 与每行代码的执行次数成正比。可以用以下公式表示:
+
其中T(n)为所有代码的执行时间
其中f(n)表示每行代码执行的次数总和
公式中的O,表示代码的执行时间 T(n) 与 f(n) 表达式成正比。我们的例子中,T(n)=O(2n+3),这就是大O时间复杂度表示法。
+大 O 时间复杂度实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势,所以,也叫作渐进时间复杂度(asymptotic time complexity),简称时间复杂度。
+时间复杂度分析
-
+
- O(1): 常数 +
- O(logn): 对数 +
- O(n): 线性 +
- O(nlogn):线性对数 +
- O(n^2): 平⽅ +
- O(n^3): 立⽅ +
- O(n^k): K次方 +
- O(2^n): 指数 +
- O(n!): 阶乘 +
O(1)
1 | int a=0; |
O(1) 只是常量级时间复杂度的一种表示方法,并不是指只执行了一行代码。只要代码的执行时间不随 n 的增大而增长,这样代码的时间复杂度我们都记作 O(1)。
+O(log n)
1 | for(int i=1; i<n; i=i*2){ |
对数阶时间复杂度非常常见,同时也是最难分析的一种时间复杂度。从代码中可以看出,变量 i 的值从 1 开始取,每循环一次就乘以 2。当大于 n 时,循环结束。还记得我们高中学过的等比数列吗?实际上,变量 i 的取值就是一个等比数列
+
通过 2^x = n 求解 x, x=log2n,这段代码的时间复杂度就是 O(log2n),忽略系数,表示为O(logn)。
+O(n)
1 | for(int i=1; i<=n; i++){ |
for循环代码执行了n遍,因此它消耗的时间是随着n的变化而变化的,所以通过O(n)来表示。如果平行存在一个for循环m遍的,如下:
+1 | for(int i=1; i<=n; i++){ |
那时间复杂度为O(m+n)。
+O(nlogn)
1 | for(int i=1; i<=n; i++){ |
线性对数O(nlogn)就是将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是n* O(logn),也就是了O(nlogn)
+O(n^2)
1 | for(int i=1; i<=n; i++){ |
平方就是O(n)代码再嵌套循环一遍,2层n循环,O(n * n) 为O(n^2)。如果嵌套3层n循环,则为O(n^3),如果嵌套k层,则为O(n^k)。
+如果其中一层循环中的n改为m,如下所示:
+1 | for(int i=1; i<=m; i++){ |
那它的时间复杂度为O(m * n)
+O(2^n)和O(n!)
1 | for(int i=1; i<=math.pow(2,n); i++){ |
1 | for(int i=1; i<=factorial(n); i++){ |
以上两个代码例子分别为O(2^n)复杂度和O(n!)复杂度,指数复杂度和阶乘复杂度属于非多项式量级,我们把时间复杂度为非多项式量级的算法问题叫作 NP(Non-Deterministic Polynomial,非确定多项式)问题。
+当数据规模 n 越来越大时,非多项式量级算法的执行时间会急剧增加,求解问题的执行时间会无限增长。所以,非多项式时间复杂度的算法其实是非常低效的算法。
+具体增长曲线如下图所示:
+
空间复杂度分析
-
+
- O(1) +
- O(n) +
- O(n²) +
O(1)
1 | int i=0; |
代码中的i,j,m不随着处理数据量变化而变化,算法需要的临时空间是固定的,可以表示为空间复杂度O(1)
+O(n)
1 | int[] m = new int[n]; |
这段代码中,第一行new了一个数组出来,这个数据占用的大小为n,这段代码的2-5行,虽然有循环,但没有再分配新的空间,所以这段代码的空间复杂度为O(n)
+O(n^2)
平方空间复杂度与O(n)类似,new出来的数组如果是n * n的多维数组,那就是O(n^2)复杂度,如果是m * n的多维数组,那就是O(m * n)复杂度。
+1 | int[][] a = new int[n][n]; |
总结
今天我们学习了复杂度分析的相关基础知识,包括了时间复杂度和空间复杂度,主要是用来分析算法执行效率与数据规模之间的增长关系。
+常见的复杂度,从低阶到高阶有:O(1)、O(logn)、O(n)、O(nlogn)、O(n^2),几乎包括了后面学习的所有数据结构和算法的复杂度,希望大家可以好好的掌握。
+
时间复杂度分析还有最好,最坏,平均,均摊时间复杂度等话题需要好好讨论,请关注后续文章,我们会在刷题过程中穿插讲解。
+参考资料
-
+
- analysis_of_algorithms: https://en.wikipedia.org/wiki/Master_theorem_(analysis_of_algorithms) +
+