七大常見的算法思想


0. 算法簡介

1. 枚舉

2. 迭代

3. 遞歸

4. 分治

5. 動態規划

6. 貪心

7. 回溯

 

 

0. 算法簡介

算法的概念

算法,簡單來說就是利用計算機解決問題的步驟。狹義的來講,算法可看作是數據傳遞和處理的方法,就像是各種排序算法等。算法的應用不單體現在編程中,廣義的來講,算法更像是種事物運行的邏輯和規則。

算法必須具備 5 個重要條件:

  1. 輸入:0 個或多個輸入數據,這些輸入必須有清楚的描述或定義。
  2. 輸出:至少會有一個輸出結果,不可以沒有輸出結果。
  3. 明確性:每一個質齡或步驟必須是簡潔明確的。
  4. 有效性:步驟清楚且可行,能讓用戶用紙筆計算而求出答案。
  5. 有限性:在有限步驟后一定會結束,不能無限循環。

數據結構可以看作是算法實現的容器,通過一系列特殊結構的數據集合,能夠將算法更為高效而可靠地執行起來。即數據結構是為算法服務的,具體選擇什么數據結構,與期望支持的算法(操作)有關。

數據結構和相關的算法就是數據進入計算機進行處理的一套完整邏輯。

算法效率的度量

時間復雜度(Time Complexity)

時間復雜度決定了運行時間的長短。一個算法花費的時間與算法中語句的執行次數成正比。

空間復雜度(Space Complexity)

空間復雜度決定了計算時所需的空間資源多少,是對一個算法在運行過程中臨時占用存儲空間大小的度量。

一個算法在計算機存儲上所占用的存儲空間包括 3 個方面:

  1. 存儲算法本身所占用的存儲空間(與算法書寫的長短成正比)。

  2. 算法的輸入輸出數據所占用的存儲空間(由要解決的問題決定,是通過函數調用而來的,不隨算法不同而改變)。

  3. 算法在運行過程中臨時占用的存儲空間(因算法而異,有的算法只需要占用少量的臨時工作單元,而且不隨問題規模的大小而改變,稱這種算法為“就地”(in-place)進行的,是節省存儲的算法)。

 

1. 枚舉

首先,最為簡單的思想,枚舉算法。枚舉也叫窮舉,顧名思義,就是窮盡列舉。枚舉思想的應用場景十分廣泛,也非常容易理解。簡單來說,枚舉就是將問題所有可能的解,按照不重復、不遺漏的原則依次列舉出來,然后一一帶入問題檢驗,從而從一系列可能解中獲得能夠解決問題的精確解。

枚舉雖然看起來簡單,但是其實還是有一些容易被人忽視的考慮點。比方說待解決問題的「可能解/候選解」的篩選條件,「可能解」之間相互的影響,窮舉「可能解」的代價,「可能解」的窮舉方式等等。

很多時候實際上不必去追求高大上的復雜算法結構,反而大道至簡,采用枚舉法就能夠很好地規避系統復雜性帶來的冗余,同時或許在一定程度上還能夠對空間進行縮減。

圖解

枚舉思想的流程可以用下圖來表示。通過實現事先確定好「可能解」,然后逐一在系統中進行驗證,根據驗證結果來對「可能解」進行分析和論證。這是一種很明顯的結果導向型的思想,簡單粗暴地試圖從最終結果反向分析「可能解」的可行性

不過,枚舉思想的劣勢當然也很明顯。在實際的運行程序中,能夠直接通過枚舉方法進行求解的問題少之又少。而當「可能解」的篩選條件不清晰,導致「可能解」的數量和范圍無法准確判斷時,枚舉就失去了意義。

然而當「可能解」的規模比較小,同時依次驗證的過程容易實施時,枚舉思想不失為一種方便快捷的方式。只不過在具體使用時,還可以針對應用場景對「可能解」的驗證進行優化。

這種優化可以從兩個方向入手:

  1. 問題的簡化,盡可能對需要處理的問題進行模型結構上的精簡。這種精簡具體可體現在問題中的變量數目,減少變量的數據,從而能夠從根本上降低「可能解」的組合。
  2. 對篩選「可能解」的范圍和條件進行嚴格判斷,盡可能的剔除大部分無效的「可能解」。

