C#算法設計之知識儲備

前言

算法的討論具有一定的規則,其中也包含一些不成文的約定,本博文旨在爲初學算法的同學指明一條通向算法的“不歸路”。


漸近記號

1、Θ(big-theta)

若存在正常量 c_{1}c_{2}n_{0} ,使得當 n\geq n_{0} 時,不等式 0\leq c_{1}g(n)\leq f(n)\leq c_{2}g(n) 恆成立,則稱g(n)是f(n)的一個漸近緊確界,記作Θ。它包含漸近上界漸近下界

簡單的理解爲在 n\geq n_{0} 時,f(n)被夾在 c_{1}g(n) 和 c_{2}g(n) 之間,c_{1}g(n) 爲f(n)的漸近下界,c_{2}g(n) 爲f(n)的漸近上界。

2、O(big-oh)

若存在正常量 c和n_{0} ,使得當 n\geq n_{0} 時,不等式 0\leq f(n)\leq cg(n) 恆成立,則稱g(n)是f(n)的一個漸近上界,記作O。

簡單的理解爲在 n\geq n_{0} 時,cg(n)總是在f(n)之上。

3、Ω(big-omege)

若存在正常量 c和n_{0} ,使得當 n\geq n_{0} 時,不等式 0\leq cg(n)\leq f(n) 恆成立,則稱g(n)是f(n)的一個漸近下界,記作Ω。

簡單的理解爲在 n\geq n_{0} 時,cg(n)總是在f(n)之下。

其它幾個記號在討論算法的複雜度時極少用到,故本博文不予介紹。

以下給出這3種常用記號的函數圖:

我們使用O表示算法在最壞的情況下所代表的時間複雜度,Ω表示算法在最好的情況下所代表的時間複雜度,Θ表示算法在平均情況下所代表的時間複雜度。

由於網絡中很多文章均沒有將這3個符號分清楚,比如我們討論冒泡排序的時間複雜度時,其在最壞的情況下的時間複雜度爲: O(n^{2}) ,在最好的情況下的時間複雜度爲: O(n^{2}) ,在平均情況下的時間複雜度也爲: O(n^{2}) 。這種記法是不正確的,但由於大家都這麼寫,本系列博文若未特殊說明,沿用此記法。


時間複雜度

在分析算法的時間複雜度時,我們一般分析其在最壞的情況下的關鍵代碼的執行次數。還以冒泡排序爲例,冒泡排序算法採用雙循環比較相鄰2個元素的值的大小,若符合預期則不管,若不符合預期則交換這2個元素的位置。設問題的規模爲n,那麼在最壞的情況下,即原數組逆序排列時,我們需要執行交換元素的代碼次數爲 n^{2} 次,所以我們認爲冒泡排序的時間複雜度爲: O(n^{2}) 。事實上冒泡排序在最壞的情況下執行的精確次數一般不是 n^{2} 次,而是略低於 n^{2} 次,因爲內循環可以被優化。每次外循環執行完畢後內循環可以少執行一次,因爲每次外循環執行完畢時,最大的值已經在最後面了(若我們需要升序排序),內循環無需處理後面的元素。具體分析可參考我的另一篇博文,C#算法設計排序篇之01-冒泡排序(附帶動畫演示程序)

還有另外幾個規則,在討論算法的時間複雜度時,我們更關心的是高階項,忽略低階項。例如一個算法在最壞的情況下的執行次數爲 n^{2}+n 次,那麼這個低階的 n 可以被忽略,其時間複雜度就是 O(n^{2}) 。因爲當 n 趨向於無窮大時,低階的 n 對整體的執行次數沒有多大影響。當一個算法執行的次數爲 n 時,那麼它的時間複雜度爲 O(n) ,也稱這個算法的時間複雜度爲線性的。n/2,n,2n,100000000n,都是線性的算法,均記爲 O(n) 。設一個算法的執行次數爲 cn 次,常量c被忽略,因爲當 n 趨向於無窮大時,常量對整體的執行次數沒有多大影響。

你可能想到另外一個問題,線性的 100000000n 和 n^{2} ,哪個效率更高?答案和你的問題規模有關。

若問題規模趨向於無窮大,線性的算法總是好於 n^{2} ,因爲高階函數的增長速度快於低階函數。對於 n^{2} 和 100000000n ,當問題的規模大於1億時,n^{2} 顯著的超越線性的 n ,但是若你的問題規模總是小於1億,那麼此時選擇 n^{2} 的算法是個不錯的選擇,因爲小於1億時, n^{2} 小於 100000000n 。不過現實開發中,你不可能設計出 100000000n 的線性算法,此處僅作討論。

