Реализация bst
Наиболее удобный способ реализации BST - динамическая структура для деревьев, рассмотренная в предыдущей лекции о деревьях общего назначения. Структура должна сопровождаться типичными операциями АТД “множество” для поиска, вставки и удаления ключей.
bstree.hpp:
#ifndef _BSTREE_HPP_
#define _BSTREE_HPP_
////////////////////////////////////////////////////////////////////////
struct BSTree
{
struct Node
{
int m_key;
Node * m_pParent;
Node * m_pLeft;
Node * m_pRight;
};
Node * m_pRoot;
};
///////////////////////////////////////////////////////////////////////
// Создание объекта-дерева
BSTree * BSTreeCreate ();
// Уничтожение объекта-дерева
void BSTreeDestroy ( BSTree * _pTree );
// Вставка указанного ключа в дерево
void BSTreeInsertKey ( BSTree & _tree, int _key );
// Проверка наличия указанного ключа в дереве
bool BSTreeHasKey ( const BSTree & _tree, int _key );
// Удаление указанного ключа из дерева
void BSTreeDeleteKey ( BSTree & _tree, int _key );
///////////////////////////////////////////////////////////////////////
#endif // _BSTREE_HPP_
Функции создания и уничтожения реализуются аналогично обычным бинарным деревьям:
bstree.cpp
#include "bstree.hpp"
#include <cassert>
// Создание объекта-дерева
BSTree * BSTreeCreate ()
{
BSTree * pTree = new BSTree;
pTree->m_pRoot = nullptr;
return pTree;
}
// Вспомогательная рекурсивная функция для удаления поддерева
void BSTreeDestroy ( BSTree::Node * _pNode )
{
if ( ! _pNode )
return;
BSTreeDestroy( _pNode->m_pLeft );
BSTreeDestroy( _pNode->m_pRight );
delete _pNode;
}
// Уничтожение объекта-дерева
void BSTreeDestroy ( BSTree * _pTree )
{
BSTreeDestroy( _pTree->m_pRoot );
}
Ниже показан пример реализации описанного алгоритма вставки в BST:
// Вспомогательная функция создания нового узла с указанным ключом
BSTree::Node * BSTreeCreateNode ( int _key )
{
// Создаем объект-узел
BSTree::Node * pNewNode = new BSTree::Node;
// Запоминаем в узле значение ключа
pNewNode->m_key = _key;
// Обнуляем все связи узла с соседями
pNewNode->m_pLeft = pNewNode->m_pRight = pNewNode->m_pParent = nullptr;
// Возвращаем созданный узел
return pNewNode;
}
// Вставка указанного ключа в дерево
void BSTreeInsertKey ( BSTree & _tree, int _key )
{
// Особый случай - вставка первого ключа в пустое дерево
BSTree::Node * pCurrent = _tree.m_pRoot;
if ( ! pCurrent )
{
_tree.m_pRoot = BSTreeCreateNode( _key );
return;
}
// Ищем позицию в дереве для вставки нового ключа, начиная с корня
while ( pCurrent )
{
// При равенстве ключей, игнорируем вставку
if ( pCurrent->m_key == _key )
return;
// Когда новый ключ меньше ключа в текущем узле, движемся в левую сторону
else if ( _key < pCurrent->m_key )
{
// Если левое поддерево уже есть, применяем алгоритм к его корню
if ( pCurrent->m_pLeft )
pCurrent = pCurrent->m_pLeft;
// Левого поддерева нет, новый узел становится левым поддеревом
else
{
BSTree::Node * pNewNode = BSTreeCreateNode( _key );
pNewNode->m_pParent = pCurrent;
pCurrent->m_pLeft = pNewNode;
return;
}
}
// Когда новый ключ больше ключа в текущем узле, движемся в правую сторону
else
{
// Если правое поддерево уже есть, применяем алгоритм к его корню
if ( pCurrent->m_pRight )
pCurrent = pCurrent->m_pRight;
else
{
// Правого поддерева нет, новый узел становится правым поддеревом
BSTree::Node * pNewNode = BSTreeCreateNode( _key );
pNewNode->m_pParent = pCurrent;
pCurrent->m_pRight = pNewNode;
return;
}
}
}
}
Теперь, если требуется выяснить имеется ли интересующий ключ в дереве, следует применить подобный рекурсивный алгоритм:
-
Начать анализ с узла-корня и назначить его текущим.
-
Сравнить искомый ключ с ключом в текущем узле:
-
Если искомый ключ меньше ключа в узле, следует рекурсивно выполнить шаг 2 на левом поддереве, либо завершить поиск, если левого поддерева нет.
-
Если искомый ключ больше ключа в узле, следует рекурсивно выполнить шаг 2 на правом поддереве, либо завершить поиск, если правого поддерева нет.
-
Если ключи равны, значит искомый узел найден.
-
Допустим, требуется узнать имеется ли число 4 в дереве. Выполняем описанный выше рекурсивный алгоритм и приходим от корня к искомому узлу. Ниже графически показан процесс обхода бинарного дерева поиска, при помощи цвета заливки выделен пройденный от корня путь:

Ниже приведена реализация данного алгоритма поиска:
// Вспомогательная функция поиска узла с интересующим ключом в дереве
BSTree::Node * BSTreeFindKeyNode ( const BSTree & _tree, int _key )
{
// Начинаем поиск с корневого узла
BSTree::Node * pCurrent = _tree.m_pRoot;
while ( pCurrent )
{
// Если искомый ключ равен ключу из текущего узла, нужный узел найден!
if ( _key == pCurrent->m_key )
return pCurrent;
// Если искомый ключ меньше ключа текущего узла, движемся по левому поддереву
else if ( _key < pCurrent->m_key )
pCurrent = pCurrent->m_pLeft;
// Если искомый ключ больше ключа текущего узла, движемся по правому поддереву
else
pCurrent = pCurrent->m_pRight;
}
// Узел с таким ключом не найден
return nullptr;
}
// Проверка наличия указанного ключа в дереве
bool BSTreeHasKey ( const BSTree & _tree, int _key )
{
// Ключ принадлежит множеству, если имеется узел с таким же ключом
return BSTreeFindKeyNode( _tree, _key ) != nullptr;
}
Фактически, алгоритмы поиска, вставки, удаления по ключу в бинарном дереве поиска во многом похожи. Следует найти либо сам узел в дереве, рекурсивно спускаясь, выбирая направление по результатам сравнения ключей, либо тем же способом найти место его потенциального расположения при вставке.
Вычислительная сложность таких алгоритмов пропорционально зависит от длины пути в дереве, т.е. от высоты дерева. В идеальном случае, когда высота листьев приблизительно одинаковая, количество рассматриваемых в процессе поиска узлов имеет логарифмическую зависимость от общего числа узлов в дереве O(log2 N). Это менее благоприятная вычислительная сложность по сравнению с хэш-таблицами O(1), однако существенно лучшая по сравнению с линейным поиском в массивах или списках O(N).