雖說如此,但是一般而言大部分枚舉不可用的場景都是由於「可能解」的數量過多,無法在有限空間或有限時間內完成所有可能性的驗證。不過實際上枚舉思想是最接近人的思維方式,在更多的時候是用來幫助我們去「理解問題」,而不是「解決問題」

常見應用

  • 選擇排序
  • 順序查找法
  • ...

 

2. 迭代

迭代法(iterative method)是指無法使用公式一次求解,而需要使用迭代,例如用循環取重復執行程序代碼的某些部分來得到答案,本質思想是遞推

遞推思想跟枚舉思想一樣,都是接近人類思維方式的思想,甚至在實際生活具有比枚舉思想更多的應用場景。人腦在遇到未知的問題時,大多數人第一直覺都會從積累的「先驗知識」出發,試圖從「已知」推導「未知」,從而解決問題,說服自己。

事實上,迭代就是一種遞推的算法思想。遞推思想的核心就是從已知條件出發,逐步推算出問題的解。實現方式很像是初高中時我們的數學考卷上一連串的「因為」「所以」。那個時候還是用三個點來表示的。

而對於計算機而言,復雜的推導其實很難實現。計算機擅長的是執行高密度重復性高的工作對於隨機性高變化多端的問題反而不好計算。相比之下,人腦在對不同維度的問題進行推導時具有更高的自由度。

比方說,人腦可以很容易的從「太陽從東邊升起」推出「太陽從西邊落下」,然后大致推出「現在的時間」。但是對於計算機而言並沒有那么容易,你可能需要設置一系列的限制條件,才能避免計算機推出「太陽/月亮/星星」從「南/北/東邊」「落下/飛走/掉落」的可能性。

這個例子的用意是在說明,計算機在運用遞推思想時,大多都是重復性的推理比方說,從「今天是1號」推出「明天是2號」。這種推理的結構十分類似,往往可以通過繼而往復的推理就可以得到最終的解。

圖解

遞推思想用圖解來表示可以參見下圖。每一次推導的結果可以作為下一次推導的開始。

常見應用

  • 冒泡排序
  • 矩陣相加、相乘、轉置
  • 鏈表
  • ...

 

3. 遞歸

「遞」的理解可以是逐次、逐步。在遞歸中可理解為逐次回歸迭代,直到跳出回歸。

遞歸算法實際上是把問題轉化成規模更小的同類子問題,先解決子問題,再通過相同的求解過程逐步解決更高層次的問題,最后獲得問題的最終解。遞歸算法要求子問題跟父問題的結構相同。

用一句話來形容遞歸算法的實現,就是在函數內部調用(函數)自身的算法。所以在實現的過程中,最重要的是確定遞歸過程終止的條件,也就是迭代過程跳出的條件判斷。否則,程序會在自我調用中無限循環,最終導致內存溢出而崩潰。

圖解

遞歸思想其實就是一個套娃過程。一般官方都是嚴禁套娃行為的。所以在使用時一定要明確「套娃」舉動的停止條件,及時止損。

 

常見應用

  • 回溯算法
  • 階乘
  • 斐波那契數列
  • 堆棧
  • 漢諾塔
  • 二叉樹遍歷
  • 圖的深度優先遍歷
  • 圖的廣度優先遍歷
  • ...

 

4. 分治

分治,分而治之。分治算法的核心步驟就是兩步,一是分,二是治。在實際的運用中,分治算法主要包括兩個維度的處理,一是自頂向下,將主要問題逐層級划分為子問題;二是自底向上,將子問題的解逐層遞增融入主問題的求解中

 

分治算法很像是一種向下管理的思想,從最高級層層划分,將子任務划分給不同的子模塊,進而可以進行大問題的拆分,對系統問題的粒度進行細化,尋求最底層的最基本的解。這樣的思路在很多領域都有運用,比如幾何數學中的正交坐標、單位坐標、基的概念等,都是通過將復雜問題依照相同的概念,分割成多個子問題,直到這些子問題足夠簡單到可以解決,最后再將各個子問題的解合並,從而得到原問題的最終解

