二叉搜索树的概念
以递归的方式来定义二叉搜索树:
- 若它的左子树不为空,那么它左子树上所有结点的值都小于根节点的值。
- 若它的右子树不为空,那么它右子树上所有结点的值都大于根结点的值。
- 它的左右子树也为二叉搜索树。
操作与实现
二叉树结点的结构体:
template<class K>
struct BSTreeNode
{
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
K _key;
BSTreeNode(const K& key)//结点的构造函数
{
_key = key;
_left = nullptr;
_right = nullptr;
}
};
二叉搜索树类:
template<class K>
class BSTree
{
public:
typedef BSTreeNode<K> node;
//构造
BSTree():_root(nullptr){}
private:
node* _root;//根结点
}
二叉搜索树
二叉搜索树的拷贝构造,赋值重载,析构函数
整棵树的拷贝是深拷贝
写一个_Copy函数,使用前序递归遍历树的同时执行深拷贝:
node* _Copy(node* root)
{
if(root==nullptr)
{
return nullptr;
}
node* copynode=new node(root->_key);
copynode->left=_Copy(root->left);
copynode->right=_Copy(root->right);
return copyNode;
}
// 拷贝构造
BSTree(const BSTree<K>& t)
{
_root=_Copy(t._root);
}
void Destroy(node* root)
{
if(root==nullptr)
{
return ;
}
Destroy(root->left);
Destroy(root->right);
delete root;
}
//赋值重载
BSTree<K>& operator=(BSTree<K> T)
{
Destroy(_root);
_root=nullptr;
swap(_root,t._root);
return *this;
}
//析构
~BSTree()
{
Destroy(_root);
_root=nullptr;
}
二叉搜索树的查找
如果根结点值 ==查找值,返回true;
如果根结点值 > 查找值,在其左子树查找;
如果根结点值 < 查找值,在其右子树查找;
否则,返回false。
分别使用递归和迭代的方法进行查找
- 使用递归进行查找
//_root是private权限,而递归查找则需要传入_root作为参数
//所以这里需要套个娃,public的查找函数嵌套另一个可调用_root的查找函数
private:
node* _FindR(const K& key,node* root)
{
if(root==nullptr)
{
return nullptr;
}
if(root->_key>key)
{
return _FindR(key,root->_left);
}
else if(root->_key<key)
{
return _FindR(key,root->_right);
}
else
{
return root;
}
}
public:
node* FindR(const K& key)
{
_FindR(key,_root);
}
- 使用迭代查找
public:
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;
}
二叉搜索树的插入
插入的过程如下:
- 树为空,则直接插入,然后返回true。
- 树不空,按二叉搜索树性质查找插入为止,插入新结点。
分别用迭代和递归的方法来实现插入结点:
- 迭代
private:
bool _insertR(const K& key,node* root)
{
if(root==nullptr)
{
root=new node(key);
return true;
}
if(root->_key>key)
{
return _insertR(key,root->left);
}
else if(root->_key<key)
{
return _insertR(key,root->right);
}
else
{
return false;
}
}
public:
bool insertR(const K& key)
{
return _insertR(key,_root);
}
- 迭代
public:
bool insert(const K& key)
{
if(_root==nullptr)
{
_root=new node(key);
return true;
}
node* cur =_root;
node* parent=_root;//记录父结点,方便插入结点后进行连接
while(cur)
{
if(root->_key>key)
{
parent=cur;
cur=cur->_left;
}
else if(root->_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;
}
二叉搜索树的删除
二叉搜索树的难点便是在于删除操作,首先依旧是查找元素是否在二叉搜索树中,如果不存在则返回,否则要删除的点可能分为如下三个情况:
- 要删除的结点A没有孩子结点,直接删除即可;
- 要删除的结点A仅有左孩子或者右孩子,直接将A的子节点连至A的父结点,并将A删除;
- A有两个子节点,用左子树的最大结点B(或者右子树的最小结点)取代A,随后删除B即可,删除B一定可以满足第一或者第二种情况。左子树的最大节点是极易获得的:从左子结点开始一直向右走至底即是。
我们亦可以使用迭代和递归来删除结点:
- 递归
private:
bool _EraseR(const K& key,node* root)
{
if(root==nullptr)
{
return false;
}
if(root->_key>key)
{
return _EraseR(key,root->_left);
}
else if(root->_key<key)
{
return _EraseR(key,root->_right);
}
else//找到删除的元素
{
if(root->_right==nullptr)
{
node* del=root;
root=root->_left;
delete del;
}
else if(root->_left==nullptr)
{
node* del=root;
root=root->_right;
delete del;
}
else//拥有左右子树
{
node* maxleft=root->left;
while(maxleft->_right)
{
maxleft=maxleft->_right;
}
K tmp=maxleft->_key;
_Erase(tmp,root->_left);
root->_key=tmp;
}
return true;
}
}
public:
bool EraseR(const K& key)
{
return _EraseR(key,_root);
}
- 迭代
public:
bool Erase(const K& 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//找到删除点位cur和其父结点parent
{
//删除的三种情况
//有一边为空,那么就用另一边 的孩子直接连上去(parent不为nullptr)
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;
}
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;
}
else//两边都不为空,需要使用替换删除
{
//找左子树中的最大结点
node* Maxleft = cur->_left;
node* MaxParent = cur;
while (Maxleft->_right)
{
MaxParent = Maxleft;
Maxleft = Maxleft->_right;
}//循环结束 Maxleft 找到了左子树的最大点
cur->_key = Maxleft->_key;
//删除掉Maxleft这个位置的结点
if (MaxParent->_right == Maxleft)//左子树最大结点在右子树的最右处
{
MaxParent->_right = Maxleft->_left;
}
else//左子树本身没有右子树,所以最大结点为左子树的根
{
MaxParent->_left = Maxleft->_left;
}
delete Maxleft;
Maxleft = nullptr;
}
return true;
}
}
return false;
}
应用
-
K模型
K模型只有一个key作为关键码,结构中只需要存储key即可,关键码即为需要搜索到的值。
例如,给一个单词word,判断该单词是否拼写正确:
- 以单词集合的每个单词作为key,构建二叉搜索树
- 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
-
KV模型——键值对
每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。
该种方式在现实生活中非常常见:比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;
再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。
比如:实现一个简单的英汉词典dict,可以通过英文找到与其对应的中文,具体实现方式如下:
- <单词,中文含义>为键值对构造二叉搜索树,注意:二叉搜索树键值对比较时只比较Key。
- 查询英文单词时,只需给出英文单词,就可快速找到与其对应的key。
关于键值对模型的二叉搜索树的代码实现我会置于文末。
性能分析
插入和删除操作之前都必须要先查找,查找效率代表了二叉搜索树中各个操作的性能。
有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度与结点在二叉搜索树的深度相关,即结点越深,则比较次数越多。
对于同一个关键码集合,如果各关键码插入次序的不同,可能得到不同结构的二叉搜索树。
最优情况下,二叉树为完全二叉树,其平均比较次数为:log2N
最差情况下,二叉搜索树为单支树,其平均比较次数为:N/2
所以在每次插入结点时,我们都可以对二叉搜索树的树形进行调整,使其成为一棵平衡的二叉树,这将在后续博客中讲述。
-end-
青山不改 绿水长流