博主会持续更新
本篇文章主要讲解 二叉搜索树 的相关内容
文章目录
1. 二叉搜索树
1.1 二叉搜索树概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的
左子树不为空
,则左子树上所有节点的值都小于根节点的值
; - 若它的
右子树不为空
,则右子树上所有节点的值都大于根节点的值
; - 它的左右子树也分别为二叉搜索树。
1.2 二叉搜索树操作
下图是一个二叉搜索树:
二叉搜索树的查找find
- 从根开始比较,查找,比根
大
则往右
边走查找,比根小
则往左
边走查找; 最多查找高度次
,走到空,还没找到,这个值不存在。
二叉搜索树的插入Insert
插入的具体过程如下:
- 树为空,则直接新增节点,赋值给root指针;
- 树不空,按二叉搜索树性质
查找插入位置
,插入新节点。
二叉搜索树的删除Erase
首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情
况
:
- 要删除的结点
无孩子
结点 - 要删除的结点
只有左孩子
结点 - 要删除的结点
只有右孩子
结点 - 要删除的结点
有左、右孩子
结点
看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来
,因此真正的删除过程
如下:
- 情况2:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点–直接删除;
- 情况3:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点–直接删除;
- 情况4:在它的右子树中寻找中序下的第一个结点(关键码
最小
),用它的值填补到被删除节点中,再来处理该结点的删除问题–替换法
删除。
2. 二叉搜索树的实现
定义节点类BSTNode
对于二叉树的每个节点,都应该有一个值_key
,和指向左右孩子的指针_left、_right
,以及一个构造函数。
template<class K>
struct BSTNode
{
typedef BSTNode<K> Node;
K _key;
Node* _left;
Node* _right;
BSTNode(const K& key)
:_key(key)
,_left(nullptr)
,_right(nullptr)
{}
};
定义二叉搜索树类成员变量
二叉搜索树中应该有一个节点类型的指针_root
。
template<class K>
class BSTree
{
public:
typedef BSTNode<K> Node;
private:
Node* _root = nullptr;
};
插入操作Insert
刚才说了具体的思路,这边直接写代码了:
bool Insert(const K& key)
{
// 树为空
if (_root == nullptr)
{
Node* newnode = new Node(key);
_root = newnode;
return true;
}
// 树不为空
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
{
return false;
}
}
// 创建新节点插入
Node* newnode = new Node(key);
if (key > parent->_key)
{
parent->_right = newnode;
}
else
{
parent->_left = newnode;
}
return true;
}
中序遍历InOrder
中序遍历要传入根节点_root
,但是这里的根节点是私有
成员变量,不能在类外进行访问,(最好不要用友元),我们可以这样写:
public:
void InOrder()
{
_InOrder(_root);
cout << endl;
}
private:
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
- 测试一下:
int main()
{
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
BSTree<int> t1;
for (auto e : a)
{
t1.Insert(e);
}
t1.InOrder();
return 0;
}
查找操作Find
查找操作就很简单了:
Node* Find(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
{
return cur;
}
}
return nullptr;
}
删除操作Erase
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 (cur == _root)
{
_root = cur->_right;
}
// 不是根节点
else
{
if (parent->_left == cur)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_left;
}
}
delete cur;
cur = nullptr;
return true;
}
// 右孩子为空,父亲的指针指向它的左孩子
else if (cur->_right == nullptr)
{
// 删除根节点时
if (cur == _root)
{
_root = cur->_right;
}
// 不是根节点
else
{
if (parent->_left == cur)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_left;
}
}
delete cur;
cur = nullptr;
return true;
}
// 左右孩子都不为空
else
{
// 找右子树最小的值
Node* RightMinNode = cur->_right;
Node* RightMinNodeParent = cur;
while (RightMinNode->_left)
{
RightMinNodeParent = RightMinNode;
RightMinNode = RightMinNode->_left;
}
swap(RightMinNode->_key, cur->_key);
if (RightMinNodeParent->_left == RightMinNode)
{
RightMinNodeParent->_left = RightMinNode->_right;
}
else
{
RightMinNodeParent->_right = RightMinNode->_right;
}
delete RightMinNode;
RightMinNode = nullptr;
return true;
}
}
}
return false;
}
- 测试一下:
int main()
{
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
BSTree<int> t1;
for (auto e : a)
{
t1.Insert(e);
}
t1.InOrder();
for (auto e : a)
{
t1.Erase(e);
t1.InOrder();
}
return 0;
}
这里要注意的细节很多:
- 当删除的节点的左子树或者右子树为空,但是这个节点就是
root
的时候,这个时候要进行特殊判断,因为我们定义的Node* parent = nullptr
,会造成空指针解引用的问题,例如这种情况:
- 当左右子树的指针都不为空的时候,定义
RightMinNodeParent
的时候,不能初始化为空,要是cur
,因为当删除root
的时候还是会造成空指针解引用的问题。
- 交换之后,一定要判断,不能直接让
RightMinNodeParent->_right = RightMinNode->_right;
,一般情况下,删除非root
的节点,都没啥问题,但是如果删除root
的节点,例如这种情况,就有坑了,所以一定要判断:
LeetCode 450. 删除二叉搜索树中的节点
LeetCode上有一道关于二叉搜索树删除节点的题目:
这是链接:450. 删除二叉搜索树中的节点
这是博主的解答,和上面的思路一样,只是返回值不同而已:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution
{
public:
typedef TreeNode Node;
TreeNode* deleteNode(Node* root, int key)
{
Node* cur = root;
Node* parent = nullptr;
while (cur)
{
if (key < cur->val)
{
parent = cur;
cur = cur->left;
}
else if (key > cur->val)
{
parent = cur;
cur = cur->right;
}
else
{
if (cur->left == nullptr)
{
if (cur == root)
{
root = cur->right;
}
else
{
if (parent->left == cur)
{
parent->left = cur->right;
}
else
{
parent->right = cur->right;
}
delete cur;
cur = nullptr;
}
return root;
}
else if (cur->right == nullptr)
{
if (cur == root)
{
root = cur->left;
}
else
{
if (parent->left == cur)
{
parent->left = cur->left;
}
else
{
parent->right = cur->left;
}
delete cur;
cur = nullptr;
}
return root;
}
else
{
Node* RightMinNode = cur->right;
Node* RightMinNodeParent = cur;
while (RightMinNode->left)
{
RightMinNodeParent = RightMinNode;
RightMinNode = RightMinNode->left;
}
swap(RightMinNode->val, cur->val);
if (RightMinNodeParent->left == RightMinNode)
{
RightMinNodeParent->left = RightMinNode->right;
}
else
{
RightMinNodeParent->right = RightMinNode->right;
}
return root;
}
}
}
return root;
}
};
3. 二叉搜索树的应用
- K模型:K模型即只有
key
作为关键码,结构中只需要存储Key
即可,关键码即为需要搜索到的值。
- 比如:给一个单词
word
,判断该单词是否拼写正确,具体方式如下:- 以词库中所有单词集合中的每个单词作为
key
,构建一棵二叉搜索树; - 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
- 以词库中所有单词集合中的每个单词作为
- KV模型:每一个关键码
key
,都有与之对应的值Value
,即<Key, Value>
的键值对。该种方式在现实生活中非常常见:
-
比如
英汉词典
就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英
文单词与其对应的中文<word, chinese>
就构成一种键值对; -
再比如
统计单词次数
,统计成功后,给定单词就可快速找到其出现的次数,单词与其出
现次数就是<word, count>
就构成一种键值对。
3.1 英汉词典简易实现
英汉词典简易实现
这个KV
模型只要区别就是每个节点和插入的时候的区别
,其他区别不大,我就展示节点和插入时候的区别:
template<class K, class V>
struct BSTNode
{
typedef BSTNode<K, V> Node;
K _key;
V _val;
Node* _left;
Node* _right;
BSTNode(const K& key, const V& val)
:_key(key)
,_val(val)
, _left(nullptr)
, _right(nullptr)
{}
};
template<class K, class V>
class BSTree
{
public:
typedef BSTNode<K, V> Node;
bool Insert(const K& key, const V& val)
{
// 树为空
if (_root == nullptr)
{
Node* newnode = new Node(key, val);
_root = newnode;
return true;
}
// 树不为空
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
{
return false;
}
}
// 创建新节点插入
Node* newnode = new Node(key, val);
if (key > parent->_key)
{
parent->_right = newnode;
}
else
{
parent->_left = newnode;
}
return true;
}
}
- 测试一下:
void test_KV_BSTree()
{
BSTree<string, string> dict;
dict.Insert("hello", "你好");
dict.Insert("world", "世界");
dict.Insert("humorous", "幽默");
string str;
while (cin >> str)
{
if (dict.Find(str) != nullptr)
{
cout << dict.Find(str)->_val << endl;
}
else
{
cout << "未查询到此单词,如有需要,请更新词库" << endl;
}
}
}
3.2 统计单词次数
统计单词次数
这里的BSTree
代码和上面的简易实现英汉词典方法类似,只是InOrder
稍微变一下,要输出一个val
。
void test_KV_BSTree2()
{
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
"苹果", "香蕉", "苹果", "香蕉" , "草莓", "草莓" };
BSTree<string, int> countTree;
for (auto& str : arr)
{
auto ret = countTree.Find(str);
if (ret == nullptr)
{
countTree.Insert(str, 1);
}
else
{
++ret->_val;
}
}
countTree.InOrder();
}