Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
СТА (лекции+лабы) / Структуры и алгоритмы обработки данных.docx
Скачиваний:
99
Добавлен:
16.03.2016
Размер:
1.9 Mб
Скачать

Типичные структуры данных для 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-файл.