目录
二叉搜索树的概念
二叉搜索树的实现
结点类
各函数接口总览
构造函数
拷贝构造函数
赋值运算符的重载
析构函数
插入函数
非递归实现
递归实现
删除函数
非递归实现
递归实现
查找函数
非递归实现
递归实现
二叉搜索树的应用
k模型
kv模型
二叉搜索树的性能分析
二叉搜索树又称二叉排序树,它或是一颗空树,或是具有一下性质的二叉树:
- 若它左子树不为空,则它左子树上的所有结点的值都小于根节点上的值
- 若它右子树不为空,则它右子树上的所有结点的值都大于跟结点上的值
- 它的左右子树也分别为二叉搜索树
列如,下面就是一课二叉搜索树:
由于二叉搜索树中,每个结点子树上所有的值都小于该节点的值,右子树上所有结点的值都大于该结点的值,因此对二叉搜索树进行中序遍历后,得到的是升序也就不难理解了.
要实现二叉搜索树,我们首先要实现一个结点类:
- 结点类当中包含三个成员变量:结点值,左指针,右指针.
- 结点类当中只需要实现一个构造函数即可,用于构造指定结点值的结点.
template
struct BSTreeNode
{
K _key;
struct BSTreeNode* _left;
struct BSTreeNode* _right;
BSTreeNode(const K& key)
:_key(key)
, _left(nullptr)
, _right(nullptr)
{
}
};
二叉搜索树需要实现的接口如下:
template
class BSTree
{
typedef BSTreeNode Node;
public:
//构造函数
BSTree();
//拷贝构造
BSTree(const BSTree& t);
//赋值运算符的重载
BSTree& operator=(BSTree t);
//析构函数
~BSTree();
//插入函数
bool Insert(const K& key);
//删除函数
bool Erase(const K& key);
//查找函数
Node* Find(const K& key);
//中序遍历
void Inoreder();
private:
Node* _root;
};
为了在实现其他接口中方便随时检查,最好实现一个二叉树的中序遍历接口,当我们对二叉搜索树进行一次操作后,可以调用中序遍历接口对二叉搜索树进行遍历,若二叉搜索树进行操作后结果仍为升序,则可以初步判断实现的接口是正确的.
void Inorder()
{
_Inorder(_root);
cout << endl;
}
void _Inorder(Node* root)
{
Node* cur = root;
if (cur == nullptr)
{
return;
}
_Inorder(cur->_left);
cout << cur->_key << " ";
_Inorder(cur->_right);
}
构造函数非常简单,构造一棵空树即可.
BSTree() = default;
拷贝构造构造函数也并不难,拷贝构造一棵和所给二叉树相同的树即可.
BSTree(const BSTree& t)
{
_root = copy(t._root);
}
Node* copy(Node* root)
{
if (root == nullptr)
return nullptr;
Node* newRoot = new Node(root->_key);
newRoot->_left = copy(root->_left);
newRoot->_right = copy(root->_right);
return newRoot;
}
注意:拷贝构造函数完成的是深拷贝.
对于复杂运算符的重载,下面提供二种写法:
传统写法:先将当前二叉搜索树的结点释放,然后完成所给二叉树的拷贝即可.
const BSTree& operator=(const BSTree& t)
{
if (this != &t)
{
destroy((*this)._root);
_root = copy(t._root);
}
return *this;
}
现代写法:
赋值运算符的现代写法非常精辟,函数在接收右值的时候并没有使用引用接收,因为这样可以间接的调用BSTree的拷贝构造函数完成拷贝构造,我们只需要将这各拷贝构造出来的对象的二叉搜索树于this对象的二叉搜索树进行交换即可,就相当于完成了赋值操作,二拷贝构造出来的对象t会在该运算符重载函数调用结束的时候自动析构.
const BSTree& operator=(BSTree& t)
{
swap(t._root, _root);
return *this;
}
这里传统写法和现代写法都是深拷贝.
析构函数完成对象中二叉搜索树结点的释放,注意释放时采用后续释放,当二叉搜索树中的结点被释放完后,将对象当中指向二叉搜索树的指针及时置为空.
~BSTree()
{
destroy(_root);
}
void destroy(Node*& root)
{
if (root == nullptr)
return;
destroy(root->_left);
destroy(root->_right);
delete root;
root = nullptr;
}
根据二叉搜索树的性质,其插入操作非常简单:
- 如果是空树,则直接将插入结点作为二叉搜索树的根结点.
- 如果不为空,则按照二叉搜索树的性质进行结点的插入.
若不是空树,插入结点的具体操作如下:
- 若待插入结点的值小于根结点的值,则需要将结点插入到左子树.
- 若待插入结点的值大于根结点的值,则需要将结点插入的右子树
- 若待插入结点的值等于根结点的值,则插入失败.
如此进行下去,直到找到于待插入结点的值相同的结点判定为插入失败,或则最终插入到某各叶子结点的左右子树当中(即空树当中).
使用非递归方式实现二叉搜索树的插入函数时,我们需要定义一个parent指针,该指针用于标记待插入结点的父结点,这样一来,当我们找到待插入结点的位置时,才能很号的将待插入结点于其父节点链接起来.
但是需要注意的在链接parent和cur时,需要判断应将cur链接到左边还时右边.
bool Insert(const K& key)
{
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
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);
if (parent->_key > key)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
return true;
}
递归实现二叉搜索树的插入操作也时很简单的,当需要注意的一点就是,递归插入函数的子函数接收参数root时必须采用引用接收,因为有这样我们才能将二叉树当中的各个结点链接起来.
bool InsertR(const K& key)
{
return _Insert(_root, key);
}
bool _Insert(Node*& root, const K& key)
{
if (root == nullptr)
{
root = new Node(key);
return true;
}
if (root->_key > key)
{
return _Insert(root->_left, key);
}
else if (root->_key < key)
{
return _Insert(root->_right, key);
}
else
{
return false;
}
}
二叉搜索树的删除函数是最难实现的,若是在二叉树当中没有找到待删除的结点,则直接返沪false表示删除失败即可,但是若是找到了待删除结点,此时就有一下三种情况:
- 待删除结点的左子树为空(待删除结点的左右子树均为空包含在内)
- 待删除结点的右子树为空.
- 待删除结点的左右子树均不为空.
下面我们分别对这三种情况进行分析处理:
待删除结点的左子树为空.
若待删除结点的左子树为空,那么当我们在二叉树种找到该结点后,只需要让其父节点指向该结点的右孩子结点,然后再将该结点释放便完成了该结点的删除,进行删除后仍保持二叉搜索树的特性.
待删除的结点右子树为空.
若待删除的结点的右子树为空,那么当我们在二叉搜索树当中找到该结点,只需让其父节点指向该结点的左孩子结点,然后再将该结点释放便完成了该结点的删除,进行删除查找后仍保持二叉树的特性.
待删除结点的左右子树均不为空
待删除结点的左右子树均不为空,那么当我们在二叉搜索树当中找到该结点后,可以使用替换法进行删除,可以将待删除结点左子树当中最大值的结点,或是待删除结点右子树当中最小的结点代替删除结点被删除(下面都以后者为例),然后将待删除结点的值改为替代其被删除的结点的值即可,而替代被删除的结点,必然左右子树当中至少有一个为空树,因此删除该结点的方法与前面说到的情况一和情况二的方法相同.
注意至少待删除结点左子树当中值最大的结点,或是待删除结点右子树当中值最小的结点代替待删除结点删除,因为只有这样才能使得进行删除操作后得二叉树仍保持二叉搜索树得特性.
实现得代码如下:
bool Erase(const K& key)
{
if (_root == nullptr)
return false;
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
{
break;
}
}
if (cur == nullptr)
return false;
if (cur->_left == nullptr)
{
if (cur == _root)
{
_root = cur->_right;
}
else
{
if (parent->_right = cur)
{
parent->_right = cur->_right;
}
else
{
parent->_left = cur->_right;
}
}
delete cur;
}
else if (cur->_right == nullptr)
{
if (cur == _root)
{
_root = cur->_left;
}
else
{
if (parent->_right = cur)
{
parent->_right = cur->_left;
}
else
{
parent->_left = cur->_left;
}
}
delete cur;
}
else
{//左右都不为空,找右树的最下结点(最左值)
parent = cur;
Node* subLeft = cur->_right;
while (subLeft->_left)
{
parent = subLeft;
subLeft = subLeft->_left;
}
swap(cur->_key, subLeft->_key);
if (parent->_left == subLeft)
{
parent->_left = subLeft->_right;
}
else
{
parent->_right = subLeft->_right;
}
delete subLeft;
}
return true;
}
递归实现得代码如下:
bool EraseR(const K& key)
{
return _EraseR(_root, key);
}
bool _EraseR(Node*& root, const K& 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
{
if (root->_left == nullptr)
{
Node* del = root;
root = root->_right;
delete del;
}
else if (root->_right == nullptr)
{
Node* del = root;
root = root->_left;
delete del;
}
else
{
Node* subLeft = root->_right;
while (subLeft->_left)
{
subLeft = subLeft->_left;
}
swap(root->_key, subLeft->_key);
return _EraseR(root->_right, key);
}
}
}
根据二叉搜索树的特性,我们在二叉搜索树当中查找指定值的结点的方式如下:
- 若树为空树,则查找失败,返回nullprt.
- 若key值小于当前结点的值,则应该在该结点的左子树当中进行查找
- 若key值大于当前结点的值,则应该在该结点的右子树当中进行查找.
- 若key值等于当前结点的值,则查找成功,返回对应结点的地址.
二叉搜索树查找函数的非递归实现如下:
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_key > key)
{
cur = cur->_left;
}
else if (cur->_key < key)
{
cur = cur->_right;
}
else
{
return cur;
}
}
return nullptr;
}
二叉搜索树查找函数的递归实现如下:
bool FindR(const K& key)
{
return _FindR(_root, key);
}
bool _FindR(Node* root, const K& 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;
}
}
K模型,即只有key作为关键码,结构中只需要存储key即可,关键码即为需要搜索到的值.
比如:给定一个单词,判断该单词是否拼写正确,具体方式如下:
- 以单词集合中的每一个单词作为key,构建一棵二叉搜索树.
- 在二叉搜索树中检索该单词是否存在,存在拼写正确,不存在拼写错误.
KV模型,对于每一个关键码key,都有与之对应的value,即
比如: 英汉词典就是英文于中文的对应关系,即
- 以<单词,中文含义>为键值对,关键一棵二叉搜索树.注意:二叉搜索树需要进行比较,键值对比较时只比较key.
- 查询英文单词时,只需要给出英文单词就可以快速找到其对应的英文含义.
对应二叉搜索树来说,无论时插入查找还是删除操作,都需要先进行查找,因此查找的效率代表了二叉搜索树各个查找的性能.
对于二叉搜索树这课特殊的树二叉树,我们每进行一次查找,若未找到目标结点,则还需要查找的树的层数就少了一层,所以我们最坏的情况下需要查找的次数就是二叉搜索树的深度,深度越深的二叉搜索树,比较次数就越多.
对于有n各结点的二叉搜索树:
- 最优情况下,二叉搜索树为完全二叉树,其平均比较次数为:logN.
- 最差的情况下,二叉搜索树退化为单枝树,其平均比较次数为:N/2.
而时间复杂度描述的是最坏情况下算法的效率,因此普通二叉搜索树各个操作的时间复杂度都是O(N).
所以实际上,二叉搜索树在极端情况下是没办法保证效率的,因此由二叉搜索树又衍生出了VAL树,红黑树等,他们对二叉搜索树的高度进行了优化,使得二叉搜索树非常接近完全二叉树,因此对于这些树来说,他们的效率是可以达到O(logN)的.
顺便说一下:B树和B+树树查找存储在磁盘当中的数据是经常用到的数据结构,B树系列对树的高度提出了更高的要求,此时二叉树已经不能满足要求了,为了降低树的高度,于是又衍生出了多叉树,而实际上这些树都是由二叉搜索树演变出来的,他们各有各的特点使用于不同的场景.