演算法複雜度

K戰神發表於2018-11-09

演算法和資料結構密不可分。演算法依賴資料結構。

資料結構和演算法解決是“如何讓計算機更快時間、更省空間的解決問題”

因此從  執行時間 和  資源佔用 兩個維度來評估資料結構和演算法的效能

也就是我們接下來講的複雜度的問題,

時間維度 即是 時間複雜度;資源空間維度 就是 空間複雜度;

複雜度是描述了 執行時間或者資源空間與資料規模的關係

 

比如一段程式,我們如何估算這段程式碼的執行時間?

在這裡,我們可以約定每一行程式碼的執行時間是一樣的,我們約定為 單位時間 unit_time。

這樣問題就可以轉換為:程式總共執行了多少個單位時間。

先看下面這段程式碼,總共執行了多少個單位時間:

 1         public static void F(int n)
 2         {
 3             int i_index = 0;  
 4             int j_todo = 0;
 5             for (; i_index < n; i_index++)
 6             {
 7                 j_todo = i_index;
 8                 Console.WriteLine($"業務呼叫次數 :{j_todo + 1}");
 9                 Console.Write($"迴圈次數 :{i_index + 1}");
10             }
11         }

第 3 行,一個單位是時間  =1 unit_time

第 4 行,也是一個執行單位時間  =1 unit_time

第 5 行,根據n的大小,所以會執行 n次 =n unit_time

同樣第 7 ,8 , 9 這三行都會執行 n次 = ( n + n + n ) unit_time

所以 以上這個方法內部的演算法執行時間是:T=1+1+n+3n = 4n+2;

T = 4n+2 是演算法的執行時間,所以我們可以看出T和n成正比。

 

我們再來看一下一個巢狀迴圈的時間複雜度:

 1         public static void F2(int n)
 2         {
 3             int i_index = 0;
 4             for (; i_index < n; i_index++)
 5             {
 6                 int j_todo = 0;
 7                 for (; j_todo < n; j_todo++)
 8                 {
 9                     Console.WriteLine($"業務呼叫次數 :{j_todo + 1}");
10                 }
11                 Console.Write($"迴圈次數 :{i_index + 1}");
12             }
13         }

第3行 = 1 unit

第4行 = n unit

第6行 = n unit

第7行 = n * n

第9行 = n * n

第10行= n

T = 3n + 2*n^2

 

其實我們也已經看到,執行時間T 和 n 執行次數的數量級成正比,

這裡就引出了一個公式:T(n) = O(f(n))

T(n) 程式碼在數量級n規模下的執行時間;

 f(n) 每行程式碼執行次數總和

O 即為 T(n) 和 f(n) 成正比關係

上面的例1:T = 4n+2  ,大O表示法為 T(n) = O(4n+2)

例2:T = 3n + 2*n^2    ,  大O表示法為 T(n) = O(3n + 2*n^2)


通過以上例子我們可以知道,當n很大時,常量,係數,低階這些對趨勢影響極小,也就是我們找到影響面最大的因子

對案例1來說,去除常量和係數,影響最大因子就是n,即可表示為 T(n)=O(n)

案例2來說,同樣去除影響面較小的因子,可表示為 T(n)=O(n^2)

我們可以總結為:去除影響趨勢較小的因子,保留影響趨勢最大的因子

1)單段程式碼看高頻:比如迴圈。

2)巢狀程式碼求乘積:比如遞迴、多重迴圈等

3)多段程式碼取最大:比如一段程式碼中有單迴圈和多重迴圈,那麼取多重迴圈的複雜度。

4)多個規模求加法:比如方法有兩個引數控制兩個迴圈的次數,那麼這時就取二者複雜度相加。

 

我們常見的或者常聽到的複雜度級別比如:

 

O(1)  常量階 

  1不是代表的程式碼行數,這是對執行次數為固定常量的統稱。

  以下FormatWriteLineMsg函式只是一個格式化展示的函式

