算法系列之搜索算法-深度優(yōu)先搜索DFS
隨著每年"金三銀四"招聘季的到來,許多求職者開始積極備戰(zhàn)面試。在眾多面試環(huán)節(jié)中,機(jī)試往往是不可或缺的一環(huán),而算法能力更是機(jī)試考核的重點(diǎn)。為此,我們特別推出算法系列文章,幫助大家系統(tǒng)復(fù)習(xí)算法知識(shí)。今天,我們將首先探討搜索算法中的重要內(nèi)容——深度度優(yōu)先搜索(DFS)。
圖的介紹
圖(Graph)是一種非線性的數(shù)據(jù)結(jié)構(gòu),由頂點(diǎn)(Vertex)和邊(Edge)組成。如下圖所示
分類如下:
- 無向圖(Undirected Graph):邊沒有方向,表示雙向關(guān)系。
- 有向圖(Directed Graph):邊有方向,表示單向關(guān)系。
- 加權(quán)圖(Weighted Graph):邊帶有權(quán)重。
- 無權(quán)圖(Unweighted Graph):邊沒有權(quán)重。
深度優(yōu)先搜索(DFS, Depth-First Search)
深度優(yōu)先搜索和廣度優(yōu)先搜索一樣,都是對(duì)圖進(jìn)行搜索的算法,目的也都是從起點(diǎn)開始搜索,直到到達(dá)頂點(diǎn)。深度優(yōu)先搜索會(huì)沿著一條路徑不斷的往下搜索,直到不能夠在繼續(xù)為止,然后在折返,開始搜索下一條候補(bǔ)路徑。
DFS 可以借助于?;蛘邅韺?shí)現(xiàn)。棧具有”后進(jìn)先出(LIFO)”特性,可以是有?;蛘哌f歸來實(shí)現(xiàn)遍歷。其實(shí)現(xiàn)步驟如下:
- 訪問節(jié)點(diǎn):從起始節(jié)點(diǎn)開始,訪問當(dāng)前節(jié)點(diǎn)。
- 遞歸
- 遞歸訪問鄰居:對(duì)于當(dāng)前節(jié)點(diǎn)的每一個(gè)未訪問過的鄰居節(jié)點(diǎn),遞歸地調(diào)用 DFS。
- 回溯:當(dāng)沒有未訪問的鄰居時(shí),回溯到上一個(gè)節(jié)點(diǎn),繼續(xù)搜索其他路徑。
- 棧
- 使用 Stack 來模擬遞歸過程,每次從棧中彈出一個(gè)節(jié)點(diǎn)并訪問它,然后將未訪問的鄰居節(jié)點(diǎn)壓入棧中。
示例代碼如下:
/**
* 深度優(yōu)先搜索示例
*/
public class DFSExample {
// 定義圖的節(jié)點(diǎn)類
static class Node {
int value;
List< Node> neighbors;
public Node(int value) {
this.value = value;
this.neighbors = new ArrayList<>();
}
// 添加鄰接節(jié)點(diǎn)
public void addNeighbor( Node neighbor) {
this.neighbors.add(neighbor);
}
}
/**
* 方式一 :棧實(shí)現(xiàn)
* dfs 函數(shù)
* @param startNode
*/
public static void dfs( Node startNode) {
if(startNode == null ) return;
// 使用隊(duì)列存儲(chǔ)待訪問的節(jié)點(diǎn)
Stack<Node> stack = new Stack<>();
// 使用HashSet記錄已訪問的節(jié)點(diǎn)
Set<Node> visited = new HashSet<>();
// 將起點(diǎn)加入棧并標(biāo)記為已訪問
stack.push(startNode);
visited.add(startNode);
// 遍歷棧
while (!stack.isEmpty()){
Node currentNode = stack.pop();
System.out.println(currentNode.value);
// 遍歷當(dāng)前節(jié)點(diǎn)的所有鄰接節(jié)點(diǎn)
for (Node neighbor : currentNode.neighbors) {
// 如果鄰接節(jié)點(diǎn)未被訪問,則加入棧并標(biāo)記為已訪問
if (!visited.contains(neighbor)) {
stack.add(neighbor);
visited.add(neighbor);
}
}
}
}
//方式二:遞歸實(shí)現(xiàn)
public static void sec( Node currentNode,Set<Node> visited) {
// 標(biāo)記當(dāng)前節(jié)點(diǎn)為已訪問
visited.add(currentNode);
System.out.println(currentNode.value);
// 遞歸訪問所有未訪問的鄰居節(jié)點(diǎn)
for (Node neighbor : currentNode.neighbors) {
if (!visited.contains(neighbor))
sec(neighbor, visited);
}
}
/**
*
* @param args
*/
public static void main(String[] args) {
Node node1 = new Node(1);
Node node2 = new Node(2);
Node node3 = new Node(3);
Node node4 = new Node(4);
Node node5 = new Node(5);
Node node6 = new Node(6);
node1.addNeighbor(node2);
node1.addNeighbor(node3);
node1.addNeighbor(node5);
node2.addNeighbor(node1);
node2.addNeighbor(node3);
node2.addNeighbor(node5);
node3.addNeighbor(node1);
node3.addNeighbor(node2);
node3.addNeighbor(node4);
node3.addNeighbor(node6);
node4.addNeighbor(node3);
node4.addNeighbor(node6);
node5.addNeighbor(node2);
node5.addNeighbor(node6);
node6.addNeighbor(node3);
node6.addNeighbor(node4);
node6.addNeighbor(node5);
//棧實(shí)現(xiàn)
dfs(node1);
System.out.println("+++++++++遞歸實(shí)現(xiàn)++++++++++++");
//遞歸實(shí)現(xiàn)
Set< Node> visited = new HashSet<>();
sec(node1,visited);
}
}
DFS的特點(diǎn)
- 時(shí)間復(fù)雜度:O(V+E)
- 空間復(fù)雜度:O(V)
- 適用場(chǎng)景:連通性檢測(cè)、路徑查找、迷宮求解
DFS 示例題
以下列舉了一些機(jī)試題
廣播服務(wù)器
題目描述:給定一個(gè)大小為 N×N 的二維矩陣 matrix,表示 N 個(gè)服務(wù)器之間的連接情況。matrix[i][j]
= 1 表示服務(wù)器i 和 j 直接連接,matrix[i][j]
= 0 表示不直接連接。計(jì)算初始需要給幾臺(tái)服務(wù)器廣播,才能使每個(gè)服務(wù)器都收到廣播。 輸入:N 行,每行 N 個(gè)數(shù)字(0 或 1),表示 N×N 的二維矩陣。 輸出:需要廣播的服務(wù)器的數(shù)量。
示例一: 輸入:
1 0 0
0 1 0
0 0 1
輸出:3
示例二: 輸入:
1 1
1 1
輸出:1
public class DFSServer {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String[] firstLine = scanner.nextLine().split(" ");
int n = firstLine.length;
//初始化二維數(shù)組
int[][] matrix = new int[n][n];
for (int i = 0; i < n; i++) {
String[] line = i==0 ? firstLine:scanner.nextLine().split(" ");
for (int j = 0; j < n; j++) {
matrix[i][j] = Integer.parseInt(line[j]);
}
}
//初始化標(biāo)記數(shù)組
int[] check = new int[n];
//需要廣播的服務(wù)器的數(shù)量
int ans = 0;
Stack<Integer> stack = new Stack<>();
// 遍歷每個(gè)服務(wù)器
for (int i = 0; i < n; i++) {
// 如果服務(wù)器 i 沒有被訪問過,就以它為起點(diǎn)進(jìn)行 DFS
if (check[i] == 0) {
ans++; // 新的連通分量,計(jì)數(shù)器加 1
stack.add(i); // 將當(dāng)前服務(wù)器加入棧
check[i] = 1; // 標(biāo)記為已訪問
// 開始 DFS
while (!stack.isEmpty()) {
// 彈出當(dāng)前服務(wù)器
int cur = stack.pop();
// 遍歷所有服務(wù)器,找到與 cur 相連且未訪問的服務(wù)器
for (int j = 0; j < n; j++) {
if (j != cur && matrix[cur][j] == 1 && check[j] == 0) {
// 將未訪問的服務(wù)器加入棧
stack.push(j);
// 將未訪問的服務(wù)器加入棧
check[j] = 1;
}
}
}
}
}
System.out.println(ans);
}
}
總結(jié)
DFS 是一種非常重要的圖遍歷算法,適用于許多場(chǎng)景。遞歸實(shí)現(xiàn)簡(jiǎn)單直觀,但可能會(huì)受到棧深度的限制;迭代實(shí)現(xiàn)則避免了這個(gè)問題,但代碼稍復(fù)雜一些。根據(jù)具體需求選擇合適的實(shí)現(xiàn)方式。