虽然现在已经有很多框架可以使用,但是背后的原理不懂,如何取舍选择哪种框架?
代码的可读性、健壮性,还是扩展性固然重要,但我们至少要学会评估代码的性能和资源的消耗。
虽然现在在学校做科研主要最求的是功能的实现,但到了公司,面对千万级甚至亿级的用户,开发的是 TB、PB 级别数据的处理系统,性能的重要性就体现了出来。比如array和linked list的选择,可能产生巨大的差别。
所以,学习数据结构和算法,目的是建立时间复杂度、空间复杂度意识,写出高质量的代码,能够设计基础架构,提升编程技能~
其中,最重要的概念是复杂度分析。
数据结构和算法解决的是如何更省、更快地存储和处理数据的问题,复杂度分析方法就是一个考量效率和资源消耗的方法。
20 个最常用的、最基础数据结构与算法:
事半功倍的学习技巧:
数据结构和算法本身解决的是“快”和“省”的问题,执行效率是算法一个非常重要的考量指标。把代码跑一遍,通过统计、监控等方法是可以评估的,但是得到的执行效率有很大局限性。一是依赖于测试的硬件环境,二是依赖于测试数据的规模。
所以,是需要一个不用具体的测试数据来测试,就可以粗略地估计算法的执行效率的方法,也就是时间、空间复杂度分析方法。
算法的执行效率,粗略地讲,就是算法代码执行的时间。
现在假设把每行代码执行的时间记为 u n i t _ t i m e unit\_time unit_time,来计算一段代码的总执行时间。
上图中,代码总的执行时间为 T ( n ) = ( 2 n 2 + 2 n + 3 ) ∗ u n i t _ t i m e T(n) = (2n^2+2n+3)*unit\_time T(n)=(2n2+2n+3)∗unit_time。
可以总结出,所有代码的执行时间 T ( n ) T(n) T(n) 与每行代码的执行次数 n n n 成正比。
也就可以写成大O复杂度表示法:
T ( n ) = O ( f ( n ) ) T(n) = O(f(n)) T(n)=O(f(n))
代码的执行时间 T(n) 与 f(n) 表达式成正比。
大 O 时间复杂度,实际上表示代码执行时间随数据规模增长的变化趋势,所以,也叫作渐进时间复杂度(asymptotic time complexity),简称时间复杂度!
当 n n n 很大时,公式中的低阶、常量、系数不会左右增长趋势,所以可以忽略,只取最大量级即可。那么, T ( n ) = O ( 2 n 2 + 2 n + 3 ) T(n) = O(2n^2+2n+3) T(n)=O(2n2+2n+3) 即可记为 T ( n ) = O ( n 2 ) T(n) = O(n^2) T(n)=O(n2)
O ( 1 ) O(1) O(1)
常量级时间复杂度
只要算法中不存在循环语句、递归语句,即使有成千上万行的代码,其时间复杂度也是Ο(1)
O ( l o g n ) O(logn) O(logn)、 O ( n l o g n ) O(nlogn) O(nlogn)
对数阶时间复杂度
O ( m + n ) O(m+n) O(m+n)、 O ( m ∗ n ) O(m * n) O(m∗n)
无法事先评估 m 和 n 谁的量级大,则都保留。
附课代表姜威总结笔记:
空间复杂度全称就是渐进空间复杂度(asymptotic space complexity),表示算法的存储空间与数据规模之间的增长关系,与时间复杂度的全称“渐进时间复杂度”类似。
常见的空间复杂度有 O ( 1 ) O(1) O(1)、 O ( n ) O(n) O(n)、 O ( n 2 ) O(n^2 ) O(n2)
越高阶复杂度的算法,执行效率越低。
从以上代码可以看出,代码的时间复杂度不能用简单的 O ( n ) O(n) O(n) 概括。
为了表示代码在不同情况下的不同时间复杂度,引入三个概念:最好情况时间复杂度、最坏情况时间复杂度和平均情况时间复杂度。
注意,他们对应的都是极端情况,并不是一般情况。
对于以上极端情况,发生的概率并不大,所以引入平均时间复杂度。表示代码在所有情况下执行的次数的加权平均值表示。
以上一段代码为例子,设 x x x 在与不在数组中的概率为 1 2 \frac{1}{2} 21,要查找的位置概率为 1 n \frac{1}{n} n1,那么它在某位置的概率就是 1 2 n \frac{1}{2n} 2n1。求期望值:
1 ∗ 1 2 n + 2 ∗ 1 2 n + . . . + n ∗ 1 2 n + n ∗ 1 2 = 3 n + 1 4 1 *\frac{1}{2n} + 2*\frac{1}{2n} + ... +n*\frac{1}{2n} + n*\frac{1}{2} = \frac{3n+1}{4} 1∗2n1+2∗2n1+...+n∗2n1+n∗21=43n+1
按照大O标记法,去掉常数项,该段代码的时间复杂度为 O ( n ) O(n) O(n)。
注意,大部分情况不需要区分这三种复杂度,只有同一块代码在不同的情况下,时间复杂度有量级的差距,才会使用他们来区分。
与平均时间复杂度不同,均摊时间复杂度应用场景更加特殊、更加有限。
跟前面例子不同的是,代码执行在大部分情况下都是 O ( 1 ) O(1) O(1),只有一种额外的情况,复杂度是 O ( n ) O(n) O(n)。数组长度为1,所以这 n + 1 n+1 n+1 种情况发生概率相同,即 1 n + 1 \frac{1}{n+1} n+11。按前面平均时间复杂度计算,得到平均时间复杂度为 O ( 1 ) O(1) O(1)。
跟前面find()函数极端情况下复杂度为 O ( 1 ) O(1) O(1) 不同,这个insert()函数是在大部分情况下复杂度为 O ( 1 ) O(1) O(1);而且它还有规律,一个 O ( 1 ) O(1) O(1) 插入之后,紧跟着 n − 1 n-1 n−1 个 O ( 1 ) O(1) O(1) 的插入操作。
注意,在能够应用均摊时间复杂度分析的场合,一般均摊时间复杂度就等于最好情况时间复杂度。