Поиск в глубину (DFS)
Последнее обновление
Поиск в глубину исследует граф, уходя как можно глубже по каждой ветви перед возвратом. Начиная с исходного узла, он посещает соседа, затем соседа этого соседа и так далее, пока не упрётся в тупик (узел без непосещённых соседей); в этот момент он возвращается и пробует следующую ветвь. Нажмите «воспроизвести» выше, чтобы увидеть, как он спускается по одному пути, достигает дна и возвращается обратно.
DFS естественно выражается через стек — либо явный стек (как анимировано здесь), либо стек вызовов через рекурсию. Он посещает каждый узел и ребро один раз, поэтому работает за время O(V + E) для графа с V вершинами и E рёбрами.
Временная и пространственная сложность
| Мера | Сложность | Примечания |
|---|---|---|
| Время | O(V + E) | Каждая вершина и ребро посещаются один раз |
| Память | O(V) | Стек + множество посещённых, в худшем случае все узлы |
| Обход | Сначала самое глубокое | Идёт по одной ветви до конца прежде других |
| Структура данных | Стек (или рекурсия) | Порядок LIFO задаёт поведение в глубину |
Пошагово
| Шаг | Что происходит |
|---|---|
| 1 | Поместите исходный узел в стек. |
| 2 | Извлеките узел; если он уже посещён, пропустите его. |
| 3 | Отметьте его посещённым и запишите ребро, которое его достигло. |
| 4 | Поместите в стек всех его непосещённых соседей. |
| 5 | Повторяйте, пока стек не опустеет. |
Разобранный пример
Итеративный DFS из A на графе A → [B, C], B → [D], C → [E], помещая соседей в указанном порядке (стек LIFO извлекает последний помещённый первым):
| Шаг | Стек (вершина справа) | Посещённые | Действие |
|---|---|---|---|
| 1 | [A] | {} | Поместить источник A. |
| 2 | [B, C] | {A} | Извлечь A, отметить посещённым, поместить соседей B, затем C. |
| 3 | [B, E] | {A, C} | Извлечь C, отметить посещённым, поместить соседа E. |
| 4 | [B] | {A, C, E} | Извлечь E, отметить посещённым, соседей нет. |
| 5 | [D] | {A, C, E, B} | Извлечь B, отметить посещённым, поместить соседа D. |
| 6 | [] | {A, C, E, B, D} | Извлечь D, отметить посещённым, стек пуст: готово. |
Когда использовать DFS
| Используйте, когда | Избегайте, когда |
|---|---|
| Нужны обнаружение циклов, топологическая сортировка или поиск компонент связности. | Нужен кратчайший путь в невзвешенном графе — это делает BFS, а не DFS. |
| Граф широкий и неглубокий, поэтому стек использует гораздо меньше памяти, чем очередь со множеством граничных узлов. | Граф очень глубокий и вы используете рекурсию — стек вызовов может переполниться. |
| Вы хотите исчерпывающе исследовать пути, как при решении лабиринтов или поиске с возвратом. | Нужно обнаруживать узлы в порядке расстояния от источника. |
| Вы проверяете достижимость или существование пути между двумя узлами. | Вы должны гарантировать минимальное число рёбер на найденном пути. |
Код Depth-First Search
Чистая, готовая к запуску реализация Depth-First Search на Python, JavaScript, Java, C++, C. Выберите язык, скопируйте код или откройте его уже загруженным в плейграунде Coddy.
Код Depth-First Search на 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 на 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 на 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 на 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 на 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?
O(V + E), где V — число вершин, а E — число рёбер, потому что он посещает каждую вершину один раз и рассматривает каждое ребро один раз. Он использует память O(V) для стека и множества посещённых.В чём разница между DFS и BFS?
DFS рекурсивный или итеративный?
Когда использовать DFS вместо BFS?
Может ли DFS найти кратчайший путь?
Зачем DFS нужно множество посещённых?
O(V + E). Частая ошибка — помечать узлы посещёнными только при извлечении, продолжая помещать дубликаты; это корректно, но может оставлять в стеке устаревшие записи, которые нужно пропускать.