二叉搜索树(BST)以"左小右大"的有序性约束组织数据,使查找、插入、删除可在树高范围内完成。本文系统梳理 BST的定义与中序有序性、性能与树高的关系、插入/查找/删除的关键边界处理,并给出 key 与 key/value 两套 C++模板实现,实现把规则、指针操作与可运行代码建立明确对应。
一、概念
二叉搜索树也称 二叉排序树。它要么是空树,要么满足下面这套"大小规则"(这就是 BST 的灵魂):
- 若它的左子树不为空,则左子树上所有结点的值均 小于 它的根结点的值。
- 若它的右子树不为空,则右子树上所有结点的值均 大于 它的根结点的值。
- 它的左右子树也分别为二叉搜索树(也要继续满足上面 1、2)。
换句话说:对任意结点,都要满足 左边都比我小,右边都比我大,而且子树也要同样遵守。
二叉搜索树支持 中序遍历(左-根-右)。如果一棵二叉树是 BST,那么对它做中序遍历得到的结果一定是一个 有序序列。

二、二叉搜索树的性能分析
BST 的插入、查找、删除,真正花多少时间,主要取决于一个东西:树高(高度越小,走的路越短)。
-
最好情况:BST 是一棵完全二叉树
此时树高约为:log2(N)
所以增删查改都大约是:O(log2 N)
-
最坏情况:BST 退化成单支树(一边倒,像链表)
此时树高约为:N
所以增删查改会变成:O(N)
综合而言,二叉搜索树的增删查改时间复杂度为:O(N)(按最坏情况算)。
为什么后面还要学二叉平衡树?
因为有些场景不能接受"最坏 O(N)",就需要 AVL 树、红黑树这类 二叉平衡树 来把高度控制在 O(logN),从而稳定地快。
另外,二分查找也可以做到 O(log2 N),但它有两个问题:
- 必须支持随机访问(典型是数组)。
- 数据必须有序。
而有序数组要插入/删除一个元素,通常需要大量移动元素,效率不高。
所以:想要"查找快",又想要"插入/删除也相对方便",平衡 BST 就很有价值。

| 情况 | 树形态 | 树高 | 查找/插入/删除 |
|---|---|---|---|
| 最好 | 完全二叉树 | log2(N) | O(log2 N) |
| 最坏 | 单支树(退化) | N | O(N) |
三、二叉搜索树的插入
BST 的插入,本质就是:按大小规则一路往下走,直到走到空位置,把新结点挂上去。
插入过程:
- 先看根结点是否为空。
- 若为空,直接用待插入值构造根结点。
- 若不为空:
- 把当前结点
cur指向根,从根开始找插入位置; - 比较插入值和
cur的值:- 小:往左子树走
- 大:往右子树走
- 当
cur走到空(nullptr)时,说明位置找到了:把新结点插入,并挂到它父结点的左/右上。
- 把当前结点
- 二叉搜索树 不允许冗余(相等)元素。
当需要冗余元素时,这里用不着二叉搜索树,用后面的二叉平衡树。
当然,如果需要实现插入相同值的元素,也是可以实现的,只需要提前规定:- 比如遇到相同值时都插入到右子树,或者都插入到左子树。
- 归结为:遇到相同元素时,一定要优先去某一个方向。
| 步骤 | 当前状态 | 判断 | 下一步 |
|---|---|---|---|
| 1 | cur 指向根 | root 是否为空 | 为空:root=new;不空:进入循环 |
| 2 | 循环 | key 与 cur->_key 比较 | 小:cur=cur->_left;大:cur=cur->_right |
| 3 | cur 为空 | 找到插入位置 | parent 挂接新结点到 left/right |
例子:给定数组,按顺序插入到 BST 中

cpp
int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};


四、二叉搜索树的查找
查找也是同一套走向规则:
-
从根开始,把
cur指向根。 -
比较要查找的值
x和cur:x > cur:往右子树走x < cur:往左子树走- 相等:找到了
-
如果一路走到
cur == nullptr还没找到,说明不存在。
因此:BST 的查找效率同样依赖 树高。
如果 BST 支持冗余数据:查找时必须返回 中序的第一个 x。
例如:查找 3,应该返回的 3 是结点 1 的右孩子。

五、二叉搜索树的删除
删除要比插入/查找更麻烦一点,因为要维护 BST 的大小规则。
删除思路先分两步:
-
先判断要删除的元素是否在 BST 中。
- 不存在:删除失败,返回
false
- 不存在:删除失败,返回
-
存在:根据被删结点
N的孩子情况分 4 类讨论1)
N的左右孩子都为空2)
N的左孩子为空,右孩子不为空3)
N的右孩子为空,左孩子不为空4)
N的左右孩子都不为空
对应以上四种情况的解决方案:
1)当 N 的左右孩子都为空:直接删除 N。(也可以按情况 2 或 3 处理,效果一样)
2)当 N 的左孩子为空,右孩子不为空:删除 N,用 N 的右孩子替代 N 的位置。
3)当 N 的右孩子为空,左孩子不为空:删除 N,用 N 的左孩子替代 N 的位置。
4)当 N 的左右孩子都不为空:不能直接删除 N。
此时需要用 替换法删除:
- 找到
N左子树的最大结点(最右结点)或者N右子树的最小结点(最左结点),记为替代结点R; - 用
R替换N(通常是把值交换/覆盖到N上); - 再删除
R。因为R一定满足情况 2 或 3,所以可以直接删除。