若一個算法總是在某一確定時間內完成其運算,我們記作 O(1) ,並稱其時間複雜度爲常量的。典型的算法是哈希算法。我們一般認爲哈希算法的時間複雜度爲常量的(本博文不討論關鍵字衝突等問題)。

以下給出幾種常見的時間複雜度曲線圖以供參考:


 空間複雜度

與時間複雜度基本類似,本博文不予贅述。


基於比較的排序算法的下界

基於比較的排序算法的下界爲 O(n*logn) ,《算法導論》第3版中文版第107~108頁(英文版第191~192頁)使用決策樹模型證明了此下界,有興趣的同學可以自行觀摩。

常見的基於比較的排序算法有:冒泡排序、快速排序、直接插入排序、選擇排序、歸併排序、堆排序、希爾排序、二叉樹排序等。

非基於比較的以空間換時間的排序算法有:計數排序、基數排序、桶排序等。

其中快速排序、歸併排序、堆排序、二叉樹排序4種排序算法都達到了比較排序算法的下界,我們一般稱它們爲基於比較的先進算法

以上排序算法都可以在我的 C#算法設計概述 系列博文中找到,其中包含了時間複雜度的分析等。


排序算法的穩定性

假定在待排序的記錄序列中,存在多個具有相同的關鍵字的記錄,若經過排序,這些記錄的相對次序保持不變,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序後的序列中,r[i]仍在r[j]之前,則稱這種排序算法是穩定的;否則稱爲不穩定的。

即保證排序前2個相等的數其在序列的前後位置順序和排序後它們兩個的前後位置順序相同。

不穩定的排序算法有快速排序、直接選擇排序、希爾排序、堆排序,簡記爲快、選、希、堆。其它的常見排序算法均爲穩定性算法。

研究排序算法穩定性的意義是什麼呢?

若你只想給一個無序數組排序,那麼排序算法的穩定性沒有任何意義。

若你要給一組對象排序,那麼其可能是有意義的。例如一個學生對象包含學號和某次考試的總數2個字段,原來已經按學號排好序了(從小到大),現在讓你按分數給這個學生對象的數組排序,但是若分數相同,那麼學號小的依然排在前面,對於這種情況,你只能選擇快、選、希、堆以外的排序算法。


內部排序與外部排序

內部排序:

待排序記錄存放在計算機隨機存儲器中(說簡單點,就是內存)進行的排序過程。

外部排序:

待排序記錄的數量很大,以致於內存不能一次容納全部記錄,所以在排序過程中需要對外存進行訪問的排序過程。

假如你有1000億條整數數據(無法一次載入內存),這些數據都是存在硬盤上的,現在你要寫一個算法判定這1000億條數據是不是升降排列的(nums[k+1] >= nums[k]),你可以每次從硬盤取出一批數據(內存可承受的),逐一判定相鄰數據的大小關係,若不滿足nums[k+1] >= nums[k]的關係直接判定false,如果1000條數據全部可以取出並沒有被判定爲false,則可以判定爲true。


聯機算法

聯機算法是在任意時刻算法對要操作的數據只讀入(掃描)一次,一旦被讀入並處理,它就不需要在被記憶了。而在此處理過程中算法能對它已經讀入的數據立即給出相應子序列問題的正確答案。

聯機算法僅需要常量空間(不包含原數據)並以線性時間運行,因此聯機算法幾乎是完美的算法。


關於算法中的對數

數學中的對數:

對數是對求冪的逆運算,正如除法是乘法的倒數,反之亦然。

如果a的x次方等於N(a>0,且a不等於1),那麼數x叫做以a爲底N的對數(logarithm),記作 x=log_{a}N 。其中,a叫做對數的底數,N叫做真數。

  • 特別地,我們稱以10爲底的對數叫做常用對數(common logarithm),並記爲lg;
  • 稱以無理數e(e=2.71828...)爲底的對數稱爲自然對數(natural logarithm),並記爲ln;
  • 零沒有對數;
  • 在實數範圍內,負數無對數。 在複數範圍內,負數是有對數的。

算法中的對數:

考慮到計算機是二進制的,並且在很多算法中均出現以2作爲基數的思想,比如分治策略、二分查找、二叉樹等。所以在算法的討論中若沒有特別的說明 log(n) 是指以 2 爲底的 n 的對數,即 log_{2}(n) ,《算法導論》第3版中均記作 lg(n) ,一般圖書或博文中記作 log(n),它們都是指以 2 爲底的 n 的對數。


總結

以上即爲學習算法的前提知識儲備,本博文沒有討論過於深奧的數學知識,僅爲算法的初學者提供些許的幫助。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章