分治算法還引申出了一系列的問題,為什么分,怎么分,怎么治,治后如何。

  • 為什么要分?這個很好解釋,由於主要問題的規模過大,無法直接求解,所以需要對主要問題進行粒度划分。

  • 那怎么分?遵循計算機最擅長的高密度重復性運算,划分出來的子問題需要相互獨立並且與原問題結構特征相同,這樣能夠保證解決子問題后,主問題也就能夠順勢而解。

  • 怎么治?這就涉及到最基本子問題的求解,我們約定最小的子問題是能夠輕易得到解決的,這樣的子問題划分才具有意義,所以在治的環節就是需要對最基本子問題的簡易求解。

  • 治后如何?子問題的求解是為了主問題而服務的。當最基本的子問題得到解后,需要層層向上遞增,逐步獲得更高層次問題的解,直到獲得原問題的最終解。

圖解

如下圖所示,通過層層粒度上的划分,將原問題划分為最小的子問題,然后再向上依次得到更高粒度的解。從上而下,再從下而上。先分解,再求解,再合並

常見應用

  • 遞歸算法
  • 歸並排序
  • 快速排序
  • 二分查找法
  • 插值查找法
  • ...

 

5. 動態規划

我們知道分治思想最重要的一點是分解出的子問題是相互獨立且結構特征相同的,這一點並不是所有問題都能滿足,許多問題划分的子問題往往都是相互重疊且互相影響的,那么就很難使用分治算法進行有效而又干凈的子問題划分。

於是乎,動態規划來了。動態規划同樣需要將問題划分為多個子問題,但是子問題之間往往不是互相獨立的。

動態規划的主要做法是:如果一個問題答案與子問題相關的話,就能將大問題拆解成各個小問題,其中與分治法最大不同的地方是可以讓每一個子問題的答案被存儲起來,以供下次求解時直接取用。這樣的做法不但能減少再次計算的時間,還可以將這些解組合成大問題的解答,故而使用動態規划可以解決重復計算的問題

動態規划是在目前看來非常不接近人類思維方式一種算法,主要原因是在於人腦在演算的過程中很難對每一次決策的結果進行記憶。動態規划在實際的操作中,往往需要額外的空間對每個階段的狀態數據進行保存,以便下次決策的使用。

動態規划主要就是用來解決多階段決策的問題,但是實際問題中往往很難有統一的處理方法,必須結合問題的特點來進行算法的設計,這也是這種算法很難真正掌握的原因。

最優化原理

當前子問題的解可看作是前多個階段問題的完整總結。因此這就需要在子問題求解的過程中進行多階段的決策,同時當前階段之前的決策都能夠構成一種最優的子結構。這就是所謂的最優化原理。

一個最優化策略具有這樣的性質:不論過去狀態和決策如何,對前面的決策所形成的狀態而言,余下的諸決策必須構成最優策略。同時,這樣的最優策略是針對已作出決策的總結,對后來的決策沒有直接影響,只能借用目前最優策略的狀態數據。這也被稱之為無后效性。

圖解

動態規划的求解思路如下圖解:動態規划的開始需要將問題按照一定順序划分為各個階段,然后確定每個階段的狀態,如圖中節點的 F0 等。然后重點是根據決策的方法來確定狀態轉移方程。也就是需要根據當前階段的狀態確定下一階段的狀態。在這個過程中,下一狀態的確定往往需要參考之前的狀態。因此需要在每一次狀態轉移的過程中將當前的狀態變量進行記錄,方便之后的查找。

常見應用

示例:根據動態規划法的思想,修改斐波那契數列的遞歸算法

 1 output = [None] * 1000  # 斐波那契數列的暫存區
 2 
 3 def fibonacci(n):
 4     result = output[n]
 5     if result is None:
 6         if n == 0:
 7             result = 0
 8         elif n == 1:
 9             result = 1
