
Обход дерева в ширину
В некоторых задачах необходимо обойти дерево в определенном порядке с посещением один раз каждой вершины для выполнения некоторой операции, например поиска чего-либо.
а) Дерево б) Обход в ширину:
A A, B, C, D, E, F
B
C D
в)
Обход в глубину:
A, B, C, E, F, D
E F
Рис. 21.3. Обход дерева в ширину и в глубину
Обход (поиск) в деревьях можно выполнять в ширину и в глубину. Для бинарных деревьев часто используют еще обходы сверху, снизу и слева направо.
Обход дерева в ширину происходит по уровням: сначала посещается вершина уровня 0 (корень), затем вершины уровня 1, затем уровень 2 и т. д. Уровень i+1 включает сыновей вершин уровня i (рис. 21.3 а, 21.3 б; алг. 1).
Алгоритм 1. Обход дерева в ширину
Пустая Очередь <== корень;
while (Очередь != пусто)
{ Очередь ==> t; Посетить t;
for (x сыновья(t))
Очередь <== x;
}
Обозначение: for (x S) . . . - цикл, выполняемый для каждого элемента x из множества S ( - "принадлежит").
Трассировочная таблица обхода дерева из рис. 7.3 а:
Посещение: |
|
|
A |
|
B |
|
C |
|
D |
|
E |
|
F |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
A |
|
B |
|
C |
|
D |
|
E |
|
F |
|
|
|
|
|
|
C |
|
D |
|
E |
|
F |
|
|
|
|
|
|
|
|
D |
|
|
|
F |
|
|
|
|
|
|
|
Обход в ширину выполняется с помощью очереди вершин на посещение. Из головы очереди выбирается и посещается вершина, а ее сыновья заносятся в хвост очереди.
Пока из очереди выбираются и посещаются все вершины уровня i, за ними в очереди расположатся все вершины уровня i+1. Первоначально в очередь помещается единственная вершина уровня 0 – корень дерева. На удивление простой и изящный алгоритм!
Подобный обход иногда называют “волновым алгоритмом”, т.к. последовательность обхода напоминает круговые волны от брошенного в воду камня.
Обход в ширину требует времени порядка O(n+m), т. к. каждая из n вершин один раз посещается, попадает в очередь и удаляется из нее. Дополнительно один раз просматривается каждая из m дуг дерева (при переборе сыновей ее начальной вершины).
Обход дерева в глубину
Деревья имеют рекурсивное строение: части дерева сами могут быть деревьями. Поэтому можно дать рекурсивное определение (корневого) дерева: дерево - либо пустое множество вершин, либо вершина (корень), связанная с конечным числом непересекающихся деревьев (называемых поддеревьями).
На рис. 21.3а, 21.3в показано дерево и последовательность его обхода в глубину по принципу: если можно – вперед (вглубь) к сыну, иначе - назад к отцу.
На основе рекурсивного определения удобно строить рекурсивные программы обработки деревьев. Примером является рекурсивный обход дерева в глубину (алг. 2).
Алгоритм 2. Рекурсивный обход в глубину дерева с корнем t
void obhod_der_gl (t)
{ if (дерево t != пусто)
{ Посетить корень t;
for (x поддеревья(t)) /*Для каждого x из поддеревьев t */
obhod_der_gl
(x); /* Обойти поддерево x */
}
} Возврат 2
Трассировочная таблица обхода дерева (рис. 16.3 а) по алгоритму 2:
Вызов: obhod_der_gl (A); /* Возврат 1 */
N вызова |
1 |
2 |
1 |
3 |
4 |
3 |
5 |
3 |
1 |
6 |
1 |
|
t = |
A |
B |
A |
C |
E |
C |
F |
C |
A |
D |
A |
|
x = |
B |
|
C |
E |
|
F |
|
|
D |
|
|
|
Посещение: |
A |
B |
|
C |
E |
|
F |
|
|
D |
|
|
Стек = |
|
|
|
|
|
|
|
|
|
|
|
|
t, возврат |
A 1 |
A 1 |
A 1 |
A 1 |
A 1 |
A 1 |
A 1 |
A 1 |
A 1 |
A 1 |
A 1 |
|
|
|
B 2 |
|
C 2 |
C 2 |
C 2 |
D 2 |
C 2 |
|
D 2 |
|
|
|
|
|
|
E 2 |
|
F 2 |
|
|
|
|
|
Алгоритм 2 удобен для представлений дерева с возможностью пустых ссылок на поддеревья (типа рис. 21.2в, 21.2д). Когда пустые поддеревья в явном виде отсутствуют (ссылки делаются только на непустые поддеревья, как на рис. 21.2б, или в нерегулярных сетях), удобнее алгоритм 2а, основанный на другом определении: (непустое корневое) дерево – это вершина (корень), связанная с корнями n деревьев (n ≥ 0).
Алгоритм 2а. Рекурсивный обход в глубину непустого дерева с корнем t
void obhod_der_gl (t)
{ Посетить t;
for (x сыновья(t)) /* Для каждого x из сыновей t */
obhod_der_gl (x); /* Обойти поддерево с корнем x */
}
Рекурсивный алгоритм часто проще итеративного, но тратит больше времени и памяти на рекурсивные вызовы. Итеративный алгоритм предпочтительнее, если он не слишком сложен.
Алгоритм 3. Итеративный обход в глубину непустого дерева
Посетить корень; Пустой Стек <== корень;
while (Стек != пусто) Идея:
{ t = верх(Стек); 2
if
(t имеет не посещенных сыновей, левый из
них x)
{ Посетить x; Стек <== x; } /* Вглубь */
else
Стек==>; /* Назад */ 1
}
Строку /* Назад */ алгоритма 3 можно заменить на три строки /* Назад или вправо */ (см. алг. 4): если невозможно двигаться вглубь, делается попытка перейти к брату и только при его отсутствии - назад. Это несколько ускоряет обход (меньше итераций) за счет усложнения алгоритма.
Трассировочная таблица обхода дерева (рис. 21.3 а) по алгоритму 3:
t= |
A |
|
А |
|
В |
|
A |
|
C |
|
E |
|
C |
|
F |
|
C |
|
A |
|
D |
|
A |
Посещение: |
А |
|
B |
|
|
|
C |
|
E |
|
|
|
F |
|
|
|
|
|
D |
|
|
|
|
Стек= |
A |
|
A |
|
A |
|
A |
|
A |
|
A |
|
A |
|
A |
|
A |
|
A |
|
A |
|
|
|
|
|
B |
|
|
|
C |
|
C |
|
C |
|
C |
|
C |
|
|
|
D |
|
|
|
|
|
|
|
|
|
|
|
|
E |
|
|
|
F |
|
|
|
|
|
|
|
|
|
|
Алгоритм 4. Итеративный обход в глубину непустого дерева
Посетить корень; Пустой Стек <== корень;
while (Стек != пусто)
{ t = верх(Стек); Идея:
if (t имеет не посещенных сыновей, левый из них x) 3
{
Посетить x; Стек <== x; } /* Вглубь */
else 2
{ Стек ==> t; /* Назад */
if (t имеет правого брата x) /* или */
{ Посетить x; Стек <== x; } /* вправо */ 1
}
}
Трассировочная таблица обхода дерева (рис. 20.3 а) по алгоритму 4:
t= |
A |
|
А |
|
В |
|
C |
|
E |
|
F |
|
C |
|
D |
|
A |
|
Посещение: |
|
|
B |
|
C |
|
E |
|
F |
|
|
|
D |
|
|
|
|
|
Стек= |
A |
|
A |
|
A |
|
A |
|
A |
|
A |
|
A |
|
A |
|
|
|
|
|
B |
|
C |
|
C |
|
C |
|
C |
|
D |
|
|
|
|
| |
|
|
|
|
|
|
E |
|
F |
|
|
|
|
|
|
|
|
|