【C++】二叉搜索树
文章目录
一、二叉搜索树的概念
二叉搜索树又称二叉排序树 ,是一棵自带排序规则 的二叉树。最大的特点是查找速度接近二分查找 ,同时插入、删除不用像数组一样批量挪动数据。
二叉搜索树要么是一棵空树,要么是具有以下性质的二叉树:
- 若左子树不为空,则左子树上所有结点的值都小于等于根结点的值
- 若右子树不为空,则右子树上所有结点的值都大于等于根结点的值
- 它的左、右子树也必须是二叉搜索树
特点 :二叉搜索树的中序遍历(左-根-右)结果是有序递增的 ,这是其最关键的特征,也是验证二叉搜索树正确性的重要方法
二叉搜索树可以支持也可以不支持插入重复值 ,如set/map不支持重复值 ,插入相同值直接失败;multiset/multimap支持相等值,统一把重复数据插到右子树,保证排序逻辑不乱
本文基础代码实现不支持重复值

二、二叉搜索树的性能分析
二叉搜索树的增删查改效率由树的高度决定,存在两种极端情况:
- 最好情况(平衡树):树接近完全二叉树,高度为log2N,增删查时间复杂度为 O(logN)
- 最差情况(单链树):如果按有序数字依次插入(1、2、3、4、5......),树会变成一条斜着的单链表,高度等于节点总数 N,操作复杂度退化到 O(N),和普通链表一样慢
正因为普通二叉搜索树最坏效率太差,后来衍生出平衡二叉树(AVL 树、红黑树),C++STL 的 map/set 底层就是红黑树

另外需要说明的是,二分查找也可也实现O(logN)的查找效率 ,但是二分查找有致命短板:
- 数组必须连续存储,插入 / 删除中间数据需要大批量挪动元素,效率极低
- 二叉搜索树只需要修改指针指向,新增、删除节点不会影响其他数据
三、二叉搜索树的核心操作
二叉搜索树的核心操作围绕插入、查找、删除展开,所有操作均遵循左小右大的核心规则
1、数据结构定义
(1)结点结构
每个节点存一个数据 key,外加左右孩子指针,构造函数简化初始化
cpp
template<class K>
struct BSTNode
{
////构造函数
BSTNode(const K& key)
:_key(key)
, _left(nullptr)
, _right(nullptr)
{ }
K _key; //节点储存的键
BSTNode<K>* _left; //左孩子指针
BSTNode<K>* _right; //右孩子指针
};
(2)树管理类
对外提供简单接口,递归遍历等复杂逻辑封装成私有函数,用户不用关心底层递归细节
cpp
template<class K>
class BSTree
{
typedef BSTNode<K> Node;
public:
//公有接口:插入、查找、删除、中序遍历
bool Insert(const K& key); // 插入数据
bool Find(const K& key); // 查找数据是否存在
bool Erase(const K& key); // 删除指定数据
void InOrder(); // 中序遍历(打印有序序列)
private:
//私有递归函数,中序遍历(对外隐藏递归细节)
void _InOrder(Node* root);
//根结点
Node* _root = nullptr;
};
2、中序遍历(验证搜索二叉树的正确性)
利用二叉搜索树中序遍历有序的特性 ,通过递归实现遍历,既是对树结构的遍历,也是验证代码实现是否正确的重要手段(若遍历结果无序,说明实现存在问题)
在二叉搜索树的结构体中实现
cpp
//中序遍历
//公有接口,对外隐藏递归细节
void InOrder()
{
_InOrder(_root);
cout << endl;
}
private:
//私有递归函数,从指定结点开始中序遍历
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);//先遍历左子树
cout << root->_key << " "; //再访问根节点
_InOrder(root->_right); //最后遍历右结点
}
3、二叉搜索树的插入
插入规则
- 若树为空,直接新建结点作为根结点
- 若树非空 ,按左小右大规则查找插入位置,从根结点开始遍历,比当前结点小则左走,比当前结点大则右走
- 找到空位置后,新建结点并通过双亲结点指针挂载到树中
- 若遇到相等值,直接返回失败(本文不支持重复值场景)
cpp
//插入
bool Insert(const K& key)
{
//1、空树,直接创建根节点
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
//2、非空树,查找插入位置
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为空时,跳出循环,找到可以插入的位置了,新建结点并挂载
cur = new Node(key);
if (parent->_key < key)
{
parent->_right = cur; //插入到双亲结点右侧
}
else
{
parent->_left = cur; //插入到双亲结点左侧
}
return true;
}
测试
cpp
void test01()
{
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
BSTree<int> t;
for (auto e : a)
{
bool ret = t.Insert(e);
}
//中序遍历验证,有序就是插入正确
cout << "中序遍历:"<<endl;
t.InOrder();
}

