前面的文章介绍过二叉树的基础概念以及完全二叉树的应用等等,本篇将介绍二叉搜索树
二叉搜索树的概念
二叉搜索树是一颗排序树,或者是一颗空树如图:
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它的左右子树也分别为二叉搜索树,并且它可以去重
二叉搜索树的操作
基本框架
template<class T>
struct BSTreeNode
{
BSTreeNode<T>* _left;
BSTreeNode<T>* _right;
T _key;
BSTreeNode(const T& key)
:_left(nullptr)
,_right(nullptr)
,_key(key)
{}
};
template<class T>
class BSTree
{
typedef BSTreeNode<T> Node;
public:
BSTree()
:_root(nullptr)
{}
private:
Node* _root;
};
二叉搜索树的插入
例如插入6和10
bool insert(const T& key)
{
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
Node* cur = _root;
Node* parent = cur;
while (cur)
{
if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else
return false;
}
cur = new Node(key);
if (parent->_key > key)
parent->_left = cur;
else
parent->_right = cur;
return true;
}
注意:会有树为空的情况需要判断
二叉搜索树的查找
查找的代码和插入非常类似,大于根去右边,小于根去左边,最多查找高度次
代码如下:
//Node* find(const T& key)
bool find(const T& key)
{
if (_root == nullptr)
return false;
//return nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_key > key)
cur = cur->_left;
else if (cur->_key < key)
cur = cur->_right;
else
return true;
//return cur;
}
return false;
//return nullptr;
}
二叉搜索树的删除
这里的删除相对于插入和查找来说是比较难的,最重要的是不能改变二叉搜索树的结构,所以这里主要分为两种情况考虑
- 删除只有一个子节点或没有子节点
- 删除左右子节点都存在的
先找到要删除的节点,代码如下:
bool erase(const T& key)
{
if (_root == nullptr)
return false;
Node* cur = _root;
Node* parent = cur;
while (cur)
{
if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else
{
删除部分的代码........
}
}
return false;
}
1. 删除只有一个子节点或没有子节点
(1)删除叶子节点(删除9举例)
(2)删除只存在一个子节点的节点(删除8举例)
情况1的删除代码:
else
{
if (cur->_left == nullptr)
{
if (cur == parent->_left)
parent->_left = cur->_right;
else
parent->_right = cur->_right;
}
else if (cur->_right == nullptr)
{
if (cur == parent->_left)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
else
{情况2部分的代码.......}
delete cur;
}
除上述代码删除的示例外,另外三种情况
所以这里需要仔细处理链接,否则会破坏搜索树的结构
2.删除左右节点都存在的节点
(1)删除根节点
(2)删除其他存在左右子节点的节点
找到的右子树最小节点一定没有左节点,且它的父节点一定不为空;所以在交换或覆盖完数值后,剩下的步骤和情况一一样,并且这里不用单独判断删除的节点存在哪边的子节点(右子树的最小节点一定没有左节点,所以只可能存在右节点)
情况2的删除代码:
else
{
Node* min_parent = cur;
Node* min = cur->_right;
while (min->_left)
{
min_parent = min;
min = min->_left;
}
cur->_key = min->_key;
if (min == min_parent->_left)
min_parent->_left = min->_right;
else
min_parent->_right = min->_right;
swap(min,cur);
}
以上就是删除主要步骤,但还有个问题就是要考虑只有一个节点,需要操作根节点。
if (_root->_key == key )
{
if (_root->_left == nullptr)
_root = _root->_right;
else
_root = _root->_left;
}
总体代码
bool erase(const T& key)
{
if (_root == nullptr)
return false;
Node* cur = _root;
Node* parent = cur;
while (cur)
{
if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else
{
if (_root->_key == key )
{
if (_root->_left == nullptr)
_root = _root->_right;
else
_root = _root->_left;
}
else if (cur->_left == nullptr)
{
if (cur == parent->_left)
parent->_left = cur->_right;
else
parent->_right = cur->_right;
}
else if (cur->_right == nullptr)
{
if (cur == parent->_left)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
else
{
Node* min_parent = cur;
Node* min = cur->_right;
while (min->_left)
{
min_parent = min;
min = min->_left;
}
//把min摘出来,交换后删除cur
if (min == min_parent->_left)
min_parent->_left = min->_right;
else
min_parent->_right = min->_right;
//目的是删除cur,所以先赋值,再交换
cur->_key = min->_key;
swap(min,cur);
}
delete cur;
return true;
}
}
return false;
}
整体代码
循环写法
bool insert(const T& key)
{
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
Node* cur = _root;
Node* parent = cur;
while (cur)
{
if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else
return false;
}
cur = new Node(key);
if (parent->_key > key)
parent->_left = cur;
else
parent->_right = cur;
return true;
}
bool find(const T& key)
{
if (_root == nullptr)
return false;
//return nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_key > key)
cur = cur->_left;
else if (cur->_key < key)
cur = cur->_right;
else
return true;
//return cur;
}
return false;
//return nullptr;
}
bool erase(const T& key)
{
if (_root == nullptr)
return false;
Node* cur = _root;
Node* parent = cur;
while (cur)
{
if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else
{
if (_root->_key == key )
{
if (_root->_left == nullptr)
_root = _root->_right;
else
_root = _root->_left;
}
else if (cur->_left == nullptr)
{
if (cur == parent->_left)
parent->_left = cur->_right;
else
parent->_right = cur->_right;
}
else if (cur->_right == nullptr)
{
if (cur == parent->_left)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
else
{
Node* min_parent = cur;
Node* min = cur->_right;
while (min->_left)
{
min_parent = min;
min = min->_left;
}
cur->_key = min->_key;
if (min == min_parent->_left)
min_parent->_left = min->_right;
else
min_parent->_right = min->_right;
swap(min,cur);
}
delete cur;
return true;
}
}
return false;
}
//打印搜索树
void print_BSTree()
{
_print_BSTree(_root);
cout << endl;
}
void _print_BSTree(Node* root)
{
if (root == nullptr)
return;
_print_BSTree(root->_left);
cout << root->_key << " ";
_print_BSTree(root->_right);
}
递归写法
1.插入
bool insertR(const T& key)
{
return insertR(_root,key)
}
bool _insertR(Node*& root,const T& key)
{
if(root == nullptrd)
{
root = new Node(key);
return true;
}
if(root->_key > key)
return insertR(root->_left,key);
else if(root->_key < key)
return insertR(root->_right,key);
else
return false;
}
注意,这里可以成功链接上,主要是运用了引用,递的是左右指针的引用
2.查找
Node* findR(const T& key)
{
if()
}
bool _insertR(Node*& root, const T& key)
{
if (root == nullptr)
{
root = new Node(key);
return true;
}
if (root->_key > key)
return _insertR(root->_left, key);
else if (root->_key < key)
return _insertR(root->_right, key);
else
return false;
}
3.删除
bool EarseR(const K& key)
{
return EarseR(_root,key);
}
bool _EraseR(Node*& root, const T& key)
{
if (root == nullptr)
return false;
if (root->_key > key)
_EraseR(root->_left, key);
else if (root->_key < key)
_EraseR(root->_right, key);
else
{
Node* era = root;
if (root->_left == nullptr)
root = root->_right;
else if (root->_right == nullptr)
root = root->_left;
else
{
Node* min = root->_right;
Node* min_parent = root;
while (min->_left)
{
min_parent = min;
min = min->_left;
}
swap(min->_key, root->_key);
return _EraseR(root->_right, key);
}
delete era;
return true;
}
}
这里讲一下删除的思路:
情况一时,操作和递归的插入基本一样,通过使用引用就可直接令要删除节点的父节点与子节点直接相连,也不用再去判断删除节点是父节点的左边还是右边
情况二时,先找到右子树最小节点,再与要删除的节点交换数值,最后递归传递的节点是,要删除节点的左子树(情况二再交换完数值后就可以按照情况一去处理了)
注意:要提前记录一下找到的要删除的节点,中间过程会改变root的值,所以最后删除的是提前记录好的节点
总体代码
bool insertR(const T& key)
{
return _insertR(_root,key);
}
Node* findR(const T& key)
{
return _findR(_root,key);
}
bool EraseR(const T& key)
{
return _EraseR(_root, key);
}
private:
bool _insertR(Node*& root, const T& key)
{
if (root == nullptr)
{
root = new Node(key);
return true;
}
if (root->_key > key)
return _insertR(root->_left, key);
else if (root->_key < key)
return _insertR(root->_right, key);
else
return false;
}
Node* _findR(Node* root, const T& key)
{
if (root == nullptr)
return nullptr;
if (root->_key > key)
return _findR(root->_left, key);
else if (root->_key < key)
return _findR(root->_right, key);
else
return root;
}
bool _EraseR(Node*& root, const T& key)
{
if (root == nullptr)
return false;
if (root->_key > key)
_EraseR(root->_left, key);
else if (root->_key < key)
_EraseR(root->_right, key);
else
{
Node* era = root;
if (root->_left == nullptr)
root = root->_right;
else if (root->_right == nullptr)
root = root->_left;
else
{
Node* min = root->_right;
Node* min_parent = root;
while (min->_left)
{
min_parent = min;
min = min->_left;
}
swap(min->_key, root->_key);
return _EraseR(root->_right, key);
}
delete era;
return true;
}
}
二叉搜索树的应用
- K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:- 以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
- 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
- KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方式在现实生活中非常常见:
- 比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;
- 再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对
c++提供了这种键值对儿的结构,叫做pair
pair<string,int> p1("one",1);
p1.first就是key
p1.second就是value
make_pair("one",p1); 可以构造一个pair
二叉搜索树的性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
但是会有极端情况:当插入的值顺着一个方向一直插入(即插入的值一直是最小或最大或者接近这种情况),那么二叉树就变成了单树,这时的查找效率就是极低了,即结点越深,则比较次数越多。
- 最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为: l o g 2 N log_2 N log2N
- 最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:
N
2
\frac{N}{2}
2N
当变为单树就失去意义了(查找元素相当于在顺序表中搜索元素,效率低下)。