
- •Структуры и алгоритмы обработки данных: план курса
- •Лабораторные работы
- •Литература
- •Ста: Лекция №1 - Введение. Данные в памяти программ
- •Введение
- •Модель памяти в прикладных программах
- •Int main ()
- •Int main ()
- •Int main ()
- •Int main ()
- •Char buf[ 2000000 ];
- •Return f(); // бесконечно долго вызываем сами себя // вызываем переполнение стека через время
- •Int main ()
- •Return f();
- •Int main ()
- •Delete[] p; // Освобождаем память
- •Int main ()
- •Int a[ n ]; // ошибка, размер нельзя вычислить во время компиляции
- •Delete[] p;
- •Int main ()
- •Проблема фиксированного размера массивов
- •Int main ()
- •Int main ()
- •Int main ()
- •Динамически растущие массивы (векторы)
- •Int main ()
- •Struct IntegerVector
- •Int * m_pData;
- •Int m_nUsed;
- •Int m_nAllocated;
- •Void IntegerVectorDestroy ( IntegerVector & _vector )
- •Int main ()
- •#Ifndef _integer_vector_hpp_
- •#Include "integer_vector.Hpp"
- •Void IntegerVectorRead ( IntegerVector & _vector, std::istream & _stream );
- •Ста: Лекция №2 - Связные списки.
- •Всегда ли хорош вектор?
- •Связные списки
- •Ста: Лекция №3 - Реализация и использование простейших атд
- •Абстрактные типы данных (атд)
- •Атд “Список” ( “Последовательность” )
- •Атд “Стек”
- •Атд “Очередь”
- •Ста: Лекция №7 - Деревья
- •Основные сведения о деревьях
- •Обход деревьев
- •Атд “Дерево”
- •Типичные структуры данных для n-арных деревьев
- •1. Массив меток и массив родительских индексов.
- •2. Массив меток и заголовок со списками дочерних узлов.
- •3. Динамическая структура с указателями
- •В результате ее выполнения в динамической памяти формируется структура объектов, в существенной степени напоминающая оригинальный пример из описания понятия деревьев:
- •Бинарные деревья
- •Глава 3 “Элементарные структуры данных”
- •Глава 4 “Абстрактные типы данных”
- •Глава 10 “Элементарные структуры данных” (подразделы 10.1-10.3)
- •Глава 2 “Основные абстрактные типы данных” (подразделы 2.1-2.4)
- •Глава 6 “Элементарные методы сортировки”.
- •Глава 5 “Рекурсия и деревья”.
2. Массив меток и заголовок со списками дочерних узлов.
В этом альтернативном подходе дерево вместо массива индексов родительских узлов хранит для каждого узла-родителя последовательности дочерних узлов. Поскольку в общем случае число сыновей у различных узлов-родителей может колебаться, обычно для хранения последовательностей дочерних узлов используются связные списки.
Эта структура также предполагает изначальную фиксацию количества узлов в дереве.
struct Tree
{
// Количество узлов в дереве
int m_nNodes;
// Массив меток узлов, тип выбирается в зависимости от задачи
char * m_pNodeLabels;
// Структура, представляющая элемент связного списка индексов дочерних узлов
struct ChildIndexElement
{
int m_childIndex;
ChildIndexElement * m_pNext;
};
// Заголовок - указатели на первые элементы список дочерних узлов
ChildIndexElement ** m_pHeader;
};
Ниже показано графическое представление состояния реализованной структуры данных после формирования дерева-примера.
Функция создания дерева из N узлов реализуется следующим образом (за исключением заголовка вместо массива индексов родительских узлов - идентично предыдущей структуре данных:
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_pHeader = new Tree::ChildIndexElement * [ _nNodes ];
memset( pTree->m_pHeader, 0, sizeof( Tree::ChildIndexElement * ) * _nNodes );
// Возвращаем созданное дерево
return pTree;
}
Функция освобождения памяти чуть усложняется из-за работы с динамически выделяемыми элементами списков индексов дочерних узлов:
void TreeDestroy ( Tree * _pTree )
{
// Уничтожаем метки узлов
delete[] _pTree->m_pNodeLabels;
// Уничтожаем списки узлов-сыновей
for ( int i = 0; i < _pTree->m_nNodes; i++ )
{
Tree::ChildIndexElement * pCurrent = _pTree->m_pHeader[ i ];
while ( pCurrent )
{
Tree::ChildIndexElement * pTemp = pCurrent->m_pNext;
delete pCurrent;
pCurrent = pTemp;
}
}
// Уничтожаем массив указателей на списки узлов-сыновей
delete[] _pTree->m_pHeader;
// Уничтожаем само дерево
delete _pTree;
}
Функции получения индекса корневого узла и работы с метками узлов реализуются полностью идентично структуре с массивом индексов родительских узлов.
Данная структура уменьшает вычислительную сложность реализации операции LEFTMOST_CHILD с линейной до константной за счет специальных структурных связей от родителя к дочерним узлам:
int TreeGetLeftmostChildIndex ( const Tree & _tree, int _nodeIndex )
{
assert( _nodeIndex < _tree.m_nNodes );
Tree::ChildIndexElement * pCurrent = _tree.m_pHeader[ _nodeIndex ];
return pCurrent ? pCurrent->m_childIndex : -1;
}
Зная родительский узел, для реализации перебора дочерних узлов слева направо необходимо лишь последовательно пройти по связному списку, на первый элемент которого указывает соответствующая узлу-родителю ячейка заголовка.
Напротив, обратная задача получения индекса родительского узла по индексу дочернего в данной реализации усложняется от константной вычислительной сложности до линейной, поскольку прямых структурных связей более не предусмотрено, и поиск будет осуществляться сканирование списков дочерних узлов всех вышестоящих по иерархии узлов-родителей:
int TreeGetParentIndex ( const Tree & _tree, int _nodeIndex )
{
// Проверяем корректность индекса
assert( _nodeIndex < _tree.m_nNodes );
// Ищем родительский узел, перебирая все списки сыновей
for ( int i = 0; i < _nodeIndex; i++ )
{
Tree::ChildIndexElement * pCurrent = _tree.m_pHeader[ i ];
while ( pCurrent )
{
if ( pCurrent->m_childIndex == _nodeIndex )
// Искомый узел найден, соответственно, это был список узла-родителя
return i;
pCurrent = pCurrent->m_pNext;
}
}
// Родительский узел не найден
return -1;
}
Аналогично, не имея индекса родительского узла, получение индекса ближайшего правого братского узла также усложняется до линейного поиска:
int TreeGetRightSiblingIndex ( const Tree & _tree, int _nodeIndex )
{
// Проверяем корректность индекса
assert( _nodeIndex < _tree.m_nNodes );
// Ищем родительский узел, перебирая все списки сыновей
for ( int i = 0; i < _nodeIndex; i++ )
{
Tree::ChildIndexElement * pCurrent = _tree.m_pHeader[ i ];
while ( pCurrent )
{
if ( pCurrent->m_childIndex == _nodeIndex )
{
// Искомый узел найден, соответственно, это был список узла-родителя.
// Следующий узел-сын известен по непосредственной структурной связи
Tree::ChildIndexElement * pNext = pCurrent->m_pNext;
return pNext ? pNext->m_childIndex : -1;
}
pCurrent = pCurrent->m_pNext;
}
}
// Братский узел не найден
return -1;
}
Ниже приведена процедура создания связи между указанными родительским и дочерними узлами. Предполагается помещение индекса дочернего узла в конец списка, соответствующего ячейке родительского узла в заголовке дерева. Если список ранее был пуст, его необходимо создать.
void TreeSetParentIndex ( Tree & _tree, int _nodeIndex, int _parentIndex )
{
// Проверка корректности индексов
assert( _nodeIndex < _tree.m_nNodes );
assert( _parentIndex < _nodeIndex );
// Создаем новый узел-сын
Tree::ChildIndexElement * pNewElement = new Tree::ChildIndexElement;
pNewElement->m_childIndex = _nodeIndex;
pNewElement->m_pNext = nullptr;
// Является ли этот узел первым сыном для родительского узла?
Tree::ChildIndexElement * pCurrent = _tree.m_pHeader[ _parentIndex ];
if ( ! pCurrent )
{
// Да, формируем первый элемент списка узлов-сыноей
_tree.m_pHeader[ _parentIndex ] = pNewElement;
return;
}
// В противном случае, прикрепляем новый узел к концу списка
while ( pCurrent->m_pNext )
pCurrent = pCurrent->m_pNext;
pCurrent->m_pNext = pNewElement;
}
Поскольку интерфейс функций, реализующих АТД “Дерево”, идентичен по сравнению с предыдущей рассмотренной структурой данных, то сформировать и обойти рассматриваемое дерево-пример способна та же самая тестовая программа без малейшей модификации.