4、查找操作
查找操作的遍历逻辑与插入完全一致
查找规则
- 从根结点开始,按左小右大规则遍历:比当前结点小则左走,比当前结点大则右走
- 若找到与目标值匹配的结点,直接返回true(找到)
- 若遍历至空结点仍未找到,返回false(未找到)
- 若支持重复值 ,一般要求查找中序遍历的第一个匹配值(保证结果唯一性)
cpp
//查找
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; //遍历结束未找到,返回失败
}
测试
cpp
void test02()
{
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
BSTree<int> t;
for (auto e : a)
{
t.Insert(e);
}
//查找测试
int arr[] = { 6,20,3,14,5 };
for (auto key : arr)
{
bool ret = t.Find(key);
cout << "查找" << key << ":" << (ret ? "存在" : "不存在") << endl;
}
}


5、删除操作
删除是二叉搜索树最复杂的操作,核心原则是:删除结点后,仍需保持二叉搜索树左小右大的排序规则 。
操作分为两步 :先查找待删除结点,若不存在则返回false;若存在,分4种情况处理(可合并为3种核心情况)
4种删除情况 :设待删除结点为cur,其双亲结点为parent,4种情况覆盖了所有可能,其中情况1可合并到情况2/3,简化代码逻辑
- cur左右孩子均为空(叶子结点):将parent对应孩子指针置空,直接删除cur
- cur左孩子为空,右孩子不为空:将parent对应孩子指针指向cur的右孩子,直接删除cur
- cur右孩子为空,左孩子不为空:将parent对应孩子指针指向cur的左孩子,直接删除cur
- cur左右孩子均不为空:无法直接删除(会导致树结构断裂),采用替换法:找cur右子树的最小结点(最左结点) 或左子树的最大结点(最右结点) 替换cur,因为这两个结点中任意一个放到cur位置,都满足二叉搜索树的规则。再删除该替换结点(替换结点必满足情况2/3,可直接删除)
cpp
//删除
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
{
//第二步,找到结点后分情况删除
//情况1+2:左孩子为空或者左右孩子均空(叶子节点)
if (cur->_left == nullptr)
{
if (cur != _root)
{
//非根节点,parent结点指向cur的右孩子
//叶子结点的右孩子是空,所以可以合并处理
if (cur == parent->_left)
parent->_left = cur->_right;
else
parent->_right = cur->_right;
}
else
{
//根结点,更新根为cur的右孩子
_root = cur->_right;
}
delete cur;
}
//情况3:右孩子为空,左孩子不为空
else if (cur->_right == nullptr)
{
if (cur != _root)
{
//非根节点,parent指向cur的左孩子
if (cur == parent->_left)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
else
{
//根节点,更新根为cur的左孩子
_root = cur->_left;
}
delete cur;
}
//情况4:左右孩子均不为空,替换法删除
else
{
//找到cur右子树的最小结点(也就是右子树的最左结点,值最接近cur)
Node* minRightParent = cur;
Node* minRight = cur->_right;
while (minRight->_left)
{
minRightParent = minRight;
minRight = minRight->_left;
}
//交换待删除结点和最小结点的键值
swap(cur->_key, minRight->_key);
//删除最小结点(必须是左空结点/叶子结点,符合情况2/3)
if (minRight == minRightParent->_left)
minRightParent->_left = minRight->_right;
else
minRightParent->_right = minRight->_right;
delete minRight;
}
return true;
}
}
//未找到待删除结点,返回失败
return false;
}
测试
cpp
void test03()
{
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
BSTree<int> t;
for (auto e : a)
{
t.Insert(e);
}
//删除测试
cout << "初始中序遍历";
t.InOrder();
t.Erase(1);
cout << "删除叶子结点1后:";
t.InOrder();
t.Erase(14);
cout << "删除单孩子结点14后:";
t.InOrder();
t.Erase(8);
cout << "删除左右孩子均不为空的结点8后:";
t.InOrder();
}

