ОБРАЗОВАНИЕ
Корнеев Георгий Александрович, Шамгунов Никита Назимович, Шалыто Анатолий Абрамович
ОБХОД ДЕРЕВЬЕВ НА ОСНОВЕ АВТОМАТНОГО ПОДХОДА
ВВЕДЕНИЕ
С 1991 г. в России развивается Б'ШСТН-технология, которая базируется на автоматном программировании [1]. Настоящая работа проведена в рамках исследования применения Б'ШСТН-технологии в вычислительных алгоритмах [2, 3] и посвящена одному из них - обходу деревьев. Первоначально в работе рассматривается обход двоичных деревьев, а затем предлагаемое решение обобщается на случай к-ичных деревьев. При этом рассматриваются алгоритмы обхода как с использованием стека, так и без его применения. Использование автоматного подхода позволяет получить наглядные и универсальные (в отличие от работы [4]) алгоритмы решения рассматриваемых задач, которые также весьма экономны по памяти по сравнению с классическими алгоритмами.
1. ПОСТАНОВКА ЗАДАЧИ ОБХОДА ДВОИЧНОГО ДЕРЕВА
(тогёег) [4, 5]. Такие порядки используются при обходе дерева в глубину. В качестве примера приведем последовательности, порождаемые обходами дерева, представленного на рисунке 1:
- прямой обход: 0, 1, 3, 4, 5, 6, 2, 7, 8, 9;
- центрированный: 3, 4, 1, 6, 5, 0, 8, 7, 9, 2;
- обратный обход: 4, 3, 6, 5, 1, 8, 9, 7, 2, 0.
Данная задача может быть решена несколькими способами: рекурсивно [4, 5], итеративно с применением стека и итеративно без использования стека [6].
Ни в одном из этих подходов не используются автоматы, что, по мнению авторов, не позволило обеспечить наглядность и универсальность предлагаемых решений.
1.1. ОПИСАНИЕ СТРУКТУР ДАННЫХ ДЛЯ ПРЕДСТАВЛЕНИЯ ДВОИЧНЫХ ДЕРЕВЬЕВ
Будем представлять бинарное дерево в программе следующим образом:
Пусть задано двоичное дерево, например, представленное на рисунке 1.
Необходимо осуществить его обход - сформировать последовательность номеров его вершин. Рассмотрим три порядка обхода дерева: прямой (ргеогёег), обратный (ро81;огёег) и центрированный
Рисунок 1. Двоичное дерево.
Необходимо осуществить еж обход.,,,
{
// Данные в узле // Левое поддерево // Правое поддерево // Указатель на родителя
struct Node int data; Node* l; Node* r; Node* p;
} ;
В этой структуре содержатся ссылки на правое и левое поддеревья, а также ссылка на родительскую вершину, что в дальнейшем, в частности, позволит реализовать рассматриваемый алгоритм без стека. В приводимых примерах на языке C++ для хранения указателей на вершины дерева будем использовать переменные типа Node*. При этом указатель на вершину объявляется как Node* node. Доступ к переменным, указывающим на левое и правое поддеревья, а также на родительскую вершину, осуществляется при помощи оператора ->. Поэтому в графах переходов в условиях, помечающих дуги, будем применять следующие обо- ftfu^efrm: (Udo-, (tfiaSo-, значения языка C++: node->l, node->r и node->p, соответственно.
... бщ&ъ (ифемЯи
1.2. ВВОД ДЕРЕВЬЕВ
Ввод деревьев в примерах будем производить в следующем виде. В первой строке записано N - число вершин в дереве. В следующих N строках - описания каждой вершины. Для г-ой вершины в строке с номером г + 1 указываются номера корней левого и правого поддеревьев. В случае отсутствия поддерева вместо номера корня записывается число - 1.
Для рассматриваемого примера исходные данные (в транспонированной для экономии места форме) имеют следующий вид:
1 3 7 -1 -1 6 -1 8 -1 -1 10 2 5 -1 4 -1 -1 -1 9 -1 -1
2. ОБХОД ДВОИЧНОГО ДЕРЕВА
БЕЗ ИСПОЛЬЗОВАНИЯ СТЕКА
Рассмотрим обход двоичного дерева без применения стека. Идея предлагаемого алгоритма состоит в том, что при обходе
двоичного дерева могут быть выделены следующие направления движения: влево, вправо, вверх. Обход начинается от корня дерева. В зависимости от номера текущей вершины и направления движения, можно принять решение о том, куда двигаться дальше. При этом стек не требуется. Как уже отмечалось выше, используется указатель на родительскую вершину.
Каждому из указанных направлений может быть сопоставлено состояние автомата, реализующего алгоритм. Также вводится начальное состояние, одновременно являющееся конечным.
Таким образом, автомат имеет следующие состояния:
0. Start - начальное (конечное) состояние.
1. Left - движение влево.
2. Right - движение вправо.
3. Parent - движение вверх. Автомат оперирует переменной node (имеющей тип Node*), являющейся указателем на текущий узел дерева.
Поясним, как строится граф переходов, описывающий поведение автомата. Переходы между состояниями определяются условиями, которыми помечаются соответствующие дуги графа переходов. Условия node->l, node->r и node->p обозначают наличие у текущего узла левого потомка, правого потомка и родителя, соответственно. Отрицание будем обозначать восклицательным знаком (!). Например, переход из состояния Parent в состояние Start осуществляется при условии, что у вершины нет родителя.
Некоторые дуги графа помечены не только условиями (расположены в числителе дроби), но и действиями (расположе-
,,,6- М&йо&яии LeftHftOUf&OfJH&Cfr Номера (й&сущей бермиНи Нл жрлН.
ны в знаменателе дроби). Например, переход из состояния Right в состояние Left производится при условии, что у вершины есть правое поддерево. При этом на указанном переходе выполняется действие - изменяется переменная node, в которой хранится указатель на текущую вершину.
Кроме действий на переходах, в автомате также выполняются действия в состояниях, например, в состоянии Left производится вывод номера текущей вершины на экран.
Граф переходов для реализации нерекурсивного обхода двоичного дерева без использования стека приведен на рисунке 2.
Отметим, что в случае безусловного перехода из одного состояния в другое дуга помечается символом 1 (переход из нулевой вершины в первую). Еще одной особенностью этого графа является пометка дуг «числами с двоеточием» - приоритетами. Эти пометки указывают последовательность, в которой будут проверяться условия на дугах, исходящих из вершины. В случае если
условия являются попарно ортогональными, приоритеты не расставляются.
Отметим, что если в работах [4, 5] рассмотрено три типа обходов и для каждого из них предложен свой алгоритм, то предлагаемый подход позволяет, используя один и тот же граф переходов автомата, осуществлять все три обхода за счет вывода номера текущей вершины только в одном из состояний: Left, Right, Parent. При этом, в зависимости от выбранного состояния, получим три стандартных обхода [4, 5]:
Состояние Обход
Left Прямой
Right Центрированный
Parent Обратный
Таким образом, предложенный автомат представляет стандартные виды обхода в единой форме.
В Приложении на диске (Листинг 1, функция traverseWithoutStack) приведена реализация этого автомата при помощи конструкции switch, построенная формально по графу переходов.
3. ОБХОД ДВОИЧНОГО ДЕРЕВА
С ИСПОЛЬЗОВАНИЕМ СТЕКА
Рассмотрим представление дерева, узлы которого не содержат ссылок на родителей. В этом случае структура Node выглядит следующим образом:
struct Node { int data; Node * l; Node * r;
};
// Данные в узле // Левое поддерево // Правое поддерево
0. Start
1. Left
out(node)
node->l
node = node->l
!node->l
node->r
node = node->r
1:!node->p
2. Right
out(node)
!node->r
2: node->p->l == node node = node->p
3. Parent
out(node)
I_)
3: node->p->r == node
node = node->p
Рисунок 2. Граф переходов автомата для реализации нерекурсивного обхода двоичного дерева.
0. Start
1: top()
1. Left
!node->l
out(node)
node->l
node->r
/ 2. Right \
out(node)
\ /
!node->r
push(node); node = node->r
2: top()->l == node node = pop()
3. Parent
push(node); node = node->l
out(node)
I_f
3: top()->r == node node=pop()
Рисунок 3. Граф переходов автомата для реализации обхода двоичного дерева с использованием стека
Для обходов деревьев, представленных таким образом, необходимо использовать стек. По аналогии с предыдущим разделом для решения данной задачи может быть построен автомат, граф переходов которого представлен на рисунке 3.
На этом рисунке используются следующие обозначения: push(node) - помещение текущего узла в стек, top () - узел в вершине стека, pop () - функция удаления узла из стека, возвращающая удаленную вершину, empty () - проверка стека на пустоту.
Заметим, что, в отличие от алгоритмов обхода, приведенных в работе [4, 5], при всех обходах дерева в стек помещается одна и та же информация. Таким образом, один стек может быть использован для реализации всех трех обходов, что позволяет экономить память.
Если вывод в состояниях Left, Right и Parent осуществлять в разные потоки, то можно получить одновременный вывод любой комбинации рассмотренных обходов.
cWetc MOtyeW ёи&ъ исаом-ъуов&Я релме^лсс/ш бсех Лрех o^xofoS, tWo fuojSoM^eW steoftomufai a&Mfafoi,
В Приложении (Листинг 1, функция traverseWithStack) приведена реализация этого автомата при помощи конструкции switch, построенная формально по графу переходов.
Граф переходов позволяет легко понять поведение программы, а также является тестом для ее проверки. Это объясняется тем, что состояния декомпозируют входные воздействия на группы, каждая из которых содержит условия, определяющие переходы только из этого состояния.
4. ОБХОД K-ИЧНОГО ДЕРЕВА
БЕЗ ИСПОЛЬЗОВАНИЯ СТЕКА
Изложенный подход может быть обобщен на случай k-ичных деревьев [5].
В программе дерево будем представлять в следующем виде:
struct KNode {
int data;
KNode * c[k];
};
где k - константа, c - массив, хранящий указатели на потомков. Метод, предложенный для обхода двоичных деревьев без использования стека, можно обобщить для обхода k-ичных деревьев (рисунок 4).
Отметим, что у построенного автомата k + 3 состояний. Это означает, что число состояний автомата зависит от значения к.
В Приложении на диске (Листинг 2, функция traverseK) приведена реализация этого автомата.
Как отмечено выше, граф переходов построенного автомата зависит от числа k.
!псСе->с[к-1]
Рисунок 4. Граф переходов автомата для реализации обхода к-ичного дерева без использования стека.
Таким образом, такая реализация обхода к-ичного дерева не является универсальной.
Предложим универсальное решение. Из графа переходов следует, что условия
перехода и действия в состояниях с первого по к-ое очень похожи. Поэтому логично объединить эти состояния в одно. В результате получается редуцированный
3: * сЫ!С_рЬ- != Ш1_1_
пос1е = * сЫ!С_р1г;сЫ!С_р1г = псСе -> с
Рисунок 5. Редуцированный граф переходов автомата для реализации обхода к-ичного дерева без использования стека.
автомат с четырьмя состояниями (рисунок 5), который позволяет произвести обход к-ичного дерева при произвольном значении к.
В Приложении (Листинг 2, функция traverseKReduced) приведена реализация этого автомата.
ЗАКЛЮЧЕНИЕ
Таким образом, в настоящей работе показано, что использование автоматного подхода позволило получить наглядные и, в отличие от классических, универсальные алгоритмы решения задач обхода деревьев, которые весьма экономны по памяти.
Литература
1. Шалыто A.A. SWITCH-технология. Алгоритмизация и программирование задач логического управления. СПб.: Наука, 1998.
2. Туккель Н.И., Шамгунов H.H., Шалыто A.A. Ханойские башни и автоматы // Программист, 2002, № 8. http://is.ifmo.ru. раздел «Статьи».
3. Туккель Н.И., Шамгунов H.H., Шалыто A.A. Задача о ходе коня // Мир ПК, 2003, № 1. http://is.ifmo.ru, раздел «Статьи».
4. Кормен Т., Лейзерсон Ч., Ривест Р. Алгоритмы: построение и анализ. М.: Центр непрер. матем. образования, 2000.
5. Кнут Д. Искусство программирования. Т. 1. Основные алгоритмы. M.: Вильямс, 2003.
6. Касьянов В.Н., Евстигнеев B.A. Графы в программировании: обработка, визуализация и применение. СПб.: БХВ-Петербург, 2003.
7. Корнеев r.A., Шамгунов H.H., Шалыто A.A. Обход деревьев на основе автоматного подхода. Полная версия статьи с приложением, опубликованная на сайте http://is.ifmo.ru, раздел «Статьи».
Корнеев Георгий Александрович, магистрант кафедры «Компьютерные технологии» Санкт-Петербургского государственного университета информационных технологий, механики и оптики (СПбГУ ИТМО), призер чемпионатов мира по программированию ACM,
Шамгунов Никита Назимович, аспирант кафедры «Компьютерные технологии» СПбГУ ИТМО, призер чемпионата мира по программированию ACM,
Шалыто Анатолий Абрамович, доктор технических наук, профессор кафедры «Компьютерные технологии» СПбГУ ИТМО.
© Наши авторы, 2004. Our authors, 2004,