概念
二叉搜索树又称为二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
· 若它的左子树不为空,则左子树上所有结点的值都小于根结点的值。
· 若它的右子树不为空,则右子树上所有结点的值都大于根结点的值。
· 它的左右子树也分别是二叉搜索树。
如下所示,是一颗二叉搜索树
- 特点:
二叉搜索树以中序遍历得到升序序列。
二叉搜索树实现
- 构造函数:
构造空树即可。
//构造函数
BSTree()
:_root(nullptr)
{}
- 拷贝构造:递归拷贝
利用递归,去构造树的左右子树,每次返回当前节点,就能链接起来左右子树。
//拷贝树
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; //返回拷贝的树
}
//拷贝构造函数
BSTree(const BSTree<K>& t)
{
_root = _Copy(t._root); //拷贝t对象的二叉搜索树
}
- 赋值运算符重载:
**现代写法:**拿来直接用
//现代写法
BSTree<K>& operator=(BSTree<K> t) //编译器接收右值的时候自动调用拷贝构造函数
{
swap(_root, t._root); //交换这两个对象的二叉搜索树
return *this; //支持连续赋值
}
- 析构:
递归地去释放
//释放树中结点
void _Destory(Node* root)
{
if (root == nullptr) //空树无需释放
return;
_Destory(root->_left); //释放左子树中的结点
_Destory(root->_right); //释放右子树中的结点
delete root; //释放根结点
}
//析构函数
~BSTree()
{
_Destory(_root); //释放二叉搜索树中的结点
_root = nullptr; //及时置空
}
- 插入:非递归方式
- 如果是空树,直接插入节点作为二叉搜索树根节点。
- 如果不是空树,按照二叉搜索树性质进行节点插入。
- 依次把key和当前待插入节点做比较,待插入节点k较大,则插入到右子树,反之去遍历左子树,找到空位置就停下,说明到了可插位置。如果待插入节点值等于根节点值,则插入失败,因为已经存在。
- 来到空位置时,需要判断当前在父节点左边还是右边。
做法:
- 二叉树需要链接,所以需要父亲节点和当前cur节点,父亲初始置null,cur置root。
- 如果是根节点,直接创建新节点,然后返回true。
- 根据二叉搜索树性质,更新父亲和cur节点。
- 如果未经过返回说明到了合适位置,判断cur需要位于parent的左还是右。
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->_left;
}
else if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else
return false;
}
// 若到此,说明cur到了空位置,可以插入,但不知道在父节点左还是右
cur = new Node(key, value);
// 对比看父的值和当前插入值比较 决定插在哪边
if (parent->_key < cur->_key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}
- 查找:非递归查找
用cur即可,根据BST的性质,key大,就找右,key小找左,返回值类型是Node*,相等时直接返回cur即可。
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;
}
- 删除:
非递归方式删除
BST删除分为三种情况:
- 待删除结点的左子树为空。待删除结点左右子树均为空包含在内。
- 待删除结点的右子树为空。
- 待删除结点的左右子树均不为空。
做法:
- 需要使用两个指针parent、cur,因为有链接关系。
- 更新parent和cur到合适位置。
情况1:左子树为空,先判断待删节点是父亲的左儿子还是右儿子,然后让父亲直接链接到该节点的右子树。
情况2:右子树为空,先判断待删节点是父亲的左儿子还是右儿子,然后让父亲直接链接到该节点的左子树。
情况3:左右均不为空,寻找右子树的最小值,我们直接把右子树最小值位置删掉,让当前节点值为右子树最小节点。
然而这样的做法也有两种情况,如下两种:
- 右子树最小节点值,是右子树的左孩子
- 右子树最小节点值是单支的。
bool Erase(const K& key)
{
Node* parent = nullptr;
Node* cur = _root;
// 先找父和它
while (cur)
{
if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
// 至此,找到了父亲和它:
else
{
// 删除节点的左子树为空
if (cur->_left == nullptr)
{
if (cur == _root) // 所删为根节点
_root = cur->_right;
else // 所删不为根节点
{
// 要判断所删是左孩子还是右孩子
if (parent->_left == cur)
parent->_left = cur->_right;
else
parent->_right = cur->_right;
}
delete cur;
return true;
// 每一分支需这两,因需停止.
}
else if (cur->_right == nullptr) // 删除节点右子树为空
{
if (cur == _root) // 所删为根
{
_root = cur->_left;
}
else // 所删不为根
{
// 要判断所删是左孩子还是右孩子
if (parent->_left == cur)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
delete cur;
return true;
// 每一节分支都需这两,因需停止.
}
else // 左右孩子都存在:找右子树中值最小的右节点,也就是右子树最左孩子
// 让当前节点位置存最小右孩子,然后删除那个最小右孩子
{
Node* minParent = cur; // 记录要删节点
Node* minRight = cur->_right; // 当前节点左右孩子都有,找右孩子最小,先确定当前右孩子
// 寻找根节点右子树值最小节点
while(minRight->_left)
{
// 一直往左走
minParent = minRight;
minRight = minRight->_left;
}
// 修改当前值,相当于删掉了当前节点
cur->_key = minRight->_key; // 不管右孩子有没有左孩子,这样都是对的。
// 如果被删节点右孩子有树结构,经过循环,minP->left == minR 直接略过minR
if (minRight == minParent->_left)
{
// minR可能有右孩子或没有右孩子,但是左边孩子一定没有了。
minParent->_left = minRight->_right;
}
// 要删节点的右孩子就一个 或它的右孩子没有左孩子:是个部分右单支结构
else
{
minParent->_right = minRight->_right; // 直接链接右孩子的右指,当前cur已经变成原来的cur->right
}
delete minRight;
return true;
}
}
}
return false;
}
-
遍历:(中序最经典的应用)
遍历我们防止root节点暴露,所以直接用_Inorder来访问,然后外部只是调用_Inorder -
代码:
#include<iostream>
using namespace std;
//结点类
template<class K, class V>
struct BSTreeNode
{
K _key; //结点值
V _val;
BSTreeNode<K, V>* _left; //左指针
BSTreeNode<K, V>* _right; //右指针
//构造函数
BSTreeNode(const K& key = 0, const V& val = 0)
:_key(key)
,_val(val)
, _left(nullptr)
, _right(nullptr)
{}
};
template<class K, class V>
class BSTree
{
typedef BSTreeNode<K, V> Node;
public:
BSTree()
:_root(nullptr) {};
// 递归拷贝BST:每个递归,只创建一个节点,且返回,它的左右节点,都递归地链接起来。
Node* _Copy(Node* root)
{
if (root == nullptr)
return nullptr;
Node* copyNode = new Node();
copyNode->_left = _Copy(root->_left);
copyNode->_right = _Copy(root->_right);
return copyNode;
}
BSTree(const BSTree<K, V>& t)
{
_root = _Copy(t._root);
}
/*
插入:插入时刻需要两个节点 父节点和子节点
找个合适位置,小于往左走 大于往右,等于就停止
*/
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->_left;
}
else if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else
return false;
}
// 若到此,说明cur到了空位置,可以插入,但不知道在父节点左还是右
cur = new Node(key, value);
// 对比看父的值和当前插入值比较 决定插在哪边
if (parent->_key < cur->_key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
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;
}
// 删除:
/*
删除之前 要先找该节点和父节点
所删为叶:不用考虑,会包含在所删孩子无左或无右里面,情况合并直接干死
此外,下面三种情况还要考虑:
当前删的是不是根节点、当前所删节点在根的左还是右
2. 有左孩子无右
3. 有右孩子无左
4. 左右孩子都有
*/
bool Erase(const K& key)
{
Node* parent = nullptr;
Node* cur = _root;
// 先找父和它
while (cur)
{
if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
// 至此,找到了父亲和它:
else
{
// 删除节点的左子树为空
if (cur->_left == nullptr)
{
if (cur == _root) // 所删为根节点
_root = cur->_right;
else // 所删不为根节点
{
// 要判断所删是左孩子还是右孩子
if (parent->_left == cur)
parent->_left = cur->_right;
else
parent->_right = cur->_right;
}
delete cur;
return true;
// 每一分支需这两,因需停止.
}
else if (cur->_right == nullptr) // 删除节点右子树为空
{
if (cur == _root) // 所删为根
{
_root = cur->_left;
}
else // 所删不为根
{
// 要判断所删是左孩子还是右孩子
if (parent->_left == cur)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
delete cur;
return true;
// 每一节分支都需这两,因需停止.
}
else // 左右孩子都存在:找右子树中值最小的右节点,也就是右子树最左孩子
// 让当前节点位置存最小右孩子,然后删除那个最小右孩子
{
Node* minParent = cur; // 记录要删节点
Node* minRight = cur->_right; // 当前节点左右孩子都有,找右孩子最小,先确定当前右孩子
// 寻找根节点右子树值最小节点
while(minRight->_left)
{
// 一直往左走
minParent = minRight;
minRight = minRight->_left;
}
// 修改当前值,相当于删掉了当前节点
cur->_key = minRight->_key; // 不管右孩子有没有左孩子,这样都是对的。
// 如果被删节点右孩子有树结构,经过循环,minP->left == minR 直接略过minR
if (minRight == minParent->_left)
{
// minR可能有右孩子或没有右孩子,但是左边孩子一定没有了。
minParent->_left = minRight->_right;
}
// 要删节点的右孩子就一个 或它的右孩子没有左孩子:是个部分右单支结构
else
{
minParent->_right = minRight->_right; // 直接链接右孩子的右指,当前cur已经变成原来的cur->right
}
delete minRight;
return true;
}
}
}
return false;
}
// 为了不暴露root节点
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_key<<" ";
_InOrder(root->_right);
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
public:
Node* _root;
};
void TestBSTree()
{
/*BSTree<string, string> dict;
dict.Insert("insert", "插入");
dict.Insert("erase", "删除");
dict.Insert("left", "左边");
dict.Insert("string", "字符串");
string str;
while (cin >> str)
{
auto ret = dict.Find(str);
if (ret)
{
cout << str << ":" << ret->_val << endl;
}
else
{
cout << "单词拼写错误" << endl;
}
}*/
string strs[] = { "苹果", "西瓜", "苹果", "樱桃", "苹果", "樱桃", "苹果", "樱桃", "苹果" };
// 统计水果出现的次
BSTree<string, int> countTree;
for (auto str : strs)
{
auto ret = countTree.Find(str);
if (ret == NULL)
{
countTree.Insert(str, 1);
}
else
{
ret->_val++;
}
}
countTree.InOrder();
}
调用:
#include"l5BST.h"
int main()
{
BSTree<int, int> bst;
bst.Insert(3, 1);
bst.Insert(6, 1);
bst.Insert(1, 1);
bst.Insert(2, 1);
bst.InOrder();
cout << "删除 3" << endl;
bst.Erase(3);
bst.InOrder();
cout << "字典BST" << endl;
TestBSTree();
return 0;
}
性能分析
对于一棵树,插入还是删除,都需要先查找,查找效率影响着其它的效率。
对于n个节点的二叉搜索树,
最优情况:二叉搜索树为完全二叉树, 我们比较树高次,logN的复杂度即可。
最差情况:二叉搜索树是单支,比较次数是N。
扩展:
B树和B+树是查找存储在磁盘当中的数据时经常用到的数据结构,B树系列对树的高度提出了更高的要求,普通二叉树不能满足要求,为降低树的高度,需要使用多叉树,而多叉树其实都是由二叉搜索树演变出来的,它们各有各的特点,适用于不同的场景。
知识点注意:
- 二叉搜索树一定是logN,错误,因为单支时退化为O(N)。
- 给定BST,根据节点值大小排序需要时间复杂度是线性的,因为中序是线性的。
- 给定一棵BST,线性时间内转为平衡二叉树,错误,因为经常涉及旋转等操作。