1. 程式人生 > 其它 >資料結構與演算法(五)

資料結構與演算法(五)

排序演算法

排序也稱 排序演算法(Sort Algorithm),排序是將一組資料,依指定的順序進行排列的過程。

排序演算法的分類

分兩類:內部排序、外部排序。

  • 內部排序:

    指將需要處理的所有資料,都載入到 內部儲存器(記憶體) 中進行排序

  • 外部排序:

    資料量過大,無法全部載入到記憶體中,需要藉助 外部儲存(檔案等)進行排序。

常見的排序演算法分類

  • 內部排序(記憶體)
    • 插入排序
      • 直接插入排序
      • 希爾排序
    • 選擇排序
      • 簡單選擇排序
      • 堆排序
    • 交換排序
      • 氣泡排序
      • 快速排序
    • 歸併排序
    • 基數排序
  • 外部排序(使用記憶體和外存結合)

演算法時間複雜度

衡量演算法的效能的好壞,可以使用時間時間複雜度

度量 一個程式(演算法)執行時間的兩種方法:

  • 事後統計法

    簡單說:就是把程式執行起來,然後檢視執行完成的總時間。

    但是有一個問題:所統計的時間,依賴於計算機的硬體、軟體等環境因素。如果要使用這種方式,需要在同一臺計算機相同狀態下執行程式,才能比較哪個演算法速度更快

  • 事前估演算法

    通過分析某個 演算法的時間複雜度 來判斷哪個演算法更優

時間頻度

一個演算法 花費的時間 與演算法中 語句的執行次數 成正比,哪個演算法中語句執行次數多,它花費時間就多。一個演算法中的 語句執行次數稱為語句頻度或時間頻度記為 T(n)

舉例-基本案例

計算 1-100 所有數字之和,下面有兩種演算法:

  • 演算法 1:迴圈累加

    int resulu = 0;
    int end = 100;
    for(int i = 1; i <= end; i++){
        result += i;
    }
    

    T(n) = n + 1,這裡的 n=100,因為要迴圈 100 次,還有一次,是跳出迴圈的判斷

  • 演算法 2:直接計算

    result = (1 + end)*end/2;
    

    T(n) = 1

對於時間頻度,有如下幾個方面可以忽略

忽略常數項
n T(n)=2*n+20 T(n)=2*n T(3*n+10) T(3*n)
1 22 2 13 3
2 24 4 16 6
5 30 10 25 15
8 36 16 34 24
15 50 30 55 45
30 80 60 100 90
100 220 200 310 300
300 620 600 910 900

上表對應的曲線圖如下

結論:

  • 2n+20 和 2n 隨著 n 變大,執行曲線無限接近, 20 可以忽略
  • 3n+10 和 3n 隨著 n 變大,執行曲線無限接近,10 可以忽略
忽略低次項
n T(n)=2n²+3n+10 T(2n²) T(n²+5n+20) T(n²)
1 15 2 26 1
2 24 8 34 4
5 75 50 70 25
8 162 128 124 64
15 505 450 320 225
30 1900 1800 1070 900
100 20310 20000 10520 10000
  • 2n²+3n+102n² 隨著 n 變大,執行曲線無限接近,可以忽略 3n+10
  • n²+5n+20 隨著 n 變大,執行曲線無限接近,可以忽略 5n+20
忽略係數
n T(3n²+2n) T(5n²+7n) T(n^3+5n) T(6n^3+4n)
1 5 12 6 10
2 16 34 18 56
5 85 160 150 770
8 208 376 552 3104
15 705 1230 3450 20310
30 2760 4710 27150 162120
100 30200 50700 1000500 6000400

結論:

  • 隨著 n 值變大,5n²+7n3n² + 2n ,執行曲線重合, 說明:這種情況下, 5 和 3 可以忽略

    對於 2 次方來說,數量級很大的情況下,係數不是很重要

  • n^3+5n6n^3+4n ,執行曲線分離,說明 多少次方是關鍵

總結

時間頻度計算還與以下三個統計注意事項:

  • 忽略常數項

    • 2n+20 和 2n 隨著 n 變大,執行曲線無限接近, 20 可以忽略
    • 3n+10 和 3n 隨著 n 變大,執行曲線無限接近,10 可以忽略
  • 忽略低次項

    • 2n²+3n+102n² 隨著 n 變大,執行曲線無限接近,可以忽略 3n+10
    • n²+5n+20 隨著 n 變大,執行曲線無限接近,可以忽略 5n+20
  • 忽略係數

    • 隨著 n 值變大,5n²+7n3n² + 2n ,執行曲線重合, 說明:這種情況下, 5 和 3 可以忽略

      對於 2 次方來說,數量級很大的情況下,係數不是很重要(筆者怎麼覺得相差也挺多的?是在對於後面更大的來說,看起來重合了而已)

    • n^3+5n6n^3+4n ,執行曲線分離,說明 多少次方是關鍵

      對於 3 次方來說,係數就不能省略了,次方越大,係數也越大的時候,相差其實是很大的

