C++必学系列:二叉搜索树
文章目录
一、前言
在C++STL容器中,map/set/multimap/multiset四大关联容器底层全部依托二叉搜索树及其优化版本(AVL树、红黑树)实现。想要吃透关联容器,第一步必须彻底弄懂基础版二叉搜索树(BST)。
本文结合手撕源码、图文逻辑、面试高频考点、从零讲解二叉搜索树,提供两份可直接编译运行的完整代码,适合零基础入门,源码学习。
二、二叉搜索树的概念
2.1 定义
二叉搜索树(Binary Search Tree,BST),也叫二叉排序树,是满足特殊规则的二叉树,空树也属于合法的二叉搜索树,三大核心性质:
- 左子树所有节点值≤ 根节点值
- 右子树所有节点值 ≥ 根节点值
- 左右子树必须同样满足二叉搜索树规则,递归定义
2.2 重复值处理规则
二叉搜索树是否允许重复节点,无强制标准,由业务场景决定:
- 不允许重复值:对应STL set、map,键值唯一,插入重复值直接失败
- 允许重复值:对应STL multiset、multimap,重复值统一放左子树/右子树,全程逻辑必须一致,不能左右随意切换
2.3 核心特性:中序遍历有序
二叉搜索树最关键的特点:中序遍历结果是严格升序序列,这也是它被称为排序树的核心原因,后续代码中会通过中序遍历验证树的合法性。
三、二叉搜索树性能分析
3.1 时间复杂度分两种极端情况
| 树形态 | 树高度 | 增删查时间复杂度 | 场景说明 |
|---|---|---|---|
| 最优:平衡二叉树(接近完全二叉树) | l o g 2 N log_2N log2N | O ( l o g 2 N ) O(log_2N) O(log2N) | 数据随机插入,查询效率极高 |
| 最差:退化成单支树 | N | O(N) | 数据有序插入(1,2,3,4,5…),彻底退化成链表 |
3.2 和二分查找的对比
有序数组二分查找同样拥有 O ( l o g 2 N ) O(log_2N) O(log2N)查询效率,但存在致命缺陷:
- 依赖连续内存,必须支持下标随机访问,且数组全程有序
- 插入、删除元素需要大批量挪动数组元素,增删效率极低O(N)
核心结论:普通二叉搜索树解决了二分查找增删慢的问题,但存在不平衡退化问题;所以衍生出AVL树、红黑树,兼顾查询和增删效率,也是STL容器真正底层实现。
四、二叉搜索树四大基础操作原理
4.1 插入操作 Insert
- 树为空:直接新建节点,赋值给根节点
- 树非空:从根节点开始遍历,比当前节点小往左走,比当前节点大往右走
- 遇到空位置:挂载新节点;遇到相同key:禁止插入(本文代码实现无重复版本)
4.2 查找操作 Find
- 从根节点开始比对,key更大向右查找,key更小向左查找
- 遍历到空节点:查找失败;匹配到key:查找成功
- 最多遍历树的高度次,效率由树的平衡度决定
4.3 删除操作 Erase
删除是二叉搜索树最难的操作,分为四种场景,可合并为三类处理逻辑:
- 场景1:待删除节点左右孩子都为空(叶子节点):直接让父节点指向空,释放当前节点
- 场景2:待删除节点只有左孩子 / 只有右孩子:父节点直接指向该节点唯一的子节点,绕过当前节点后释放
- 场景3:待删除节点左右孩子全部存在(最难):采用替换法,不直接删除当前节点
- 方案一:找右子树最小值节点(右子树最左节点)
- 方案二:找左子树最大值节点(左子树最右节点)
- 交换当前节点和替代节点的key值,转而删除替代节点(替代节点一定满足前两种简单删除场景)
4.4 中序遍历 InOrder
递归顺序:左子树 → 根节点 → 右子树,输出结果升序,用来校验二叉搜索树构建是否合法。
五、二叉搜索树两种模型
实际开发中,二叉搜索树分为两类,对应不同业务场景,也是STL容器两种设计思想:
5.1 Key模型:只存关键字
节点仅存储key值,只需要判断元素是否存在,不存储额外数据。
业务场景:车牌门禁校验、英文单词拼写检测;特点:不支持修改key(破坏树结构),无value可修改。
5.2 Key-Value模型:关键字+映射值
节点存储key和value,key用于排序查找,value存储业务数据。
业务场景:中英字典翻译、停车计费系统、文章单词词频统计;特点:不可修改key,可以随意修改value。
六、完整可运行源码
6.1 Key版二叉搜索树源码
#include <iostream>
using namespace std;
// Key版本节点结构
template<class K>
struct BSTNode
{
K _key;
BSTNode<K>* _left;
BSTNode<K>* _right;
BSTNode(const K& key)
:_key(key)
,_left(nullptr)
,_right(nullptr)
{}
};
// Key版本二叉搜索树类
template<class K>
class BSTree
{
typedef BSTNode<K> Node;
public:
// 构造函数
BSTree() = default;
// 析构函数
~BSTree()
{
Destroy(_root);
_root = nullptr;
}
// 插入节点
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
{
// 存在重复key,插入失败
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
{
// 情况1:左孩子为空
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;
}
// 情况2:右孩子为空
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;
}
// 情况3:左右孩子都存在,替换法删除
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 << " ";
_InOrder(root->_right);
}
// 递归销毁树
void Destroy(Node* root)
{
if (root == nullptr)
return;
Destroy(root->_left);
Destroy(root->_right);
delete root;
}
Node* _root = nullptr;
};
// Key版本测试用例
void TestBSTKey()
{
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(8);
cout << "删除8后中序遍历:";
t.InOrder();
t.Erase(3);
cout << "删除3后中序遍历:";
t.InOrder();
}
int main()
{
TestBSTKey();
return 0;
}
6.2 Key-Value版二叉搜索树源码
#include <iostream>
#include <string>
using namespace std;
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)
{}
};
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;
}
// 交换key
cur->_key = rightMin->_key;
cur->_value = rightMin->_value;
// 删除最小节点
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 << " ";
_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;
}
Node* _root = nullptr;
};
// 测试1:中英字典
void TestDict()
{
BSTree<string, string> 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;
}
}
// 测试2:水果词频统计
void TestCount()
{
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
BSTree<string, int> countTree;
for (const auto& str : arr)
{
auto ret = countTree.Find(str);
if (ret == nullptr)
countTree.Insert(str, 1);
else
ret->_value++;
}
cout << "水果词频统计结果:" << endl;
countTree.InOrder();
}
int main()
{
TestCount();
// TestDict();
return 0;
}
七、易错点总结
- 删除双孩子节点为什么选右子树最小值/左子树最大值?
:这两个节点是距离当前key最近的值,替换后不会破坏二叉搜索树有序规则,且这两个节点必然最多只有一个右/左孩子,删除简单 - 为什么有序数组插入会退化?
:1,2,3,4顺序插入,所有节点全部挂在右子树,树高等于节点数,彻底退化成链表 - key和value能否修改?
:绝对不能修改key,会直接破坏树的排序规则;value无限制,可以随意修改 - 中序遍历一定有序吗?
:合法二叉搜索树中序遍历必然升序,这是校验树合法性最简单的方式
八、文末总结
二叉搜索树是树结构和STL关联容器的敲门砖,核心记住三点:
- 规则:左小右大,递归定义,中序遍历升序
- 痛点:有序数据插入退化成链表,最坏时间复杂度O(N)
- 难点:双孩子节点替换删除法,面试手写代码核心考点
弄懂基础BST之后,再学习AVL树和红黑树会事半功倍,彻底吃透STL关联容器底层原理。
更多推荐
所有评论(0)