四、二叉搜索树的两种经典使用场景
1、key搜索场景
节点只存单个 key,业务只需要完成存在 / 不存在校验 ,无配套数据
典型案例:
- 小区车库车牌验证:判断车牌是否是本小区车辆
- 英文单词拼写检查 :判断单词是否在词库中(检查拼写正确)
支持增删查操作,不支持修改key,修改key会破坏二叉搜索树的排序规则,导致所有操作失效
单词拼写检查
cpp
void test04()
{
//模拟词库
string dict[] = { "apple","banana","orange", "pear", "grape" };
BSTree<string> spellCheck;
for (auto& word : dict)
{
spellCheck.Insert(word);
}
//拼写检查测试
string testWords[] = { "apple", "orage", "banana", "peer" };
for (auto& word : testWords)
{
bool ret = spellCheck.Find(word);
cout << "单词\"" << word << "\":" << (ret ? "拼写正确" : "拼写错误") << endl;
}
}
单词拼写检查的核心逻辑------将词库存入二叉搜索树,通过查找操作判断输入单词是否在词库中

2、存key/value的场景
适用场景
每个key关联一个对应的value(value可为任意类型),通过key快速查找对应的value,典型案例:
- 中英互译字典:key为英文单词,value为中文释义;
- 统计单词出现次数:key为单词,value为出现次数;
- 车库停车计费:key为车牌,value为入场时间。
支持增、删、查操作,不支持修改key,但可修改value(value不参与树的排序,修改不会影响树结构)
仅需修改结点结构和树管理类,增加模板参数V表示value类型,核心遍历和操作逻辑与仅存key版本完全一致,仅插入和遍历需处理value
(1)键值对结点结构
cpp
template<class K,class V>
struct BSTNode
{
//构造函数,初始化键值对
BSTNode(const K& key,const V& value )
:_key(key)
,_value(value)
, _left(nullptr)
, _right(nullptr)
{}
K _key; //节点储存的键
V _value; //关联数据,不参与排序
BSTNode<K,V>* _left; //左孩子指针
BSTNode<K,V>* _right; //右孩子指针
};
(2)键值对树管理类核心接口
cpp
template<class K,class V>
class BSTree
{
typedef BSTNode<K,V> Node;
public:
//公有接口:插入、查找、删除、中序遍历
//插入:需同时传入key和value
bool Insert(const K& key,const V& value)
{
//1、空树,直接创建根节点
if (_root == nullptr)
{
_root = new Node(key,value);
return true;
}
//2、非空树,查找插入位置
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为空时,跳出循环,找到可以插入的位置了,新建结点并挂载
cur = new Node(key,value);
if (parent->_key < key)
{
parent->_right = cur; //插入到双亲结点右侧
}
else
{
parent->_left = cur; //插入到双亲结点左侧
}
return true;
}
//查找:返回结点指针,可通过指针修改value
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; //遍历结束未找到,返回空
}
//中序遍历:输出key:value,检验同时查看关联数据
void InOrder()
{
_InOrder(_root);
cout << endl;
}
private:
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_key << ":"<<root->_value<<" ";
_InOrder(root->_right);
}
//根结点
Node* _root = nullptr;
};
(3)单词次数统计测试
单词统计的核心逻辑------通过查找key判断是否已存在,存在则修改value(次数+1),不存在则插入新键值对
cpp
//单词次数统计测试
void test05()
{
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "香蕉", "苹果" };
BSTree<string, int> countTree;
for (const auto& str : arr)
{
auto ret = countTree.Find(str);
if (ret == nullptr)
{
//单词首次出现,插入<单词, 1>
countTree.Insert(str, 1);
}
else
{
// 单词已存在,value++统计次数
ret->_value++;
}
}
// 中序遍历输出统计结果
cout << "单词出现次数统计:";
countTree.InOrder();
}

