二叉搜索树的概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
以上则是一个二叉搜索树,左子树的值都小于根节点,右子树的值都大于根节点
一、二叉搜索树的查找和插入
查找:
a,从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
b,最多查找高度次,走到到空,还没找到,这个值不存在。
我们要在这颗树查找是否有13这个数,13比8大,则进入到根节点8的右子树,13比10大,则进入到根节点10的右子树,13比14小,则进入到根节点14的左子树,这时我们找到了13,返回true;
查找的代码实现:
//树的节点
template<class K>
struct BTSNood
{
BTSNood(const K& key = K())
:_key(key)
,_left(nullptr)
,_right(nullptr)
{
}
BTSNood<K>* _left;
BTSNood<K>* _right;
K _key;
};
template <class T>
class BTSTree
{
typedef BTSNood<T> Node;
public:
//非递归版本,查找
bool Find(const T& key)
{
Node* cur = _root;
while(cur)
{
//key比根节点要大,则进入根节点的右子树继续查找
if(cur->_key > key)
{
cur = cur->_left;
}
//key比根节点要小,则进入根节点的左子树继续查找
else if(cur->_key < key)
{
cur = cur->_right;
}
//找到了返回true
else
{
return true;
}
}
//循环找完还没有找到就不存在,返回false
return false;
}
private:
Node* _root;
}
插入:
a,当树刚开始为空时,则直接新增一个节点,赋值给root指针
b,树不空时,按二叉搜索树的性质,查找到插入位置,链接上
插入2时,2比根节点8要小,则进入根节点8的左子树,2比根节点3要小,则进入根节点3的左子树,2比根节点1要大则进入右子树,右子树刚好为空则插入2;
插入18时,18比根节点8要大,则进入根节点8的右子树,18比根节点10要大,则进入根节点10的右子树,18比根节点14要大则进入右子树,右子树刚好为空则插入18;
插入代码实现:
//树的节点
template<class K>
struct BTSNood
{
BTSNood(const K& key = K())
:_key(key)
,_left(nullptr)
,_right(nullptr)
{
}
BTSNood<K>* _left;
BTSNood<K>* _right;
K _key;
};
template <class T>
class BTSTree
{
typedef BTSNood<T> Node;
public:
//非递归版本,插入
bool Insert(const T& key)
{
//如果树一开始为空,则直接创建新节点,给_root指针赋值
if(_root == nullptr)
{
_root = new Node(key);
return true;
}
//因为新节点要跟parent节点链接,所以我们在查找插入的节点时
//需要记住parent节点
Node* cur = _root;
Node* parent = nullptr;
while(cur)
{
parent = cur;
if(cur->_key > key)
{
cur = cur->_left;
}
else if(cur->_key < key)
{
cur = cur->_right;
}
//也有可能插入失败,目前我们这个版本的二叉搜索树不允许相同的值出现
else
{
return false;
}
}
cur = new Node(key);
//找到可插入的位置后,需要判断一下是在parent的左节点,还是在右节点
//再进行链接
if(parent->_key > key)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
return true;
}
private:
Node* _root;
}
二、二叉树的删除
首先要查找要删除的节点是否存在,如果不存在直接返回false,存在的话,删除分下面的四种情况:
- 要删除的节点无左右结点
- 要删除的节点只有右节点
- 要删除的节点只有左节点
- 要删除的节点有左右节点
实际1可以与2和3合并为一种情况,最后分为三种情况:
- 如果是情况2,要删除的节点只有右节点,则修改parent父节点指向该节点的右树,直接删除该节点。
- 如果是情况3,要删除的节点只有左节点,则修改parent父节点指向该节点的左树,直接删除该节点。
- 可以在该节点的左子树找最大的值,或右子树找最小的值,跟要删除的节点替换,最后删除被替换的节点即可。
代码实现:
//树的节点
template<class K>
struct BTSNood
{
BTSNood(const K& key = K())
:_key(key)
,_left(nullptr)
,_right(nullptr)
{
}
BTSNood<K>* _left;
BTSNood<K>* _right;
K _key;
};
template <class T>
class BTSTree
{
typedef BTSNood<T> Node;
public:
//非递归版本,删除
bool Erase(const T& key)
{
//首先先跟查找一样,找到要删除的值
Node* cur = _root;
Node* parent = nullptr;
while(cur)
{
if(cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else if(cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else
{
//找到后分三种情况处理
//左为空,托孤,让parent节点,指向cur->_right
if(cur->_left == nullptr)
{
//如果这个值是根节点,没办法托孤,需要单独处理
if(cur == _root)
{
_root = cur->_right;
}
else
{
//需要注意要判断parent的左节点是cur还是右节点
if(parent->_left == cur)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
delete cur;
}
//右为空,托孤,让parent节点,指向cur->_left
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;
}
//左右不为空,则需要找替换删除
else
{
//找到右边最小的值,跟要删除的值替换
//或找到左边最大的值,跟要删除的值替换
Node* parent = cur;
Node* minRight = cur->_right;
while(minRight->_left)
{
parent = minRight;
minRight = minRight->_left;
}
cur->_key = minRight->_key;
if(parent->_left == minRight)
{
parent->_left = minRight->_right;
}
else
{
parent->_right = minRight->_right;
}
delete minRight;
}
return true;
}
}
return false;
}
private:
Node* _root;
}
1.插入,查找,删除的递归版本
代码如下:
void InOrder()
{
_InOrder(_root);
std::cout << std::endl;
}
bool EraseR(const T& key)
{
return _EraseR(_root,key);
}
bool InsertR(const T& key)
{
return _InsertR(_root,key);
}
//递归版本
bool FindR(const T& key)
{
return _FindR(_root,key);
}
private:
bool _EraseR(Node*& root,const T& key)
{
//查找的代码
if(root == nullptr)
{
return false;
}
if(root->_key > key)
{
return _EraseR(root->_left, key);
}
else if(root->_key < key)
{
return _EraseR(root->_right, key);
}
else
{
Node* del = root;
//找到后分三种情况处理
if(root->_left == nullptr)
{
root = root->_right;
}
else if(root->_right == nullptr)
{
root = root->_left;
}
else
{
//如果左右子树不为空,则找右边最小的数替换
//然后递归替换后节点的右子树删除
Node* minRight = root->_right;
while(minRight->_left)
{
minRight = minRight->_left;
}
std::swap(root->_key,minRight->_key);
return _EraseR(root->_right, key);
}
delete del;
return true;
}
}
bool _InsertR(Node*& root,const T& 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;
}
}
bool _FindR(Node* root,const T& key)
{
if(root == nullptr)
{
return false;
}
if(root->_key > key)
{
return _FindR(root->_left,key);
}
else if(root->_key < key)
{
return _FindR(root->_right, key);
}
else
{
return true;
}
}
2.二叉搜索树的应用场景
KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方式在现实生活中非常常见:
- 比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;
- 再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对
改造搜索二叉树
//节点增加一个模版参数
template<class K,class V>
struct BSTreeNode
{
BSTreeNode<K,V>* _left;
BSTreeNode<K,V>* _right;
K _key;
V _value;
BSTreeNode(const K& key,const V& value)
:_key(key)
,_value(value)
,_left(nullptr)
,_right(nullptr)
{
}
};
template<class K, class V>
class BSTree
{
typedef BSTreeNode<K, V> Node;
public:
bool Insert(const K& key, const V& value)
{
if(_root == nullptr)
{
_root = new Node(key,value);
return true;
}
Node* cur = _root;
Node* parent = nullptr;
while(cur)
{
parent = cur;
if(key > cur->_key)
{
cur = cur->_right;
}
else if (key < cur->_key)
{
cur = cur->_left;
}
else
{
return false;
}
}
if(parent->_key < key)
{
parent->_right = new Node(key,value);
}
else
{
parent->_left = new Node(key,value);
}
return true;
}
Node* 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 cur;
}
}
return nullptr;
}
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(_root == cur)
{
_root = cur->_right;
}
else
{
//需要知道,cur是parent的左树还是右树
if(parent->_left == cur)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
delete cur;
}
else if(cur->_right == nullptr)
{
//如果这个值是根节点需要单独处理
if(_root == cur)
{
_root = cur->_left;
}
else
{
//需要知道,cur是parent的左树还是右树
if(parent->_left == cur)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
delete cur;
}
else
{
//要找到左树最大的值,或右树最小的值进行替换删除
Node* parent = cur;
Node* minRight = cur->_right;
while(minRight->_right)
{
parent = minRight;
minRight = minRight->_right;
}
cur->_key = minRight->_key;
if(parent->_left == minRight)
{
parent->_left = minRight->_left;
}
else
{
parent->_right = minRight->_left;
}
delete minRight;
}
}
}
}
void _InOrder(Node* root)
{
if(root == nullptr)
{
return ;
}
_InOrder(root->_left);
std::cout << root->_key << ":" << root->_value << std::endl;
_InOrder(root->_right);
}
void InOrder()
{
_InOrder(_root);
}
private:
Node* _root = nullptr;
};
//测试用例
void TestBSTree4()
{
// 统计水果出现的次数
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
"苹果", "香蕉", "苹果", "香蕉" };
BSTree<string, int> countTree;
for (const auto& str : arr)
{
// 先查找水果在不在搜索树中
// 1、不在,说明水果第一次出现,则插入<水果, 1>
// 2、在,则查找到的节点中水果对应的次数++
//BSTreeNode<string, int>* ret = countTree.Find(str);
auto ret = countTree.Find(str);
if (ret == NULL)
{
countTree.Insert(str, 1);
}
else
{
ret->_value++;
}
}
countTree.InOrder();
}
测试结果
总结
例如:以上就是今天要讲的内容,本文仅仅介绍二叉搜索树的查找,插入,删除的代码实现,和一些细节注意。