js版本的(廣、深)度優先搜尋

lhyt發表於2018-04-28

0. 前言

廣度優先搜尋(BFS)和深度優先搜尋(DFS),大家可能在oj上見過,各種求路徑、最短路徑、最優方法、組合等等。於是,我們不妨動手試一下js版本怎麼玩。

1.佇列、棧

佇列是先進先出,後進後出,常用的操作是取第一個元素(shift)、尾部加入一個元素(push)。

棧是後進先出,就像一個垃圾桶,後入的垃圾先被倒出來。常用的操作是,尾部加入元素(push),尾部取出元素(pop)

2.BFS

BFS是靠一個佇列來輔助執行的。顧名思義,廣度搜尋,就是對於一個樹形結構,我們一層層節點去尋找目標節點。

image
按照這個順序進行廣度優先遍歷,明顯是佇列可以完美配合整個過程:

  1. 1進佇列 【1】
  2. 取出佇列第一個元素1,將1的子節點234按順序加入佇列後面 【2,3,4】
  3. 取出隊首元素2,將他的子節點按順序加入佇列 【3,4,5,6】
  4. 取出3,將子節點7加入 【4,5,6,7】
  5. 取出4,將子節點89加入【5,6,7,8,9】
  6. 取出5,沒有子節點,沒有什麼幹
  7. 繼續一個個取出

到了最後,佇列清空,樹也遍歷了一次

1.1 矩陣形式的圖的遍歷

假設有幾個點,我們需要設計一個演算法,判定兩個點有沒有相通

假設點12345是這樣的結構:

image

問:1能不能到達5

顯然我們一眼看上去是不會到達的,如果是設計演算法的話,怎麼做呢?

我們把點之間的關係用一個矩陣表示,0表示不連線,n行m列的1表示點n通向點m

var map = [ 
[0,1,1,0,0],
[0,0,1,1,0],
[0,1,1,1,0],
[1,0,0,0,0],
[0,0,1,1,0]
]
複製程式碼
function bfs(arr,start,end){
	var row = arr.length
	var quene = []
	var i = start
	var visited = {}//記錄遍歷順序
	var order = [] //記錄順序,給自己看的
	quene.push(i) //先把根節點加入
	while(quene.length){ //如果佇列沒有被清空,也就是還沒遍歷完畢
		for(var j = 0;j<row;j++){
			if(arr[i][j]){ //如果是1
				if(!visited[j]){
					quene.push(j)//佇列加入未訪問
				}
			}
		}
		quene.shift()//取出佇列第一個
		visited[i] = true//記錄已經訪問
		while(visited[quene[0]]){
			quene.shift()
		}
		order.push(i)//記錄順序
		i = quene[0]
	}
	return {visited:visited,result:!!visited[end],order:order}
}
bfs(map,0,4)
複製程式碼

1.2 樹的BFS舉例

舉個例子,3月24號今日頭條筆試題第二題的最少操作步數:

定義兩個字串變數:s和m,再定義兩種操作, 第一種操作: m = s; s = s + s; 第二種操作: s = s + m; 假設s, m初始化如下: s = "a"; m = s; 求最小的操作步驟數,可以將s拼接到長度等於n 輸入一個整數n,表明我們需要得到s字元長度,0<n<10000 案例: in: 6 out: 3

思路:利用廣度優先搜尋,假設左節點是操作1,右節點是操作2,這樣子就形成了操作樹。利用bfs的規則,把上層的父節點按順序加入佇列,然後從前面按順序移除,同時在佇列尾部加上移除的父節點的子節點。我這裡,先把父節點拿出來對比,他的子節點放在temp,對比完了再把子節點追加上去

image
每個節點分別用兩個數記錄s,m。發現第一次兩種操作是一樣的,所以我就略去右邊的了

function bfs(n){
	if(n<2||n!==parseInt(n)||typeof n !=='number') return
	if(n==2) return 1
	var quene = [[2,1]]//從2開始
	var temp = []//存放父節點佇列的子節點
	var count = 0
	var state = false//判斷是否結束迴圈
	while(!state){
		while(quene.length){//如果佇列不是空,從前面一個個取,並把他的子節點放在temp
			var arr = quene.pop()
			if(arr[0]==n){//找到了直接結束
				state = true
				break
			}
				temp.push([arr[0]*2,arr[1]*2])
				temp.push([arr[0]+arr[1],arr[1]])
		}
		count++//佇列已經空,說明這層的節點已經全部檢索完,而且子節點也儲存好了
		quene = [...temp]//佇列是子節點所有的元素集合,重複前面操作
		temp = []
	}
	return count
}
複製程式碼