六、二叉搜索树代码实现
重点:
Insert/Find:比较后往左/右走Erase:要处理"删根"和"删两孩子"的指针调整
cpp
template<class K>
struct BSTNode
{
BSTNode(const K& key)
:_key(key)
,_left(nullptr)
,_right(nullptr)
{}
K _key;
BSTNode<K>* _left;
BSTNode<K>* _right;
};
//Binary Search Tree
template<class K>
class BSTree
{
typedef BSTNode<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
{
//0-1个孩子的情况
//删除情况1 2 3均可以直接删除,改变父亲对应孩子指针指向即可
if (cur->_left == nullptr)
{
if (parent == nullptr)
{
_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 (parent == nullptr)
{
_root = cur->_left;
}
else
{
if (parent->_left == cur)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
delete cur;
return true;
}
else
{
//2个孩子的情况
//删除情况4,替换法删除
//假设这里我们取右子树的最小结点作为替代结点去删除
//这里尤其要注意右子树的根就是最小情况的情况的处理,对应课件图中删除8的情况
//一定要把cur给rightMinP,否则会报错。
Node* rightMinP = cur;
Node* rightMin = cur->_right;
while (rightMin->_left)
{
rightMinP = rightMin;
rightMin = rightMin->_left;
}
cur->_key = rightMin->_key;
if (rightMinP->_left == rightMin)
rightMinP->_left = rightMin->_right;
else
rightMinP->_right = rightMin->_right;
delete rightMin;
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;
};
注意:
Insert必须用parent记住"cur 的上一个结点",否则你走到空结点时就不知道该把新结点挂到谁下面。Erase要特别注意 删除根结点:代码用parent == nullptr来区分根。Erase的两孩子情况,用的是"右子树最小结点 rightMin"来替换,再把 rightMin 删除掉(rightMin 最终一定能落到 0/1 孩子的分支里处理)。
七、二叉搜索树key和key/value使用场景
7.1 key搜索场景:
这里只存 key,重点是:能不能快速判断"某个 key 是否存在"。
支持的操作:插入、删除、查找。
但通常不支持 修改:因为 BST 的结构就是靠 key 的大小关系组织的,你把 key 改了,整棵树的大小规则就可能被破坏。
场景:
1)停车场:每一辆车都具有一个唯一的车牌号,使用车牌号作为 key 值,建立 BST,车辆进场和出场分别对应插入和删除,查找即查找某辆车是否在场。
2)拼写检查:单词作为 key,建立 BST,查找时判断是否存在,若不存在则拼写错误。

7.2 key/value搜索场景:
这里每个 key 都有一个对应的 value。value 的类型可以很灵活(看业务需求)。
BST 的结点需要存 key 和 value。插入、删除、查找时,仍然按 BST 规则用 key 比较大小 来走左/右子树,因此可以快速找到 key 对应的 value。
key/value BST 支持 修改(通常指修改 value),但不支持修改 key:key 还是结构的"排序依据",改 key 会破坏整棵树。
场景:
1)字典:英文单词 key,对应翻译 value。
2)购物广场停车场:车牌 key,进场时间 value。
3)统计单词出现次数:单词 key,出现次数 value。
第一次出现:插入 <word, 1>
再次出现:找到结点,把次数 ++

7.3 key/value二叉搜索树代码实现
下面是课件给出的 key/value BST 实现与两个例子(字典、词频统计)。注意:代码里"比较大小"仍然只比较 _key。
cpp
template<class K, class V>
struct BSTNode
{
//pair<K, V> _kv;
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)
{}
};
template<class K, class V>
class BSTree
{
typedef BSTNode<K, V> Node;
public:
BSTree() = default;
BSTree(const BSTree<K, V>& t)
{
_root = Copy(t._root);
}
BSTree<K, V>& operator=(BSTree<K, V> t)
{
swap(_root, t._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 (parent == nullptr)
{
_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 (parent == nullptr)
{
_root = cur->_left;
}
else
{
if (parent->_left == cur)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
delete cur;
return true;
}
else
{
Node* rightMinP = cur;
Node* rightMin = cur->_right;
while (rightMin->_left)
{
rightMinP = rightMin;
rightMin = rightMin->_left;
}
cur->_key = rightMin->_key;
if (rightMinP->_left == rightMin)
rightMinP->_left = rightMin->_right;
else
rightMinP->_right = rightMin->_right;
delete rightMin;
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;
};
int main()
{
BSTree<string, string> dict;
//BSTree<string, string> copy = dict;
dict.Insert("left", "左边");
dict.Insert("right", "右边");
dict.Insert("insert", "插入");
dict.Insert("string", "字符串");
string str;
while (cin >> str)
{
auto ret = dict.Find(str);
if (ret)
{
cout << "->" << ret->_value << endl;
}
else
{
cout << "无此单词,请重新输入" << endl;
}
}
return 0;
}
int main()
{
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
BSTree<string, int> countTree;
for (const auto& str : arr)
{
//先查找水果在不在搜索树中
//1、不在,说明水果第一次出现,则插入<水果,1>
//2、在,则查找到的结点中水果对应的次数++
//BSTreeNode<string, int>* ret = countTree.Find(str);
auto ret = countTree.Find(str);
if (ret == NULL)
{
countTree.Insert(str, 1);
}
else
{
ret->_value++;
}
}
countTree.InOrder();
return 0;
}
重点:
Insert:比较_key决定走左/右;最终用parent挂接新结点。Find:一路比较_key,返回结点指针,这样外部就能直接读/改_value。- 词频统计:先
Find,找不到就Insert(str, 1),找到就ret->_value++。
完