學好數據結構和算法 —— 複雜度分析

複雜度也稱爲漸進複雜度,包括漸進時間複雜度和漸進空間複雜度,描述算法隨數據規模變化而逐漸變化的趨勢複雜度分析是評估算法好壞的基礎理論方法,因此掌握好複雜度分析方法是頗有必要的。算法

時間複雜度

  首先,學習數據結構是爲了解決「快」和「省」的問題,那麼如何去評估算法的速度快和省空間呢?這就須要掌握時間和空間複雜度分析。同一段代碼運行在不一樣環境、不一樣配置機器、處理不一樣量級數據…效率確定不會相同。時間複雜度和空間複雜度是不運行代碼,從理論上粗略估計算法執行效率的方法。時間複雜度通常用O來表示,以下例子:計算1,2,3…n的和。CPU執行每行代碼時間很快,假設每行執行時間都同樣unit_time,第2行爲一個unit_time,第三、4行都執行了n遍,那麼下面這段代碼執行的耗時時間能夠這麼計算:(1+2*n) * unit_time。數組

1     public int sum(int n) { 2 int sum = 0; 3 for (int i = 1; i <= n; i++) { 4 sum = sum + i; 5  } 6 return sum; 7 }

相似的再看一個例子:數據結構

 1     public int sum(int n) {  2 int sum = 0;  3 int i = 1;  4 int j;  5 for (; i <= n; i++) {  6 j = 1;  7 for (; j <= n; j++) {  8 sum = sum + i * j;  9  } 10  } 11 return sum; 12 }

第二、三、4行分別執行執行了一次,時間爲3unit_time,第五、6兩行循環了n次爲2n * unit_time,第七、8兩行執行了n*n次爲(n²) * unit_time,因此總的執行時間爲:(2n²+2n+3) * unit_time學習

能夠看出來,全部代碼執行時間T(n)與每行代碼執行次數成正比。能夠用以下公式來表示:spa

T(n) = O(f(n))code

T(n)表示代碼的執行時間;blog

n表示數據規模大小;it

f(n)表示每行代碼執行的次數和,是一個表達式;class

O表示執行時間T(n)和f(n)表達式成正比效率

那麼上面兩個時間複雜度能夠表示爲:

T(n) = O(1+2*n) 和 T(n) = O(2n²+2n+3)

實際上O並不表示具體的執行時間,只是表示代碼執行時間隨數據規模變化的趨勢,因此時間複雜度其實是漸進時間複雜度的簡稱。當n很大時,係數對結果的影響很小能夠忽略,上面兩個例子的時間複雜度能夠粗略簡化爲:

T(n) = O(n) 和 T(n) = O(n²)

由於時間複雜度是表示的一種趨勢,因此經常忽略常量、低階、係數,只須要最大階量級就能夠了。

分析時間複雜度的幾個常見法則

一、只關注代碼執行最多的一段代碼

上面例子能夠看出,複雜度忽略了低階、常量和係數,因此執行最多的那一段最能表達時間複雜度的趨勢。

二、加法法則:總複雜度等於各部分求和,而後取複雜度量級最高的

仍是上面的例子,總的時間複雜度等於各部分代碼時間複雜度的和,求和以後再用最能表達趨勢的項來表示整段代碼的時間複雜度。

三、乘法法則:嵌套代碼複雜度等於嵌套內外代碼複雜度的乘積

上面第二段代碼,j 循環段嵌套在 i 循環內部,因此 j 循環體內的時間複雜度等於單獨 i 的時間複雜度乘以單獨 j 的時間複雜度。

常見的時間複雜度表示

常見的複雜度有如下幾種

  • 常量階:O(1)
  • 對數階:O(logn)
  • 線性階:O(n)
  • 線性對數階:O(nlogn)
  • 平方階:O(n²)、立方階O(n³)……
  • 指數階:O(2ⁿ)
  • 階乘階:O(n!)

能夠這麼來理解:若是一段代碼有1000或10000行甚至更多,行數是一個常量,不會隨着數據規模增大而變化,咱們就認爲時間複雜度爲一個常量,用O(1)表示。

這幾種複雜度效率曲線比較

模擬一個數組動態擴容例子,若是數組長度夠,直接往裏面插入一條數據;反之,將數組擴充一倍,而後往裏面插入一條數據:

 1     int[] arr = new int[10];
 2     int len = arr.length;
 3     int i = 0;
 4     public void add(int item) {
 5         if (i >= len) {
 6             int[] new_arr = new int[len * 2];
 7             for (int i = 0; i < len; i++) {
 8                 new_arr[i] = arr[i];
 9             }
10             arr = new_arr;
11             len = arr.length;
12         }
13         arr[i] = item;
14         i++;
15     }

最好時間複雜度(best case time complexity)

  最好狀況下某個算法的時間複雜度。最好狀況下,數組空間足夠,只須要執行插入數據就能夠了,此時時間複雜度是O(1)。

最壞時間複雜度(worst case time complexity)

  最壞狀況下某個算法的時間複雜度。最壞狀況下數組滿了,須要先申請一個空間爲原來兩倍的數組,而後將數據拷貝進去,此時時間複雜度爲O(n)。通常狀況下咱們說算法複雜度就是指的最壞狀況時間複雜度,由於算法時間複雜度不會比最壞狀況複雜度更差了。

平均時間複雜度(average case time complexity)

  最好時間複雜度和最壞時間複雜度都是極端狀況下的時間複雜度,發生的機率並不算很大。平均時間複雜度是描述各類狀況下平均的時間複雜度。上面的動態擴容例子將1到n+1次爲一組來分析,前面n次的時間複雜度都是1,第n+1次時間複雜度是n,將一個數插入數組裏的1 至 (n+1)個位置機率都爲1/(n+1),因此平均時間複雜度爲:

  O(n) = (1 + 1 + 1 + …+n)/(n+1) = O(1)

均攤時間複雜度(amortized time complexity)

  對一個數據結構進行一組連續的操做中,大部分狀況下時間複雜度都很低,只有個別狀況下時間複雜度比較高,並且這些操做之間存在先後連續的關係。而且和這組數據類型的狀況循環往復出現,這時候能夠將這一組數據做爲一個總體來分析,看看是否能夠將最後一個耗時的操做複雜度均攤到其餘的操做上,若是能夠,那麼這種分析方法就是均攤時間複雜度分析法。上面的例子來說,第n+1次插入數據時候,數組恰好發生擴容,時間複雜度爲O(n),前面n次恰好將數組填滿,每次時間複雜度都爲O(1),此時能夠將第n+1次均攤到前面的n次上去,因此總的均攤時間複雜度仍是O(1)。

空間複雜度

 類比時間複雜度,以下代碼所示,第2行申請了一個長度爲n的數據,第三行申請一個變量i爲常量能夠忽略,因此空間複雜度爲O(n)

1     public void init(int n) {
2         int[] arr = new int[n];
3         int i = 0;
4         for (; i < n; i++) {
5             arr[i] = i + 1;
6         }
7     }

通常狀況下,一個程序在機器上執行時,除了須要存儲程序自己的指令、常數、變量和輸入數據外,還須要存儲對數據操做的存儲單元,若輸入數據所佔空間只取決於問題自己,和算法無關,這樣只須要分析該算法在實現時所需的輔助單元便可。若算法執行時所需的輔助空間相對於輸入數據量而言是個常數,則稱此算法爲原地工做,空間複雜度爲O(1)。

相關文章
相關標籤/搜索