在開始前想先說一下關於這個課題的感想——能學以至用是一件很快樂的事情。算法
深度優先算法(簡稱DFS),在大學的數據結構課本中有這一個章節,依稀記得另一個叫廣度優先算法(簡稱BFS),在當時的我看來,它們都還只是理論。萬萬沒想到的是,在畢業後的兩年,我會接觸到它們,並寫下關於這個算法的應用文章,而契機是一個跟性格測試有關的遊戲。數組
這個系列文章的重點,是如何利用DFS算法來檢測有向圖的迴路,而具體的應用場景,就是性格測試。相比於純講理論,我更喜歡從實際應用出發,若是你對此感興趣,就請繼續看下去吧。數據結構
想必你確定玩過問答類的性格測試遊戲,遊戲規則很是簡單,按照心中所想回答問題便可。回答完一個問題後會跳轉到另一個問題,不一樣的回答可能進入不一樣的分支。回答完全部問題後會給出一個關於你性格的解答,以下圖。app
問題就來了,這種性格測試遊戲的模型實際上是一張有向圖。通常而言,題目及答案都是做者設定好的,所以不會出現死循環,也就是環路。例如 1->2->4->1,就是一個死循環,玩家可能一直在第一、二、4這三道題一直循環,遊戲不能結束。性能
若是遊戲很複雜,有不少道題目,有可能會設計出死循環。那麼像這種環路,咱們能用程序檢測出來嗎?答案是確定的。測試
下面先來POST一些概念。spa
摘自:百度百科 - 圖.net
在數學中,一個圖(Graph)是表示物件與物件之間的關係的數學對象,是圖論的基本研究。設計
摘自:百度百科 - 圖3d
若是給圖的每條邊規定一個方向,那麼獲得的圖稱爲有向圖。
深度優先搜索算法(Depth-First-Search),是搜索算法的一種。是沿着樹或圖的深度遍歷節點,儘量深的搜索分支。當節點v的全部邊都己被探尋過,搜索將回溯到發現節點v的那條邊的起始節點。這一過程一直進行到已發現從源節點可達的全部節點爲止。若是還存在未被發現的節點,則選擇其中一個做爲源節點並重復以上過程,整個進程反覆進行直到全部節點都被訪問爲止。
如上圖,按DFS的方式以A爲起點去遍歷的話,遍歷順序爲:
A-B-D-E-C-F-G
若是還有不明白的能夠自行Google一下。
/** * 測試數據,1表明第一題,2表明第二題,-1表明結果A,-2表明結果B,以此類推 * @type {Array} */ var testData = [ [2, 3], [4, -3], [-1, -2], [1, -2] ]; /** * 遞歸測試,使用深度優先算法 * @param {Array} data 測試數據 * @param {Number} qIndex 問題下標 * @param {Number} aIndex 答案下標 * @param {Array} path 當前回答路徑,例如[1,2,4]表明1->2->4的回答順序 */ function recurseTest(data, qIndex, aIndex, path) { var question = data[qIndex]; // 當前問題 var answer = question[aIndex]; // 要遍歷的答案 // 1.判斷是否跳轉到結果 if (answer > 0) { // 跳轉到其餘問題 if (path.indexOf(answer) > -1) { // 邏輯錯誤,當前回答路徑已存在,死循環 var result = path.concat([answer, 'wrong']).join(', '); showResult(result); } else { // 邏輯正確,繼續沿着這個答案遍歷下去 path.push(answer); recurseTest(data, answer - 1, 0, path); } } else { // 跳轉到結果 path.push(answer); } // 2.判斷是否最後一個答案 if (aIndex === question.length - 1) { // 已是當前這道題的最後一個答案,返回上層 var result = path.concat(['true']).join(', '); showResult(result); path.pop(); } else if (aIndex < question.length - 1) { // 還有其餘答案,使用下一個答案遍歷下去 recurseTest(data, qIndex, aIndex + 1, path); } } /** * 顯示回答結果 * @param {String} content 內容 */ function showResult(content) { console.log(content); if (typeof document !== 'undefined') { var div = document.createElement('div'); div.innerText = content; document.body.appendChild(div); } } // 測試一下 showResult('測試結果:'); recurseTest(testData, 0, 0, [1]);
https://jsfiddle.net/Vincent_...
上述代碼中的數組path,應該理解成一個棧,它記錄的是當前遞歸的回答順序,好比[1, 2, 4]
,表明着,先回答第一題,再回答第二題,再回答第四題。
假以下一個要移動到的問題的序號,存在於棧中,就表明出現了環路,例如[1, 2, 4, 1]
,此時表明出現了死循環。
這個時候就體現出棧的做用了,好比咱們跑完了1->2->?
的分支後,須要跑1->3->?
的分支,即返回上層,則使2出棧,3入棧。
DFS算法的時間複雜度是:O(b^m) (b-分支系數,m-圖的最大深度)
所以能夠看出若是分支系數越大(也就是每一題的答案越多),圖深度越大(題目的數量越多),時間複雜度就越高。
爲此,咱們能夠來看看運行這個檢測的方法,花了多少時間,遞歸了多少次:
上面咱們只有幾個節點,每一個節點只有2個出度,所以運算起來很快。若是增長到12個節點呢,每一個節點4個出度呢?
沒錯,是兩千多萬次遞歸,時間也來到了接近300ms,越多的頂點和邊將帶來更多的檢測時間,所以檢測過多的頂點和邊將帶來性能問題,這是使用深度優先算法來檢測的時候須要注意的。(以前就是由於一個遊戲配了20道題,運行一下這個檢測方法,直接跑到崩潰。。。)
使用深度優先算法,咱們可以檢測性格測試遊戲的邏輯正確性,相比以往課堂上的理論,在這裏算是一個具體的應用場景吧。其實深度優先算法的應用面也很廣,早晚還會再碰面的。
另外一方面,咱們討論了DFS算法的時間複雜度,當圖的頂點數增長到必定程度時,運算量暴漲,也所以拋出了一個性能的問題。在看似簡單的實現中,咱們其實要注意處理好細節,畢竟,放大到1億次運算,都不是小事!
最後,但願你們會喜歡這樣的文章吧。