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


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

時間復雜度

  首先,學習數據結構是為了解決“快”和“省”的問題,那么如何去評估算法的速度快和省空間呢?這就需要掌握時間和空間復雜度分析。同一段代碼運行在不同環境、不同配置機器、處理不同量級數據…效率肯定不會相同。時間復雜度和空間復雜度是不運行代碼,從理論上粗略估計算法執行效率的方法。時間復雜度一般用O來表示,如下例子:計算1,2,3…n的和。CPU執行每行代碼時間很快,假設每行執行時間都一樣unit_time,第2行為一個unit_time,第3、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 }

第2、3、4行分別執行執行了一次,時間為3unit_time,第5、6兩行循環了n次為2n * unit_time,第7、8兩行執行了n*n次為(n²) * unit_time,所以總的執行時間為:(2n²+2n+3) * unit_time

可以看出來,所有代碼執行時間T(n)與每行代碼執行次數成正比。可以用如下公式來表示:

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

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

n表示數據規模大小;

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

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²)

因為時間復雜度是表示的一種趨勢,所以常常忽略常量、低階、系數,只需要最大階量級就可以了。

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

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

上面例子可以看出,復雜度忽略了低階、常量和系數,所以執行最多的那一段最能表達時間復雜度的趨勢。

2、加法法則:總復雜度等於各部分求和,然后取復雜度量級最高的

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

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

上面第二段代碼,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)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM