一、二叉搜索树的相关概念
1.二叉搜索树的定义
(1)二叉搜索树首先是一颗二叉树。
(2)树本身和子树都满足左子树的节点比根小,右子树节点比根大。
(3)二叉搜索树节点的值不允许重复。
2.二叉搜索树的使用价值
(1)搜索:最多查找高度次。
(2)排序:中序遍历是有序的,所以搜索二叉树也叫做排序二叉树。
二、二叉搜索树的操作
1.二叉搜索树的查找
//查找
Node* Find(const K& key)
{
Node* cur = _root;
while(cur)
{
//找小的,去左边找
if(key < cur->_key)
cur = cur->_left;
//找大的,去右边找
else if(key > cur->_key)
cur = cur->_right;
//找到相等的了,返回true
else
return cur;
}
//走到空都没有,就证明没有
return nullptr;
}
2.二叉搜索树的插入
(1)树为空:直接插入。
(2)树不为空:按照二叉搜索树的性质找到要插入的位置,然后插入。
//插入
bool Insert(const K& key)
{
//树为空,直接插入
if(_root == nullptr)
{
_root = new Node(key);
return true;
}
//树不为空
Node* cur = _root;
Node* parent = nullptr;
//找到要插入的位置
while(cur)
{
//插小的
if(key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
//插大的
else if(key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
//插一样的(不允许有一样的)
else
{
return false;
}
}
//插入
cur = new Node(key);
//连接
if(key < parent->_key)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
return true;
}
3.二叉搜索树的删除
可以分为以下四种情况
(1)删除叶子节点:直接删除
(2)要删除的节点 左子树为空:父亲指向我的右
(3)要删除的节点 右子树为空:父亲指向我的左
(4)要删除的节点 左右子树都不为空:不能直接删除,使用替换法删除
找左子树最大节点(左子树的最右节点)或右子树的最小节点(左子树的最左节点)去替代他
但是删除叶子节点可以归类到上面提到的(2)(3)类中,所以删除只分为以下三种情况
(1)要删除的节点 左子树为空:父亲指向我的右
(2)要删除的节点 右子树为空:父亲指向我的左
(3)要删除的节点 左右子树都不为空:不能直接删除,使用替换法删除
展示删除的完整代码
//删除(分成三种情况,详细见博客)
bool Erase(const K& key)
{
Node* parent = nullptr;
Node* cur = _root;
//找到要删除的位置
while(cur)
{
//删除小的,去左边删
if(key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
//删除大的,去右边删
else if(key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
//找到要删的了,开始删除(cur指向要被删除的节点)
else
{
//1.左为空
if(cur->_left == nullptr)
{
//删除根节点(因为删除根节点,_root会发生改变)
if(cur == _root)
{
_root = cur->_right;
}
//删除不是根的节点
else
{
//连接新节点
if(cur == parent->_left)
parent->_left = cur->_right;
else
parent->_right = cur->_right;
}
//删除
delete cur;
}
//2.右为空
else if(cur->_right == nullptr)
{
//删除根节点(因为删除根节点,_root会发生改变)
if(cur == _root)
{
_root = cur->_left;
}
//删除不是根的节点
else
{
//连接新节点
if(cur == parent->_left)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
//删除
delete cur;
}
//3.左右都不为空
else
{
Node* rightMin = cur->_right;//最终要指向右子树的最小的节点
Node* rightMinParent = cur;
//找到右子树最小的节点
while(rightMin->_left)
{
rightMinParent = rightMin;
rightMin = rightMin->_left;
}
//节点替换:将找到的右子树最小的节点 和 要删除的节点交换
//std::swap(cur->_key,rightMin->_key);
cur->_key = rightMin->_key;//把cur改了就行,反正rightMin也要被删掉了
//转换成删除rightMin:只会出现情况一和情况二
//rightMin是左孩子
if(rightMin == rightMinParent->_left)
rightMinParent->_left = rightMin->_right;
//rightMin是右孩子
else
rightMinParent->_right = rightMin->_right;
delete rightMin;
}
//到这成功删除,返回true
return true;
}
}
//走到这没找到要删除的节点,返回false
return false;
}
删除的大体框架:1.先找到要删除的节点 2.删除节点
先找到要删除的节点(cur会指向要删除的节点)
//删除的大体框架
bool Erase(const K& key)
{
Node* parent = nullptr;
Node* cur = _root;
//找到要删除的位置
while(cur)
{
//删除小的,去左边删
if(key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
//删除大的,去右边删
else if(key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
//找到要删的了,开始删除(cur指向要被删除的节点)
else
{
//开始删除
}
}
//走到这没找到要删除的节点,返回false
return false;
}
删除cur所指向的节点
1.要删除的节点 左子树为空:父亲指向我的右
下列两个图对应解释了出现的两种情况
(1)如果要删除的节点是根节点
(2)要删除的节点有可能是左孩子也可能是右孩子,这就导致重新连接时会有不同
//1.左为空
if(cur->_left == nullptr)
{
//删除根节点(因为删除根节点,_root会发生改变)
if(cur == _root)
{
_root = cur->_right;
}
//删除不是根的节点
else
{
//连接新节点
if(cur == parent->_left)
parent->_left = cur->_right;
else
parent->_right = cur->_right;
}
//删除
delete cur;
}
2.要删除的节点 右子树为空:父亲指向我的左
和第一种情况差不多,所以就不在这里配图了,整体的逻辑都是一样的。
//2.右为空
else if(cur->_right == nullptr)
{
//删除根节点(因为删除根节点,_root会发生改变)
if(cur == _root)
{
_root = cur->_left;
}
//删除不是根的节点
else
{
//连接新节点
if(cur == parent->_left)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
//删除
delete cur;
}
3.要删除的节点 左右子树都不为空:不能直接删除,使用替换法删除
(1)首先找到用谁替换要被删除的节点:左子树的最大节点/右子树的最小节点
左子树的最大节点:左子树最右边的节点 右子树的最小节点:右子树最左边的节点
(2)节点替换:将找到的左子树的最大节点/右子树的最小节点 和 要删除的节点交换
(3)转换成删除 左子树的最大节点/右子树的最小节点 这时的删除一定满足情况1或情况2
下面用图来解释:我们选择用 右子树的最小节点 替换被删节点
同样,这里也要区分被删除的节点是左孩子还是右孩子
//3.左右都不为空
else
{
Node* rightMin = cur->_right;//最终要指向右子树的最小的节点
Node* rightMinParent = cur;
//找到右子树最小的节点
while(rightMin->_left)
{
rightMinParent = rightMin;
rightMin = rightMin->_left;
}
//节点替换:将找到的右子树最小的节点 和 要删除的节点交换
//把cur改了就行,反正rightMin也要被删掉了
cur->_key = rightMin->_key;
//转换成删除rightMin:只会出现情况一和情况二
//rightMin是左孩子
if(rightMin == rightMinParent->_left)
rightMinParent->_left = rightMin->_right;
//rightMin是右孩子
else
rightMinParent->_right = rightMin->_right;
delete rightMin;
}
三、二叉搜索树的应用
1.K模型
K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值
key模型 -> 判断key在不在(这个比较简单,不在举例说明)
//Binary Search Tree Node
template<class K>
struct BSTreeNode{
public:
BSTreeNode(const K& key = K())
:_left(nullptr)
,_right(nullptr)
,_key(key)
{}
public:
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
K _key;
};
//Binary Search Tree
template<class K>
class BSTree{
public:
typedef BSTreeNode<K> Node;
public:
//构造函数
BSTree();
//析构函数
~BSTree();
//查找
bool Find(const K& key);
//插入
bool Insert(const K& key);
//删除
bool Erase(const K& key);
private:
Node* _root;
};
2.KV模型
每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对
key/value模型 -> 通过key找对应的value -> 中英词典互译/统计次数等等
下面的代码只展示KV模型搜索树的大体框架,具体实现和K模型搜索树基本一致
//Binary Search Tree Node KV
template<class K, class V>
struct BSTreeNodeKV{
public:
BSTreeNodeKV(const K& key = K(), const V& value = V())
:_left(nullptr)
,_right(nullptr)
,_key(key)
,_value(value)
{}
public:
BSTreeNodeKV<K, V>* _left;
BSTreeNodeKV<K, V>* _right;
K _key;
V _value;
};
//Binary Search Tree
template<class K, class V>
class BSTreeKV{
public:
typedef BSTreeNodeKV<K, V> Node;
public:
//构造函数
BSTreeKV();
//析构函数
~BSTreeKV();
//查找(是查找key)
Node* Find(const K& key);
//插入(把<key,value>插入进去)
bool Insert(const K& key, const V& value);
//删除(删除key对应的节点)
bool Erase(const K& key);
private:
Node* _root;
};
(1)中英词典互译:英文单词与其对应的中文<word, chinese>就构成一种键值对
void TestBSTreeKVdict()
{
//字典
BSTreeKV<string, string> dict;
dict.Insert("sort", "排序");
dict.Insert("string", "字符串");
dict.Insert("tree", "树");
dict.Insert("insert", "插入");
string str;
while (cin >> str) {
BSTreeNodeKV<string, string>* ret = dict.Find(str);
if(ret)
cout << ret->_value << endl;
else
cout << "没有这个单词" << endl;
}
}
(2)统计次数:水果种类和次数<fruit, count>就构成一种键值对
void TestBSTreeKVcount()
{
string strArr[] = {"西瓜", "苹果", "香蕉","西瓜", "苹果",
"香蕉","西瓜", "苹果", "香蕉", "西瓜","西瓜","西瓜"};
BSTreeKV<string, int> countTree;
for(auto str:strArr)
{
BSTreeNodeKV<string, int>* ret = countTree.Find(str);
//如果计数的树中还没有,将新水果插入
if(ret == nullptr)
countTree.Insert(str, 1);
//计数的树中已经有了这种水果,value++
else
ret->_value++;
}
countTree.InOrder();
}
四、二叉搜索树的性能分析
插入的数据是有序或者接近有序的时候,搜索树的效率就完全没办法保证了(因为出现右单支)
在这种最坏的情况下,搜索树的效率是o(N) -> 如何解决?平衡树:AVLTree/红黑树。
当然如果是完全二叉树这种情况,搜索树的效率是o(logN)