1. 程式人生 > >數據結構與算法之美05

數據結構與算法之美05

github spa 數組 額外 思路 發生 清空 技術 出現

最好最壞及平均時間復雜度

// n  表示數組  array  的長度
int find(int[] array, int n, int x) {
  int i = 0;
  int pos = -1;
  for (; i < n; ++i) {
    if (array[i] == x) 
        pos = i;
  }
  return pos;
}

這段代碼的功能是:長度為n的數組中,返回等於x的數組元素的下標,不等於則返回-1

缺點:全部元素遍歷,不夠高效

時間復雜度:O(n)

// n  表示數組  array  的長度
int find(int
[] array, int n, int x) { int i = 0; int pos = -1; for (; i < n; ++i) { if (array[i] == x) { pos = i; break; } } return pos; }

優化後時間復雜度則由x出現的位置決定。最好第一個元素,最差最後一個元素。

最好情況時間復雜度和最壞情況時間復雜度對應的都是極端情況下的代碼復雜度,發生的概率其實並不大。為了更好地表示平均情況下的復雜度,需要引入另一個概念:平均情況時間復雜度,後面我簡稱為平均時間復雜度。

例:

要查找的變量 x 在數組中的位置,有 n+1 種情況:在數組的 0 ~ n-1 位置中和不在數組中。我們把每種情況下,查找需要遍歷的元素個數累加起來,然後再除以 n+1 ,就可以得到需要遍歷的元素個數的平均值,

即:

技術分享圖片

由大O記號表示可知,可以省略掉系數,低階,常量,所以簡化後的時間復雜度為O(n).

這個結論雖然是正確的,但是計算過程稍微有點兒問題。剛講的這 n+1 種情況,出現的概率並不是一樣的。我們知道,要查找的變量 x ,要麽在數組裏,要麽就不在數組裏。這兩種情況對應的概率統計起來很麻煩,為了方便理解,我們假設在數組中與不在數組中的概率都為 1/2 。另外,要查找的數據出現在 0 ~ n-1 這 n 個位置的概率也是一樣的,為 1/n 。所以,根據概率乘法法則,要查找的數據出現在 0 ~ n-1 中任意位置的概率就是 1/(2n).

因此,前面的推導過程中存在的最大問題就是,沒有將各種情況發生的概率考慮進去。如果我們把每種情況發生的概率也考慮進去,那平均時間復雜度的計算過程就變成了這樣:

技術分享圖片

這個值就是概率論中的加權平均值,也叫作期望值,所以平均時間復雜度的全稱應該叫加權平均時間復雜度或者期望時間復雜度。 引入概率之後,前面那段代碼的加權平均值為 (3n+1)/4 。用大 O 表示法來表示,去掉系數和常量,這段代碼的加權平均時間復雜度仍然是 O(n) 。

由上可得:不是所有情況都要進行最好,最糟或平均時間復雜度的分析,量級相差較大時才進行區別。

均攤時間復雜度

例子:

// array  表示一個長度為  n  的數組
 //  代碼中的  array.length  就等於  n
 int[] array = new int[n];
 int count = 0;
 
 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.length 時,我們用 for 循環遍歷數組求和,並清空數組,將求和之後的sum 值放到數組的第一個位置,然後再將新的數據插入。但如果數組一開始就有空閑空間,則直接將數據插入數組。 最理想的情況下,數組中有空閑空間,我們只需要將數據插入到數組下標為 count 的位置就可以了,所以最好情況時間復雜度為 O(1) 。最壞的情況下,數組中沒有空閑空間了,我們需要先做一次數組的遍歷求和,然後再將數據插入,所以最壞情況時間復雜度為 O(n)。

平均復雜度分析:

假設數組的長度是 n ,根據數據插入的位置的不同,我們可以分為 n 種情況,每種情況的時間復雜度是 O(1) 。除此之外,還有一種 “ 額外 ” 的情況,就是在數組沒有空閑空間時插入一個數據,這個時候的時間復雜度是 O(n) 。而且,這 n+1 種情況發生的概率一樣,都是 1/(n+1) 。所以,根據加權平均的計算方法,我們求得的平均時間復雜度就是:

技術分享圖片

這個例子不需要引入概率論的知識。這是為什麽呢?我們先來對比一下這個 insert() 的例子和前面那個 find() 的例子,你就會發現這兩者有很大差別。 首先, find() 函數在極端情況下,復雜度才為 O(1) 。但 insert() 在大部分情況下,時間復雜度都為O(1) 。只有個別情況下,復雜度才比較高,為 O(n) 。這是 insert() 第一個區別於 find() 的地方。 第二個不同的地方,對於 insert() 函數來說, O(1) 時間復雜度的插入和 O(n) 時間復雜度的插入,出現的頻率是非常有規律的,而且有一定的前後時序關系,一般都是一個 O(n) 插入之後,緊跟著 n-1 個 O(1) 的插入操作,循環往復。 所以,針對這樣一種特殊場景的復雜度分析,我們並不需要像之前講平均復雜度分析方法那樣,找出所有的輸入情況及相應的發生概率,然後再計算加權平均值。

因此引入了攤還分析法,通過攤還分析得到的時間復雜度叫做均攤時間復雜度。

具體怎樣使用攤還分析法分析算法的均攤時間復雜度?

我們還是繼續看在數組中插入數據的這個例子。每一次 O(n) 的插入操作,都會跟著 n-1 次 O(1) 的插入操作,所以把耗時多的那次操作均攤到接下來的 n-1 次耗時少的操作上,均攤下來,這一組連續的操作的均攤時間復雜度就是 O(1) 。這就是均攤分析的大致思路。 均攤時間復雜度和攤還分析應用場景比較特殊,所以我們並不會經常用到。

應用場景

對一個數據結構進行一組連續操作中,大部分情況下時間復雜度都很低,只有個別情況下時間復雜 度比較高,而且這些操作之間存在前後連貫的時序關系,這個時候,我們就可以將這一組操作放在 一塊兒分析,看是否能將較高時間復雜度那次操作的耗時,平攤到其他那些時間復雜度比較低的操 作上。而且,在能夠應用均攤時間復雜度分析的場合,一般均攤時間復雜度就等於最好情況時間復 雜度。

一、復雜度分析的 4 個概念

最好情況時間復雜度:代碼在最理想情況下執行的時間復雜度。

最壞情況時間復雜度:代碼在最壞情況下執行的時間復雜度。

平均時間復雜度:用代碼在所有情況下執行的次數的加權平均值表示。

均攤時間復雜度:在代碼執行的所有復雜度情況中絕大部分是低級別的復雜度,個別情況是高級 別復雜度且發生具有時序關系時,可以將個別高級別復雜度均攤到低級別復雜度上。基本上均攤結 果就等於低級別復雜度。

二、為什麽要引入這 4 個概念?

  1. 同一段代碼在不同情況下時間復雜度會出現量級差異,為了更全面,更準確的描述代碼的時間復 雜度,所以引入這 4 個概念。

  2. 代碼復雜度在不同情況下出現量級差別時才需要區別這四種復雜度。大多數情況下,是不需要區 別分析它們的。

三、註意

  1. 平均時間復雜度 代碼在不同情況下復雜度出現量級差別,則用代碼所有可能情況下執行次數的加權平均值表示。

  2. 均攤時間復雜度 兩個條件滿足時使用:

    1 )代碼在絕大多數情況下是低級別復雜度,只有極少數情況是高級別復雜度;

    2 )低級別和高級別復雜度出現具有時序規律。均攤結果一般都等於低級別復雜度。

數據結構與算法之美05