Поиск в глубину (DFS)
Идём по одной ветке как можно глубже, затем возвращаемся к последнему незавершённому вызову. Наблюдайте граф и стек одновременно — и проверьте, почему путь DFS не обязательно кратчайший.
Загружаем интерактивный граф…
Тот же граф, другой обход
Вершины A–H; неориентированные рёбра AB, AC, BD, BE, CE, CF, DG, EG и FG. H изолирована. Изначально старт — A, цель — F. Граф и порядок соседей совпадают с примером BFS.
Сначала одна ветка, потом следующая
DFS сразу входит в ещё не обнаруженного соседа. Текущий вызов приостанавливается, пока не завершатся этот сосед и все его потомки. Только затем вызов продолжает проверку следующего соседа. Граф остаётся неподвижным: длина линии на рисунке не влияет на обход.
Визуализатор использует явные кадры вызовов, а не стек JavaScript. Каждый кадр хранит вершину и число уже проверенных соседей. Порядок совпадает с рекурсивным DFS при тех же списках смежности. Если просто положить в стек всех соседей сразу, дерево обхода может получиться другим.
Стек — текущая ветка обхода
В стеке находятся ровно незавершённые вызовы от старта до текущей вершины. На вершине стека — текущий вызов, остальные приостановлены. Отмечаем вершину обнаруженной до добавления кадра, поэтому входим в неё не более одного раза, даже при наличии цикла.
Указатель соседа в кадре увеличивается до входа в потомка. После возврата продолжаем со следующего соседа, а не проверяем одно ребро бесконечно. Завершённые вершины остаются обнаруженными, их предки не меняются. Страница обходит только компоненту старта, а не лес всех компонент графа.
Вход, приостановка, возврат
В исходном примере порядок обнаружения: A, B, D, G, E, C, F. В момент входа в F стек равен A, B, D, G, E, C, F. Все соседи F уже обнаружены: завершаем F и возвращаемся в C, затем в E и приостановленные вызовы выше. Порядок возвратов: F, C, E, G, D, B, A.
Обход обнаруживает и завершает 7 вершин и проверяет 18 записей соседей: каждое из 9 неориентированных рёбер достижимой компоненты читается с двух концов. Максимум стека — 7 кадров, включая A на глубине ноль. Восстановление пути эти счётчики не меняет. H остаётся необнаруженной; недостижимой её можно считать только после окончания обхода.
Шесть рёбер у DFS, два у BFS
Цепочка предков до F даёт A → B → D → G → E → C → F: 6 рёбер. Но существует A → C → F всего из 2 рёбер. DFS первым построил допустимый путь, а не кратчайший. Глубина 6 — положение F в этом дереве DFS, а не кратчайшее расстояние в графе.
Откройте связанный пример BFS, оставьте тот же граф и выберите цель F. BFS получит расстояние 2. Порядок соседей может изменить дерево DFS, глубины и длину пути; BFS всё равно гарантирует минимум рёбер в невзвешенном графе.
Плеер полностью обходит достижимую компоненту и затем идёт по предкам от цели к старту. При восстановлении зелёный частичный путь направлен обратно, окончательный — от старта к цели. Выберите H для недостижимой цели, сам старт для пути из нуля рёбер или «Без цели» только для обхода.
Память алгоритма и история плеера
Со списками смежности обход занимает O(V + E) времени, включая инициализацию записей всех вершин. Явный стек, состояния, глубины и предки требуют O(V) дополнительной памяти; хранение графа — O(V + E). Восстановление пути в худшем случае требует O(V) времени и памяти результата. У рекурсивного DFS такая же асимптотика стека вызовов, но можно упереться в ограничение глубины рекурсии среды.
Визуализатор дополнительно сохраняет O(V + E) неизменяемых снимков с O(V) данными вершин и стека в каждом. История требует O(V(V + E)) памяти плюс общий граф. Пресеты до восьми вершин ограничены читаемостью сцены, а не возможностями DFS.
Реализация на JavaScript
Код принимает корректный простой неориентированный граф как Map от id к упорядоченному массиву соседей, существующий старт и необязательную существующую цель. Он не меняет вход и совпадает с плеером по порядку обнаружения и возвратов, предкам, глубинам и счётчикам. Плеер дополнительно проверяет и копирует граф перед созданием снимков.
function depthFirstSearch(adjacency, start, target = null) {
// A valid undirected graph: Map<vertexId, vertexId[]>.
const depth = new Map([...adjacency.keys()].map(id => [id, null]));
const parent = new Map([...adjacency.keys()].map(id => [id, null]));
const order = [start], finishOrder = [];
const stack = [{ vertex: start, nextNeighbor: 0 }];
depth.set(start, 0);
let edgeChecks = 0, maxStackSize = 1;
while (stack.length) {
const call = stack[stack.length - 1];
const neighbors = adjacency.get(call.vertex);
if (call.nextNeighbor === neighbors.length) {
finishOrder.push(call.vertex);
stack.pop();
continue;
}
const neighbor = neighbors[call.nextNeighbor++];
edgeChecks++;
if (depth.get(neighbor) !== null) continue;
parent.set(neighbor, call.vertex);
depth.set(neighbor, depth.get(call.vertex) + 1);
order.push(neighbor);
stack.push({ vertex: neighbor, nextNeighbor: 0 });
maxStackSize = Math.max(maxStackSize, stack.length);
}
const path = [];
if (target !== null && depth.get(target) !== null) {
for (let id = target; id !== null; id = parent.get(id)) path.push(id);
path.reverse();
}
return { depth, parent, order, finishOrder, path, edgeChecks, maxStackSize };
}Когда выбирать DFS
DFS полезен для проверки достижимости, поиска компонент связности и исследования ветвей. Для обнаружения циклов, топологической сортировки ориентированного ациклического графа и поиска сильно связанных компонент нужна дополнительная логика; этот пример её не реализует. В неориентированном графе уже обнаруженный сосед может быть просто предком, поэтому пропуск сам по себе не означает обнаружение цикла.
Для минимального числа рёбер в невзвешенном графе выбирайте BFS. Для минимальной суммарной стоимости ни этот DFS, ни обычный BFS не работают с произвольными весами. Обход определён на сайте; AlgiViz предоставляет оболочки неизменяемых шагов, временную шкалу и пользовательские объекты сцены.