- •Ста: Лекция №7 - Деревья
- •Основные сведения о деревьях
- •Обход деревьев
- •Атд “Дерево”
- •Типичные структуры данных для n-арных деревьев
- •1. Массив меток и массив родительских индексов.
- •2. Массив меток и заголовок со списками дочерних узлов.
- •3. Динамическая структура с указателями
- •В результате ее выполнения в динамической памяти формируется структура объектов, в существенной степени напоминающая оригинальный пример из описания понятия деревьев:
- •Бинарные деревья
3. Динамическая структура с указателями
Вышеописанные структуры данных плохо подходят для реализации деревьев, о которых заранее не известно количество узлов. В ситуации, когда структура дерева изменяется динамически, следует воспользоваться более гибким решением, напоминающим связный список.
Структура узла, представленная ниже, предполагает хранение в нем некоторого значения-метки, а также 3 переменных-указателей: на родительский узел, на самый левый дочерний узел и на соседний братский узел справа:
struct TreeNode
{
TreeNodeLabel m_label;
TreeNode* m_pParent;
TreeNode* m_pLeftMostChild;
TreeNode* m_pRightSibling;
};
Собственно, структура-дерево сводится к хранению указателя на корневой узел:
struct Tree
{
TreeNode * m_pRoot;
};
Создается дерево такого типа одновременно с корневым узлом:
TreeNode * TreeCreateNode ( TreeNodeLabel _label )
{
TreeNode * pNode = new TreeNode;
pNode->m_label = _label;
pNode->m_pParent = pNode->m_pLeftMostChild = pNode->m_pRightSibling = nullptr;
return pNode;
}
Tree * TreeCreate ( TreeNodeLabel _rootLabel )
{
Tree * pTree = new Tree;
pTree->m_pRoot = TreeCreateNode ( _rootLabel );
return pTree;
};
Чтобы добавить новый дочерний узел к конкретному узлу дерева, необходимо лишь настроить соответствующим образом переменные-указатели:
TreeNode * TreeInsertNodeChild ( TreeNode * _pNode, char _newLabel )
{
// Создаем новый узел
TreeNode * pNewNode = TreeCreateNode( _newLabel );
// Родительским узлом будет указанный свыше узел
pNewNode->m_pParent = _pNode;
// Присоединяем новый узел к родительскому.
// Это первый дочерний узел у данного родителя?
if ( ! _pNode->m_pLeftMostChild )
// Да, пусть новый узел будет его самым левым дочерним
_pNode->m_pLeftMostChild = pNewNode;
else
{
// Находим самый правый дочерний узел данного родителя
TreeNode * pTemp = _pNode->m_pLeftMostChild;
while ( pTemp->m_pRightSibling )
pTemp = pTemp->m_pRightSibling;
// Новый узел будет прикреплен к самому правому братскому узлу
pTemp->m_pRightSibling = pNewNode;
}
return pNewNode;
};
Логика уничтожения узлов дерева при освобождении памяти реализуется рекурсивно:
void TreeNodeDestroy ( TreeNode * _pNode )
{
// Сначала уничтожаем все зарегистрированные дочерние узлы слева-направо
TreeNode * pCurrent = _pNode->m_pLeftMostChild;
while ( pCurrent )
{
TreeNode * pNext = pCurrent->m_pRightSibling;
TreeNodeDestroy( pCurrent ); // рекурсивный спуск к листьям дерева
pCurrent = pNext;
}
delete _pNode;
}
void TreeDestroy ( Tree * _pTree )
{
TreeNodeDestroy( _pTree->m_pRoot );
delete _pTree;
}
Вместо номеров позиций во всех запросах следует возвращать указатели на узлы дерева. Очевидно, что все соответствующие операции, такие как ROOT, PARENT, LEFTMOST_CHILD, RIGHT_SIBLING, LABEL - при такой организации данных будут иметь константную вычислительную сложность, поскольку реализация может напрямую задействовать указатели-связи в узлах:
TreeNode * TreeGetRootNode ( Tree * _pTree )
{
return _pTree->m_pRoot;
}
TreeNodeLabel TreeGetLabel ( TreeNode * _pNode )
{
return _pNode->m_label;
}
TreeNode * TreeGetParent ( TreeNode * _pNode )
{
return _pNode->m_pParent;
}
TreeNode * TreeGetLeftMostChild ( TreeNode * _pNode )
{
return _pNode->m_pLeftMostChild;
}
TreeNode * TreeGetRightSibling ( TreeNode * _pNode )
{
return _pNode->m_pRightSibling;
}
Несколько иначе выглядит обход деревьев такого рода. Во-первых, видоизменяется форма типа для функции обратного вызова, теперь принимающая ссылку на узел вместо номера узла:
typedef void ( * TreeNodeVisitFunction ) ( const TreeNode & _node );
Во-вторых, несколько преображается способ навигации между узлами дерева:
// Прямой обход дерева, начиная с указанного узла
void TreeDirectWalk ( const TreeNode * _pNode, TreeNodeVisitFunction _f )
{
// Посещаем узел-родитель
( * _f )( * _pNode );
// Посещаем дочерние узлы рекурсивно
TreeNode * pChild = _pNode->m_pLeftMostChild;
while ( pChild )
{
TreeDirectWalk( pChild, _f );
pChild = pChild->m_pRightSibling;
}
}
// Обратный обход дерева, начиная с указанного узла
void TreeReverseWalk ( const TreeNode * _pNode,
TreeNodeVisitFunction _f )
{
// Посещаем дочерние узлы рекурсивно
TreeNode * pChild = _pNode->m_pLeftMostChild;
while ( pChild )
{
TreeReverseWalk( pChild, _f );
pChild = pChild->m_pRightSibling;
}
// В конце посещаем узел-родитель
( * _f )( * _pNode );
}
// Симметричный обход дерева, начиная с указанного узла
void TreeSymmetricWalk ( const TreeNode * _pNode,
TreeNodeVisitFunction _f )
{
// Посещаем рекурсивно левый дочерний узел, если такой имеется
if ( _pNode->m_pLeftMostChild )
TreeSymmetricWalk( _pNode->m_pLeftMostChild, _f );
// Посещаем узел-родитель
( * _f )( * _pNode );
// Если дочерних узлов нет, обход закончен
TreeNode * pChild = _pNode->m_pLeftMostChild;
if ( ! pChild )
return;
// Посещаем рекурсивно все оставшиеся дочерние узлі
while ( true )
{
pChild = pChild->m_pRightSibling;
if ( ! pChild )
break;
TreeSymmetricWalk( pChild, _f );
}
}
// Прямой обход дерева, начиная с корня
void TreeDirectWalk ( const Tree & _t, TreeNodeVisitFunction _f )
{
TreeDirectWalk( _t.m_pRoot, _f );
}
// Обратный обход дерева, начиная с корня
void TreeReverseWalk ( const Tree & _t, TreeNodeVisitFunction _f )
{
TreeReverseWalk( _t.m_pRoot, _f );
}
// Симметричный обход дерева, начиная с корня
void TreeSymmetricWalk ( const Tree & _t, TreeNodeVisitFunction _f )
{
TreeSymmetricWalk( _t.m_pRoot, _f );
}
Ниже приведена тестовая программа, формирующая рассматриваемое дерево-пример при помощи динамического дерева:
// Функция посещения узла при обходе
void PrintNodeLabel ( const TreeNode & _node )
{
std::cout << _node.m_label << ' ';
}
int main ()
{
// Создание дерева с меткой корня ‘A’
Tree * pTree = TreeCreate( 'A' );
// Добавляем дочерние узлы к корню
TreeNode * pNodeB = TreeInsertChild( pTree->m_pRoot, 'B' );
TreeNode * pNodeC = TreeInsertChild( pTree->m_pRoot, 'C' );
TreeInsertChild( pTree->m_pRoot, 'D' );
// Добавляем дочерние узлы к узлу ‘B’
TreeInsertChild( pNodeB, 'E' );
TreeInsertChild( pNodeB, 'F' );
// Добавляем дочерние узлы к узлу ‘C’
TreeInsertChild( pNodeC, 'G' );
// Обход дерева 3 способами с распечаткой значений:
// 1) Прямой
TreeDirectWalk( * pTree, & PrintNodeLabel );
std::cout << std::endl;
// 2) Обратный
TreeReverseWalk( * pTree, & PrintNodeLabel );
std::cout << std::endl;
// 3) Симметричный
TreeSymmetricWalk( * pTree, & PrintNodeLabel );
std::cout << std::endl;
// Уничтожение дерева
TreeDestroy( pTree );
}
