前言:今天我们学习相对来说比前面轻松一点的内容,二叉搜索树,在之前我们学习过二叉树今天的内容对于我们就会比较简单一点了,一起加油。
💖 博主CSDN主页:卫卫卫的个人主页 💞
👉 专栏分类:高质量C++学习 👈
💯代码仓库:卫卫周大胖的学习日记💫
💪关注博主和博主一起学习!一起努力!
目录标题
- 什么是二叉搜索树
- 二叉搜索树的常见操作
- 二叉搜索树的基本框架
- 二叉树的接口
- 1. 插入
- 2.遍历
- 3. 查找
- 4. 删除
- 5.析构函数
- 整体代码:
- 二叉搜索树的应用
什么是二叉搜索树
二叉搜索树(Binary Search Tree,BST)是一种二叉树的特殊形式,它满足以下性质:
- 每个节点最多有两个子节点,分别称为左子节点和右子节点。
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值。
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 对于树中的任意节点,它的左子树中的所有节点的值都小于该节点的值,它的右子树中的所有节点的值都大于该节点的值。
- 它的左右子树也分别为二叉搜索树。
这些性质确保了二叉搜索树中的数据按照一定的顺序排列,左子树中的节点值小于父节点,右子树中的节点值大于父节点。这样的性质使得二叉搜索树能够提供高效的查找、插入和删除操作。
二叉搜索树常用于实现有序集合或映射,例如在数据库系统中用于索引数据,或是实现搜索功能等(如下图就是标准的一个二叉搜索树)。
二叉搜索树的常见操作
在C++中,可以使用以下操作实现二叉搜索树(Binary Search Tree,BST):
- 插入(Insertion):将一个新的节点插入到二叉搜索树中,保持树的有序性。
- 查找(Search):在二叉搜索树中搜索指定值的节点,返回该节点或者NULL(如果没有找到)。
- 删除(Deletion):从二叉搜索树中删除指定值的节点,保持树的有序性。
- 中序遍历(Inorder Traversal):按照左子树 -> 根节点 -> 右子树的顺序遍历二叉搜索树,这个遍历方式会输出有序的节点值。
这些操作可以通过递归或迭代的方式来实现,具体的实现方式和算法可以根据具体的需求和编程习惯进行选择。
二叉搜索树的基本框架
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 K>//树类,接口实现区域
class BSTree
{
typedef BSTreeNode<K> Node;
public:
//基本接口的实现区域;
private:
Node* _root = nullptr;
};
};
说明:
- 这里写成两个类,一个struct一个class的原因是为了避免在BSTree这个类中额外的写一个接口去专门获取结点。
- 这里当的BSTree主要是用于接口的实现。
- 这里自己显示的实现默认构造的原因是编译器的默认构造无法满足我们的需求。
二叉树的接口
1. 插入
代码思路:
- 插入该数时,此树中没有元素,就直接在该树中开辟一个结点存放插入的数。
- 树中不为空,依据二叉搜索树的特性找到指定的位置插入值:①当插入的值比当前结点的值大,就往当前结点的右结点走,反之就往左结点走。②找到应当插入的位置时,将原本该位置的值的连接到插入的值,要保持搜索树原本的特性不能被修改。
- 如果发现插入的值和搜索树中的值相等,那就插入失败,停止插入。
- 在提前找到插入的位置时候,我们需要用一个partent记录要插入位置的父亲结点,以防止找不到连接处。
bool insert(const K& key)//插入一个元素
{
if (_root == nullptr)//如果树为空,就直接插入该值
{
_root = new Node(key);
return true;
}
//树不为空,就先确定位置在插入
Node* parent = nullptr;//记录父亲结点,以防插入后找不到变动的位置
Node* cur = _root;
while (cur)
{
if (key > cur->_key)//如果插入的值比当前结点的值大,就往右走
{
parent = cur;
cur = cur->_right;
}
else if (key < cur->_key)//如果插入的值比当前结点的值小,就往左走
{
parent = cur;
cur = cur->_left;
}
else
return false;
}
cur = new Node(key);
if (key > parent->_key)//判断此时要插入的数据是父亲结点的左子树还是右子树
{
parent->_right = cur;
}
else
parent->_left = cur;
return true;
}
2.遍历
这里我们提一个小知识:二叉搜索树的中序遍历的结果是一个递增有序序列。因为二叉搜索树的定义,左子树上的所有节点的值都小于根节点的值,右子树上的所有节点的值都大于根节点的值,因此,进行中序遍历时,先遍历左子树,然后遍历根节点,最后遍历右子树,所得到的序列是一个递增有序序列。
代码思路:
- 由于_root是私有成员,类外无法访问,因此我们通过一个函数对其进行封装。
- 关于中序遍历的遍历过程我们这里就不会在过多叙述了,不懂二叉树部分的内容的可以去看看我之前的博客二叉树的模拟实现
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 K>//树类,接口实现区域
class BSTree
{
typedef BSTreeNode<K> Node;
public:
//基本接口的实现区域;
bool insert(const K& key);//插入
void InOrder()
{
_InOrder(_root);
cout << endl;
}
private:
void _InOrder(Node* root)//中序遍历
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
private:
Node* _root = nullptr;
};
3. 查找
代码思路:
- 查找和插入的思路很类似,我们根据搜索树的基本特性,看看查找的这个元素是比当前结点大或者小,大就去当前结点的右子树找,反之就去左子树找。
- 如果找到了就返回当前结点的位置,没找到就返回空即可。
Node* Find(const K& key)//查找一个元素
{
Node* cur = _root;
while (cur)
{
if (cur->_key < key)//当前结点的值小于key值,就去他的右子树找
{
cur = cur->_right;
}
else if (cur->_key > key)//当前结点的值大于key值,就去他的左子树找
{
cur = cur->_left;
}
else//找到了就返回当前结点的值
return cur;
}
return nullptr;//找不到返回空
}
4. 删除
代码思路:
1.删除我们需要分三种情况:①左子树为空,要删除的结点只存在右子树。②右子树为空,要删除的结点只存在左子树。③左右两边都有结点的情况。那么我们来根据下面的图像来逐一分析!
2. 这里我着重强调左右两边都有结点的情况如何删除,我们查找到要被删除结点的右子树的最左节点(最左结点可能是双亲孩子的右子树也可能是左子树,如图三所示)去和当前需要被删除的结点进行替换,然后将替换后的结点进行删除即可(中间还有点小坑,我会在图中说明)。
图一: 左子树为空,要删除的结点只存在右子树
图二: 右子树为空,要输出的结点只存在左子树,这里我需要强调一下,在图一和图二写代码的时候,我们需要提前记录下来删除结点的父亲结点的位置,以防找不到该结点。
图三: 左右两边都有结点的情况。
bool Erase(const K& key)//删除
{
//树不为空,就先确定位置在插入
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (key > cur->_key)//如果插入的值比当前结点的值大,就往右找
{
parent = cur;
cur = cur->_right;
}
else if (key < cur->_key)//如果插入的值比当前结点的值小,就往左找
{
parent = cur;
cur = cur->_left;
}
else//找到要删除的值
{
//1.如果左子树为空,要删除的结点只存在右子树
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)//2.右子树为空,要删除的结点只存在左子树
{
if (cur == _root)//判断是否就是极端情况
{
_root = cur->_left;
}
else
{
if (cur == parent->_left)
{
parent->_left = cur->_left;
}
else
parent->_right = cur->_left;
}
delete cur;
}
else//左右两边都有结点的情况,查找到要被删除结点的右子树的最左节点替代删除
{
Node* rightmin = cur->_right;
Node* rightminparent = cur;
while (rightmin->_left)//当他的左子树为空的时候,就是找到了当前的最小结点
{
rightminparent = rightmin;
rightmin = rightmin->_left;
}
swap(cur->_key, rightmin->_key);//将两个结点的值交换
if (rightminparent->_left == rightmin)//是父亲结点的左孩子
{
rightminparent->_left = rightmin->_right;
}
else//是父亲结点的右孩子
rightminparent->_right = rightmin->_right;
delete rightmin;
}
return true;
}
}
return false;
}
5.析构函数
代码思路:这里和前面的中序遍历一样的道理,我们把他封装成一个接口,因为无法直接访问_root,这里你用前序遍历、中序遍历还是后序遍历都可以,不懂如何销毁的可以去看看之前二叉树销毁的博客,一样的玩法。
template <class K>//树类,接口实现区域
class BSTree
{
typedef BSTreeNode<K> Node;
public:
~BSTree()//析构函数
{
destory(_root);
}
private:
void destory(Node* root)
{
if (root == nullptr)
{
return;
}
destory(root->_left);
destory(root->_right);
delete root;
}
private:
Node* _root = nullptr;
};
};
整体代码:
#include <iostream>
using namespace std;
namespace BSTree
{
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 K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
BSTree() = default;//编译器自动生成默认构造
Node* Find(const K& key)//查找一个元素
{
Node* cur = _root;
while (cur)
{
if (cur->_key < key)//当前结点的值小于key值,就去他的右子树找
{
cur = cur->_right;
}
else if (cur->_key > key)//当前结点的值大于key值,就去他的左子树找
{
cur = cur->_left;
}
else//找到了就返回当前结点的值
return cur;
}
return nullptr;//找不到返回空
}
bool insert(const K& key)//插入一个元素
{
if (_root == nullptr)//如果树为空,就直接插入该值
{
_root = new Node(key);
return true;
}
//树不为空,就先确定位置在插入
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (key > cur->_key)//如果插入的值比当前结点的值大,就往右走
{
parent = cur;
cur = cur->_right;
}
else if (key < cur->_key)//如果插入的值比当前结点的值小,就往左走
{
parent = cur;
cur = cur->_left;
}
else
return false;
}
cur = new Node(key);
if (key > parent->_key)
{
parent->_right = cur;
}
else
parent->_left = cur;
return true;
}
bool Erase(const K& key)//删除
{
if (_root == nullptr)//如果树为空,就直接插入该值
{
_root = new Node(key);
return true;
}
//树不为空,就先确定位置在插入
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (key > cur->_key)//如果插入的值比当前结点的值大,就往右找
{
parent = cur;
cur = cur->_right;
}
else if (key < cur->_key)//如果插入的值比当前结点的值小,就往左找
{
parent = cur;
cur = cur->_left;
}
else//找到要删除的值
{
//1.如果左子树为空,要删除的结点只存在右子树
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)//2.右子树为空,要删除的结点只存在左子树
{
if (cur == _root)//判断是否就是极端情况
{
_root = cur->_left;
}
else
{
if (cur == parent->_left)
{
parent->_left = cur->_left;
}
else
parent->_right = cur->_left;
}
delete cur;
}
else//左右两边都有结点的情况,查找到要被删除结点的右子树的最左节点替代删除
{
Node* rightmin = cur->_right;
Node* rightminparent = cur;
while (rightmin->_left)//当他的左子树为空的时候,就是找到了当前的最小结点
{
rightminparent = rightmin;
rightmin = rightmin->_left;
}
swap(cur->_key, rightmin->_key);//将两个结点的值交换
if (rightminparent->_left == rightmin)//是父亲结点的左孩子
{
rightminparent->_left = rightmin->_right;
}
else//右孩子
rightminparent->_right = rightmin->_right;
delete rightmin;
}
return true;
}
}
return false;
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
~BSTree()//析构
{
destory(_root);
}
private:
void destory(Node* root)
{
if (root == nullptr)
{
return;
}
destory(root->_left);
destory(root->_right);
delete root;
}
void _InOrder(Node* root)//中序遍历
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
BSTree(const BSTree<K>& t)
{
_root = _Copy(t._root);
}
BSTree<K>& operator=(BSTree<K> t)
{
swap(_root, t._root);//和之前一样的现代写法
}
protected:
Node* _Copy(const 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;
}
private:
Node* _root = nullptr;
};
二叉搜索树的应用
- K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。 - KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方式在现实生活中非常常见:
比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。
源码实现:
namespace key_value
{
template<class K, class V>
struct BSTreeNode
{
BSTreeNode<K, V>* _left;
BSTreeNode<K, V>* _right;
K _key;
V _value;
// pair<K, V> _kv;
BSTreeNode(const K& key, const V& value)
:_left(nullptr)
, _right(nullptr)
, _key(key)
, _value(value)
{}
};
template<class K, class V>
class BSTree
{
typedef BSTreeNode<K, V> Node;
public:
// logN
bool Insert(const K& key, const V& value)
{
if (_root == nullptr)
{
_root = new Node(key, value);
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, value);
if (parent->_key < key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}
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 cur;
}
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(parent == 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(parent == 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* rightMinParent = cur;
Node* rightMin = cur->_right;
while (rightMin->_left)
{
rightMinParent = rightMin;
rightMin = rightMin->_left;
}
swap(cur->_key, rightMin->_key);
if (rightMinParent->_left == rightMin)
rightMinParent->_left = rightMin->_right;
else
rightMinParent->_right = rightMin->_right;
delete rightMin;
}
return true;
}
}
return false;
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
private:
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_key << ":" << root->_value << endl;
_InOrder(root->_right);
}
private:
Node* _root = nullptr;
};
好啦,今天的内容就到这里啦,下期内容预告map与set的使用.
结语:进阶的内容有点繁杂,大家一起加油呐!。