二叉搜索树
二叉搜索树的概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
这样一颗树就是标准的二叉搜索树。
二叉搜索树的实现
二叉搜索树最重要的就是插入和删除两个接口,我们重点说这两个接口。
结构:
- 它的结构和二叉树差不多,一个左孩子指针一个右孩子指针,还有一个存储的值,但是这里我们需要套一层模板。
cpp
template <class T>
struct BSTreeNode
{
BSTreeNode<T>* _left;
BSTreeNode<T>* _right;
T _key;
//构造函数
BSTreeNode(const T& key = 0)
: _key(key)
, _left(nullptr)
, _right(nullptr)
{}
};
插入
插入还是比较简单的,如果插入的是第一个节点,既根节点是空的时候,我们直接new一个节点给_root就可以了,接下来就是需要找插入的位置,既然是二叉搜索树我们就要满足搜索二叉树的规则,如果要插入的值比_key大,我们就去搜索树的左边插入,如果要插入的值比_key小我们就去搜索树的左边插入,直到遇到nullptr节点,我们就可以开始插入了。
但是有一个问题,我们找到了nullptr,但是我们找不到它的父亲,所以我们还需要一个父节点指针来保存它的父亲。知道了它的父亲我们还是不知道要往父亲的左边插入还是父亲的右边插入,所以我们找到了也不能直接插入,还需要判断一下要插入的key和父亲的_key比较一下,大就往右边插入,小就往左边插入。
ps:二叉搜索树不能插入相同的值!!
cpp
bool Insert(const K& key)
{
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
parent = cur;
if (key > cur->_key)
{
cur = cur->_right;
}
else
{
if (key < cur->_key)
{
cur = cur->_left;
}
else
{
return false;
}
}
}
if (key > parent->_key)
{
parent->_right = new Node(key);
}
else
{
parent->_left = new Node(key);
}
return true;
}
删除
删除的话肯定要先在二叉树中找到该节点,找到后删除节点,但是不能直接删除节点,因为删除后依然要保持搜索二叉树的性质,所以找到需要删除的节点后,就有4种情况。
假设我们要删除3这个节点。
-
要删除的结点无孩子结点
-
要删除的结点只有左孩子结点
-
要删除的结点只有右孩子结点
-
要删除的结点有左、右孩子结点
前三种情况都好说,如果是只有左孩子节点,我们可以直接让它的父亲指向它的左孩子,如果是只有右孩子节点,我们可以让父亲直接指向它的右孩子,但是在这之前,我们需要看自己是父亲的左还是右,然后对应改变父亲指针就可以了,第一种情况可以分到第二种和第三种中,所以前三种是比较简单的。
最麻烦的就是它的左孩子和右孩子都存在,我们不能把孩子托管出去,这时我们需要使用替代法,我们需要找一个能够替代它的人,然后把它俩换一下,在进行删除。那么如何找这个能够替代它的节点呢?
我们想一下,这个节点需要满足搜索二叉树的性质,所以这个节点的值一定要比左孩子的孩子都大,并且要比右孩子的所有节点都小,基于这个原因,我们可以找左孩子的最大值节点or右孩子的最小节点来当这个替代节点,在搜索二叉树中,左边的最大节点一定是在最右边,右边的最小节点也一定在最左边,基于这个原因,我们就可以找到这个替代的节点。
ps:如果我们要找的这个节点就是这个左孩子的根或者右孩子的根节点,那么我们就不能直接连接,需要判断一下!
cpp
bool Erase(const K& key)
{
Node* cur = _root;
Node* parent = nullptr;
//找需要删除的元素
while (cur)
{
if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else
{
if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else
{
//找到,开始删除
//第一种情况,无左孩子
if (cur->_left == nullptr)
{
if (parent == nullptr)
{
_root = cur->_right;
}
else
{
if (cur == parent->_left)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
}
else
{
//第二种情况,无右孩子
if (cur->_right == nullptr)
{
if (parent == nullptr)
{
_root = cur->_left;
}
else
{
if (cur == parent->_left)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
}
else
{
//有两个孩子
//找左孩子最大节点 -----右孩子最小节点同理
Node* MaxRight = cur->_left;
Node* parent = cur;
while (MaxRight->_right)
{
parent = MaxRight;
MaxRight = MaxRight->_right;
}
swap(cur->_key, MaxRight->_key);
//到这里MaxRight一定无右孩子,如果parent->left==cur,说明一次也没找
//那么就需要连接parent的_left,所以下面不能直接连接右孩子,需要进行判断
if (parent->_left == MaxRight)
{
parent->_left = MaxRight->_left;
}
else
{
parent->_right = MaxRight->_left;
}
cur = MaxRight;
}
}
delete cur;
return true;
}
}
}
//找不到直接return
return false;
}
查找
查找最简单,基于搜索二叉树的特性,我们可以和二分查找一样,大就去右边,小就去左边,到nullptr就是找不到。最多查找高度次!
cpp
bool Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (key > cur->_key)
{
cur = cur->_right;
}
else
{
if (key < cur->_key)
{
cur = cur->_left;
}
else
{
return true;
}
}
}
return false;
}
由于搜索二叉树的特性,我们会发现二叉搜索树走一个中序遍历就是一个有序的数据,也由此二叉搜索树又称二叉排序树。
二叉搜索树的应用
K模型
K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
例如:给一个单词word,判断该单词是否拼写正确,我们可以这样做
- 以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
- 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误
KV模型
每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方式在现实生活中非常常见。
比如英汉词典就是英文与中文的对应关系。
总结:
二叉搜索树在最优情况心下的效率是O(logN),但是如果插入的是有序的数据搜索二叉树就会退化成单支树,此时效率会大大折扣,AVL树和红黑树不会出现这种情况!
那么今天的分享就到这里了,有什么不懂得可以私信博主,或者添加博主的微信,欢迎交流。