演算法和資料結構密不可分。演算法依賴資料結構。
資料結構和演算法解決是“如何讓計算機更快時間、更省空間的解決問題”
因此從 執行時間 和 資源佔用 兩個維度來評估資料結構和演算法的效能
也就是我們接下來講的複雜度的問題,
時間維度 即是 時間複雜度;資源空間維度 就是 空間複雜度;
複雜度是描述了 執行時間或者資源空間與資料規模的關係
比如一段程式,我們如何估算這段程式碼的執行時間?
在這裡,我們可以約定每一行程式碼的執行時間是一樣的,我們約定為 單位時間 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
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();
均攤情況時間複雜度,其實是一種更加特殊的平均情況時間複雜度,
均攤也是存在複雜度級別的差距,但是級別較高的複雜度會被平攤到級別較低的複雜度執行中去。這樣來看演算法複雜度進行了均攤。Redis很多場景都使用了這種思想。
平均情況演算法複雜度,程式碼在不同的情況下有不同的複雜度級別,參考相關發生的概率,進行平均計算。
至於平均和均攤的複雜度來說,哪種情況出現的可能性比較多,那麼他的複雜度越是趨近於這種複雜度。
以上的四個複雜度概念,平時見的不多,但是很多的流行的開源工具框架,很多都有這種思想在裡面。