Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
СТА (лекции+лабы) / СТА Лекция 7.docx
Скачиваний:
54
Добавлен:
16.03.2016
Размер:
42.05 Кб
Скачать

Типичные структуры данных для n-арных деревьев

Как и с простыми АТД, для деревьев существует несколько вариантов реализации в виде структур данных, каждая из которых обладает различной вычислительной сложностью операций.

1. Массив меток и массив родительских индексов.

В этом простейшем подходе дерево хранит:

  • массив меток, соответствующих узлам по порядковым номерам;

  • массив порядковых номеров узлов-родителей для каждого узла-потомка.

struct Tree

{

// Количество узлов в дереве

int m_nNodes;

// Массив меток узлов, тип выбирается в зависимости от задачи

char * m_pNodeLabels;

// Массив индексов родительских узлов

int * m_pParentIndices;

};

Предполагается, что количество узлов в дереве заранее известно и не меняется за время существования дерева, в противном случае такая структура неэффективна.

Графически рассматриваемый пример дерева можно представить следующим образом:

Для создания такой структуры данных необходимо:

  1. Выделить память для самой структуры.

  2. Выделить массив для хранения меток узлов и инициализировать значениями по-умолчанию.

  3. Выделить массив для хранения индексов родительских узлов, и связать все узлы с корневым по умолчанию. Родителем корня пусть будет назначен несуществующий узел с индексом -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-файл.