五、搜索二叉树完整代码
1、BinarySearchTree.h头文件
cpp
template<class K>
struct BSTNode
{
////构造函数
BSTNode(const K& key)
:_key(key)
, _left(nullptr)
, _right(nullptr)
{
}
K _key; //节点储存的键
BSTNode<K>* _left; //左孩子指针
BSTNode<K>* _right; //右孩子指针
};
template<class K>
class BSTree
{
typedef BSTNode<K> Node;
public:
//公有接口:插入、查找、删除、中序遍历
//插入
bool Insert(const K& key)
{
//1、空树,直接创建根节点
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
//2、非空树,查找插入位置
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为空时,跳出循环,找到可以插入的位置了,新建结点并挂载
cur = new Node(key);
if (parent->_key < key)
{
parent->_right = cur; //插入到双亲结点右侧
}
else
{
parent->_left = cur; //插入到双亲结点左侧
}
return true;
}
//查找
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; //遍历结束未找到,返回失败
}
//删除
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
{
//第二步,找到结点后分情况删除
//情况1+2:左孩子为空(包含叶子结点)
if (cur->_left == nullptr)
{
if (cur != _root)
{
//非根节点,parent结点指向cur的右孩子
if (cur == parent->_left)
parent->_left = cur->_right;
else
parent->_right = cur->_right;
}
else
{
//根结点,更新根为cur的右孩子
_root = cur->_right;
}
delete cur;
}
//情况3:右孩子为空,左孩子不为空
else if (cur->_right == nullptr)
{
if (cur != _root)
{
//非根节点,parent指向cur的左孩子
if (cur == parent->_left)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
else
{
//根节点,更新根为cur的左孩子
_root = cur->_left;
}
delete cur;
}
//情况4:左右孩子均不为空,替换法删除
else
{
//找到cur右子树的最小结点(也就是右子树的最左结点,值最接近cur)
Node* minRightParent = cur;
Node* minRight = cur->_right;
while (minRight->_left)
{
minRightParent = minRight;
minRight = minRight->_left;
}
//交换待删除结点和最小结点的键值
swap(cur->_key, minRight->_key);
//删除最小结点(必须是左空结点/叶子结点,符合情况2/3)
if (minRight == minRightParent->_left)
minRightParent->_left = minRight->_right;
else
minRightParent->_right = minRight->_right;
delete minRight;
}
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 << " "; //再访问根节点
_InOrder(root->_right); //最后遍历右结点
}
//根结点
Node* _root = nullptr;
};
2、test.c测试文件
cpp
#include<iostream>
using namespace std;
#include"BinarySearchTree.h"
void test01()
{
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
BSTree<int> t;
for (auto e : a)
{
bool ret = t.Insert(e);
}
//中序遍历验证,有序就是插入正确
cout << "中序遍历:"<<endl;
t.InOrder();
}
void test02()
{
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
BSTree<int> t;
for (auto e : a)
{
t.Insert(e);
}
//查找测试
int arr[] = { 6,20,3,14,5 };
for (auto key : arr)
{
bool ret = t.Find(key);
cout << "查找" << key << ":" << (ret ? "存在" : "不存在") << endl;
}
}
void test03()
{
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
BSTree<int> t;
for (auto e : a)
{
t.Insert(e);
}
//删除测试
cout << "初始中序遍历";
t.InOrder();
t.Erase(1);
cout << "删除叶子结点1后:";
t.InOrder();
t.Erase(14);
cout << "删除单孩子结点14后:";
t.InOrder();
t.Erase(8);
cout << "删除左右孩子均不为空的结点8后:";
t.InOrder();
}
void test04()
{
//模拟词库
string dict[] = { "apple","banana","orange", "pear", "grape" };
BSTree<string> spellCheck;
for (auto& word : dict)
{
spellCheck.Insert(word);
}
//拼写检查测试
string testWords[] = { "apple", "orage", "banana", "peer" };
for (auto& word : testWords)
{
bool ret = spellCheck.Find(word);
cout << "单词\"" << word << "\":" << (ret ? "拼写正确" : "拼写错误") << endl;
}
}
int main()
{
//test01();
//test02();
//test03();
test04();
return 0;
}