1         public static void N1(int n)
2         {
3             int i_index = 0;
4             Console.WriteLine($"時間複雜度-常量階-i :{i_index + 1}");
5         }    
1         int n = 10;
2 
3         FormatWriteLineMsg("時間複雜度-常量階: O(1)");
4         N1(n);

 

 

O(n)  線性階   

  我們剛才案例1已經提到過,成正比關係。

1         public static void N(int n)
2         {
3             int i_index = 0;
4 
5             for (; i_index < n; i_index++)
6             {
7                 Console.WriteLine($"時間複雜度-線性階-i :{i_index + 1}");
8             }
9         }
1             int n = 10;
2 
3             FormatWriteLineMsg("時間複雜度-線性階: O(n)");
4             N(n);        

 

  

O(n^k)  次方階

  案例2也是平方階,當然還有給根據巢狀的層數對應的k的增加。

 1         public static void NxN(int n)
 2         {
 3             int i_index = 0;
 4             for (; i_index < n; i_index++)
 5             {
 6                 int j_todo = 0;
 7                 for (; j_todo < n; j_todo++)
 8                 {
 9                     Console.WriteLine($"時間複雜度-X方階 :i*j ={i_index + 1} *{j_todo + 1}");
10                 }
11             }
12         }
1             int n = 10;
2 
3             FormatWriteLineMsg("時間複雜度-X方階: O(n^x)");
4             NxN(n);

......

 

 

 

O(m+n)、O(m*n)

  複雜度與兩個資料資源有關

 

 

O(log n)  對數階

 1         public static void LogN(int n)
 2         {
 3             double flag = 1;
 4             double step = 2;
 5             int forCount = 0;
 6             while (flag <= n)
 7             {
 8                 forCount++;
 9                 flag = flag * step;
10                 Console.WriteLine($"時間複雜度-對數階:{forCount}   ==>   {step}^{Math.Log(flag, step)}");
11             }
12         }
1             int n = 10;
2 
3             FormatWriteLineMsg("時間複雜度-對數階: O(log n)");
4             LogN(n);

迴圈最多的程式碼是 第6、8、9、10行,我們看一下它們執行了多少次:

   次數  =>    1        2       3      4

 flag   =>   1*2    2*2   4*2    8*2 

        n  =>    2^1   2^2   2^3   2^4 

 所以 對於n來說,n=2^x,執行了x次。x=log2n,時間複雜度為  O( log2n ).

   我i什麼會將這些對數同意規整為 logn?--忽略對數的底,即為 O( logn )

 

 

空間複雜度

演算法的儲存空間與資料規模之間的增長關係

通常空間複雜度級別:O(1)   O(n)  O(n^2)

 

 

一般分析到這已經可以應對我們平時的開發需求,因為下面的很多的概念思想可以借鑑。也是很多的開源工具框架運用了其中的一些演算法的精髓。

所以我們有必要再對演算法複雜度深入一下,那麼接下來我們再進一步瞭解一下幾個概念:

最壞情況時間複雜度、最好情況時間複雜度 、平均情況時間複雜度、均攤時間複雜度

來看一下下面這一段程式碼:查詢某個元素,找到就結束返回索引,沒找到為-1。

 1             string source = "8,4,5,6,2,3,1,9,0,7";
 2             string[] data = source.Split(',').ToArray();
 3             Console.WriteLine($"Source ={source} | find =>{search}");
 4             Console.WriteLine();
 5 
 6             int count = data.Count();
 7             int search_index = -1;
 8 
 9             int i_index = 0;
10             for (; i_index < count; i_index++)
11             {
12                 Console.WriteLine($"todo:{i_index + 1}");
13                 if (search == data[i_index])
14                 {
15                     search_index = i_index;
16                     break;
17                 }
18             }        

 

這就會出現多種情況:

1、比如上來第一個就找到了,那麼時間複雜度為O(1),因為只迴圈了一次就找到了;