3.DFS

DFS著重於這個搜尋的過程,一般以“染色”的形式配合棧來執行,也比較徹底。V8老生代的垃圾回收機制中的標記-清除也利用了DFS。我們定義三種顏色:黑白灰,白色是未處理過的,灰是已經經過了但沒有處理,黑色是已經處理過了 還是前面那幅圖

image

我們用兩個陣列,一個是棧,一個是儲存我們遍歷順序的,陣列的元素拿到的都是原物件樹的引用,是會改變原物件的節點顏色的

  1. 從根節點開始,把根節點1壓入棧,染成灰色 【1:灰】
  2. 發現1的白色子節點2,壓入棧染色【1:灰,2:灰】
  3. 發現2的白色子節點5,入棧染色【1:灰,2:灰,5:灰】
  4. 發現5沒有白色子節點,於是5已經確認是遍歷過的,5出棧染黑色【1:灰,2:灰】,【5:黑】
  5. 回溯2,發現2還有白色子節點6,6入棧染灰,發現6沒有子節點,6出棧染黑色,【1:灰,2:灰】,【5:黑,6:黑】;又發現2沒有白色子節點,2出棧染黑色【1:灰】,【5:黑,6:黑,2:黑】
  6. 2又回溯1,發現1還有白色子節點3,3入棧染灰【1:灰,3:灰】,【5:黑,6:黑,2:黑】
  7. 同樣的,7沒有白色子節點,7入棧直接出棧染黑,【1:灰,3:灰】,【5:黑,6:黑,2:黑,7:黑】;3沒有白色子節點【1:灰】出棧染黑,【5:黑,6:黑,2:黑,7:黑,3:黑】
  8. 到了4,【1:灰,4:灰】,他有白色子節點89,而89沒有白色子節點,所以89入棧又直接出棧了【1:灰,4:灰】,【5:黑,6:黑,2:黑,7:黑,3:黑,8:黑,9:黑】
  9. 4這次就沒有白色子節點了,到他出棧染黑,【1:灰】,【5:黑,6:黑,2:黑,7:黑,3:黑,8:黑,9:黑,4:黑】
  10. 回溯,發現1沒有白色子節點,最後1出棧染黑,【5:黑,6:黑,2:黑,7:黑,3:黑,8:黑,9:黑,4:黑,1:黑】

我們可以看到,入棧的時候,從白色染灰色,出棧的時候,從灰色到黑色。整個過程中,染黑的順序類似於二叉樹的後序遍歷

v8的垃圾回收,將持有引用的變數留下,沒有引用的變數清除。因為如果持有引用,他們必然在全域性的樹中被遍歷到。如果沒有引用,那這個變數必然永遠是白色,就會被清理

我們用物件來表示上面這棵樹:

var tree = {
	val: 1,
	children: [
		{val: 2,children: [{val:5,children:null,color:'white'},{val: 6,children:null,color:'white'}],color:'white'},
		{val: 3,children: [{val: 7,children:null,color:'white'}],color:'white'},
		{val: 4,children: [{val:8,children:null,color:'white'},{val: 9,children:null,color:'white'}],color:'white'}
	],
	color: 'white'
}
複製程式碼

開始我們的DFS:

function dfs ( tree ) {
	var stack = []//記錄棧
	var order = []//記錄遍歷順序
	!function travel (node) {
		stack.push(node)//入棧
		node.color = 'gray'
		console.log(node)
		if(!node.children) {//沒有子節點的說明已經遍歷到底
			node.color = 'black'
			console.log(node)
			stack.pop()
			order.push(node)
			return
		}
		var children = node.children	
		children.forEach(child=>{
			travel(child)
		})
		node.color = 'black'
		stack.pop()//出棧
		order.push(node)
		console.log(node)
	}(tree)
	return order
}
複製程式碼

過程用遞迴比較簡單,上面大部分程式碼都是除錯程式碼,自己可以改一下測試其他的類似場景。遍歷完成後,tree上面每一個節點都是黑色了。遍歷中間過程,每一個節點入棧的時候是灰色的,出棧的時候是黑色的。

後續更新(2019-10)

寫此文章的時候,水平低了一些,有一個實戰的例子可以見另一篇文章,程式碼更加優雅簡潔

相關文章