Обход дереваОбход дерева (известный также как поиск по дереву) — вид обхода графа[англ.], обусловливающий процесс посещения (проверки и/или обновления) каждого узла структуры дерева данных ровно один раз. Такие обходы классифицируются по порядку, в котором узлы посещаются. Алгоритмы в статье относятся к двоичным деревьям, но могут быть обобщены и для других деревьев. ТипыВ отличие от связных списков, одномерных массивов и других линейных структур данных, которые канонически обходятся в линейном порядке, деревья можно обходить различными путями. Деревья можно обходить «в глубину» или «в ширину». Существует три основных способа обхода «в глубину»
Структуры данных для обхода дереваОбход дерева итеративно проходит по всем узлам согласно некоторому алгоритму. Поскольку из данного узла имеется более одного следующего узла (это не линейная структура данных), то, в предположении последовательных вычислений (а не параллельных), некоторые узлы должны быть отложены, то есть запомнены некоторым способом для дальнейшего посещения. Часто это делается с помощью стека (LIFO = последний вошёл — первый вышел) или очереди (FIFO = первый вошёл — первым вышел). Так как дерево самореферентная (ссылающаяся на себя, определённая рекурсивно) структура данных, обход может быть определён рекурсией или, более тонко, корекурсией естественным и ясным образом. В этих случаях отложенные узлы запоминаются либо явно в обычном стеке, либо неявно в стеке вызовов, либо явно в очереди. Поиск в глубину легко имплементируется через стек, включая имплементацию через рекурсию (стек вызовов), в то время как поиск в ширину легко имплементируется через очередь, включая имплементацию через корекурсию. Поиск в глубинуЭти поиски называются поиском в глубину ввиду того, что дерево поиска проходится вниз насколько это можно на каждом потомке прежде чем переходить к следующей родственной ветке. Для двоичного дерева они определяются как операции обработки вершины рекурсивно в каждом узле, начиная с корня. Алгоритм обхода следующий[2] [3] Основной рекурсивный подход для обхода (непустого) бинарного дерева: Начиная с узла N делаем следующее: (L) Рекурсивно обходим левое поддерево. Этот шаг завершается при попадании опять в узел N. (R) Рекурсивно обходим правое поддерево. Этот шаг завершается при попадании опять в узел N. (N) Обрабатываем сам узел N. Эти шаги могут быть проделаны в любом порядке. Если (L) осуществляется перед (R), процесс называется обходом слева направо, в противном случае — обходом справа налево. Следующие методы показывают обходы слева направо: Прямой обход (NLR)
Центрированный обход (LNR)
Обратный обход (LRN)
Последовательность обхода называется секвенциализацией дерева. Последовательность обхода — это список всех посещённых узлов. Ни одна из секвенциализаций согласно прямому, обратному или центрированному порядку не описывает дерево однозначно. Если задано дерево с различными элементами, прямой или обратный обход вместе с центрированным обходом достаточны для описания дерева однозначно. Однако прямой обход вместе с обратным оставляет некоторую неоднозначность в структуре дерева[5]. Дерево общего видаЧтобы обойти любое дерево поиском в глубину, осуществляются рекурсивно следующие операции для каждого узла:
В зависимости от текущей задачи операции прямого, обратного или центрированного обхода могут быть пустыми, или вы можете хотеть лишь посетить конкретного потомка, так что эти операции опциональны. На практике может потребоваться более чем одна операция прямого, обратного или центрированного обхода. Например, когда осуществляется вставка в троичное дерево, операция прямого обхода выполняется путём сравнения элементов. Операция обратного обхода может потребоваться после этого для балансировки дерева. Поиск в ширинуДеревья можно обходить также в порядке уровней, где мы посещаем каждый узел на уровне прежде чем перейти на следующий уровень. Такой поиск называется поиском в ширину (breadth-first search, BFS). Другие видыСуществуют также алгоритмы обхода, которые не классифицируются ни как поиск в глубину, ни как поиск в ширину. Один из таких алгоритмов — метод Монте-Карло[англ.], который сосредотачивается на анализе наиболее обещающих ходов, основываясь на расширении дерева поиска при случайном выборе пространства поиска. ПриложенияПрямой обход при дублировании узлов и рёбер может сделать полный дубликат двоичного дерева. Это можно использовать для создания префиксного выражения (польской нотации) из деревья выражений[англ.], для чего обходим выражение в прямом порядке. Центрированный обход наиболее часто используется в двоичных деревьев поиска, поскольку он возвращает значения из низлежащего множества в порядке, определяемом функцией сравнения, которая определяет двоичное дерево поиска. Обратный обход при удалении или освобождении узлов может удалить или освободить всё бинарное дерево. Обход также образует постфиксное представление бинарного дерева. РеализацияПоиск в глубинуПрямой обход
Центрированный обход
Обратный обход
Все приведённые имплементации требуют стек, пропорциональный высоте дерева, который является стеком вызовов для рекурсивной имплементации и стеком родителей для итеративной. В плохо сбалансированном дереве этот стек может быть значительным. В итеративной имплементации мы можем избавиться от стека путём сохранения для каждого узла его родителя или с помощью прошивки дерева (следующий раздел). Центрированный обход Морриса с помощью прошивкиДвоичное дерево прошивается путём присвоения каждому левому указателю потомка (который в противном случае всегда пуст = null) указателя на предшественника узла в центрированном порядке (если таковой существует), а каждому правому указателю потомка (который в противном случае всегда пуст) указателя на следующий узел в центрированном порядке (если таковой существует). Преимущества:
Недостатки:
Обход Морриса является имплементацией центрированного обхода, использующей прошивку[6]:
Поиск в ширинуНиже приведён псевдокод для простого, основывающегося на очереди, поуровневого обхода. Алгоритм требует пространство, пропорциональное максимальному числу узлов на уровнях. Эта величина может достигать половины всех узлов. Более эффективный по памяти подход для этого типа обхода может быть имплементирован с помощью поиска в глубину с итеративным углублением[англ.]. levelorder(root) q ← empty queue q.enqueue(root) while (not q.isEmpty()) node ← q.dequeue() visit(node) if (node.left ≠ null) q.enqueue(node.left) if (node.right ≠ null) q.enqueue(node.right) void wide(TreeNode* root){
std::queue<TreeNode*> q;
q.push(root);
TreeNode* node;
while (!q.empty()){
node = q.front();
q.pop();
visit(node);
if (node->left)
q.push(node->left);
if (node->right)
q.push(node->right);
}
}
Бесконечные деревьяОбход обычно осуществляется для деревьев с конечным числом узлов (а следовательно, с конечной глубиной и конечным коэффициентом ветвления), но также он может быть осуществлён для бесконечных деревьев. Такой обход представляет интерес, в частности, в функциональном программировании (для отложенных вычислений), так как бесконечные структуры данных можно часто легко определить и работать с ними, хотя они не могут быть (строго) вычислены, так как потребовалось бы бесконечное время. Некоторые конечные деревья слишком велики, чтобы представить их явно, такие как дерево игры[англ.] шахмат или го, так что имеет смысл работать с ними как с бесконечными. Главное требование обхода — посетить все узлы. Для бесконечных деревьев простые алгоритмы это осуществить не могут. Например, если имеется двоичное дерево бесконечной глубины, поиск в глубину будет двигаться вдоль одной стороны (обычно — по левой стороне) дерева, никогда не посетив остальные вершины, и более того, центрированный или обратный обход никогда не посетит никакой узел, так как никогда не достигнет листа. Для контраста, обход в ширину (поуровневый) обходит двоичное дерево бесконечной глубины без проблем и более того, обходит любое дерево с ограниченным коэффициентом ветвления. С другой стороны, если дано дерево глубины 2, в котором корень имеет бесконечное число детей, а каждый узел-ребёнок имеет двух детей, поиск в глубину посетит все узлы, так как он, обойдя внуков (детей второго уровня), передвигается к следующему узлу (в предположении, что это не обратный обход, при котором никогда не достигается корень). Для контраста, поиск в ширину никогда не доберётся до внуков, поскольку он должен перебрать сначала всех детей (1-го уровня). Более сложный анализ времени работы может быть дан с помощью бесконечных порядковых чисел. Например, поиск в ширину в дереве глубины 2 (как выше) будет занимать ω·2 шагов — ω для первого уровня и другие ω для второго уровня. Таким образом, простые поиски в глубину и в ширину не обходят любое бесконечное дерево и неэффективны на очень больших деревьях. Однако гибридные методы могут обходить любое (счётное) бесконечное дерево, главным образом через диагональный аргумент[англ.] («диагональ», комбинация вертикали и горизонтали, соответствует комбинации поиска в глубину и в ширину). Конкретно, если дано бесконечно ветвящееся дерево бесконечной глубины, помечаем корень (), детей корня (1), (2), …, внуков (1, 1), (1, 2), …, (2, 1), (2, 2), …, и так далее. Узлы тогда находятся в один-к-одному соответствии с конечными (возможно пустыми) последовательностями положительных чисел, множество которых счётно и может быть упорядочено сначала по сумме элементов, а затем по лексикографическому порядку внутри последовательностей с данной суммой (только конечное число последовательностей даёт заданную сумму, так что все узлы достигаются; формально говоря, существует конечное число композиций заданного натурального числа, а именно 2n−1 композиций). Этот порядок задаёт обход дерева. Конкретно: 0: () 1: (1) 2: (1, 1) (2) 3: (1, 1, 1) (1, 2) (2, 1) (3) 4: (1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1) (4) и т. д.. Это может быть проинтерпретировано как отображение бесконечно глубокого двоичного дерева в такого вида дерево и применение поиска в ширину — заменяем рёбра «вниз», соединяющие родительский узел со вторым и далее потомками, с «правыми» рёбрами от первого потомка ко второму, от второго к третьему и т. д.. Тогда на каждом шаге мы двигаемся либо вниз (добавляется (, 1) в конец) или идём вправо (добавляем единицу к последнему числу) (за исключением корня, от которого можно идти только вниз), что показывает связь между бесконечным бинарным деревом и приведённой выше нумерацией. Сумма входов (без единицы) соответствует расстоянию от корня, что согласуется с 2n−1 узлов и глубиной n − 1 в бесконечном двоичном дереве (2 соответствует бинарности дерева). Примечания
Литература
Ссылки
|