1. 程式人生 > >【一起學習排序演算法】1 演算法特性及大O記法

【一起學習排序演算法】1 演算法特性及大O記法

本系列的文章列表和相關說明,請檢視【一起學習排序演算法】0 序言
也可以直接到github上檢視完整的文章和原始碼!

排序演算法

排序演算法(Sorting algorithms)是什麼? Wikipedia 如是說:

In computer science, a sorting algorithm is an algorithm that puts elements of a list in a certain order.

也就是說,排序演算法,就是某種演算法,將列表中的元素按照某種規則排序。常見的如數字大小排序、字典序排序等。本系列例子約定為從小到大的數字排序,其他的類似,關鍵在於思路。

演算法特性

1、內部排序和外部排序

按照陣列規模的大小,排序可以分為內部排序外部排序
內部排序(internal sorting): 全部陣列都可放在記憶體中排序。
外部排序(external sorting): 陣列太大,不能全部放在記憶體中,部分資料在硬碟中。

本系列約定為內部排序,關於海量資料的排序,後續補充。

2、穩定性
排序法的穩定性(stability): 取決於值相等的兩個元素,排序之後是否保持原來的順序。

3、比較排序和非比較排序
比較排序(comparison sort):
比較排序中,每一步通過比較元素大小來決定元素的位置。其複雜度由比較次數交換次數來決定。比較排序比較好實現,但是時間複雜度無法突破O(nlogn)

。證明過程,可以參考這篇文章

非比較排序(non-comparison sort):
非比較排序,如桶排序,不通過比較,後續將會講解。這類演算法可以突破O(nlogn)

排序演算法有很多種,每一種都各自有自己的優點缺點和不同的應用場景,沒有一種排序是絕對完美的。如何評價一個演算法的優劣呢,我們通過演算法複雜度來衡量。

演算法複雜度

演算法複雜度(complexity),可以從時間複雜度空間複雜度兩個維度來考慮。
空間複雜度,是指演算法所需要的額外的儲存單元。目前的硬體條件,這一塊通常可以不考慮了。演算法優化,更多是來優化演算法的時間。

下面將介紹如何來估算時間複雜度。下面的介紹的方法,目前只夠勉強說服我自己。如果覺得不想了解這個理論,可以直接記住下面的結論。如果覺得講得不是那麼容易懂,可以參考別的資料仔細研究。

時間複雜度

如果一個列表的大小為n,則演算法耗費的時間T(n)。但是由於機器、CPU等的不同,同一個演算法執行的時間可能都不一樣。所以通常不是按耗費的時間來計算,而是用某個演算法實現的指令執行的次數,來衡量時間複雜度。如下面這個程式:

for( i = 0; i < n; i++)   // i = 0; 執行1次
       			  // i < n; 執行n+1次
			  // i++    執行n次
  sum = sum + i;          //    執行n次
  
// 總次數f(n) = 1 + n+1 + n +n = 3n+2
複製程式碼

通過上面計數運算元的方法,顯得很麻煩。所以通常是通過一個函式來估算,確保它是演算法運算元f(n)的上界。這種方法就是大O記法

大O記法

對於單調函式 f(n) 和 g(n), n為正整數,如果存在常數c > 0, n0 > 0,且

f(n) ≤ c * g(n), n ≥ n_0

則我們稱

f(n) = O(g(n))

如下圖所示。

大O記法

簡單來說,就是當n→∞時,f(n)的增長率不大於g(n),也就是說g(n)時f(n)的上界。 在這裡,f(n)就是演算法的指令運算元,而g(n)就是我們估算的複雜度上界。 它還有兩個特性。

O(g1(N)) + O(g2(N)) = max(O(g1(N)), O(g2(N)))
O(g1(N)) * O(g2(N)) = O(g1(N) * g2(N))

所以,上面程式的時間複雜度是:

f(n) = 3n+1 = O(1) + O(n) + O(n) + O(n) = O(n)
  • 常數時間 O(1)
    常數時間(constant time),演算法的執行時間和列表大小無關。

  • 線性時間 O(n)
    線性時間(linear time), 演算法執行時間和列表大小成正比。

  • 對數時間 O(logn)
    對數時間(logarithmic time), 稍微顯得難理解一點。不過如果你瞭解對數,其實也很簡單。例如二分查詢,每一次查詢都會去掉一半的元素,但最後一次元素個數就是1。假設陣列大小為n, 要經過x輪查詢,則

    n * (1/2)^x = 1
    x = log_{2}n

logn是簡寫,一般忽略底數。

  • 二次項時間 O(n2)
    二次項時間(quadratic time), 通常是兩層迴圈的演算法。

簡易估算方法

對於一個演算法的時間複雜度,根據以上理論,大體按下面的步驟來估算複雜度。 以這個程式為例:

sum = 0;            
for( i = 0; i < n; i++)
    for( j = i; j < n; j++)
        sum++;
複製程式碼

1. 忽略簡單語句
對於簡單複雜語句,它執行次數是一個常數,複雜度為O(1)。如果還存在迴圈,O(1)對結果不影響。

2. 關注迴圈語句
對於迴圈語句,要認真分析其迴圈執行的次數。例子中,外層迴圈要執行 n 次,內層迴圈要

n + (n-1) + ... + 2 + 1 = (n + 1)/2

所以總次數T(n)為

T(n) = n * (n+1)/2 = 1/2*n^2 + 1/2*n

3. 忽略常數項,保留高次項
對於一個多項式,當n→∞時,完全由最高項次決定。所以

T(n) = O(1/2*n^2 + 1/2*n) = O(n^2 + n) = O(n^2)

對於有的程式,複雜度還是很不好計算。所以要多練習,寫一個程式之後,自己主動去算一下它的複雜度,慢慢就熟練了。

演算法評價

對於排序演算法,一個演算法的執行效能,和輸入的資料有很大的關係。對於某些特定的資料,某些演算法的效率很高,但通常演算法的效能又很低。所以通常存在:

  • 最優時間複雜度:某些資料,執行的次數最少
  • 最差時間複雜度:某些資料,執行的次數最多
  • 平均時間複雜度:平均需要執行的次數

通常還是以平均時間複雜度,來衡量演算法。例如氣泡排序,當陣列元素有序時,最優時間複雜度為O(n)。當逆序是,為O(n2)。平均還是O(n2)。演算法複雜度的優劣,可以參考此圖:

演算法比較

總結

本章節主要介紹了一下排序演算法的型別,以及如果通過大O記法來評價一個演算法。對於如何計算演算法的時間複雜度,很多人都感覺很頭疼。我給的建議是,按照上面的步驟多練習,多去主動算程式的時間複雜度。這樣慢慢自己就會掌握技巧,並且提醒自己保證自己程式的執行效率。共勉!

資源與參考

[1] About the #sorting-algorithms series
[2] 凱耐基梅隆大學資料結構與演算法-排序演算法
[3] CMU algorithm complexity
[4] Big O cheat sheet
[5] You need to understand Big O notation, now