1. 程式人生 > >3L-最好、最壞、平均、均攤時間複雜度

3L-最好、最壞、平均、均攤時間複雜度

> 關注公眾號 MageByte,設定星標點「在看」是我們創造好文的動力。後臺回覆 “加群” 進入技術交流群獲更多技術成長。 本文來自 MageByte-青葉編寫 上次我們說過 [時間複雜度與空間復度](https://mp.weixin.qq.com/s/2yDJjVJC4N404g7ISmvAzw),列舉了一些分析技巧以及一些常見的複雜度分析比如 O(1)、O(logn)、O(n)、O(nlogn),今天會繼續細化時間複雜度。 **1. 最好情況時間複雜度(best case time complexity)** **2.最壞情況時間複雜度(worst case time complexity)** **3. 平均情況時間複雜度(average case time complexity)** **4.均攤時間複雜度(amortized time complexity)** ## 複雜度分析 ```java public int findGirl(int[] girlArray, int number) { int i = 0; int pos = -1; int n = girlArray.lentgh(); for (; i < n; ++i) { if (girlArray[i] == number) { pos = i; break; } } return pos; } ``` 程式碼邏輯你應該很容易看出來,在無序陣列 中查詢 number 出現的位置,如果沒找到就返回 -1。《唐伯虎點秋香》主角星爺通過這個方法遍歷陣列找到秋香,因為此刻我們還沒有學會各種風騷的演算法,只能從頭到尾查驗是不是秋香,所以只能遍歷陣列。girlArray 陣列儲存著秋香、冬香、春香……的編碼,現在唐伯虎通過 選擇 number 這個編碼比對是否是秋香。 ![公眾號](https://magebyte.oss-cn-shenzhen.aliyuncs.com/演算法/20200308110924.png) 這段程式碼在不同的情況下,時間複雜度是不一樣的,所以為了**描述程式碼在不同情況下的不同時間複雜度,我們引入了**==最好、最壞、平均時間複雜度==。n = girlArray 陣列的長度。 1. 當如秋香在第一個,那 程式碼的時間複雜度就是 O(1)。 2. 當秋香在隊伍的最後一個,那程式碼的時間複雜度就是 O(n)。 3. 當 秋香在隊伍中但是不在隊伍第一個,也不再最後一個,那麼就不確定。 4. 假如華府使詐,隊伍裡也根本不存在秋香,唐波徐也需要把隊伍一個個查驗完畢才知道,時間複雜度就成了 O(n) ## 最好情況時間複雜度 在最理想的情況下,執行這段程式碼的時間,也就是「唐伯虎」最快點中秋香。假如 這一排姑娘就代表 girlArray 陣列,number 變數就是秋香的編碼。假如第一個姑娘就是「秋香」那時間複雜度就是 O(1)。 ## 最壞情況時間複雜度 在最糟糕的情況下,執行這段程式碼的時間複雜度。也就是要一個個查驗真個陣列的長度 O(n)。 ## 平均情況時間複雜度 其實最好與最壞情況是極端情況,發生的概率並不大。所以為了更準確的表示平均情況下的時間複雜度,引入另一個改變:**平均情況時間複雜度**。 還是上面的「找秋香」程式碼,判斷 number 編碼在迴圈中出現的位置,有 ==n + 1==種情況: 在陣列 0~n-1 中和不在這個陣列中。在陣列中共有 n 種情況,加上不在陣列中則就是 n + 1 種了。 每種情況要遍歷的姑娘人數都不同。我們把每種情況需要查詢姑娘的數量累加,然後再除以 所有情況數量 (n + 1),就得到需要遍歷次數的平均值。**敲黑板了:公式就是平均情況複雜度 = 累加每種遍歷的元素個數 / 所有的情況數量** **平均情況複雜度為:** $$\frac {((1+2+3… +n) + n)} {(n+1)} = \frac {n(n+3)} {2(n+1)}$$ 推導過程: $$\because 1+2+3 …+ n = n + (n-1) + (n-2)… + 1$$ $$\therefore (1 +2 +3… + n) = \frac {n(1+n)} {2}$$ $$\therefore (1+2+3+…+n) + n = \frac {n(3+n)} {2}$$ 根據我們之前學的 [時間複雜度與空間復度](https://mp.weixin.qq.com/s/2yDJjVJC4N404g7ISmvAzw) 大 O 表示法,省略係數、地接、常量,所以平均情況時間複雜度是 **O(n)**。 ### 期望時間複雜度 上面的平均情況時間複雜度推導**沒有考慮每種情況的發生概率,**這裡的 n+1 種情況,每種情況發生的概率是不一樣的,所以還要引入各自發生的概率再具體分析。 秋香的編號 number 要麼在 0 ~ n-1 中,要麼不在 0~n-1 中,所以他們的概率是 $\frac {1} {2}$。 同時 number 在 0~n-1 各個位置的概率是一樣的為 1/n。根據概率乘法法則,number 在 0~n-1 中任意位置的概率是 $$\frac {1} {2n}$$。 所以在前面推導的基礎上,我們再把每種情況發生的概率考慮進去,那麼平均情況時間複雜度的計算過程就是: **考慮概率的平均情況複雜度:** $$(1 \frac {1} {2n} + 2 \frac {1} {2n}+ 3 \frac {1} {2n}…+n\frac {n} {2n} ) + n \frac {1} {2} = \frac {3n+1} {4}$$ 這就是概率論中的加權平均值,也叫做期望值,所以平均時間複雜度全稱叫:**加權平均時間複雜度**或者**期望時間複雜度**。 引入概率之後,平均複雜度變為 **O($$\frac {3n+1} {4}$$)**,忽略係數以及常量,最後得到的加權平均時間複雜度為 O(n)。終於分析推導完了,同學們可以鬆一口氣。 **注意:** 多數情況下,我們**不需要區分最好、最壞、平均情況時間複雜度**。只有同一塊程式碼在**不同情況下時間複雜度有量級差距**,我們才會區分 3 種情況,為的是更**有效的描述**程式碼的時間複雜度。 ## 均攤情況時間複雜度 最後一個硬骨頭來了,瞭解了上面加上概率的期望時間複雜度再看這個就容易多了。均攤時間複雜度,聽起來跟平均時間複雜度有點兒像。 均攤複雜度是一個更加高階的概念,它是一種特殊的情況,應用的場景也更加特殊和有限。 對應的分析方式稱為:攤還分析或平攤分析。 ```java // array 表示一個長度為 n 的陣列 // 程式碼中的 array.length 就等於 n int[] array = new int[n]; int count = 0; public void insert(int val) { if (count == array.length) { int sum = 0; for (int i = 0; i < array.length; ++i) { sum = sum + array[i]; } array[0] = sum; count = 1; } array[count] = val; ++count; } ``` 程式碼邏輯:向一個數組插入資料,當陣列滿了後 count == array.lenth,遍歷陣列求和,將求和之後的 sum 值放到陣列的第一個位置,然後再將新的資料插入。但如果陣列一開始就有空閒空間,則直接將資料插入陣列。這裡的資料滿:對於可反覆讀寫的儲存空間,使用者認為它是空的它就是空的。如果你定義清空是全部重寫為 0 或者某個值,那也可以!使用者只關心要存的新值! 分析上述的時間複雜度: 1. 最理想情況,有空閒空間則直接插入到陣列下標 count 的位置即可。所以是 O(1)。 2. 最壞的情況,陣列沒有空閒空間,需要先做一次迴圈遍歷求和,然後再插入。時間複雜度 O(n)。 **平均時間複雜度** 陣列長度為 n,因為可以插入不同位置,所以有 n 種情況,每種複雜度為 O(1)。 還有一種特殊情況,沒有空閒空間插入的時候,複雜度是 O(n),一共就是 n+1 種情況,且每種情況的概率都是 $$\frac{1} {n+1}$$。所以根據加權平均計演算法,平均時間複雜度: $$(1 \frac {1} {n+1} + 1 \frac {1} {n+1}+ 1 \frac {1} {n+1}…+1\frac {1} {n+1} ) + n \frac {1} {n+1} = \frac {2n} {n+1}$$ 當省略係數及常量後,平均時間複雜度為 O(1)。 其實我們不需要這麼複雜,對比 findGirl 跟 insert 方法。 1. findGirl 在極端情況下複雜度 O(1),而 insert 基本情況是 O(1)。只有當陣列滿了才是 O(n)。 2. 對於 insert() 函式來說,O(1) 時間複雜度的插入和 O(n) 時間複雜度的插入,出現的頻率是非常有規律的,而且有一定的前後時序關係,一般都是一個 O(n) 插入之後,緊跟著 n-1 個 O(1) 的插入操作,迴圈往復。 **攤還分析法** 分析上述示例的平均複雜度分析並不需要如此複雜,無需引入概率論的知識。 因為通過分析可以看出,上述示例程式碼複雜度大多數為 O(1),極端情況下複雜度才較高為 O(n)。同時複雜度遵循一定的規律,一般為 1 個 O(n),和 n 個 O(1)。針對這樣一種特殊場景使用更簡單的分析方法:**攤還分析法**。 通過攤還分析法得到的時間複雜度為**均攤時間複雜度**。 **大致思路:**每一次 O(n)都會跟著 n 次 O(1),所以**把耗時多的複雜度均攤到耗時低的複雜度**。得到的均攤時間複雜度為 O(1)。 **應用場景**:均攤時間複雜度和攤還分析應用場景較為特殊,對一個數據進行連續操作,大部分情況下時間複雜度都很低,只有個別情況下時間複雜度較高。而這組操作其存在前後連貫的時序關係。 這個時候我們將這一組操作放在一起分析,將**高複雜度均攤到其餘低複雜度上**,所以一般均攤時間複雜度就等於最好情況時間複雜度。 **注意:** 均攤時間複雜度是一種特殊的平均複雜度(特殊應用場景下使用),掌握分析方式即可。 **均攤時間複雜度就是一種特殊的平均時間複雜度**,我們沒必要花太多精力去區分它們。你最應該掌握的是它的分析方法,攤還分析。至於分析出來的結果是叫平均還是叫均攤,這只是個說法,並不重要。 ## 文末思考 最後留一個問題給大家,用本文學習的只是分析下面程式碼的「最好」、「最壞」、「均攤」時間複雜度。 ```java / 全域性變數,大小為 10 的陣列 array,長度 len,下標 i。 int array[] = new int[10]; int len = 10; int i = 0; // 往陣列中新增一個元素 void add(int element) { if (i >= len) { // 陣列空間不夠了 // 重新申請一個 2 倍大小的陣列空間 int new_array[] = new int[len*2]; // 把原來 array 陣列中的資料依次 copy 到 new_array for (int j = 0; j < len; ++j) { new_array[j] = array[j]; } // new_array 複製給 array,array 現在大小就是 2 倍 len 了 array = new_array; len = 2 * len; } // 將 element 放到下標為 i 的位置,下標 i 加一 array[i] = element; ++i; } ``` 總體的含義就是向陣列新增一個元素,當空間不夠的時候重新生情一個原來兩倍空間的陣列並把原來的陣列資料依次複製到新陣列中。 其實同學們這裡還可以拓展到 HashMap 的拓容,當元素大刀負載因子 0.75 的容量,HashMap 需要拓容為原來的兩倍然後再重新 把元素放到新陣列中。那麼時間複雜度又是多少呢? **關注公眾號 MageByte 後臺回覆 「add」獲取本題目答案,也可以回覆「加群」加入技術群跟我們一起分享你的看法,我們第一是時間反饋。** ![MageByte](https://magebyte.oss-cn-shenzhen.aliyuncs.com/wechat/Snip20200314_5.png) > 參考文獻:《資料結構與演算法之美