“时间增长趋势”这个概念比较抽象,我们通过一个例子来加以理解。假设输入数据大小为 \(n\) ,给定三个算法 A、B 和 C :
1 2 3 4 5 6 7 8 91011
# 算法 A 的时间复杂度:常数阶defalgorithm_A(n:int):print(0)# 算法 B 的时间复杂度:线性阶defalgorithm_B(n:int):for_inrange(n):print(0)# 算法 C 的时间复杂度:常数阶defalgorithm_C(n:int):for_inrange(1000000):print(0)
1 2 3 4 5 6 7 8 910111213141516
// 算法 A 的时间复杂度:常数阶voidalgorithm_A(intn){cout<<0<<endl;}// 算法 B 的时间复杂度:线性阶voidalgorithm_B(intn){for(inti=0;i<n;i++){cout<<0<<endl;}}// 算法 C 的时间复杂度:常数阶voidalgorithm_C(intn){for(inti=0;i<1000000;i++){cout<<0<<endl;}}
1 2 3 4 5 6 7 8 910111213141516
// 算法 A 的时间复杂度:常数阶voidalgorithm_A(intn){System.out.println(0);}// 算法 B 的时间复杂度:线性阶voidalgorithm_B(intn){for(inti=0;i<n;i++){System.out.println(0);}}// 算法 C 的时间复杂度:常数阶voidalgorithm_C(intn){for(inti=0;i<1000000;i++){System.out.println(0);}}
1 2 3 4 5 6 7 8 910111213141516
// 算法 A 的时间复杂度:常数阶voidAlgorithmA(intn){Console.WriteLine(0);}// 算法 B 的时间复杂度:线性阶voidAlgorithmB(intn){for(inti=0;i<n;i++){Console.WriteLine(0);}}// 算法 C 的时间复杂度:常数阶voidAlgorithmC(intn){for(inti=0;i<1000000;i++){Console.WriteLine(0);}}
1 2 3 4 5 6 7 8 910111213141516
// 算法 A 的时间复杂度:常数阶funcalgorithm_A(nint){fmt.Println(0)}// 算法 B 的时间复杂度:线性阶funcalgorithm_B(nint){fori:=0;i<n;i++{fmt.Println(0)}}// 算法 C 的时间复杂度:常数阶funcalgorithm_C(nint){fori:=0;i<1000000;i++{fmt.Println(0)}}
1 2 3 4 5 6 7 8 9101112131415161718
// 算法 A 的时间复杂度:常数阶funcalgorithmA(n:Int){print(0)}// 算法 B 的时间复杂度:线性阶funcalgorithmB(n:Int){for_in0..<n{print(0)}}// 算法 C 的时间复杂度:常数阶funcalgorithmC(n:Int){for_in0..<1_000_000{print(0)}}
1 2 3 4 5 6 7 8 910111213141516
// 算法 A 的时间复杂度:常数阶functionalgorithm_A(n){console.log(0);}// 算法 B 的时间复杂度:线性阶functionalgorithm_B(n){for(leti=0;i<n;i++){console.log(0);}}// 算法 C 的时间复杂度:常数阶functionalgorithm_C(n){for(leti=0;i<1000000;i++){console.log(0);}}
1 2 3 4 5 6 7 8 910111213141516
// 算法 A 的时间复杂度:常数阶functionalgorithm_A(n:number):void{console.log(0);}// 算法 B 的时间复杂度:线性阶functionalgorithm_B(n:number):void{for(leti=0;i<n;i++){console.log(0);}}// 算法 C 的时间复杂度:常数阶functionalgorithm_C(n:number):void{for(leti=0;i<1000000;i++){console.log(0);}}
1 2 3 4 5 6 7 8 910111213141516
// 算法 A 的时间复杂度:常数阶voidalgorithmA(intn){print(0);}// 算法 B 的时间复杂度:线性阶voidalgorithmB(intn){for(inti=0;i<n;i++){print(0);}}// 算法 C 的时间复杂度:常数阶voidalgorithmC(intn){for(inti=0;i<1000000;i++){print(0);}}
1 2 3 4 5 6 7 8 910111213141516
// 算法 A 的时间复杂度:常数阶fnalgorithm_A(n: i32){println!("{}",0);}// 算法 B 的时间复杂度:线性阶fnalgorithm_B(n: i32){for_in0..n{println!("{}",0);}}// 算法 C 的时间复杂度:常数阶fnalgorithm_C(n: i32){for_in0..1000000{println!("{}",0);}}
1 2 3 4 5 6 7 8 910111213141516
// 算法 A 的时间复杂度:常数阶voidalgorithm_A(intn){printf("%d",0);}// 算法 B 的时间复杂度:线性阶voidalgorithm_B(intn){for(inti=0;i<n;i++){printf("%d",0);}}// 算法 C 的时间复杂度:常数阶voidalgorithm_C(intn){for(inti=0;i<1000000;i++){printf("%d",0);}}
1 2 3 4 5 6 7 8 910111213141516
// 算法 A 的时间复杂度:常数阶funalgoritm_A(n:Int){println(0)}// 算法 B 的时间复杂度:线性阶funalgorithm_B(n:Int){for(iin0..<n){println(0)}}// 算法 C 的时间复杂度:常数阶funalgorithm_C(n:Int){for(iin0..<1000000){println(0)}}
1 2 3 4 5 6 7 8 91011121314
# 算法 A 的时间复杂度:常数阶defalgorithm_A(n)puts0end# 算法 B 的时间复杂度:线性阶defalgorithm_B(n)(0...n).each{puts0}end# 算法 C 的时间复杂度:常数阶defalgorithm_C(n)(0...1_000_000).each{puts0}end
1 2 3 4 5 6 7 8 9101112131415161718
// 算法 A 的时间复杂度:常数阶fnalgorithm_A(n:usize)void{_=n;std.debug.print("{}\n",.{0});}// 算法 B 的时间复杂度:线性阶fnalgorithm_B(n:i32)void{for(0..n)|_|{std.debug.print("{}\n",.{0});}}// 算法 C 的时间复杂度:常数阶fnalgorithm_C(n:i32)void{_=n;for(0..1000000)|_|{std.debug.print("{}\n",.{0});}}
下图展示了以上三个算法函数的时间复杂度。
算法 A 只有 \(1\) 个打印操作,算法运行时间不随着 \(n\) 增大而增长。我们称此算法的时间复杂度为“常数阶”。
算法 B 中的打印操作需要循环 \(n\) 次,算法运行时间随着 \(n\) 增大呈线性增长。此算法的时间复杂度被称为“线性阶”。
算法 C 中的打印操作需要循环 \(1000000\) 次,虽然运行时间很长,但它与输入数据大小 \(n\) 无关。因此 C 的时间复杂度和 A 相同,仍为“常数阶”。
相较于直接统计算法的运行时间,时间复杂度分析有哪些特点呢?
时间复杂度能够有效评估算法效率。例如,算法 B 的运行时间呈线性增长,在 \(n > 1\) 时比算法 A 更慢,在 \(n > 1000000\) 时比算法 C 更慢。事实上,只要输入数据大小 \(n\) 足够大,复杂度为“常数阶”的算法一定优于“线性阶”的算法,这正是时间增长趋势的含义。
时间复杂度也存在一定的局限性。例如,尽管算法 A 和 C 的时间复杂度相同,但实际运行时间差别很大。同样,尽管算法 B 的时间复杂度比 C 高,但在输入数据大小 \(n\) 较小时,算法 B 明显优于算法 C 。在这些情况下,我们很难仅凭时间复杂度判断算法效率的高低。当然,尽管存在上述问题,复杂度分析仍然是评判算法效率最有效且常用的方法。
函数渐近上界
给定一个输入大小为 \(n\) 的函数:
1234567
defalgorithm(n:int):a=1# +1a=a+1# +1a=a*2# +1# 循环 n 次foriinrange(n):# +1print(0)# +1
123456789
voidalgorithm(intn){inta=1;// +1a=a+1;// +1a=a*2;// +1// 循环 n 次for(inti=0;i<n;i++){// +1(每轮都执行 i ++)cout<<0<<endl;// +1}}
123456789
voidalgorithm(intn){inta=1;// +1a=a+1;// +1a=a*2;// +1// 循环 n 次for(inti=0;i<n;i++){// +1(每轮都执行 i ++)System.out.println(0);// +1}}
123456789
voidAlgorithm(intn){inta=1;// +1a=a+1;// +1a=a*2;// +1// 循环 n 次for(inti=0;i<n;i++){// +1(每轮都执行 i ++)Console.WriteLine(0);// +1}}
123456789
funcalgorithm(nint){a:=1// +1a=a+1// +1a=a*2// +1// 循环 n 次fori:=0;i<n;i++{// +1fmt.Println(a)// +1}}
123456789
funcalgorithm(n:Int){vara=1// +1a=a+1// +1a=a*2// +1// 循环 n 次for_in0..<n{// +1print(0)// +1}}
123456789
functionalgorithm(n){vara=1;// +1a+=1;// +1a*=2;// +1// 循环 n 次for(leti=0;i<n;i++){// +1(每轮都执行 i ++)console.log(0);// +1}}
123456789
functionalgorithm(n:number):void{vara:number=1;// +1a+=1;// +1a*=2;// +1// 循环 n 次for(leti=0;i<n;i++){// +1(每轮都执行 i ++)console.log(0);// +1}}
123456789
voidalgorithm(intn){inta=1;// +1a=a+1;// +1a=a*2;// +1// 循环 n 次for(inti=0;i<n;i++){// +1(每轮都执行 i ++)print(0);// +1}}
1 2 3 4 5 6 7 8 910
fnalgorithm(n: i32){letmuta=1;// +1a=a+1;// +1a=a*2;// +1// 循环 n 次for_in0..n{// +1(每轮都执行 i ++)println!("{}",0);// +1}}
123456789
voidalgorithm(intn){inta=1;// +1a=a+1;// +1a=a*2;// +1// 循环 n 次for(inti=0;i<n;i++){// +1(每轮都执行 i ++)printf("%d",0);// +1}}
123456789
funalgorithm(n:Int){vara=1// +1a=a+1// +1a=a*2// +1// 循环 n 次for(iin0..<n){// +1(每轮都执行 i ++)println(0)// +1}}
123456789
defalgorithm(n)a=1# +1a=a+1# +1a=a*2# +1# 循环 n 次(0...n).eachdo# +1puts0# +1endend
123456789
fnalgorithm(n:usize)void{vara:i32=1;// +1a+=1;// +1a*=2;// +1// 循环 n 次for(0..n)|_|{// +1(每轮都执行 i ++)std.debug.print("{}\n",.{0});// +1}}