DFS (przeszukiwanie w głąb)
Ostatnia aktualizacja
Przeszukiwanie w głąb przegląda graf, schodząc jak najgłębiej wzdłuż każdej gałęzi, zanim się wycofa. Zaczynając od węzła źródłowego, odwiedza sąsiada, potem sąsiada tego sąsiada i tak dalej, aż trafi w ślepy zaułek (węzeł bez nieodwiedzonych sąsiadów); wtedy wraca i próbuje następnej gałęzi. Kliknij odtwarzanie powyżej i zobacz, jak algorytm schodzi jedną ścieżką, dociera do dna i wraca.
DFS naturalnie wyraża się za pomocą stosu: jawnego stosu (jak w tej animacji) albo stosu wywołań w rekurencji. Odwiedza każdy węzeł i każdą krawędź raz, więc działa w czasie O(V + E) dla grafu z V wierzchołkami i E krawędziami.
Złożoność czasowa i pamięciowa
| Miara | Złożoność | Uwagi |
|---|---|---|
| Czas | O(V + E) | Każdy wierzchołek i krawędź odwiedzane raz |
| Pamięć | O(V) | Stos + zbiór odwiedzonych, w najgorszym razie wszystkie węzły |
| Przechodzenie | Najpierw najgłębiej | Podąża jedną gałęzią do końca, zanim zajmie się innymi |
| Struktura danych | Stos (lub rekurencja) | Kolejność LIFO wymusza działanie w głąb |
Krok po kroku
| Krok | Co się dzieje |
|---|---|
| 1 | Połóż węzeł źródłowy na stosie. |
| 2 | Zdejmij węzeł; jeśli był już odwiedzony, pomiń go. |
| 3 | Oznacz go jako odwiedzony i zapamiętaj krawędź, która do niego doprowadziła. |
| 4 | Połóż na stosie wszystkich jego nieodwiedzonych sąsiadów. |
| 5 | Powtarzaj, aż stos będzie pusty. |
Przykład krok po kroku
Iteracyjne DFS od A w grafie A → [B, C], B → [D], C → [E], z sąsiadami kładzionymi w podanej kolejności (stos LIFO zdejmuje najpierw ostatnio położony):
| Krok | Stos (szczyt po prawej) | Odwiedzone | Działanie |
|---|---|---|---|
| 1 | [A] | {} | Połóż źródło A. |
| 2 | [B, C] | {A} | Zdejmij A, oznacz jako odwiedzony, połóż sąsiadów B, a potem C. |
| 3 | [B, E] | {A, C} | Zdejmij C, oznacz jako odwiedzony, połóż sąsiada E. |
| 4 | [B] | {A, C, E} | Zdejmij E, oznacz jako odwiedzony, brak sąsiadów. |
| 5 | [D] | {A, C, E, B} | Zdejmij B, oznacz jako odwiedzony, połóż sąsiada D. |
| 6 | [] | {A, C, E, B, D} | Zdejmij D, oznacz jako odwiedzony, stos pusty: koniec. |
Kiedy używać DFS
| Używaj, gdy | Unikaj, gdy |
|---|---|
| Potrzebujesz wykrywania cykli, sortowania topologicznego lub znajdowania spójnych składowych. | Potrzebujesz najkrótszej ścieżki w grafie bez wag: robi to BFS, a DFS nie. |
| Graf jest szeroki i płytki, więc stos zużywa znacznie mniej pamięci niż kolejka z wieloma węzłami granicy. | Graf jest bardzo głęboki i używasz rekurencji: stos wywołań może się przepełnić. |
| Chcesz wyczerpująco przeglądać ścieżki, jak przy rozwiązywaniu labiryntów lub backtrackingu. | Potrzebujesz węzłów odkrywanych w kolejności odległości od źródła. |
| Sprawdzasz osiągalność lub to, czy istnieje ścieżka między dwoma węzłami. | Musisz zagwarantować minimalną liczbę krawędzi na znalezionej ścieżce. |
Depth-First Search: kod
Przejrzysta, gotowa do uruchomienia implementacja algorytmu Depth-First Search w językach: Python, JavaScript, Java, C++, C. Wybierz język, skopiuj kod albo otwórz go od razu w edytorze online Coddy.
Depth-First Search: kod (Python)
1def dfs(graph, node, visited):2 visited.add(node)3 order = [node]4 for neighbor in graph[node]:5 if neighbor not in visited:6 order.extend(dfs(graph, neighbor, visited))7 return order8
9
10graph = {11 "A": ["B", "C"],12 "B": ["D", "E"],13 "C": ["F"],14 "D": [],15 "E": ["F"],16 "F": [],17}18
19order = dfs(graph, "A", set())20print("DFS order:", " -> ".join(order))Depth-First Search: kod (JavaScript)
1const graph = {2 A: ["B", "C"],3 B: ["D", "E"],4 C: ["F"],5 D: [],6 E: ["F"],7 F: [],8};9
10function dfs(node, visited = new Set(), order = []) {11 if (visited.has(node)) return order;12 visited.add(node);13 order.push(node);14 for (const next of graph[node]) {15 dfs(next, visited, order);16 }17 return order;18}19
20console.log("DFS from A:", dfs("A").join(" -> "));Depth-First Search: kod (Java)
1import java.util.List;2
3public class Main {4 static List<List<Integer>> adj = List.of(5 List.of(1, 2), // neighbors of 06 List.of(0, 3, 4), // neighbors of 17 List.of(0, 5), // neighbors of 28 List.of(1),9 List.of(1, 5),10 List.of(2, 4)11 );12 static boolean[] visited = new boolean[6];13
14 static void dfs(int node) {15 visited[node] = true;16 System.out.print(" " + node);17 for (int next : adj.get(node)) {18 if (!visited[next]) dfs(next);19 }20 }21
22 public static void main(String[] args) {23 System.out.print("DFS from 0:");24 dfs(0);25 System.out.println();26 }27}Depth-First Search: kod (C++)
1#include <iostream>2#include <vector>3
4void dfs(int node, const std::vector<std::vector<int>>& adj,5 std::vector<bool>& visited) {6 visited[node] = true;7 std::cout << node << " ";8 // Recurse into every unvisited neighbor9 for (int next : adj[node]) {10 if (!visited[next]) dfs(next, adj, visited);11 }12}13
14int main() {15 // 6-node undirected graph as an adjacency list16 std::vector<std::vector<int>> adj = {17 {1, 2}, // 018 {0, 3, 4}, // 119 {0, 5}, // 220 {1}, // 321 {1, 5}, // 422 {2, 4}, // 523 };24 std::vector<bool> visited(adj.size(), false);25 std::cout << "DFS from node 0: ";26 dfs(0, adj, visited);27 std::cout << "\n";28 return 0;29}Depth-First Search: kod (C)
1#include <stdbool.h>2#include <stdio.h>3
4#define N 65
6// 6-node undirected graph as an adjacency matrix7int adj[N][N] = {8 {0, 1, 1, 0, 0, 0},9 {1, 0, 0, 1, 1, 0},10 {1, 0, 0, 0, 0, 1},11 {0, 1, 0, 0, 0, 0},12 {0, 1, 0, 0, 0, 1},13 {0, 0, 1, 0, 1, 0},14};15bool visited[N];16
17void dfs(int node) {18 visited[node] = true;19 printf("%d ", node);20 // Recurse into every unvisited neighbor21 for (int next = 0; next < N; next++) {22 if (adj[node][next] && !visited[next]) dfs(next);23 }24}25
26int main(void) {27 printf("DFS from node 0: ");28 dfs(0);29 printf("\n");30 return 0;31}DFS (przeszukiwanie w głąb): najczęstsze pytania
Jaka jest złożoność czasowa DFS?
O(V + E), gdzie V to liczba wierzchołków, a E liczba krawędzi, bo każdy wierzchołek odwiedza raz i każdą krawędź sprawdza raz. Zużywa O(V) pamięci na stos i zbiór odwiedzonych.Czym różni się DFS od BFS?
Czy DFS jest rekurencyjne czy iteracyjne?
Kiedy użyć DFS zamiast BFS?
Czy DFS znajduje najkrótszą ścieżkę?
Dlaczego DFS potrzebuje zbioru odwiedzonych węzłów?
O(V + E). Częsty błąd to oznaczanie węzłów tylko przy zdejmowaniu przy jednoczesnym kładzeniu duplikatów: to poprawne, ale zostawia na stosie nieaktualne wpisy, które trzeba pomijać.