系列文章目录
提示:这里是系列文章的专栏
并不喜欢吃鱼的C++专栏
提示:以下是文章目录哦!
文章目录
[1.先看左边的树(合法 BST)](#1.先看左边的树(合法 BST))
[2.再看右边的树(不合法 BST)](#2.再看右边的树(不合法 BST))
[二、 二叉搜索树的性能分析](#二、 二叉搜索树的性能分析)
[3.1 插入位置的寻找](#3.1 插入位置的寻找)
[3.2 插入的实现](#3.2 插入的实现)
[3.3 插入的特殊位置实现](#3.3 插入的特殊位置实现)
[6.1 中序遍历](#6.1 中序遍历)
[6.2 树的析构函数](#6.2 树的析构函数)
[6.3 树的拷贝函数](#6.3 树的拷贝函数)
[6.4 树的默认构造](#6.4 树的默认构造)
前言
提示:今天这里我们来新学一个数据结构哦
提示:以下是本篇文章正文内容,下面案例可供参考
一、搜索二叉树的概念
二叉搜索树(英文名:Binary Search Tree, BST)是一种满足以下性质的二叉树:
- 左子树所有节点的值 小于 根节点的值
- 右子树所有节点的值 大于 根节点的值
- 左右子树也都是二叉搜索树
- 关键推论:中序遍历结果是严格升序的序列
- 二叉搜索树在设计时,既可以支持插入相等的值,也可以不支持插入相等的值,具体规则完全取决于实际使用场景和需求。
在后续我们会学习的 map/set/multimap/multiset 等标准容器中,它们的底层结构正是二叉搜索树:
map / set:不支持插入重复的键值,保证键的唯一性;
multimap / multiset:支持插入重复的键值,允许相同值多次存储

1.先看左边的树(合法 BST)
它完全符合 BST 的核心规则:
对于根节点 8:左子树所有节点都小于 8(3,1,6,4,7),右子树所有节点都大于 8(10,14,13)
对于节点 3:左 1 < 3,右 6 > 3
对于节点 6:左 4 < 6,右 7 > 6
对于节点 10:右 14 > 10
对于节点 14:左 13 < 14
中序遍历结果 :1, 3, 4, 6, 7, 8, 10, 13, 14,是严格升序的,完美验证了 BST 的性质
2.再看右边的树(不合法 BST)
这里有两处明显的错误,直接违反了 BST 的定义:
- 节点
6的左孩子是3- 节点
6位于根节点8的左子树中,按规则,它的所有后代都必须小于8,这点没问题。 - 但它的父节点是
3,所以它的左孩子3并不小于父节点3,而是等于,违反了 "左子树所有节点 < 根节点" 的规则(在不支持重复值的标准 BST 中)
- 节点
- 节点
10的右孩子是10- 节点
10是根节点8的右孩子,它的右孩子也是10,不满足 "右子树所有节点> 根节点" 的规则,出现了重复值
- 节点
所以,右边的树不满足二叉搜索树的定义
二、 二叉搜索树的性能分析
BST 的效率完全取决于树的高度:
- 最好情况(完全平衡):插入 / 查找 / 删除的时间复杂度是 O(log n),和二分查找一样高效
- 最坏情况(退化成单链):时间复杂度会退化成 O(n),和普通数组遍历没区别

接下来讲讲最好情况和最坏情况是怎么出现的:
1.最好情况:
BST 的插入 / 查找 / 删除操作,本质上都是:
从根节点开始,每次和当前节点比较,然后往左或往右走,直到找到位置或空节点
在最好情况下(完全平衡):
- 每次比较,都能排除掉一半的节点(往左走,就排除了当前节点 + 整个右子树;往右走,就排除了当前节点 + 整个左子树)
- 这个过程和二分查找完全一样,每次问题规模减半
我们用节点总数 n 来推导一下:



2.最坏情况:
当你按有序序列(1,2,3,4...)插入节点时 ,树会变成一条斜链,完全失去 "二分" 的优势,1为根节点,然后2>1,因此为在1的右节点,随后3>2,以此类推就像上图的右边一样,将一直连下去退化成一条单链,当数据为n时,正好对应的时间复杂度就为O(n)
3.二叉搜索树,链表,数组的效率对比
| 对比维度 | 有序数组 | 链表 | 二叉搜索树(BST) |
| 查找效率 | 二分查找 O (log n) | 只能遍历 O (n) | 最好情况 O (log n) |
| 插入 / 删除效率 | 需移动元素 O (n) | 找到后操作 O (1) | 最好情况 O (log n) |
| 天然有序性 | 天生有序 | 无序 | 中序遍历直接得到有序序列 |
|---|
三、二叉搜索树的实现
1.二叉搜索树模版节点的实现
cpp
template<class T>
struct BSTNode
{
T _data;
BSTNode<T>* _left;
BSTNode<T>* _right;
BSTNode(const T& data)
: _data(data)
, _left(nullptr)
, _right(nullptr)
{}
};
- 模板
template<class T>支持int / double / string任意类型,和 C++ STL set/map 设计一致 - 成员变量
_key:节点存储的关键字(比较大小依据)_left:左孩子 → 所有值 < 当前节点_right:右孩子 → 所有值 > 当前节点
3.构造函数必须手动初始化 _left 和 _right 为 nullptr
易错点
- 不初始化指针 → 野指针,运行崩溃
- 写结构体时漏加
BSTNode<T>模板参数
2.二叉搜索树模版树节点的实现
cpp
template <class K>
class SBTree{
using Node = BSTnode<K>;
public:
..................
protected:
Node* _root = nullptr;
};
这里的 using Node = SBTNode<K>是C++11新增的用法给
BSTNode<K>这个类型起个新的、更短的名字 ,叫Node
3.二叉搜索树的插入的实现
插入的具体过程如下:
- 树为空,则直接新增结点,赋值给 root 指针
- 树不为空,按照二叉搜索树性质,插入值比当前结点大则往右走,插入值比当前结点小则往左走,找到空位置后,插入新结点。
- 如果支持插入相等的值,插入值与当前结点相等时,可以统一往右走,也可以统一往左走,找到空位置后插入新结点**(需要注意保持逻辑一致性,插入相等的值不要一会儿往右走,一会儿往左走。)**
接下来我们用一个例子具体解释一下:

如上图的一个搜索二叉树,我们此时要插入一个数为16,既然要插入,那我们就得找到值为14的节点,不断满足二叉搜索树的性质向下查找:

如下图,我们在上图的基础上插入一个重复的数字3,根据二叉搜索树的性质往下寻找值为4的节点插入:

对于以上的插入,都有一个前提,需要我们找到被插入的节点 ,如上面两张图值为14的节点,和值为4的节点 ,但插入可能插到左边或者右边 ,以及如果这棵树一个节点也没有的特殊情况 ,可以分为4步来思考插入:
1.找到插入的位置 2.插入的实现 3.插入的特殊情况处理
3.1 插入位置的寻找
这里的cur和parent需要解释一下**,cur是用来遍历二叉树的,用来寻找需要插入的位置,最后走到空,**
每次往下寻找前都需要更新一下parent,parent是用来充当插入的新节点的父亲,当然有人说这里为
什么不能直接用cur来实现,一般我们遵循一个指针只干一件事情,因此cur用来寻找位置,parent用来
记录位置
cpp
Node* cur = _root;
Node* parent = nullptr;
// 2. 寻找合适插入位置
while (cur != nullptr)
{
// 待插入值更小,往左走
if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
// 待插入值更大,往右走
else if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
// 存在重复值,set/map规则:不允许插入
else
{
return false;
}
}
3.2 插入的实现
cpp
// 3. 新建节点,挂载到父节点
cur = new Node(key);
if (key < parent->_key)
parent->_left = cur;
else
parent->_right = cur;
既然要插入,那就首先要创建新节点,根据搜索二叉树的性质,比当前parent节点值小的往左边插入,大的往右边插入
3.3 插入的特殊位置实现
cpp
// 1. 空树:直接新建根节点
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
如果为空,直接创建新节点即可
插入的完整代码如下:
cpp
// 1. 插入数据
bool Insert(const T& key)
{
// 1. 空树:直接新建根节点
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
Node* cur = _root;
Node* parent = nullptr;
// 2. 寻找合适插入位置
while (cur != nullptr)
{
// 待插入值更小,往左走
if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
// 待插入值更大,往右走
else if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
// 存在重复值,set/map规则:不允许插入
else
{
return false;
}
}
// 3. 新建节点,挂载到父节点
cur = new Node(key);
if (key < parent->_key)
parent->_left = cur;
else
parent->_right = cur;
return true;
}
4.二叉树查找功能的实现
查找的具体过程如下:
- 从根开始比较,查找 x,x 比根的值大则往右边走查找,x 比根值小则往左边走查找
- 最多查找高度次,走到空还没找到,说明这个值不存在
- 如果不支持插入相等的值,找到 x 即可返回
- 如果支持插入相等的值,意味着有多个 x 存在,一般要求查找中序的第一个 x
这里怕部分同学不太清楚:
在支持重复值的二叉搜索树(对应
multiset/multimap)中,当我们查找x时,"中序的第一个 x" 就是:在上面这个升序序列里,第一个出现的x对应的节点以下图里的
x=3为例:
- 中序序列里,
3第一次出现的位置,是1的右孩子那个3,也就是图中下方的那个红色节点- 而根节点左孩子的那个
3,是序列里第二个出现的3所以,查找
3时,我们要返回的是中序遍历里第一个3,也就是1的右孩子节点

代码具体实现如下:
cpp
// 2. 查找数据
Node* Find(const T& key)
{
Node* cur = _root;
while (cur != nullptr)
{
if (key < cur->_key)
cur = cur->_left;
else if (key > cur->_key)
cur = cur->_right;
else
return cur; // 找到目标节点
}
return nullptr; // 遍历到空,查找失败
}
5.二叉树的节点删除功能的实现
首先查找元素是否在二叉搜索树中,如果不存在,则返回 false。如果查找元素存在,则分以下四种情况分别处理:(假设要删除的结点为 N)
- 要删除结点 N 左右孩子均为空
- 要删除的结点 N 左孩子为空,右孩子结点不为空
- 要删除的结点 N 右孩子为空,左孩子结点不为空
- 要删除的结点 N 左右孩子结点均不为空
对于以上四种情况**,可具体总结为3种解决方案**:
假如要删除的节点为N,父节点为P,它指向N
5.1直接删除法
适用对象:树中所有没有子节点的叶子节点,包括树的根节点(当树只有一个节点时)
- 处理方法 :
- 直接将父节点
P指向N的指针置为nullptr - 释放节点
N的内存
- 直接将父节点
- 核心逻辑:因为没有子节点,删除后不会影响其他节点,直接 "断链删除" 即可
5.2更换指向法
适用对象:只有左或者右孩子的节点
- 处理方法 :
- 让父节点
P原本指向N的指针,改为指向N的左/右孩子 - 释放节点
N的内存
- 让父节点
- 核心逻辑 :用左/右孩子直接顶替
N的位置,保持 BST 的大小关系不变
5.3替换删除法
适用对象:所有同时拥有左、右子树的节点
- 处理方法(后继替换法,标准做法) :
- 找后继节点 :在
N的右子树中找到最左节点 (即中序遍历中N的下一个节点),记为S - 值替换 :将
N的值替换为S的值 - 删除后继节点 :此时问题转化为删除节点
S,而S必然满足情况 1 或情况 2(左孩子为空),按对应方法删除即可
- 找后继节点 :在
- 核心逻辑 :不直接删除
N,而是用后继节点的值覆盖它,再删除更容易处理的后继节点,保证 BST 的有序性不被破坏
----------- 以下是这三种方法的运用展示-----------

对于值为1的节点删除 十分简单,直接采用直接删除法即可,随后让值为3的节点左指针指向nullptr
对于值为10的节点的删除要稍微复杂一点,这里很明显可以看到,它还有右孩子,因此**直接删除法不可行,这里就需要采用更换指向法,**将当前值为14的节点先记录下来,随后删除值为10的节点,将8的右指针指向值为14的节点即可

这里的删除方式,和上面删除10节点的方式一样,自行理解

对于值为3的节点的删除 ,观察可知,它还有左孩子和右孩子,因此直接删除法和更换指向法 都不能直接完成删除,这里需要用到替换删除法 ,为了删除掉值为3的节点同时满足搜索二叉树的性质,我们需要找到一个比1,以及右子树都小的和比8小的节点:
首先来说比8大,那必须在值为8节点的左子树里寻找,而不能是右子树,因为右边一定比它大,如这里的10,14,13,那我们只能从左子树找,同时我们要大于1,因此不能往值为3的节点的左子树去找,得往右子树找,由于我们要找一个比右子树都小的,因此必须是右子树最左边的节点
再来看这里,我们要删除掉3.找到右子树最左边的值为4的节点,首先将值为4的节点覆盖到3上,随后将替换前的值为4的节点删掉(也就是值为6的左孩子)
注意:这里还有个特树情况,就是删除值为8的根节点,方法也是一样的,这里不过多赘述
这里展示完整代码,已做实现区分,请看其中注释:
cpp
bool Erase(const T& key)
{
Node* cur = _root;
Node* parent = nullptr;
// 1. 先找到要删除的节点
while (cur != nullptr)
{
if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
// 找到需要删除的节点 cur
else
{
// 情况1:左子树为空
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;
}
// 情况2:右子树为空
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;
}
// 情况3:左右子树都不为空(最难)
else
{
// 找右子树最左节点 -> 中序后继
Node* minParent = cur;
Node* minRight = cur->_right;
while (minRight->_left != nullptr)
{
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;
}
需要注意的是,为什么这里面没有把无孩子的节点分类出来,因为他们的删除逻辑已经包含在其中了
6.搜索二叉树其余接口的实现
6.1 中序遍历
可能有同学要问到为什么要用中序遍历, 因为二叉搜索树的中序遍历结果,必然是严格升序的序列

而中序遍历的顺序是:左子树 → 根节点 → 右子树
cpp
// 模板 二叉搜索树类
template<class T>
class BSTree
{
// 类型重定义,简化代码
typedef BSTNode<T> Node;
private:
Node* _root; // 根节点
// 中序遍历 子函数(递归)
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
这里的_InOrder函数封装在该类的内部为私有成员,这是一种封装的思想**,Node是树内部的结构,不应该暴露在外面**
cpp
tree.InOrder(root); // ❌ 错误,用户不应该接触节点
tree.InOrder(); // ✅ 正确,用户只需要调用
因此写成私有类后,外部无法直接调用,但是可以通过函数间接调用,因此我们把它封装起来
cpp
public:
void InOrder() {
_InOrder(_root); // 内部调用私有实现
}
6.2 树的析构函数
和中序遍历一样,这里也需要封装成私有类,原因一样,不过多赘述,同时也用递归写
cpp
~BSTree()
{
Destroy(_root);
_root = nullptr;
}
private:
void Destroy(Node* root)
{
if (root == nullptr)
return;
Destroy(root->_left);
Destroy(root->_right);
delete root;
}
6.3 树的拷贝函数
这里的
Copy函数,用的是 前序遍历 顺序:根 → 左 → 右作用:二叉搜索树深拷贝拷贝构造函数,用来克隆一整棵 BST,两棵树完全独立,互不影响
同样树的拷贝函数的实现也需要封装起来:
1. 拷贝构造函数
cpp
BSTree(const BSTree& t)
{
_root = Copy(t._root);
}
- 这是拷贝构造 :用一个已存在的树
t,创建一个一模一样的新树 t._root:传入原树的根节点- 调用私有递归函数
Copy,递归复制整棵树,最终把新树根赋值给当前对象_root
2. 递归拷贝核心函数
cpp
private:
Node* Copy(Node* root)
{
// 递归终止条件:空节点直接返回空
if (root == nullptr)
return nullptr;
// 1. 先创建当前根节点 👉 前序第一步:根
Node* newRoot = new Node(root->_key, root->_value);
// 2. 递归拷贝左子树 👉 前序第二步:左
newRoot->_left = Copy(root->_left);
// 3. 递归拷贝右子树 👉 前序第三步:右
newRoot->_right = Copy(root->_right);
// 返回当前新节点,给上层接收
return newRoot;
}
为什么这里必须是前序遍历呢?
- 必须先造当前节点 你要先
new出根节点,才能给它的_left、_right赋值。 - 如果换成中序 / 后序,逻辑有问题
- 后序:左→右→根,最后才创建当前节点,没法提前挂载左右孩子
- 中序:左→根→右,顺序错乱,树结构无法正确构建
6.4 树的默认构造
上面讲完树的拷贝构造这里就要讲一下树的默认构造,由于拷贝构造也是构造,所以编译器不会再自动生成默认构造,但我们又必须要默认构造,当我们创建一个新的树的时候,此时根节点为空,因此这里初始时需要一个默认构造来创建空树
cpp
// 强制生成构造
BSTree() = default;
这里就要用到C++11的强制生成默认构造了
完整代码实现如下:
cpp
#pragma once
#include<iostream>
using namespace std;
namespace key_value
{
template<class K, class V>
struct BSTNode
{
K _key;
V _value;
BSTNode<K, V>* _left;
BSTNode<K, V>* _right;
BSTNode(const K& key, const V& value)
:_key(key)
, _value(value)
, _left(nullptr)
, _right(nullptr)
{}
};
// Binary Search Tree
// Key/value
template<class K, class V>
class BSTree
{
//typedef BSTNode<K> Node;
using Node = BSTNode<K, V>;
public:
// 强制生成构造
BSTree() = default;
BSTree(const BSTree& t)
{
_root = Copy(t._root);
}
BSTree& operator=(BSTree tmp)
{
swap(_root, tmp._root);
return *this;
}
~BSTree()
{
Destroy(_root);
_root = nullptr;
}
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 nullptr;
}
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 (parent->_left == cur)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
delete cur;
}
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;
}
else
{
// 左右都不为空
// 右子树最左节点
Node* replaceParent = cur;
Node* replace = cur->_right;
while (replace->_left)
{
replaceParent = replace;
replace = replace->_left;
}
cur->_key = replace->_key;
if (replaceParent->_left == replace)
replaceParent->_left = replace->_right;
else
replaceParent->_right = replace->_right;
delete replace;
}
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);
}
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* newRoot = new Node(root->_key, root->_value);
newRoot->_left = Copy(root->_left);
newRoot->_right = Copy(root->_right);
return newRoot;
}
private:
Node* _root = nullptr;
};
}
namespace key
{
template<class K>
struct BSTNode
{
K _key;
BSTNode<K>* _left;
BSTNode<K>* _right;
BSTNode(const K& key)
:_key(key)
, _left(nullptr)
, _right(nullptr)
{}
};
// Binary Search Tree
// Key
template<class K>
class BSTree
{
//typedef BSTNode<K> Node;
using Node = BSTNode<K>;
public:
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;
}
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
{
// 删除
// 左为空
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;
}
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;
}
else
{
// 左右都不为空
// 右子树最左节点
Node* replaceParent = cur;
Node* replace = cur->_right;
while (replace->_left)
{
replaceParent = replace;
replace = replace->_left;
}
cur->_key = replace->_key;
if (replaceParent->_left == replace)
replaceParent->_left = replace->_right;
else
replaceParent->_right = replace->_right;
delete replace;
}
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);
}
private:
Node* _root = nullptr;
};
}
搜索二叉树整体来讲难度不大,后面我们还会接触AVL树和红黑树,以及学习map和set等他们的底层都是红黑树,那这个就会更复杂一点
