大家好呀!我是小桑。
要我说,谈到计算机就不能不说数据结构与算法,谈到数据结构与算法就不能不说复杂度分析。
作为编程界的老大哥,他的重要性不言而喻。在我看来,这是数据结构与算法中最重要的知识点。有多重要呢?反正很重要就是了。
尼尼 :真假的!就这玩意儿能有多重要,我才不信。
小桑 :切,你可别不信,人们判断一个算法的优劣可离不开他。
尼尼 :这么厉害,那我可要好好听了。快讲,快讲。
小桑:先别急,还不赶快拿个小板凳做好。
复杂度分析
从【时效性】和【存储】两方面看待问题,我们不难理解好的算法具备高时效性和低存储需求的特点。对于人类而言,我们总是希望在做一件事情时付出最小的代价,获得最大的回报。在算法领域,这被翻译成了在解决问题时花最少时间和最少存储,做成最出色的解决方案。
科学家们深谋研究,发现将算法步骤数量作为度量指标是一个合理而有效的选择。这种独立于具体机器和编程语言的度量方式,为我们提供了一种公正的比较算法优劣的方法。
那如何去考量"更少的时间和更少的存储",复杂度分析为此而生。
时间复杂度
ini
1 int sum = 0;
2 int i = 1;
3
4 for (i = 1; i <= n; i++) {
5 sum += i;
6 }
在上面假设的情况,这段求累加和的代码总的运行时间是多少呢?
- 第 1.2 行代码需要
1 Btime
的运行时间 - 第 4 行和第 5 行运行了 n 次,所以每个需要
n * Btime
的运行时间。
所以总的运行时间就是 (2 + 2n) * Btime
。
嘿嘿,那我们怎么表示这段时间复杂度呢?
那我们就不得不介绍我们最隆重的嘉宾大 O 出场了。
什么是大 O?
按照算法导论给出的解释:大 O 是用来表示上界的,作为算法的最坏情况运行时间的上界。
代表着一种趋势,一种随着数据集的规模增大,算法代码运行时间变化的一种趋势。记为
乍一看这个公式是不是目瞪口呆?让我来给你细细演示一番。
还是以之前的代码为例。当 n = 1000 时, 2n + 2 = 2002, 当 n = 10000 时,2n + 2 = 20002,当 n 持续增大时,常数 2 和系数 2 对于最后的结果越来越没存在感,即对趋势的变化影响不大。
最终T(n) = O(n)
时间复杂度分析
代码
ini
1 int i = 0;
2 int j = 0;
3 for(i = 0; i < n; i++){
4 for(j = 0; j < n; j++){
5 //some O(1) expressions
6 }
7 }
第 1 行和第 2 行各需要运行 1 次 ,第 3 行和第 4 行的嵌套循环共需要运行 n² 次。所以总的运行次数 f(n) = 2 + n²。
当 n 为 5 的时候,f(n) = 2 + 25,当 n 为 10000 的时候,f(n) = 2 + 100000000,当 n 更大呢?
这个时候其实很明显的就可以看出来 n² 起到了决定性的作用,像常数 1 和系数 2 对最终的结果(即趋势)影响不大,所以我们可以把它们直接忽略掉,所以执行的总步数就可以看成是"主导"结果的那个,也就是 f(n) = n²。
自然代码的运行时间 T(n) = O(f(n)) = O(n²)。
小桑 :尼尼,看懂了吗?
尼尼 :当然,也就是说对于时间复杂度,我们只要关心最高的那个复杂度就行,其他的不用考虑。
小桑 :可以啊,领悟的很快。
尼尼 :那可不,也不看看我是谁。
小桑:。。。
补充
一般我们计算时间复杂度总是考虑最坏的情况下的时间复杂度,以保证算法的运行时间不会比他更长。
在分析一个程序的时间复杂性是,有以下两条规则:
1)加法规则: <math xmlns="http://www.w3.org/1998/Math/MathML"> T ( n ) T(n) </math>T(n) = <math xmlns="http://www.w3.org/1998/Math/MathML"> T 1 ( n ) {T1}(n) </math>T1(n) + <math xmlns="http://www.w3.org/1998/Math/MathML"> T 2 ( n ) {T2}(n) </math>T2(n) = <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( f ( n ) ) + O ( g ( n ) ) = O ( m a x ( f ( n ) , g ( n ) ) ) O( f(n) ) + O( g(n) ) = O( max( f(n) , g(n) ) ) </math>O(f(n))+O(g(n))=O(max(f(n),g(n)))
2)乘法规则: <math xmlns="http://www.w3.org/1998/Math/MathML"> T ( n ) = T 1 ( n ) ∗ T 2 ( n ) = O ( f ( n ) ) ∗ O ( g ( n ) ) = O ( ( f ( n ) ∗ g ( n ) ) ) T(n) = T1(n)*T2(n) = O( f(n) )*O( g(n) ) = O( (f(n) * g(n) ) ) </math>T(n)=T1(n)∗T2(n)=O(f(n))∗O(g(n))=O((f(n)∗g(n)))
常见的时间复杂度
顺便在这里给大家把常见的时间复杂度排排序,记住这些就足够了。
O(1) < O(logn) < O(n) < O(nlogn) < O( <math xmlns="http://www.w3.org/1998/Math/MathML"> n 2 n^2 </math>n2) < O( <math xmlns="http://www.w3.org/1998/Math/MathML"> n 3 n^3 </math>n3) < O( <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 n 2^n </math>2n) < O(n!) < O( <math xmlns="http://www.w3.org/1998/Math/MathML"> n n n^n </math>nn)
O(1)
O(1) 是 O(1) 常量级时间复杂度
arduino
1 void sunny(){
2 printf("1");
3 printf("2");
4 printf("3");
5 printf("4");
6 }
只要算法是有限次(常量、和规模 n 无关)执行次数,就都是常量级。可以看到无论 printf 写了多少行都是 O(1)。
比如上面这段代码,他运行了 4 次,但他的时间复杂度仍然是 O(1),而不是 O(4)。
O(logn,nlogn)
这里先引入高中数学的换底公式,还记得不?
在计算过程中我们忽略了底,直接用 O(logn) 来表示对数时间复杂度。
ini
1 int i = 0;
2 int j = 0;
3 for(i = 0;i < n; i++){
4 for(j = 0; j < n; j = j * 2){
5 //some O(1) expressions
6 }
7 }
例如这段代码,这次是O(n)
和 O(logn)
的结合,外层循环 n
次,所以最终的算法复杂度是O(nlogn)
时间复杂度算是我们学习数据结构的敲门砖。当然了,还有他的好兄弟空间复杂度。
希望各位童鞋们看完这篇文章能对时间复杂度有个最基本的认识。多编码,多练习,熟能生巧,你一定可以的!