目录
一、概念:
二、代码实现:
大致结构:
1、遍历:
2、insert
3、find
4、erase
三、总结:
一、概念:
二叉搜索树又称为二叉排序树,是一种具有特殊性质的二叉树,对于每一个节点而言,节点的左子树要比节点的值小,节点的右子树要比节点的值大,也就是说节点的左右子树也是二叉搜索树。
例如如下二叉树:
拿根节点8来讲,8的左子树都是比8小的值,而8的右子树都是比8大的值,与此同时,8的左右子树也是二叉搜索树,拿8的左子树的根节点3来讲,3的左子树都是比3大的值,3的右子树都是比3小的值。以此反复。
二、代码实现:
大致结构:
// 二叉树的节点:
template<class K>
struct BSTreeNode
{
BSTreeNode<K>* left;
BSTreeNode<K>* right;
K _key;
// 节点的构造函数
BSTreeNode(const K& key)
:left(nullptr)
,right(nullptr)
,_key(key)
{}
};
// 搜索二叉树的功能:
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
// 插入:
bool insert(const K& key);
// 查找:
bool Find(const K& key);
// 删除:
bool erase(const K& key)
// 遍历:
void InOrder();
private:
Node* _root = nullptr; // 根节点
};
1、遍历:
前面说过,在搜索二叉树中对于每一个节点而言,比它大的值在右边,比它小的值在左边,这种特性如果用二叉树的中序遍历打印出来的就是一个有序的数列,所以搜索二叉树的遍历可以直接写中序遍历,但要注意的是,在上面的大致结构中二叉搜索树的类中的根节点是私有的,而中序遍历又需要用到根节点,所以我们可以直接嵌套一层:
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
// 其他成员函数
void InOrder()
{
_InOrder(_root);
}
private:
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->left);
cout << root->_key << " ";
_InOrder(root->right);
}
private:
Node* _root = nullptr;
};
2、insert
值插入需要从根节点开始往下遍历到空位置(比节点大的往右走,比节点小的往左走),需要注意的是,期间如果遇到相同的值(也就是重复的情况)就可以不用进行插入了,因为搜索树不支持冗余。
思路:如果树为空,那么直接定义一个节点即可,不为空则设置一个指针cur指向根节点并往下遍历,注意cur注定会走到空,所以要多设置一个指针parent代表cur的父节点。最后如果插入值比父节点大则是parent的右孩子,否则就是左孩子。
bool insert(const K& key)
{
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
// 从根节点往下遍历
while (cur)
{
if (cur->_key < key) // 插入值比当前节点值大
{
parent = cur;
cur = cur->right;
}
else if (cur->_key > key) // 插入值比当前节点值小
{
parent = cur;
cur = cur->left;
}
else
{
return false; // 插入值等于节点值,直接不用插入了
}
}
cur = new Node(key);
// 如果插入值比父节点大则是右节点
if (parent->_key < key)
{
parent->right = cur;
}
else // 否则就是左节点
{
parent->left = cur;
}
return true;
}
代码测试:
// 往搜索树中插入一段数据,然后遍历出来(搜索树的中序遍历出来是有序的数列)
int main()
{
int a[] = { 8,3,1,10,6,4,7,14,13 };
BSTree<int> t1;
for (int i : a)
{
t1.insert(i);
}
t1.InOrder();
}
3、find
值查找就是从根节点向下遍历,比节点值大的往右走,比节点值小的往左走,找到返回true找不到返回false。
bool 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 true;
}
}
return false;
}
4、erase
搜索二叉树的删除值操作分为好几种情况,以下一一列举:
情况一:无左右孩子,例如节点 4、7、13,这种就可以直接删除。(这里有一种特殊情况需要特殊处理,那就是整棵树只有这一个节点,需要把该节点置空,不能直接删)
情况二:只有左孩子或者只右孩子,例如节点 10、14,这种需要记录其父节点,然后让其父节点指向自己的孩子节点,再删除该节点。
情况三:节点的左右孩子同时存在,例如节点8,3,6,此时需要用到替换法(以节点8为例),用该节点的左子树最大的值(7)或者右子树最小的值(10)替换该节点,然后再删除被替换的节点即可(此时还需注意被替换的点还有没有孩子,有的话删除前还需要将被替换节点的孩子交给被替换节点的父节点保管)。(也就是左子树找最右的叶子节点或者右子树找最左的叶子节点)
代码实现:
bool erase(const K& key)
{
Node* parent = nullptr; // 记录被删除节点的父节点,以便链接被删除节点的孩子
Node* cur = _root;
while (cur)
{
if (cur->_key < key)
{
parent = cur;
cur = cur->right;
}
else if (cur->_key > key)
{
parent = cur;
cur = cur->left;
}
else // 找到该值,进行条件划分并删除
{
if (cur->left == nullptr) // 左为空,父节点指向我的右孩子
{
if (cur == _root) _root = cur->right; // 处理单枝或单节点情况
else
{
if (cur == parent->left) // 判断被删除的节点是父节点的左还是右,用于子节点链接
{
parent->left = cur->right;
}
else
{
parent->right = cur->right;
}
delete cur;
}
}
else if (cur->right == nullptr) // 右为空,父节点指向我的左孩子
{
if (cur == _root) // 处理单枝情况
{
_root = cur->left;
}
else
{
if (cur == parent->left) // 判断被删除的节点是父节点的左还是右,用于子节点链接
{
parent->left = cur->left;
}
else
{
parent->right = cur->left;
}
}
delete cur;
}
else // 左右孩子都不为空,使用替换法删除节点
{
// 这里使用左子树的最右节点替代
Node* leftMaxParent = cur;
Node* leftMax = cur->left;
while (leftMax->right)
{
leftMaxParent = leftMax;
leftMax = leftMax->right;
}
swap(cur->_key, leftMax->_key);
// 更改子节点指向
if (leftMax == leftMaxParent->left) leftMaxParent->left = leftMax->left;
else leftMaxParent->right = leftMax->left;
delete leftMax;
}
return true;
}
}
return false;
}
三、总结:
二叉搜索树的应用场景:
K模型:K模型即只有Key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
例如:
构建一棵二叉搜索树,将词库中的每个单词作为Key,然后输入一个单词word,判断该单词是否拼写正确。就是在树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
KV模型:每一个关键码Key,都有与之对应的值Value,即<Key,Value>的键值对。
例如英汉字典就是一种英文与中文的对应关系,英文单词与其对应的中文<word,chinese>就构成一种键值对。将其存入树中,输入英文就可以返回对应中文。
二叉搜索树的性能效率:
在对二叉搜索树进行insert或是erase时都先必须进行find操作,因此find的效率也就代表了各个操作的性能。但是这也取决于树的形状。
最优情况下:二叉搜索树为完全二叉树(或者接近完全二叉树),效率为O(logn)
最差情况下:二叉搜索树退化为单支树(或者类似单支),效率为O(n)