10         else:
11             result = fibonacci(n-1) + fibonacci(n-2)
12         output[n] = result
13     return result
14 
15 if __name__ == "__main__":
16 for i in range(10):
17     print(fibonacci(i))

 

6. 貪心

人活在世上,不可能每一個選擇都那么恰到好處,很多問題根本沒有准確解,甚至於無解。所以在某些場景下,傻傻地去追求問題的最精確解是沒有意義的。

有人說,那還有最優解呢。難道最優解都不要了嗎?沒錯,許多問題雖然找不到最精確的解,但是的確會存在一個或者一些最優解。但是一定要去追求這些最優解嗎?我看不一定。

算法的存在不是單純地為了對問題求解,更多的是提供一種「策略」。何謂「策略」,解決問題的一種方式,一個角度,一條路。所以,貪心思想是有價值的

從貪心二字可得知,這個算法的目的就是為了「貪圖更多」。但是這種貪心是「目光短淺」的,這就導致貪心算法無法從長遠出發,只看重眼前的利益。

具體點說,貪心算法在執行的過程中,每一次都會選擇當前最大的收益,但是總收益卻不一定最大。所以這樣傻白甜的思路帶來的好處就是選擇簡單,不需要糾結,不需要考慮未來

貪心算法的實現過程就是從問題的一個初始解出發,每一次都作出「當前最優」的選擇,直至遇到局部極值點

  • 缺點:貪心所帶來的局限性很明顯,就是無法保證最后的解是最優的,很容易陷入局部最優的情況。
  • 優點:但是它每一次做選擇的速度很快,同時判斷條件簡單,能夠比較快速地給出一種差不多的解決方案。

圖解

下圖表示的是對多條直線的交點進行求解。很顯然,圖中的直線是不存在統一交點的,但是可以通過算法求得統一交點的最優解。若是采用貪心算法,那么在進行迭代時,每一次都會選擇離此時位置最近的直線進行更新。這樣一來,在經過多次迭代后,交點的位置就會在某一片區域無限輪回跳轉。而這片區域也就是能得出的大致的最優解區域。

常見應用

  • 最小生成樹
  • 圖的最短路徑法
  • ...

  

7. 回溯

回溯算法也可稱作試探算法,是不是讓你回憶起了在女神面前的小心翼翼。簡單來說,回溯的過程就是在搜索過程中尋找問題的解,當發現不滿足求解問題時,就回溯(即不再遞歸到下一層,而是返回到上一層,以節省時間),嘗試別的路徑,避免無效搜索,是一種走不通就退回再走的方法

這樣看起來,回溯算法很像是一種進行中的枚舉算法,在行進的過程中對所有可能性進行枚舉並判斷。常用的應用場景就在對樹結構、圖結構以及棋盤落子的遍歷上。

回溯思想在許多大規模的問題的求解上都能得到有效的運用。回溯能夠將復雜問題進行分步調整,從而在中間的過程中可對所有可能運用枚舉思想進行遍歷。這樣往往能夠清晰地看到問題解決的層次,從而可以幫助更好地理解問題的最終解結構。

圖解

假設目的是從 O0 到達 O4,需要對所有節點進行回溯遍歷路徑。那么回溯算法的過程則需要在前進的每一步對所有可能的路徑進行試探。

比方說,O0 節點前進的路徑有三條,分別是上中下條的 O1。回溯過程的開始,先走上面的 O1,然后能夠到達上面 O2,但是這時是一條死路。那么就需要從 O2 退回到 O1,而此時 O1 的唯一選擇也走不通,所以還需要從 O1 退回到 O0。然后繼續試探中間的 O1。

回溯算法的過程就是不斷進行這樣的試探、判斷、退回並重新試探,直至找到一條完整的前進路徑。只不過在這個過程中,可以通過「剪枝」等限制條件降低試探搜索的空間,從而避免重復無效的試探。比方說上下的 O2 節點,在經過 O0-O1-O2 的試探之后,就已經驗證了該節點不可行性,下次就無須從 O1 開始重復對 O2 的試探。

常見應用

  • 八皇后問題
  • ...

 


免責聲明!

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



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