目录
2. KV模型:每一个关键码key,都有与之对应的值Value,即的键值对。,>
前言:
本文主要讲解二叉搜索树的概念、操作以及实现,为后面的的map、set容器实现做准备,因为他们同样也是树形结构。
二叉搜索树是目前为止搜索效率比较高的一种容器,之前是数组,维护成本很高,比如中间或者头部插入删除数据要挪动数据,是O(N)。而二叉搜索树删除和插入结点相交而言简单许多,而且搜索二叉树的中序遍历是有序的~
一、二叉搜索树的概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
二、二叉搜索树的操作
1. 二叉搜索树的查找
a、从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
b、最多查找高度次,走到到空,还没找到,这个值不存在。
2. 二叉搜索树的插入
插入的具体过程如下:
a. 树为空,则直接新增节点,赋值给root指针
b. 树不空,按二叉搜索树性质查找插入位置,插入新节点
3、二叉搜索树的删除(替换法删除)
注意前面的查找和插入实现比较简单,但是删除较为复杂,需要分情况讨论~
首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:
a. 要删除的结点无孩子结点(没有孩子)
b. 要删除的结点只有左孩子结点(只有一个孩子)
c. 要删除的结点只有右孩子结点(只有一个孩子)
d. 要删除的结点有左、右孩子结点(有两个孩子)
看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来,因此真正的删除过程如下:
情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点--直接删除
情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点--直接删除
情况d:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中,再来处理该结点的删除问题--替换法删除!
直接删除容易理解,下面我们来重点讲解什么是替换法删除:
替换法删除的本质是:找一个能替换我的结点,交换值,转换为删除他。
那从哪里找一个能替换我的结点呢?
一共有两类:
- 左子树的最大结点(左子树最右结点)
- 右子树的最小结点(右子树最左结点)
举个例子:
比如我们要删除左子树中 3 这个结点,我们可以从3的右子树中找到最小结点,与之交换,接着再将交换过的右子树进行erase操作,这样可以成功将3这个结点删除,并且保证了搜索二叉树的有序性。
三、二叉搜索树的实现(递归实现)
注意下面代码实现,博主都是用递归进行实现,因为一般用递归实现要减少很多特殊情况和代码量,相比较于普通循环实现。
插入操作insert():
结点参数使用引用的精妙之处:
用递归实现插入有一个问题:那就是如何将一个新的结点与原先的树相连接,也就是如何真正完成插入操作。
这里我们结点参数采用引用的方式传参,就精妙的解决了这个问题!
这里的root就是父节点,因此可以直接连接~
//参数使用引用,不用多余的参数,便可完成插入操作
bool Insert(const K& key, const V& value)
{
return _Insert(_root, key, value);
}
bool _Insert(Node*& root, const K& key, const V& value)
{
if (root == nullptr)
{
root = new Node(key, value);
return true;
}
if (key > root->_key)
{
return _Insert(root->_right, key, value);
}
else if (key < root->_key)
{
return _Insert(root->_left, key, value);
}
else
{
return false;
}
}
查找操作find():
Node* Find(const K& key)
{
return _Find(_root, key);
}
Node* _Find(Node* root, const K& key)
{
if (root == nullptr) return nullptr;
if (root->_key > key)
{
return _Find(root->_left, key);
}
else if (root->_key < key)
{
return _Find(root->_right, key);
}
else
{
return root;
}
删除操作erase():
bool Erase(const K& key)
{
return _Erase(_root, key);
}
bool _Erase(Node*& root, const K& key)
{
if (root == nullptr) return false;
if (root->_key > key)
{
return _Erase(root->_left, key);
}
else if (root->_key < key)
{
return _Erase(root->_right, key);
}
else
{
Node* del = root;
if (root->_left == nullptr)
{
root = root->_right;
}
else if (root->_right = nullptr)
{
root = root->_left;
}
else
{
//替换法删除
Node* rightMin = root->_right;
while (rightMin->_left)
{
rightMin = rightMin->_left;
}
swap(rightMin->_key, root->_key);
return _Erase(root->_right, key);
}
delete del;
return true;
}
}
中序遍历inorder():
void _InOrder(Node* root)
{
if (root == nullptr) return;
_InOrder(root->_left);
cout << root->_key << ' ' << endl;
_InOrder(root->_right);
}
void InOrder()
{
return _InOrder(_root);
}
TIP:
注意在类里面的递归函数都得套一层,因为在外部不能调用私有成员,比如这里的_root,在类外面是不能访问私有变量_root,因此类里面的递归函数一般都要封装一层。
四、二叉搜索树的应用
1. K模型:
K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
小区内部车库
2. KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。
该种方式在现实生活中非常常见:
比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;
再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值~