2、運氣較差最有一個找到了,時間複雜度為O(n);

3、那麼在第二個,第三個,第n個找到呢 ?

4、找遍所有還是沒有找到,那麼這個時間複雜度仍為 O(n);

 

第1、2、4 只是一種特殊情況,我們很多情況會是3.

當然,第1就是我們上面提到的 最好情況時間複雜度=O(1);

第2、4 就是最壞情況時間複雜度 = O(n);

我們接下來所分析的就是 第3,平均情況時間複雜度:

  • 所有發生的情況為 n +1 (n內找到+沒找到)
  • 概率,對於每一次查詢 是的概率為 1/2
  • 對於n+1種情況來說,n內找到的概率為 1/n

1+2+3...+n+n / n+1     =>    + 概率      =>      1*(1/2n)+2*(1/2n)+3*(1/2n)...+n*(1/2n)+n*1/2  => 3n+1 / 4 

所以 平均時間複雜度為 O(n)

Demo

 1         public static void Best_Worst_Avg_CaseTime(string search)
 2         {
 3             string source = "8,4,5,6,2,3,1,9,0,7";
 4             string[] data = source.Split(',').ToArray();
 5             Console.WriteLine($"Source ={source} | find =>{search}");
 6             Console.WriteLine();
 7 
 8             int count = data.Count();
 9             int search_index = -1;
10 
11             int i_index = 0;
12             for (; i_index < count; i_index++)
13             {
14                 Console.WriteLine($"todo:{i_index + 1}");
15                 if (search == data[i_index])
16                 {
17                     search_index = i_index;
18                     break;
19                 }
20             }
21 
22             if (i_index == 0)
23             {
24                 Console.WriteLine($">>>>>>>>>>>>>>>>>Best:O(1)");
25                 Console.WriteLine();
26             }
27             else
28             {
29                 if (search_index == -1)
30                 {
31                     Console.WriteLine($">>>>>>>>>>>>>>>>>Worst:O(n)");
32                     Console.WriteLine();
33                 }
34                 else
35                 {
36                     Console.WriteLine($@">>>>>>>>>>>>>>>>>Other:
37                         => O( 1+2+...+n+n / n+1 ) 
38                         => O( 2n / n+1 ) 
39                         => O(n)");
40 
41                     Console.WriteLine($@">>>>>>>>>>>>>>>>>Avg: 
42                         =>O( 1*(1/n)*(1/2)+...n*(1/2n)+n*(1/2) ) 
43                         => O( 3n/4 ) 
44                         => O(n)");
45                     Console.WriteLine();
46                 }
47             }
48             
49         }
50         
View Code
 1             FormatWriteLineMsg("最好情況時間複雜度");
 2             string best = "8";
 3             Best_Worst_Avg_CaseTime(best);
 4 
 5             FormatWriteLineMsg("最壞情況時間複雜度");
 6             string worst = "10";
 7             Best_Worst_Avg_CaseTime(worst);
 8 
 9             FormatWriteLineMsg("平均情況時間複雜度");
10             string avg = "6";
11             Best_Worst_Avg_CaseTime(avg);
12 
13             Console.ReadKey();    
View Code

 

均攤情況時間複雜度,其實是一種更加特殊的平均情況時間複雜度,

均攤也是存在複雜度級別的差距,但是級別較高的複雜度會被平攤到級別較低的複雜度執行中去。這樣來看演算法複雜度進行了均攤。Redis很多場景都使用了這種思想。

平均情況演算法複雜度,程式碼在不同的情況下有不同的複雜度級別,參考相關發生的概率,進行平均計算。

至於平均和均攤的複雜度來說,哪種情況出現的可能性比較多,那麼他的複雜度越是趨近於這種複雜度。

以上的四個複雜度概念,平時見的不多,但是很多的流行的開源工具框架,很多都有這種思想在裡面。

 

相關文章