跳到主要内容
C++ 二叉搜索树原理与实战:插入查找删除及 key/value 场景 | 极客日志
C++ 算法
C++ 二叉搜索树原理与实战:插入查找删除及 key/value 场景 二叉搜索树(BST)是左子树小于根、右子树大于根的树形结构,平均时间复杂度 O(logN),最坏 O(N)。 BST 节点定义、插入、查找及删除操作,重点分析删除时的四种情况及替换策略。探讨 Key 与 Key/Value 两种应用场景,如车牌识别与字典映射,并提供完整 C++ 代码示例。
极客工坊 发布于 2026/3/15 更新于 2026/4/25 1 浏览C++ 的两个参考文档
非官方文档:cplusplus
官方文档(同步更新):cppreference
前言
从二叉搜索树到 map 和 set 的使用、AVL 树实现、红黑树,再到封装红黑树实现 mymap 和 myset,这是一个整体。接下来的重点是介绍平衡搜索二叉树相关的内容。在接触 AVL 树和红黑树之前,我们需要先掌握基础的数据结构——二叉搜索树(BST)。有了这个基础,后续理解更复杂的平衡树会更容易。
1 理解二叉搜索树
1.1 二叉搜索树的概念
二叉搜索树又称二叉排序树 ,它或者是一棵空树,或者是具有以下性质的二叉树:
若它的左子树不为空,则左子树上所有结点的值都小于等于根结点的值 ;
若它的右子树不为空,则右子树上所有结点的值都大于等于根结点的值 ;
它的左右子树也分别为二叉搜索树;
二叉搜索树中可以支持插入相等的值,也可以不支持插入相等的值,具体看使用场景定义。例如 map/set 不支持插入相等值,multimap/multiset 支持插入相等值。
如下图所示就是两个搜索二叉树:
1.2 核心特性
BST 支持动态数据集合的高效操作,适合频繁插入、删除和查找的场景。
利用二叉树的分支特性,BST 在平均情况下能实现 O(logN) 的搜索效率。
2 二叉搜索树性能分析
2.1 时间复杂度分析
最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其高度为 logN;最差情况下,二叉搜索树退化为单支树(或者类似单支),其高度为 N。所以综合而言二叉搜索树增删查改时间复杂度为 O(N)。
这样的效率显然无法满足我们需求,因此后面会介绍二叉搜索树的变形——平衡二叉搜索树 AVL 树和红黑树 ,才能适用于我们在内存中存储和搜索数据。
2.2 二分查找的局限性 另外需要说明的是,二分查找也可以实现 O(logN) 级别的查找效率,但是二分查找有两大缺陷:
需要存储在支持下标随机访问的结构中,并且有序;
插入和删除数据效率很低,因为存储在下标随机访问的结构中,插入和删除数据一般需要挪动数据。
数据越有序,插入结果越坏——高度高、递归深、效率低;
插入越无序的数据,左右会平衡一点,结果反而越好。
3 实现二叉搜索树的定义
3.1 命名规范 二叉搜索树常简写为 BST,提高代码可读性。二叉搜索树也叫搜索二叉树。
3.2 定义节点 template <class K > struct BSTreeNode {
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
K _key;
BSTreeNode (const K& key) : _left(nullptr ), _right(nullptr ), _key(key) {}
};
3.3 实践:完整的类定义
4 二叉搜索树的插入操作详解
4.1 插入算法流程 从根节点开始,根据键值大小选择左子树或右子树,直到找到空位置插入新节点。
树为空,则直接新增结点,赋值给 root 指针;
树不空,按二叉搜索树性质,插入值比当前结点大就往右走,插入值比当前结点小就往左走,找到空位置,插入新结点;
如果支持插入相等的值,插入值跟当前结点相等的值可以往右走,也可以往左走,找到空位置,插入新结点(要注意保持逻辑一致性)。
4.2 代码实践
4.2.1 代码演示 int a[] = {8 , 3 , 1 , 10 , 6 , 4 , 7 , 14 , 13 };
template <class K > class BSTree {
typedef BSTreeNode<K> Node;
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 ;
}
private :
Node* _root = nullptr ;
};
4.2.2 测试用例设计 在 Test.cpp 文件里面包一下头文件,给一个数组,再定义出一棵树,因为数组也是支持范围 for 的,这里我们用范围 for 把数据插入进去。
4.2.3 C++ 递归的麻烦之处 C++ 递归很麻烦,这里要传根,但是根是私有的。在类里面的递归基本上要这样玩,尤其是树的递归,因为树的递归起始条件一般是根,都要套一层(内部/外部),套一层,因为外部拿不到根,内部是可以拿到根的,这种方式是最推荐的拿根方式。
调的是不需要传根的,再去调用这个子函数,把根传过去,这样外面就不需要传了,也不需要提供 get root,不过 get root 调用的时候也方便。
4.3 InOrder:中序遍历验证 利用 BST 的中序遍历必然有序的特性验证插入正确性。
要写成这样套一层的结构,原因上面已经提到了,这里不再赘述。
中序遍历:最简单的递归;
中序遍历有序,并且数据都在,并且能够很好地验证功能。
4.4 运行演示 运行一下,成功插入进去了,并且因为是中序遍历,结果是有序的。
4.5 实现要点 平衡和旋转这一块非常复杂,仅凭三言两语是说不清楚的,我们后面会详细介绍。
5 查找操作实现
5.1 查找算法 利用 BST 的排序特性,通过比较键值快速定位目标节点。
从根开始比较,查找 x,x 比根的值大则往右边走查找,x 比根值小则往左边走查找;
最多查找高度次,走到到空,还没找到,这个值不存在;
如果不支持插入相等的值,找到 x 即可返回;
如果支持插入相等的值,意味着有多个 x 存在,一般要求查找中序的第一个 x。
5.2 代码实践
5.3 测试用例设计
5.4 运行
6 删除操作深度解析
6.1 删除前的定位:要先查找一下
6.1.1 查找元素存在分四种情况 首先查找元素是否在二叉搜索树中,如果不存在,则返回 false。
如果查找元素存在则分以下四种情况需要分别处理一下(假设要删除的结点为 N):
要删除结点 N 左右孩子均为空;
要删除的结点 N 左孩子位空,右孩子结点不为空;
要删除的结点 N 右孩子位空,左孩子结点不为空;
要删除的结点 N 左右孩子结点均不为空。
6.1.2 对应以上四种情况的解决方案
把 N 结点的父亲对应孩子指针指向空,直接删除 N 结点(情况 1 可以当成 2 或者 3 进行处理,效果是一样的);
把 N 结点的父亲对应孩子指针指向 N 的右孩子,直接删除 N 结点;
把 N 结点的父亲对应孩子指针指向 N 的左孩子,直接删除 N 结点;
无法直接删除 N 结点,因为 N 的两个孩子无处安放,只能用替换法删除。找 N 左子树的值最大结点 R(最右结点)或者 N 右子树的值最小结点 R(最左结点)替代 N,因为这两个结点中任意一个,放到 N 的位置,都满足二叉搜索树的规则。替代 N 的意思就是 N 和 R 的两个结点的值交换,转而变成删除 R 结点,R 结点符合情况 2 或情况 3,可以直接删除。
6.2 示例分析
6.3 实践:代码实现
6.3.1 节点定位:查找要删除的节点 查找过程图示:假设要删除 key = 6,如下图所示。
查找路径:
cur = 8, 6 < 8 → 向左
cur = 3, 6 > 3 → 向右
cur = 6, 6 == 6 → 找到!
此时:parent = 节点 3, cur = 节点 6
6.3.2 左子树为空的情况
6.3.3 右子树为空的情况
6.3.4 左右子树都存在的情况
6.3.5 完整的 Erase 实现
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 (cur == parent->_left) {
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 (cur == parent->_left) {
parent->_left = cur->_left;
} else {
parent->_right = cur->_left;
}
}
delete cur;
return true ;
}
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 ;
}
6.4 测试用例设计
6.4.1 替代节点的父节点就是当前节点:replace 的 parent 就是 cur
6.4.2 需要判断一下:连接判断逻辑
6.5 访问 for 重新删
6.6 运行
6.7 实现技巧
7 二叉搜索树的完整代码示例与实践演示
SearchBinaryTree.h: #pragma once
#include <iostream>
using namespace std;
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 :
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 (cur == parent->_left) {
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 (cur == parent->_left) {
parent->_left = cur->_left;
} else {
parent->_right = cur->_left;
}
}
delete cur;
return true ;
} 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 ;
};
Test.cpp: #define _CRT_SECURE_NO_WARNINGS 1
#include "SearchBinaryTree.h"
int main () {
int a[] = { 8 ,3 ,1 ,10 ,1 ,6 ,4 ,7 ,14 ,13 };
BSTree<int > t;
for (auto e : a) {
t.Insert (e);
}
t.InOrder ();
t.Find (1 );
t.InOrder ();
t.Erase (3 );
t.Erase (8 );
t.InOrder ();
t.Erase (1 );
t.InOrder ();
t.Erase (10 );
t.InOrder ();
for (auto e : a) {
t.Insert (e);
}
return 0 ;
}
运行演示
8 二叉搜索树 key 和 key / value 使用场景
8.1 key 搜索场景 只有 key 作为关键码,结构中只需要存储 key 即可,关键码即为需要搜索到的值,搜索场景只需要判断 key 在不在。key 的搜索场景实现的二叉搜索树支持增删查,但是不支持修改,修改 key 破坏搜索树结构了。
场景 1:小区无人值守车库,小区车库买了车位的业主车才能进小区,那么物业会把买了车位的业主的车牌号录入后台系统,车辆进入时扫描车牌在不在系统中,在则抬杆,不在则提示非本小区车辆,无法进入。
场景 2:检查一篇英文文章单词拼写是否正确,将词库中所有单词放入二叉搜索树,读取文章中的单词,查找是否在二叉搜索树中,不在则波浪线标红提示。
8.2 key / value 使用场景 每一个关键码 key,都有与之对应的值 value,value 可以任意类型对象。树的结构中(结点)除了需要存储 key 还要存储对应的 value,增/删/查还是以 key 为关键字走二叉搜索树的规则进行比较,可以快速查找到 key 对应的 value。key/value 的搜索场景实现的二叉搜索树支持修改,但是不支持修改 key,修改 key 破坏搜索树性质了,可以修改 value。
场景 1:简单中英互译字典,树的结构中(结点)存储 key(英文)和 vlaue(中文),搜索时输入英文,则同时查找到了英文对应的中文。
场景 2:商场无人值守车库,入口进场时扫描车牌,记录车牌和入场时间,出口离场时,扫描车牌,查找入场时间,用当前时间 - 入场时间计算出停车时长,计算出停车费用,缴费后抬杆,车辆离场。
场景 3:统计一篇文章中单词出现的次数,读取一个单词,查找单词是否存在,不存在这个说明第一次出现,( 单词 , 1),单词存在,则++单词对应的次数。
8.3 实践:key / value 代码实现 key / value 场景下的代码实现如下所示:
namespace key_value {
template <class K ,class V > struct BSTreeNode {
BSTreeNode<K,V>* _left;
BSTreeNode<K,V>* _right;
K _key;
V _value;
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 :
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 (cur == parent->_left) {
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 (cur == parent->_left) {
parent->_left = cur->_left;
} else {
parent->_right = cur->_left;
}
}
delete cur;
return true ;
} else {
Node* replaceParent = cur;
Node* replace = cur->_right;
while (replace->_left) {
replaceParent = replace;
replace = replace->_left;
}
cur->_key = replace->_key;
cur->_value = replace->_value;
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 ;
};
}
8.4 设计测试用例
8.4.1 测试用例一
8.4.2 测试用例二
8.5 运行演示
8.5.1 测试用例一运行演示
8.5.2 测试用例二运行演示
8.5.3 取消运行 ^Z:Ctrl + Z + Enter(回车),就能取消运行。
8.6 博主手记 相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online