深度優先搜索
概述
- 定義
深度優先搜索是對一個連通圖進行遍歷的算法
算法是作用於具體數據結構之上的,深度優先搜索算法是基於“圖”這種數據結構的
- 適用場景
深度優先搜索適合節點數量多,樹的層次比較深的情況下
DFS適合的題目:給定初始狀態跟目標狀態,要求判斷從初始狀態到目標狀態是否有解
- 優缺點
深度優先搜索 缺點:難以尋找最優解 優點:內存消耗小
- 搜索算法
圖上的搜索算法,就是在圖中找一個頂點出發,到另一個頂點的位置
深度優先搜索算法屬於回溯思想,這種思想解決問題的過程適合用遞歸實現
深度優先搜索的搜索過程及代碼實現
我們一起看下圖,搜索的起始頂點是s,終止頂點是t,要在圖中尋找一條s到t的路徑。圖中實線表示遍歷,虛線表示回退,整個深度遞歸算法的搜索路勁如箭頭方向所示。
我們來實現下該圖的路徑尋找,並打印出該路徑
-
使用Java實現
-
Graph.java
圖的存儲,將上圖存儲到圖結構中
我們這里使用鄰接表的方式存儲
import java.util.LinkedList;
//存儲結構無向圖
public class Graph {
private int vertex; //頂點個數
public LinkedList<Integer> Adjacency[];//鄰接表
/**
* 鄰接表方式存儲
* @param vertex int 頂點個數
*/
public Graph(int vertex) {
this.vertex = vertex;
Adjacency = new LinkedList[vertex];
for (int i=0; i<vertex; ++i) {
Adjacency[i] = new LinkedList<>();
}
}
/**
* 無向圖的一條邊存兩次
* @param s int 起始頂點
* @param t int 終止頂點
*/
public void addEdge(int s, int t) {
Adjacency[s].add(t);
Adjacency[t].add(s);
}
}
- DepthFirstSearch.java
import java.lang.System;
public class DepthFirstSearch {
private static int vertex; //頂點個數
private static Graph g;//圖的存儲
static boolean found = false;
public static void main(String[] args){
vertex = 8;
g = new Graph(vertex);
g.addEdge(0,1);
g.addEdge(0,3);
g.addEdge(1,4);
g.addEdge(1,2);
g.addEdge(2,5);
g.addEdge(3,4);
g.addEdge(4,5);
g.addEdge(4,6);
g.addEdge(5,7);
g.addEdge(6,7);
DFSraverse(0, 6);
}
public static void DFSraverse(int s, int t) {
found = false;
boolean[] visited = new boolean[vertex];//記錄訪問狀態
int[] prev = new int[vertex];//記錄搜索路徑
for (int i=0; i<vertex; ++i) {
prev[i] = -1;
}
dfs(s, t, visited, prev);
print(prev, s, t);
}
public static void dfs(int w, int t, boolean[] visited, int[] prev) {
if(found==true) return;
visited[w] = true;//記錄節點已經被訪問
if(w==t) {//起始頂點等於終止頂點
found = true;
return;
}
//對每個頂點的鏈表進行遍歷(鄰接表存儲方式,每個頂點存儲指向頂點的鏈表,無向鏈表一個邊要存儲兩次)
for (int i=0; i<g.Adjacency[w].size(); ++i) {
int q = g.Adjacency[w].get(i);
if(!visited[q]) {
prev[q] = w;
dfs(q, t, visited, prev);
}
}
}
private static void print(int[] prev, int s, int t) { // 遞歸打印s->t的路徑
if (prev[t] != -1 && t != s) {
print(prev, s, prev[t]);
}
System.out.print(t + " ");
}
}
- 運行
javac DepthFirstSearch.java
java DepthFirstSearch
0 1 4 5 7 6
- 搜索過程簡述
存儲好圖結構,調用函數DFSraverse進入搜索
初始化布爾數據visited用於記錄某個頂點是否有被訪問過,在深度優先搜索中,被訪問過的節點不會再次被訪問
初始化整型數據prev用於記錄搜索路徑,也用於搜索路徑結果輸出,給prev數組中每個訂單賦值為-1,表示該頂點沒有被訪問過,用於輸出控制
調用dfs進入搜索,深度優先搜索的本質是遞歸
found表示已經到達終點頂點,如果已經到終點則直接返回
visited記錄當前頂點已經被訪問
接着遍歷頂點的子鏈表(即頂點的連接點)
如果有未訪問過的連接點(被訪問過表示該頂點走不通,無法到達終點頂點),則直接遞歸調用該連接點的連接點(往深度搜索)
重復該遞歸調用,直到找到終點頂點,設置found為true,函數返回
深度優先搜索的時間、空間復雜度
- 時間復雜度
從前面的圖可以看出來,每條邊最多被訪問兩次,一次是遍歷,一次回退,所以深度優先搜索算法的時間復雜度是O(E),E表示邊的個數。
- 空間復雜度
深度優先搜索算法的消耗內存主要是visited,prev數組和遞歸調用棧。visited、prev數組的大小跟頂點的個數V成正比,遞歸調用棧的最大深度不會超過頂點的個數,所以總的空間復雜度就是O(V)。
實踐
接下來一起看看leetcode上的一道題來加深理解
該題求解電話號碼的組合,可以使用深度優先搜索求解
- 使用GO實現
func letterCombinations(digits string) []string {
var data []string
digits_arr := strings.Split(digits, "")
start := 97//a的ASCII十進制值
var ca_len int//每個數字對應的字母長度為3,z特殊對應四位
var match_letter string
setp := make(map[string]int)
//拿到所有的字母
for _,val := range digits_arr {
var_int,err := strconv.Atoi(val)
if(err!=nil) {
return data
}
//7、9對應4個字母
ca_len = 3
if(var_int==9 || var_int==7) {
ca_len = ca_len+1
}
start_distrbute_num := (var_int - 2)*3
if(var_int==8 || var_int==9) {
start_distrbute_num = start_distrbute_num+1
}
rel_start := start + start_distrbute_num
for i := 0; i < ca_len; i++ {
match_letter_num := rel_start + i
letter := string(rune(match_letter_num))
setp[letter] = ca_len
match_letter += letter
}
}
letters_arr := strings.Split(match_letter, "")
data = dfsLetter(letters_arr, setp)
return data
}
func dfsLetter(letters_arr []string, setp map[string]int) []string {
var _step int
var data []string
t := 0
for __key,letter := range letters_arr {
_step = setp[letter]
//頂點只循環第一輪[即第一個數字]
//超過一個數字的放到遞歸中
if(__key==0) {
t = len(letters_arr[:_step])
//count_step = _step
}
if(t!=0 && __key>=t) {
continue
}
//存在下一個數
if(len(letters_arr[_step:])>0) {
//去除步長之后的字母繼續循環
_match_letter_arr := letters_arr[_step:]
res := dfsLetter(_match_letter_arr, setp)
for _, _key := range res {
r := letter + _key
data = append(data, r)
}
} else {
//沒有產生遞歸返回數據,即當前只有一個數字
data = append(data, letter)
}
}
return data
}
func main() {
digits := "6789"
data := letterCombinations(digits)
fmt.Println("v%", data)
}
- 運行
go run letterCombinations.go
[mptw mptx mpty mptz mpuw mpux mpuy mpuz mpvw mpvx mpvy mpvz mqtw mqtx mqty mqtz mquw mqux mquy mquz mqvw mqvx mqvy mqvz mrtw mrtx mrty mrtz mruw mrux mruy mruz mrvw mrvx mrvy mrvz mstw mstx msty mstz msuw msux msuy msuz msvw msvx msvy msvz nptw nptx npty nptz npuw npux npuy npuz npvw npvx npvy npvz nqtw nqtx nqty nqtz nquw nqux nquy nquz nqvw nqvx nqvy nqvz nrtw nrtx nrty nrtz nruw nrux nruy nruz nrvw nrvx nrvy nrvz nstw nstx nsty nstz nsuw nsux nsuy nsuz nsvw nsvx nsvy nsvz optw optx opty optz opuw opux opuy opuz opvw opvx opvy opvz oqtw oqtx oqty oqtz oquw oqux oquy oquz oqvw oqvx oqvy oqvz ortw ortx orty ortz oruw orux oruy oruz orvw orvx orvy orvz ostw ostx osty ostz osuw osux osuy osuz osvw osvx osvy osvz]
- 分析
同樣我們通過畫圖來分析
假設我們填寫的數字是"2345"[題目限制要求最多填寫四個數字]
首先我們把輸入的數字轉換成對應的字母,我采用的是ASCII碼的方式
轉換后調用函數dfsLetter進行字符拼接,進入函數后即遍歷字母切片,僅遍歷第一個數字對應的字母
如果存在下一個數字(前序記錄好數字對應字母長度),即遍歷調用dfsLetter,接受遍歷的字母組合返回
如果是最后一個數字,則返回當前字母組合
以“2345”為例,從數字2開始,循環遍歷拿到a,存在數字3,則找到字母d,存在數字4,找到字母g,存在數字5,找到字母j,沒有下一個數字,則繼續拿到j、k、l,然后與g組裝返回,gj、gk、gl,接着返回給d,組成dgj、dgk、dgl,然后再返回上層調用,組成adgj、adgk、adgl,然后繼續循環字母b,重復之前的調用步驟,直到找到所有的字母組合。
其他要注意的地方:
數字7跟數字9的字母長度是4位,要特別注意
- 參考