時間複雜度

  1. 一般情況下:

    演算法中的 基本操作語句的重複執行次數是問題規模 n 的某個函式,用 T(n) 表示(就是前面的時間頻度)

    若有某個輔助函式 f(n),使得當 n 趨近於無窮大時,T(n)/f(n) 的極限值為不等於零的常數。(前面的頻度分好幾種,比如 T(n) = n+1,那麼 f(n) = n,他們相除的話,就差不多是 1),則稱 f(n)是 T(n) 的同數量級函式,記作 T(n)=O(f(n))簡稱 O(f(n)) 為演算法的漸進時間複雜度,簡稱時間複雜度

    理解起來就差不多是,將時間頻度的計算找到一個可以簡寫的函式 f(n),然後計算它的世界複雜度

  2. T(n) 不同,但時間複雜度可能相同。

    如:T(n) = n²+7n+6T(n) = n²+2n+2,他們的 T(n) 不同,但時間複雜度相同,都為 O(n²)。 過程是這樣:

    f(n) = n² ; // 去掉了常數和係數,轉換為 f(n) 函式
    O(f(n)) = O(n²)
    

    時間頻度中說過,當 n 變大,係數和常數可以忽略

  3. 計算時間複雜度的方法

    1. 用常數 1 代替執行時間中的所有加法常數 T(n)=n²+7n+6 => T(n)=n²+7n+1

    2. 修改後的執行次數函式中,只保留最高階項 T(n)=n²+7n+1 => T(n) = n²

    3. 去除最高階項的係數 T(n) = n² => T(n) = n² => O(n²)n2 的係數是 1,1n² = n²

常見時間複雜度

  • 常數階 O(1)

  • 對數階 O(log2n)

  • 線性階 O(n)

  • 線性對數階 O(nlog2n)

  • 平方階 O(n²)

    比如:雙層巢狀 for 迴圈

  • 立方階 O(n3)

    比如:3 層巢狀 for 迴圈

  • k 次方階 O(n^k)

    比如:嵌套了 k 次的 for 迴圈

  • 指數階 O(2^n)

以上常見的複雜度排列順序是由小較大排列的,隨著問題規模 n 的不斷增大,上述時間複雜度不斷增大,演算法的執行效率越低

上圖的 指數階,就是 2^n,當 n 不是很大的時候,就猛的往上走了,可見當出現了指數階的時候,這個演算法基本上是最慢的。上圖在 n 為 10 的時候,就已經遠遠高於其他的複雜度了

常數階

無論程式碼執行了多少行,只要是沒有迴圈等複雜結構,那麼這個程式碼的時間複雜度就都是 O(1)

result = (1 + end)*end/2;

上述程式碼在執行的時候,它消耗的時間並不隨著某個變數的增長而增長(比如 i 和 j 的數值變大或變小,它的執行時間都是差不多的,不像迴圈次數那樣,增大就多執行一次)。那麼物理這類程式碼有多長,即使有幾萬幾十萬行,都可以用 O(1) 來表示它的時間複雜度。

對數階
int i = 1;
while(i < n){
  i = i * 2;   // 以 2 為底,這裡的演算法恰好是 * 2
}

在 while 迴圈裡面,每次都將 i 乘以 2,乘完之後,i 距離 n 就越來越近了。假設迴圈 x 次之後,i 就大於 2 了,此時這個迴圈就退出了,也就是說 2 的 x 次方等於 n,那麼 x = log2n 也就是說當迴圈 log2n 次以後,這個程式碼就結束了。因此這個程式碼的時間複雜度為:O(log2n) 。 O(log2n) 的這個 2 時間上是根據程式碼變化的,i = i * 3 ,則是 O(log3n) .

繼續說明,假設上面的 n = 1024,這個是規模問題 n,它執行幾次結束?使用這裡的對數階則為 log2 1024 = 10 (2^10 = 1024),所以規模問題雖然很大,但是對於對數階來說說,執行次數並沒有那麼大

線性階
for(i = 1; i <= n ; ++i){
  j = i;
  j++
}

for 迴圈裡的程式碼會執行 n 遍,因此它消耗的時間是隨著 n 的變化而變化的,因此這類程式碼都可以用 O(n) 來表示它的時間複雜度

線性對數階
for(m = 1; m < n; m++){
  i = 1;
  while(i < n){
    i = i * 2
  }
}

線性對數階 O(nlog2n) 其實非常容易理解,將實際複雜度 線性對數階 O(log2n) 的程式碼迴圈 N 遍,那麼它的時間複雜度就是 n * O(log2n),也就是 O(nlog2n)

平方階
for(x = 1; i <=n; x++){
  for (i = 1; i <= n; i++){
    j = i;
    j++;
  }
}

平方階 O(n²) 就更容易理解了,如果把 O(n) 的程式碼再巢狀迴圈一遍,它的時間複雜度就是 O(n²),這段程式碼其實就是嵌套了 2 層 n 迴圈,它的時間複雜度就是 O(n x n),即 O(n²) 如果將其中一層迴圈的 n 改成 m,那它的時間複雜度就變成了 O(m x n)

平均時間複雜度和最壞時間複雜度

  1. 平均時間複雜度

    指所有可能的輸入例項 均以等概率出現 的情況下,該演算法的執行時間

  2. 最壞時間複雜度

    最壞情況下的世界複雜度稱為最壞時間複雜度。

    一般討論的時間複雜度均是最壞情況下的世界複雜度,因為:最壞情況下的時間複雜度是演算法在 任何輸入例項上執行時間的界限,這就保證了演算法的執行時間不會比最壞情況更長

平均時間複雜度和最壞時間複雜度是否一致,和演算法有關,如下圖:

演算法空間複雜度

類似於時間複雜度的討論,一個演算法的 空間複雜度(Space Complexity) 定義為:該演算法所耗費的儲存空間,它也是問題規模 n 的函式

空間複雜度是對一個演算法在執行過程中 臨時佔用儲存空間大小的度量。有的演算法 需要佔用的臨時工作單元數解決問題的規模 n 有關,它隨著 n 的增大而增大,當 n 較大時,將佔用較多的儲存單元。例如:快速排序和歸併排序演算法就屬於這種情況

在做演算法分析時,主要討論的是 時間複雜度。從使用者體驗上看,更看重的是 程式執行的速度。如一些快取產品(redis、memcache) 和演算法(基數排序)本質就是 用空間換時間