資料結構與演算法(二)——複雜度分析(下)
資料結構與演算法(二)—— 複雜度分析(下)
除了前面記錄的複雜度的基礎知識,還有四個複雜度分析方面的知識點:最好情況時間複雜度、最壞情況時間複雜度、平均情況時間複雜度、均攤時間複雜度。
一、最好、最壞情況時間複雜度
- 最好情況時間複雜度,就是在最理想的情況下,執行這段程式碼的時間複雜度。
- 最壞情況時間複雜度,就是最糟糕的情況下執行這段程式碼的時間複雜度
先看一段程式碼
public int find(int[] array, int n,int x){ int pos = -1; for (int i = 0;i < n; i++){ if (array[i] == x ) pos = i ; } return pos; }
這是在一個無序的陣列中查詢x的位置,並返回x的下標,如果沒找到就返回-1。由於這段程式碼需要迴圈執行n次,所以時間複雜度為O(n)。
但是實際上,在陣列查詢一個數據時,不需要把整個陣列都遍歷,在找到所需要的資料之後就應該停止遍歷並返回資料的位置,所以,在優化程式碼之後,再來看它的時間複雜度。
public int find(int[] array, int n,int x){ int pos = -1; for (int i = 0;i < n; i++){ if (array[i] == x ){ pos = i ; break; } } return pos; }
在優化之後,在不同情況下break執行的時機不同,也就是說迴圈執行的次數是不能確定的。比如說,如果要查詢的資料x就是陣列的第一個元素,那麼在執行一次迴圈之後就退出迴圈並返回下標,不需要再遍歷剩下的n-1個數據了,此時時間複雜度就是O(1);而如果陣列不存在資料x或者資料x是陣列的最後一個元素,那麼就需要遍歷整個陣列,此時時間複雜度就是O(n)。
所以,為了表示程式碼在不同情況下的不同時間複雜度,就需要用最好情況時間複雜度、最壞情況時間複雜度和平均情況時間複雜度來表示。 在上面的程式碼中,最好情況時間複雜度就是在最理想的情況下,即陣列的第一個元素就是所要找的資料x的時間複雜度,為O(1);最壞情況時間複雜度就是在最糟糕的情況下,即陣列不存在資料x或者資料x是陣列的最後一個元素的時間複雜度,為O(n)。
二、平均情況時間複雜度
一般來說,最好情況時間複雜度和最壞情況時間複雜度對應的都是極端情況下的複雜度,也就是說發生的概率不大。所以為了更好地表示不同情況下程式碼的不同時間複雜度,需要進行平均情況的時間複雜度分析。
在上面程式碼的分析中,要查詢的x的位置有兩種情況,在陣列中和不在陣列中,假設兩種情況的概率都為1/2。而在陣列中又有n種情況,即存在於下標為0n-1中的任意一個位置,這n種情況的概率也是一樣,即在每個位置的概率都是1/n,根據概率乘法法則,要查詢的x出現在0n-1中任意位置的概率為1/(2n)。
然後,把每種情況下查詢需要遍歷的元素的個數累加起來,就能得到需要遍歷元素個數的平均值,即:
這個值就是需要遍歷元素個數或次數的加權平均值,也叫期望值,所以平均時間複雜度也叫加權平均時間複雜度或期望時間複雜度。
用大O表示法表示這個平均值,去掉係數和常量,得到這段程式碼的平均時間複雜度仍然為O(n)。
三、均攤時間複雜度
平均複雜度只在某些特殊情況下才會用到,而均攤時間複雜度應用的場景比它更加特殊、更加有限,比如在下面的例子中:
//往陣列中插入陣列,當陣列滿了即count==array.length成立時,用for迴圈遍歷陣列求和
//並清空陣列,將求和之後的sum值放在陣列的第一個位置,再將新的資料插入。
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的位置就可以了,所以最好情況時間複雜度為O(1); 在最壞的情況下,陣列沒有空閒空間,這時候需要先對陣列做遍歷求和,再將資料插入,所以最壞情況時間複雜度為O(n)。
至於平均時間複雜度,假設陣列的長度為n,根據資料插入的位置不同,就有n種插入情況,每種情況的時間複雜度都是O(1)。此外,還有一種情況,就是在陣列沒有空閒空間時插入一個數據,這時時間複雜度為 O(n)。由於每次操作都是要插入一個數據,都要事先判斷陣列是否空閒再進行插入操作,所以每種插入情況的概率都是相同的,由於一共有n+1種情況,所以每種情況發生的概率為1/(n+1)。 那麼,根據加權平均的計算方法,求得的平均時間複雜度就是:
即平均時間複雜度為O(1)。
那麼這種情況和上面分析的find()函式程式碼的平均時間複雜度有什麼不同?
- 首先find()函式在極端情況下,複雜度才為O(1),但這裡的insert()在大部分情況下,時間複雜度都為O(1),只有在一種情況就是陣列沒有空閒空間的情況下,複雜度才比較高,為O(n)。
- 對於insert()函式,O(1)時間複雜度的插入和O(n)時間複雜度的插入,出現的頻率是有規律的,而且有一定的前後時序關係:在n+1次插入操作中,必然有1次O(n)時間複雜度的插入和n次O(1)時間複雜度的插入,而且一般是在一個O(n)時間複雜度的插入之後,緊跟著n個O(1)時間複雜度的插入,或者順序相反。而find()函式中陣列的遍歷次數是隨機的,即時間複雜度變化的規律是不確定的。
所以,為了更方便地分析類似insert()函式這種特殊場景的時間複雜度,就需要使用攤還分析法,通過攤還分析法得到的時間複雜度成為均攤時間複雜度。
使用攤還分析法分析
在insert()函式中,每一次O(n)的插入操作,都會跟著n-1次O(1)的插入操作,所以可以把耗時多的那次操作均攤到接下來的n-1次耗時少的操作上,均攤下來,這一組連續的操作的均攤時間複雜度就是O(1),這就是均攤分析的大致思路。
也就是說,對一個數據結構進行一組連續操作中,大部分情況下時間複雜度都很低,只有個別情況下時間複雜度比較高,而且這些操作之間存在前後連貫的時序關係,這個時候就可以將這一組操作放在一起分析,看是否能將較高時間複雜度的那次操作的耗時,平攤到其他那些時間複雜度較低的操作上。一般均攤時間複雜度就是較低級別的時間複雜度或最好時間複雜度。