今天我會繼續給你講四個複雜度分析方面的知識點,最好狀況時間複雜度(best+case+time+complexity)、最壞狀況時間複雜度(worst+case+time+complexity)、平均狀況時間複雜度(average+case+time+complexity)、均攤時間複雜度(amortized+time+complexity)。若是這幾個概念你都能掌握,那對你來講,複雜度分析這部份內容就沒什麼大問題了。算法
上一節我舉的分析複雜度的例子都很簡單,今天咱們來看一個稍微複雜的。你能夠用我上節教你的分析技巧,本身先試着分析一下這段代碼的時間複雜度。數組
// 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; }
你應該能夠看出來,這段代碼要實現的功能是,在一個無序的數組(array)中,查找變量x出現的位置。若是沒有找到,就返回-1。按照上節課講的分析方法,這段代碼的複雜度是O(n),其中,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; }
這個時候,問題就來了。咱們優化完以後,這段代碼的時間複雜度仍是O(n)嗎?很顯然,我們上一節講的分析方法,解決不了這個問題。函數
由於,要查找的變量x可能出如今數組的任意位置。若是數組中第一個元素正好是要查找的變量x,那就不須要繼續遍歷剩下的n-1個數據了,那時間複雜度就是O(1)。但若是數組中不存在變量x,那咱們就須要把整個數組都遍歷一遍,時間複雜度就成了O(n)。因此,不一樣的狀況下,這段代碼的時間複雜度是不同的。學習
爲了表示代碼在不一樣狀況下的不一樣時間複雜度,咱們須要引入三個概念:最好狀況時間複雜度、最壞狀況時間複雜度和平均狀況時間複雜度。優化
顧名思義,最好狀況時間複雜度就是,在最理想的狀況下,執行這段代碼的時間複雜度。就像咱們剛剛講到的,在最理想的狀況下,要查找的變量x正好是數組的第一個元素,這個時候對應的時間複雜度就是最好狀況時間複雜度。blog
同理,最壞狀況時間複雜度就是,在最糟糕的狀況下,執行這段代碼的時間複雜度。就像剛舉的那個例子,若是數組中沒有要查找的變量x,咱們須要把整個數組都遍歷一遍才行,因此這種最糟糕狀況下對應的時間複雜度就是最壞狀況時間複雜度。element
咱們都知道,最好狀況時間複雜度和最壞狀況時間複雜度對應的都是極端狀況下的代碼複雜度,發生的機率其實並不大。爲了更好地表示平均狀況下的複雜度,咱們須要引入另外一個概念:平均狀況時間複雜度,後面我簡稱爲平均時間複雜度。it
平均時間複雜度又該怎麼分析呢?我仍是藉助剛纔查找變量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)。
那平均時間複雜度是多少呢?答案是 O(1)。咱們仍是能夠經過前面講的機率論的方法來分析。
假設數組的長度是+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)。這就是均攤分析的大體思路。你都理解了嗎?
均攤時間複雜度和攤還分析應用場景比較特殊,因此咱們並不會常常用到。爲了方便你理解、記憶,我這裏簡單總結一下它們的應用場景。若是你遇到了,知道是怎麼回事兒就好了。
對一個數據結構進行一組連續操做中,大部分狀況下時間複雜度都很低,只有個別狀況下時間複雜度比較高,並且這些操做之間存在先後連貫的時序關係,這個時候,咱們就能夠將這一組操做放在一起分析,看是否能將較高時間複雜度那次操做的耗時,平攤到其餘那些時間複雜度比較低的操做上。並且,在可以應用均攤時間複雜度分析的場合,通常均攤時間複雜度就等於最好狀況時間複雜度。
儘管不少數據結構和算法書籍都花了很大力氣來區分平均時間複雜度和均攤時間複雜度,但其實我我的認爲,均攤時間複雜度就是一種特殊的平均時間複雜度,咱們不必花太多精力去區分它們。你最應該掌握的是它的分析方法,攤還分析。至於分析出來的結果是叫平均仍是叫均攤,這只是個說法,並不重要。
今天咱們學習了幾個複雜度分析相關的概念,分別有:最好狀況時間複雜度、最壞狀況時間複雜度、平均狀況時間複雜度、均攤時間複雜度。之因此引入這幾個複雜度概念,是由於,同一段代碼,在不一樣輸入的狀況下,複雜度量級有多是不同的。
在引入這幾個概念以後,咱們能夠更加全面地表示一段代碼的執行效率。並且,這幾個概念理解起來都不難。最好、最壞狀況下的時間複雜度分析起來比較簡單,但平均、均攤兩個複雜度分析相對比較複雜。若是你以爲理解得還不是很深刻,不用擔憂,在後續具體的數據結構和算法學習中,咱們能夠繼續慢慢實踐!
課後思考
分析一下下面這個 add() 函數的時間複雜度。
// 全局變量,大小爲 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; }