文章目录:
二叉搜索树的概念
二叉搜索树(BST , Binary Search Tree),又称作二叉查找树或二叉排序树,或者是具有以下性质的二叉树:
- 非空左子树上所有节点的值均小于其根节点的值
- 非空右子树上所有节点的值均大于其根节点的值
- 其左右子树也分别为二叉搜索树。
二叉搜索树既有链表的快速插入和删除的特性,又有数组快速查找的优势,因此应用很广泛。在文件系统和数据库系统中一般会采用这种数据结构进行高效率的排序与检索操作。
⬇️如下就是一棵二叉搜索树:
二叉搜索树的实现
基础框架实现
🎯若要实现一棵二叉搜索树,那么我们需要定义一个存储信息的结点类。结点类中包括:左指针、右指针和存储的值。
namespace hyr
{
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:
BSTree()
:_root(nullptr)
{}
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;
};
}
二叉搜索树的数据插入(insert)
- 树为空,新增一个节点作为根,赋值给root指针。
- 树不为空,则按照二叉搜索树的性质找到对应的位置,插入新的节点。
- 若想要插入的节点在树中已经存在,则不用插入,即插入失败。
例如:在一棵二叉搜索树中插入数值 15 :
非递归版本
💻使用非递归的方法来实现二叉搜索树的插入时,为了在找到新插入节点的位置之后与它的上一个节点链接上,我们需要用一个 parent 指针来记录 cur(新插入节点)的上一个位置。
当 cur 走到空位置时,我们申请一个值为 key 的节点,这里我们需要判断一下新插入的值与 parent 的值的大小关系,若比 parent 指向的值大,则插入在 parent 右指针上;若比 parent 指向的值小,则插入在 parent 左指针上。
bool insert(const K& key)
{
// 若一开始树为空,则将新的节点赋值给根节点
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
Node* parent = _root;
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;
}
递归版本
💻递归实现时,我们不方便记录 cur 的前一个节点,因此我们插入新节点时无法与上一个节点正确链接。因此在递归插入的子函数接受参数 root 时需要采用引用接收,即代表当前参数的别名。在递归调用时用的是 root 左指针或者右指针的别名,插入新节点时就可以直接链接上了。例如以下部分图示:
bool insertR(const K& key)
{
return _insertR(_root, key);
}
// 套一个子函数,方便递归
bool _insertR(Node*& root, const K& key) // 将root指针的别名传入,便于新节点的插入
{
// 空树,直接插入节点,且作为搜索树的根节点
if (root == nullptr)
{
root = new Node(key);
return true;
}
if (root->_key < key) // key比root中的值大,递归右子树
return _insertR(root->_right, key);
else if (root->_key > key)//key比root中的值小,递归左子树
return _insertR(root->_left, key);
else
return false; // key与root中的相等,即在树中已经存在目标值,则插入失败
}
二叉搜索树的数据删除(erase)
首先需要查找要删除的数据是否存在搜索树中,若不存在,则返回 false。若存在,那么存在的节点可能分为以下情况:
- 要删除的节点无孩子节点
- 要删除的节点只有一个孩子节点
- 要删除的节点有左、右孩子节点
因此我们要对以上情况分别处理。
情况1:要删除的节点无孩子节点
例如:删除节点 7, 直接删除节点,让其父亲原本指向7的指针指向 nullptr 。
情况2:要删除的节点只有一个孩子节点
例如:
1、删除的节点右子树为空,如14。让其父节点指向该孩子的左孩子节点,然后再释放删除该节点。
2、删除的节点左子树为空,如10。让其父节点指向该孩子的右孩子节点,然后再释放删除该节点。
情况3:要删除的节点有左、右孩子节点
若待删除的节点的左右子树均不为空,那么我们需要在二叉搜索树中查找一个替代节点替换需要删除的值,使用替换法进行删除。替换删除后仍需满足二叉搜索树的性质,因此可以有两种选择替代节点的方法:1. 待删除节点左子树中最右的节点 2. 待删除节点右子树中最左的节点(以下使用这个)。然后将待删除节点的值与替换节点的值进行交换,替代节点中至少有一棵为空树,因此,将其转换成了情况二或者情况一的删除。
非递归版本
方法一:
若待删除的节点左右子树均不为空。
- 使用 minParent 标记待删除节点右子树中的最左节点(最小节点)的父节点,minRight 标记待删除节点右子树中的最小节点,即替换节点。
- 然后将待删除节点的值改为 minRight 的值。判断 minRight 是 minParent 的左孩子还是右孩子,然后让 minParent 的左指针或者右指针指向 minRight 的右孩子。
- 最后释放掉 minRight 节点并删除。
bool Erase(const K& key)
{
Node* parent = _root;
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 (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 // 左右子树均不为空,替换法删除
{
// 找到右子树中最左节点(最小值节点)去替换
Node* minParent = cur;
Node* minRight = cur->_right;
while (minRight->_left)
{
minParent = minRight;
minRight = minRight->_left;
}
// 保存替换节点的值
cur->_key = minRight->_key;
// 删除替换节点
if (minParent->_left == minRight)
minParent->_left = minRight->_right;
else
minParent->_right = minRight->_right;
delete minRight;
}
return true;
}
}
return false;
}
方法二:
待删除节点左右子树均为空或者仅有一棵子树为空的情况与上述处理方法一样。当待删除节点的左右子树均不为空时,我们找到待删除节点右子树中的最小节点记录为 minRight ,将 minRight 的值用一个变量 min 记录下来,然后递归调用自己删除 minRight 节点,最后将 min 赋值给待删除的节点 cur,因此实现了替换删除。
🔋注意:在以下删除代码中这两句代码的位置不能颠倒顺序,若在没删除之前将替换节点的值赋值给了待删除节点,那么在递归调用删除目标节点的时候将会出错。
Erase(min);
cur->_key = min;
bool Erase(const K& key)
{
Node* parent = _root;
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 (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 // 左右子树均不为空,替换法删除
{
//寻找待删除节点右子树中值最小的节点
Node* minRight = cur->_right;
while (minRight->_left)
{
minRight = minRight->_left;
}
//将最小的值用变量记录下来
K min = minRight->_key;
// 递归调用自己去删除替换节点,一定会走到左为空的情况(因为替换节点为待删除节点左子树中的最左节点)
Erase(min);
// 将待删除节点的值更改为已删除节点的值
cur->_key = min;
}
return true;// 删除成功
}
}
return false;// 删除失败
}
ps: 实际上方法一的效率相对来说更加高效,因为方法二递归删除需要从根节点再查找一遍,第一种方法直接删除。所以更推荐第一种写法。
递归版本
递归删除二叉搜索树种指定数据思路:
- 若为空树,返回false
- 待删除值 key 大于根节点的值,则递归到根节点的右子树种删除值为 key 的节点
- 待删除值 key 小于根节点的值,则递归到根节点的左子树种删除值为 key 的节点
- 找到要删除的节点之后按照规则对其进行删除
找到待删除的节点之后,用 minRight 记录待删除节点右子树中的最小值节点,然后用变量将最小值节点对应的值保存下来,调整递归删除函数的子函数从当前待删除节点的右子树开始,删除右子树中的 minRight 节点。然后将之前变量保存的值赋值给待删除节点,完成替换删除。
bool eraseR(const K& key)
{
return _eraseR(_root, key);
}
// 子函数递归删除,这里注意要用引用接收根节点
bool _eraseR(Node*& root, const K& key)
{
// 空树返回false
if (root == nullptr)
return false;
if (root->_key < key) // key大于根节点的值,递归到右子树中删除
return _eraseR(root->_right, key);
else if (root->_key > key) // key小于根节点的值,递归到左子树中删除
return _eraseR(root->_left, 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* minRight = root->_right;
while (minRight->_left)
{
minRight = minRight->_left;
}
// 保存最小节点的值
K min = minRight->_key;
// 转换成root的右子树删除min
_eraseR(root->_right, min);
// 将保存的值赋值给待删除节点,完成替换删除
root->_key = min;
}
}
return true;
}
二叉搜索树数据查找(find)
- 查找的值比当前节点的值大,到右子树中去找。
- 查找的值比当前节点的值小,到左子树中去找。
非递归版本
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;
}
递归版本
Node* findR(const K& key)
{
return _findR(_root, key);
}
Node* _findR(Node* root, const K& key)
{
// 空树,返回空指针(nullptr)
if (root == nullptr)
return nullptr;
if (root->_key < key)
return _findR(root->_right, key); // 递归到右子树中找
else if (root->_key > key)
return _findR(root->_left, key); // 递归到左子树中找
else
return root; // 返回找到的节点指针
}
拷贝构造函数
用一个子函数分别递归左子树和右子树拷贝构造。
BSTree(const BSTree<K>& t)
{
_root = _Copy(t._root); // 拷贝对象t的二叉搜索树
}
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; // 返回已经拷贝完成的树
}
赋值运算符重载
函数接收参数时使用传值传参,传参中调用拷贝构造函数拷贝了形参对象 t , 将拷贝构造出来的 t 对象与 this 对象进行交换,即完成了赋值操作。且拷贝构造出来的对象 t 会在该函数调用完成结束时自动析构,不需要我们手动析构。
// 传值传参,参数t是自动调用拷贝构造得到的目标对象
BSTree<K>& operator=(BSTree<K> t)
{
// 将需要被拷贝的对象的根节点与t的根节点进行交换
swap(_root, t._root);
//返回对象
return *this;
}
析构函数
采用后序的方式对二叉搜索树中的节点进行释放,最后将根节点置为空。
~BSTree()
{
_Destroy(_root); // 释放二叉搜索树中节点
_root = nullptr; // 将根节点置为空
}
void _Destroy(Node* root)
{
if (root == nullptr) // 空树直接返回
return;
_Destroy(root->_left); // 递归析构左子树
_Destroy(root->_right); // 递归析构右子树
delete root; // 释放根节点
}
二叉搜索树的应用
K 模型
K模型:K模型只有 key 作为关键码,结构中只存储 key 即可,关键码即为需要搜索到的值。
例如:给一个单词 word , 判断该单词是否拼写正确,方式如下:
- 以词库中所有单词集合中的每个单词作为 key , 构建一棵二叉搜索树
- 在二叉搜索树中检索此单词是否存在,存在则拼写正确,不存在则拼写错误
KV模型
KV模型:每一个关键码 key ,都有与之对应的值 value ,即<key ,valur>的键值对,这种模型在生活中也非常常见:
- 高铁买票中人的身份证信息与是否买票的对应关系,通过身份证信息可以快速查询其是否买票的状态,身份信息与其对应的购票信息<ID,BuyTicket>就构成一种键值对。
- 英汉词典中英文与中文的对应关系,通过英文可以快速检索其对应的中文,英文单词与其对应的中文<word,chinese>就构成一种键值对。
- 再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出
现次数就是<word, count>就构成一种键值对。
二叉搜索树性能分析
二叉搜索树中插入和删除都需要进行查找,因此查找的效率代表了二叉搜索树中各操作的性能。
对于有 n 各节点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树的平均查找长度是节点在二叉搜索树的深度的函数,节点越深,则比较次数越多。
如下所示:
同一个关键码集合,按照不同的次序插入,得到的二叉搜索树的结构可能不同:
最优情况下,二叉搜索树接近于完全二叉树,其平均搜索次数:O(logN)
最差情况下,二叉搜索树类似于单支结构,其平均搜索次数:O(N)
那么我们该如何解决此问题呢?为了提高搜索性能,我们后面还要学习二叉搜索树的改良版:AVL树和红黑树。它们完美的解决了此问题。
二叉搜索树K模型完整实现代码
#pragma once
#include<iostream>
using namespace std;
namespace hyr
{
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:
BSTree()
:_root(nullptr)
{}
BSTree(const BSTree<K>& t)
{
_root = _Copy(t._root);
}
BSTree<K>& operator=(BSTree<K> t)
{
swap(_root, t._root);
return *this;
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
bool insert(const K& key)
{
// 若一开始树为空,则将新的节点赋值给根节点
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
Node* parent = _root;
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;
}
bool Erase(const K& key)
{
Node* parent = _root;
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 (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 // 左右子树均不为空,替换法删除
{
// // 找到右子树中最左节点(最小值节点)去替换
// Node* minParent = cur;
// Node* minRight = cur->_right;
// while (minRight->_left)
// {
// minParent = minRight;
// minRight = minRight->_left;
// }
// // 保存替换节点的值
// cur->_key = minRight->_key;
// // 删除替换节点
// if (minParent->_left == minRight)
// minParent->_left = minRight->_right;
// else
// minParent->_right = minRight->_right;
// delete minRight;
//寻找待删除节点右子树中值最小的节点
Node* minRight = cur->_right;
while (minRight->_left)
{
minRight = minRight->_left;
}
//将最小的值用变量记录下来
K min = minRight->_key;
// 递归调用自己去删除替换节点,一定会走到左为空的情况(因为替换节点为待删除节点左子树中的最左节点)
Erase(min);
// 将待删除节点的值更改为已删除节点的值
cur->_key = min;
}
return true;// 删除成功
}
}
return false;// 删除失败
}
bool insertR(const K& key)
{
return _insertR(_root, key);
}
bool eraseR(const K& key)
{
return _eraseR(_root, key);
}
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;
}
Node* findR(const K& key)
{
return _findR(_root, key);
}
~BSTree()
{
_Destroy(_root);
_root = nullptr;
}
private:
void _Destroy(Node* root)
{
if (root == nullptr)
return;
_Destroy(root->_left);
_Destroy(root->_right);
delete root;
}
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;
}
Node* _findR(Node* root, const K& key)
{
if (root == nullptr)
return nullptr;
if (root->_key < key)
return _findR(root->_right, key);
else if (root->_key > key)
return _findR(root->_left, key);
else
return root;
}
bool _eraseR(Node*& root, const K& key)
{
if (root == nullptr)
return false;
if (root->_key < key)
return _eraseR(root->_right, key);
else if (root->_key > key)
return _eraseR(root->_left, 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* minRight = root->_right;
while (minRight->_left)
{
minRight = minRight->_left;
}
K min = minRight->_key;
// 转换成root的右子树删除min
_eraseR(root->_right, min);
root->_key = min;
}
}
return true;
}
bool _insertR(Node*& root, const K& key)
{
if (root == nullptr)
{
root = new Node(key);
return true;
}
if (root->_key < key)
return _insertR(root->_right, key);
else if (root->_key > key)
return _insertR(root->_left, key);
else
return false;
}
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
private:
Node* _root;
};
}