
- •Ста: Лекция №7 - Деревья
- •Основные сведения о деревьях
- •Обход деревьев
- •Атд “Дерево”
- •Типичные структуры данных для n-арных деревьев
- •1. Массив меток и массив родительских индексов.
- •2. Массив меток и заголовок со списками дочерних узлов.
- •3. Динамическая структура с указателями
- •В результате ее выполнения в динамической памяти формируется структура объектов, в существенной степени напоминающая оригинальный пример из описания понятия деревьев:
- •Бинарные деревья
Типичные структуры данных для n-арных деревьев
Как и с простыми АТД, для деревьев существует несколько вариантов реализации в виде структур данных, каждая из которых обладает различной вычислительной сложностью операций.
1. Массив меток и массив родительских индексов.
В этом простейшем подходе дерево хранит:
массив меток, соответствующих узлам по порядковым номерам;
массив порядковых номеров узлов-родителей для каждого узла-потомка.
struct Tree
{
// Количество узлов в дереве
int m_nNodes;
// Массив меток узлов, тип выбирается в зависимости от задачи
char * m_pNodeLabels;
// Массив индексов родительских узлов
int * m_pParentIndices;
};
Предполагается, что количество узлов в дереве заранее известно и не меняется за время существования дерева, в противном случае такая структура неэффективна.
Графически рассматриваемый пример дерева можно представить следующим образом:
Для создания такой структуры данных необходимо:
Выделить память для самой структуры.
Выделить массив для хранения меток узлов и инициализировать значениями по-умолчанию.
Выделить массив для хранения индексов родительских узлов, и связать все узлы с корневым по умолчанию. Родителем корня пусть будет назначен несуществующий узел с индексом -1.
Tree * TreeCreate ( int _nNodes )
{
// Число узлов должно быть положительным
assert( _nNodes > 0 );
// Создаем объект-дерево в динамической памяти
Tree * pTree = new Tree;
pTree->m_nNodes = _nNodes;
// Создаем массив меток
pTree->m_pNodeLabels = new char[ _nNodes ];
memset( pTree->m_pNodeLabels, 0, _nNodes );
// Создаем массив родительских индексов
pTree->m_pParentIndices = new int[ _nNodes ];
memset( pTree->m_pParentIndices, 0, _nNodes * sizeof( int ) );
pTree->m_pParentIndices[ 0 ] = -1;
// Возвращаем созданное дерево
return pTree;
}
Необходима соответствующая функция освобождения памяти дерева:
void TreeDestroy ( Tree * _pTree )
{
// Уничтожаем метки узлов
delete[] _pTree->m_pNodeLabels;
// Уничтожаем массив родительских индексов
delete[] _pTree->m_pParentIndices;
// Уничтожаем само дерево
delete _pTree;
}
Корневой узел в такой структуре всегда размещается в нулевой позиции, в связи с чем операция ROOT имеет тривиальную реализацию (и вообще не использует конкретное дерево):
int TreeGetRootIndex ( const Tree & )
{
return 0;
}
Для работы с метками узлов необходима пара простых функций, скрывающих работу над внутренним массивом:
TreeNodeLabel TreeGetLabel ( const Tree & _tree, int _nodeIndex )
{
assert( _nodeIndex < _tree.m_nNodes );
return _tree.m_pNodeLabels[ _nodeIndex ];
}
void TreeSetLabel ( const Tree & _tree, int _nodeIndex, TreeNodeLabel _label )
{
assert( _nodeIndex < _tree.m_nNodes );
_tree.m_pNodeLabels[ _nodeIndex ] = _label;
}
Также, не вызывает сложности запрос и модификация индекса родительского узла PARENT, поскольку данное отношение явно представлено в структуре дерева специальным массивом::
int TreeGetParentIndex ( const Tree & _tree, int _nodeIndex )
{
assert( _nodeIndex < _tree.m_nNodes );
return _tree.m_pParentIndices[ _nodeIndex ];
}
void TreeSetParentIndex ( Tree & _tree, int _nodeIndex, int _parentIndex )
{
assert( _nodeIndex < _tree.m_nNodes );
assert( _parentIndex < _nodeIndex );
_tree.m_pParentIndices[ _nodeIndex ] = _parentIndex;
}
В такой структуре данных операции ROOT, LABEL и PARENT имеют константную вычислительную сложность O(1), что свидетельствует об отличной приспособленности данной структуры данных к вышеуказанным операциям АТД “Дерево”.
В то же время, операции LEFTMOST_CHILD и RIGHT_SIBLING требуют прохода индексного массива в цикле и характеризуются линейной вычислительной сложностью O(n). В частности, для получения индекса самого левого дочернего узла по индексу родителя, следует перебрать все ячейки индексов родительских узлов в поиске ближайшего, чей индекс узла-родителю равен интересующему:
int TreeGetLeftmostChildIndex( const Tree & _tree, int _nodeIndex )
{
// Проверяем корректность индекса
assert( _nodeIndex < _tree.m_nNodes );
// Ищем первый узел, индекс родителя которого равен искомому
for ( int i = _nodeIndex + 1; i < _tree.m_nNodes; i++ )
if ( _tree.m_pParentIndices[ i ] == _nodeIndex )
// Конкретный узел найден
return i;
// Дочерних узлов не найдено
return -1;
}
Аналогичным образом реализуется поиск ближайшего правого братского узла:
int TreeGetRightSiblingIndex ( const Tree & _tree, int _nodeIndex )
{
// Проверяем корректность индекса
assert( _nodeIndex < _tree.m_nNodes );
// Получаем индекс узла-родителя
int parentIndex = TreeGetParentIndex( _tree, _nodeIndex );
// Начинаем поиск правого братского узла сразу после своего узла.
// Братский узел будет иметь такой же индекс узла-родителя, как и данный узел.
for ( int i = _nodeIndex + 1; i < _tree.m_nNodes; i++ )
if ( _tree.m_pParentIndices[ i ] == parentIndex )
// Конкретный узел найден
return i;
// Братских узлов не найдено
return -1;
}
Наконец, следующим образом выглядят функции обхода:
// Прямой обход дерева, начиная с конкретного узла
void TreeDirectWalk ( const Tree & _tree, int _nodeIndex, TreeNodeVisitFunction _f )
{
// Посещаем начальный узел
( *_f )( _tree, _nodeIndex );
// Рекурсивно вызываем обход для каждого дочернего узла
int childIndex = TreeGetLeftmostChildIndex( _tree, _nodeIndex );
while ( childIndex != -1 )
{
TreeDirectWalk( _tree, childIndex, _f );
childIndex = TreeGetRightSiblingIndex( _tree, childIndex );
}
}
// Обратный обход дерева, начиная с конкретного узла
void TreeReverseWalk ( const Tree & _tree, int _nodeIndex, TreeNodeVisitFunction _f )
{
// Рекурсивно вызываем обход для каждого дочернего узла
int childIndex = TreeGetLeftmostChildIndex( _tree, _nodeIndex );
while ( childIndex != -1 )
{
TreeReverseWalk( _tree, childIndex, _f );
childIndex = TreeGetRightSiblingIndex( _tree, childIndex );
}
// В конце посещаем узел-родитель
( *_f )( _tree, _nodeIndex );
}
// Симметричный обход дерева, начиная с конкретного узла
void TreeSymmetricWalk ( const Tree & _tree, int _nodeIndex, TreeNodeVisitFunction _f )
{
// Рекурсивно посещаем самый левый дочерний узел, если такой имеется
int childIndex = TreeGetLeftmostChildIndex( _tree, _nodeIndex );
if ( childIndex != -1 )
TreeSymmetricWalk( _tree, childIndex, _f );
// Посещаем узел-родитель
( *_f )( _tree, _nodeIndex );
// Без дочерних узлов дальше двигаться не куда
if ( childIndex == -1 )
return;
// Рекурсивно посещаем каждый следующий дочерний узел
while ( true )
{
childIndex = TreeGetRightSiblingIndex( _tree, childIndex );
if ( childIndex == -1 )
break;
TreeSymmetricWalk( _tree, childIndex, _f );
}
}
// Прямой обход дерева, начиная с корня
void TreeDirectWalk ( const Tree & _tree, TreeNodeVisitFunction _f )
{
TreeDirectWalk( _tree, TreeGetRootIndex( _tree ), _f );
}
// Обратный обход дерева, начиная с корня
void TreeReverseWalk ( const Tree & _tree, TreeNodeVisitFunction _f )
{
TreeReverseWalk( _tree, TreeGetRootIndex( _tree ), _f );
}
// Симметричный обход дерева, начиная с корня
void TreeSymmetricWalk ( const Tree & _tree, TreeNodeVisitFunction _f )
{
TreeSymmetricWalk( _tree, TreeGetRootIndex( _tree ), _f );
}
Отметим, что функции обхода не используют внутреннее представление дерева, полностью удовлетворяясь доступными операциями. Это означает, что для повторного использования в других реализациях деревьев, можно вынести этот набор функций в отдельный CPP-файл.