資料結構與演算法(五)
排序演算法
排序也稱 排序演算法(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+10
和2n²
隨著 n 變大,執行曲線無限接近,可以忽略3n+10
n²+5n+20
和n²
隨著 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²+7n
和3n² + 2n
,執行曲線重合, 說明:這種情況下, 5 和 3 可以忽略對於 2 次方來說,數量級很大的情況下,係數不是很重要
-
而
n^3+5n
和6n^3+4n
,執行曲線分離,說明 多少次方是關鍵
總結
時間頻度計算還與以下三個統計注意事項:
-
忽略常數項
2n+20
和 2n 隨著 n 變大,執行曲線無限接近, 20 可以忽略3n+10
和 3n 隨著 n 變大,執行曲線無限接近,10 可以忽略
-
忽略低次項
2n²+3n+10
和2n²
隨著 n 變大,執行曲線無限接近,可以忽略3n+10
n²+5n+20
和n²
隨著 n 變大,執行曲線無限接近,可以忽略5n+20
-
忽略係數
-
隨著 n 值變大,
5n²+7n
和3n² + 2n
,執行曲線重合, 說明:這種情況下, 5 和 3 可以忽略對於 2 次方來說,數量級很大的情況下,係數不是很重要(筆者怎麼覺得相差也挺多的?是在對於後面更大的來說,看起來重合了而已)
-
而
n^3+5n
和6n^3+4n
,執行曲線分離,說明 多少次方是關鍵對於 3 次方來說,係數就不能省略了,次方越大,係數也越大的時候,相差其實是很大的
-
時間複雜度
-
一般情況下:
演算法中的 基本操作語句的重複執行次數是問題規模 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)
,然後計算它的世界複雜度 -
T(n)
不同,但時間複雜度可能相同。如:
T(n) = n²+7n+6
與T(n) = n²+2n+2
,他們的T(n)
不同,但時間複雜度相同,都為O(n²)
。 過程是這樣:f(n) = n² ; // 去掉了常數和係數,轉換為 f(n) 函式 O(f(n)) = O(n²)
時間頻度中說過,當 n 變大,係數和常數可以忽略
-
計算時間複雜度的方法
-
用常數 1 代替執行時間中的所有加法常數
T(n)=n²+7n+6 => T(n)=n²+7n+1
-
修改後的執行次數函式中,只保留最高階項
T(n)=n²+7n+1 => T(n) = n²
-
去除最高階項的係數
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)
平均時間複雜度和最壞時間複雜度
-
平均時間複雜度
指所有可能的輸入例項 均以等概率出現 的情況下,該演算法的執行時間
-
最壞時間複雜度
最壞情況下的世界複雜度稱為最壞時間複雜度。
一般討論的時間複雜度均是最壞情況下的世界複雜度,因為:最壞情況下的時間複雜度是演算法在 任何輸入例項上執行時間的界限,這就保證了演算法的執行時間不會比最壞情況更長
平均時間複雜度和最壞時間複雜度是否一致,和演算法有關,如下圖:
演算法空間複雜度
類似於時間複雜度的討論,一個演算法的 空間複雜度(Space Complexity) 定義為:該演算法所耗費的儲存空間,它也是問題規模 n 的函式
空間複雜度是對一個演算法在執行過程中 臨時佔用儲存空間大小的度量。有的演算法 需要佔用的臨時工作單元數 與 解決問題的規模 n 有關,它隨著 n 的增大而增大,當 n 較大時,將佔用較多的儲存單元。例如:快速排序和歸併排序演算法就屬於這種情況
在做演算法分析時,主要討論的是 時間複雜度。從使用者體驗上看,更看重的是 程式執行的速度。如一些快取產品(redis、memcache) 和演算法(基數排序)本質就是 用空間換時間。