目录
一、二叉搜索树概念
1、概念
2、结构
3、性质
二、二叉搜索树模拟实现
1、二叉搜索树节点
2、二叉搜索树构造函数
3、二叉搜索树查找
(1)迭代版本
(2)递归版本
4、二叉搜索树插入
(1)迭代版本
(2)递归版本
5、二叉搜索树节点删除
(1)迭代版本
(2)递归版本
6、二叉搜索树拷贝构造和operator=
7、二叉搜索树的析构函数
三、二叉搜索树应用
1、K模型
2、KV模型
3、二叉搜索树性能分析
总结
二叉搜索树(Binary Search Tree),(又:二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根节点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉搜索树。二叉搜索树作为一种经典的数据结构,它既有链表的快速插入与删除操作的特点,又有数组快速查找的优势;所以应用十分广泛,例如在文件系统和数据库系统一般会采用这种数据结构进行高效率的排序与检索操作。
二叉搜索树是能够高效地进行如下操作的数据结构。
1.插入一个数值
2.查询是否包含某个数值
3.删除某个数值
设x是二叉搜索树中的一个结点。如果y是x左子树中的一个结点,那么y.key≤x.key。如果y是x右子树中的一个结点,那么y.key≥x.key。
在二叉搜索树中:
1.若任意结点的左子树不空,则左子树上所有结点的值均不大于它的根结点的值。
2. 若任意结点的右子树不空,则右子树上所有结点的值均不小于它的根结点的值。
3.任意结点的左、右子树也分别为二叉搜索树
这也就说明二叉搜索树的中序遍历是升序的
二叉搜索树的节点与我们普通的二叉树没有什么区别,唯一的区别是这次使用C++来实现,增加了模板参数
template
struct BSTreeNode
{
BSTreeNode* _left;
BSTreeNode* _right;
K _key;
BSTreeNode(const K& key)
:_key(key)
,_left(nullptr)
,_right(nullptr)
{}
};
我们使用struct而不使用class的原因是,我们需要频繁的修改二叉树节点的属性,直接暴露给我们的二叉搜索树类比较方便使用
二叉搜索树这个类只要存入二叉树的根节点就可以了
template
class BSTree
{
typedef BSTreeNode Node;
public:
BSTree() = default;
Node* _root = nullptr;
}
编译器默认生成的构造函数就可以了
这里加入default关键字是让编译器强制生成默认构造函数,因为等会我们要写拷贝构造,根据C++的类和对象的知识,我们只要显式的写一个构造函数,编译器就不会生成默认构造函数
二叉搜索树的查找天生适合用循环,因为二叉搜索树它是有序的,左树比根小,右树比根大
我们可以根据这个特性去寻找,走到空的位置还没有找到,证明该树中没有该元素
bool Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_key < key)
{
cur = cur->_right;
}
else if (cur->_key > key)
{
cur = cur->_left;
}
else
{
return true;
}
}
return false;
}
递归这里有一点小问题,因为在C++类中,实现递归是有一些问题的,因为我们把根直接封在类中了,函数根本就不需要参数就可以访问根节点,但是我们的递归函数需要参数来控制向哪棵树递归
这时我们可以采用子函数的方式来解决
bool FindR(const K& key)
{
}
bool _FindR(Node* root, const K& key)
{
}
完整代码
bool FindR(const K& key)
{
return _FindR(_root, key);
}
bool _FindR(Node* root, const K& key)
{
if (root == nullptr)
{
return false;
}
if (root->_key < key)
{
return _FindR(root->_right);
}
else if (root->_key > key)
{
return _FindR(root->_left);
}
else
{
return true;
}
}
二叉搜索树的插入是按照我们前面的规律遍历,只要走到空,该位置就是我们要进行插入的位置
但是对于链式结构来说,我们直接向这个空节点赋值,表面上是链接了,实际上根本没有链接上,因为我们遍历所使用的节点是一个临时变量,出了作用域就会自动销毁,所以根本没有链接上,所以我们还要记住最后走到的空节点的父节点,用父节点来链接新插入的节点
并且因为二叉搜索树的特点是有序+去重,所以如果找到树中与插入节点相同的值,就会终止插入
bool Insert(const K& key)
{
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
//查找
Node* cur = _root;
Node* parent = nullptr;
//cur走到空开始插入
while (cur)
{
if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(key);
if (parent->_key < key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}
bool InsertR(const K& key)
{
return _InsertR(_root, key);
}
bool _InsertR(Node*& root, const K& key)
{
if (root == nullptr)
{
root = new Node(key);
return true;
}
if (root->_key < key)
{
return _InsertR(root->_right, key);
}
else if (root->_key > key)
{
return _InsertR(root->_left, key);
}
else
{
return false;
}
}
二叉搜索树删除节点十分的复杂
首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情 况:a. 要删除的结点无孩子结点b. 要删除的结点只有左孩子结点c. 要删除的结点只有右孩子结点d. 要删除的结点有左、右孩子结点
看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来,因此真正的删除过程 如下:情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点--直接删除情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点--直接删除情况d:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中,再来处理该结点的删除问题--替换法删除
首先我们先写寻找的逻辑
bool Erase(const K& key)
{
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else
{
}
}
return false;
}
这是最简单的架子,接下来我们通过画图来观察
首先我们来看cur->left == nullptr的情况
首先删除3,我们可以让3的右孩子6去顶替3,然后让parent->left = cur->right
这是其中一种情况
另一种情况是,删除6,这时是cur = parent->right
我们还是老样子让parent->right = cur->right;
不过我们忽略了一个最为重要的点,我们的parent有可能为空,我们在进行指针解引用的时候,都要注意指针是否为空的问题。parent为空,证明我们的cur就是根节点,同时它的左子树为空
我们要删除的是8,直接让根节点指向cur的右子树,然后释放8,就可以了
else
{
//删除
if (cur->_left == nullptr)
{
if (cur == _root)
{
_root = cur->_right;
}
else
{
if (cur == parent->_left)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
delete cur;
cur = nullptr;
}
接下来就是右子树为空的情况了,思路与左子树为空类似
else if (cur->_right == nullptr)
{
if (cur == _root)
{
_root = cur->_left;
}
else
{
if (cur == parent->_left)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
delete cur;
cur = nullptr;
}
最为关键重要的就是删除左右子树均不为空的情况了,这种情况才是最复杂的
我们要删除3,很多人都会直接懵掉了,这到底要怎么删除?
我们采用替换法来解决,我们知道cur的右子树中最左边的节点的值是与cur节点的值是最接近的
所以我们交换3和4,这样就转换为删除叶子节点了,这样就会把问题的难度降低
else
{
Node* minParent = cur;
Node* min = cur->_right;
while (min->_left)
{
minParent = min;
min = min->_left;
}
swap(min->_key, cur->_key);
if (minParent->_left == min)
{
minParent->_left = min->_right;
}
else
{
minParent->_right = min->_right;
}
delete min;
min = nullptr;
}
同时还要注意一点minparent->left == min和minparent->right == min两种情况
完整代码
bool Erase(const K& key)
{
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else
{
//删除
if (cur->_left == nullptr)
{
if (cur == _root)
{
_root = cur->_right;
}
else
{
if (cur == parent->_left)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
delete cur;
cur = nullptr;
}
else if (cur->_right == nullptr)
{
if (cur == _root)
{
_root = cur->_left;
}
else
{
if (cur == parent->_left)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
delete cur;
cur = nullptr;
}
else
{
Node* minParent = cur;
Node* min = cur->_right;
while (min->_left)
{
minParent = min;
min = min->_left;
}
swap(min->_key, cur->_key);
if (minParent->_left == min)
{
minParent->_left = min->_right;
}
else
{
minParent->_right = min->_right;
}
delete min;
min = nullptr;
}
return true;
}
}
return false;
}
在我看来,递归版本比迭代版本好写,递归版本代码有一个神之一手,它传参时用的是引用,这样就不用记录父节点了
前面搜索的过程与我们前面所写的递归搜索差不多
分的情况也与前面差不多
cur->left == nullptr时和cur->right == nullptr,我们就不用分的特别的细了,因为我们传的是引用,相当于传的是上一层节点的别名,我们直接操作就可以
Node* del = root;
if (root->_left == nullptr)
{
root = root->_right;
}
else if (root->_right == nullptr)
{
root = root->_left;
}
最神奇的不是引用而是删除左右都不为空的时候,我们还是按照前面的逻辑找右树中最左边的节点,然后交换,然后我们直接调用递归,去右树找删除的节点,为什么是右树呢?因为我们把节点交换到cur的右树了。
bool EraseR(const K& key)
{
return _EraseR(_root, key);
}
bool _EraseR(Node*& root, const K& key)
{
if (root == nullptr)
{
return false;
}
if (root->_key < key)
{
return _EraseR(root->_right, key);
}
else if (root->_key > key)
{
return _EraseR(root->_left, key);
}
else
{
Node* del = root;
if (root->_left == nullptr)
{
root = root->_right;
}
else if (root->_right == nullptr)
{
root = root->_left;
}
else
{
Node* min = root->_right;
while (min->_left)
{
min = min->_left;
}
swap(min->_key, root->_key);
return _EraseR(root->_right, key);
}
delete del;
del = nullptr;
}
}
拷贝构造,我们可以参考前面的二叉树重建,直接递归解决,所以我们让二叉搜索树的拷贝构造调用我们的_Copy函数
BSTree(const BSTree& t)
{
_root = _Copy(t._root);
}
Node* _Copy(Node* root)
{
if (root == nullptr)
{
return nullptr;
}
Node* copyNode = new Node(root->_key);
copyNode->_left = _Copy(root->_left);
copyNode->_right = _Copy(root->_right);
return copyNode;
}
operator=我们采用现代写法,传参时会调用拷贝构造,我们直接将形参与我们当前对象交换
BSTree& operator=(BSTree t)
{
swap(_root, t._root);
return *this;
}
析构函数也要递归实现,采用后序遍历,一个一个节点的删除
~BSTree()
{
_Destory(_root);
}
void _Destory(Node* root)
{
if (root == nullptr)
{
return;
}
_Destory(root->_left);
_Destory(root->_right);
delete root;
root = nullptr;
}
我们前面所写的就是K模型
二叉搜索树的性能取决于树的形状,因为它的每一个操作都是要进行高度次
时间复杂度就是O(h) h是树的高度
二叉搜索树的形状可能十分的规整,类似完全二叉树,这样的效率就十分的高接近O(logn)
这样的树也是二叉搜索树,不过它的效率就退化为O(n)了
以上就是今天要讲的内容,本文仅仅简单介绍了二叉搜索树的简单应用及实现。