C++数据结构 : 二叉搜索树
C++数据结构 : 二叉搜索树
目录
- C++数据结构 : 二叉搜索树
- 引言
- 1. 二叉搜索树的概念
- 2. 二叉搜索树的性能分析
- 3. 二叉搜索树的插入
- 4. 二叉搜索树的查找
- 5. 二叉搜索树的删除
- 6. 二叉搜索树的实现代码
- 7. 二叉搜索树`key`和`key/value`使用场景
引言
二叉搜索树(BST)是一种高效的数据结构,通过有序的树形存储实现快速查找、插入和删除操作。其核心特性是:左子节点的值 ≤ 当前节点值 ≤ 右子节点的值。
本文将详解BST的原理、实现及应用,包括基础操作、性能分析,以及key
和key/value
两种模式的代码实现,帮助你在实际开发中灵活运用这一结构。
1. 二叉搜索树的概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有结点的值都小于等于根结点的值;
- 若它的右子树不为空,则右子树上所有结点的值都大于等于根结点的值;
- 它的左右子树也分别为二叉搜索树。
- 二叉搜索树中可以支持插入相等的值,也可以不支持插入相等的值,具体看使用场景定义,后续我们学习
map
/set
/multimap
/multiset
系列容器底层就是二叉搜索树,其中map
/set
不支持插入相等值,multimap
/multiset
支持插入相等值。
2. 二叉搜索树的性能分析
二叉搜索树的时间复杂度分析:
- 最优情况下,二叉搜索树为完全二叉树(或接近完全二叉树),其高度为 log₂N;
- 最差情况下,二叉搜索树退化为单支树(或类似单支),其高度为 N。
- 综合而言,二叉搜索树增删查改的时间复杂度为 O(N)。这样的效率显然无法满足需求,因此后续需要学习二叉搜索树的变形——平衡二叉搜索树(AVL 树和红黑树),才能适用于内存中的数据存储和搜索。
二分查找的局限性:
虽然二分查找可以实现 O(log₂N) 的查找效率,但它有两大缺陷:(1) 数据必须存储在支持下标随机访问的有序结构中;(2) 插入和删除效率很低,因为需要移动数据以维护顺序。这也体现了平衡二叉搜索树的优势。
3. 二叉搜索树的插入
二叉搜索树插入的具体过程如下:
- 若树为空,则直接新增结点,赋值给root指针;
- 若树不空,则按二叉搜索树性质,插入值比当前结点大往右走,插入值比当前结点小往左走,直至找到空位置并插入新结点;
- 若支持插入相等的值,则插入值与当前结点相等的值可统一往右或往左走(需保持逻辑一致性,避免左右随机选择),最终找到空位置并插入新结点。
4. 二叉搜索树的查找
- 从根开始比较,查找x,x比根的值大则往右边走查找,x比根值小则往左边走查找;
- 最多查找高度次,走到空还没找到,则该值不存在;
- 如果不支持插入相等的值,找到x即可返回;
- 如果支持插入相等的值,意味着有多个x存在,一般要求查找中序的第一个x。如下图,查找3,要找到1的右孩子的那个3返回。
5. 二叉搜索树的删除
二叉搜索树删除操作流程:
首先查找元素是否在二叉搜索树中,如果不存在,则返回false;如果存在则分以下四种情况处理(假设要删除的结点为N):
- 要删除的结点N左右孩子均为空;
- 要删除的结点N左孩子为空,右孩子结点不为空;
- 要删除的结点N右孩子为空,左孩子结点不为空;
- 要删除的结点N左右孩子结点均不为空。
对应解决方案:
- 把N结点的父亲对应孩子指针指向空,直接删除N结点(情况1可以当成2或3处理,效果相同);
- 把N结点的父亲对应孩子指针指向N的右孩子,直接删除N结点;
- 把N结点的父亲对应孩子指针指向N的左孩子,直接删除N结点;
- 无法直接删除N结点(需替换法):找N左子树的值最大结点R(最右结点)或右子树的值最小结点R(最左结点)替代N(交换N和R的值),转而删除R结点(此时R必符合情况2或3,可直接删除)。
6. 二叉搜索树的实现代码
// 二叉搜索树节点模板类
template<class K>
struct BSTNode {K _key; // 节点存储的关键值BSTNode<K>* _left; // 左子节点指针BSTNode<K>* _right; // 右子节点指针// 构造函数,初始化节点BSTNode(const K& key): _key(key) // 初始化关键值, _left(nullptr) // 左子树初始为空, _right(nullptr) // 右子树初始为空{}
};// 二叉搜索树模板类
template<class K>
class BSTree {typedef BSTNode<K> Node; // 类型别名,方便使用//using Node = BSTNode<K> 也可以使用using平替typedef进行类型重定义
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) { // 如果当前节点值小于key,向右子树查找parent = cur;cur = cur->_right;}else if (cur->_key > 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) { // 当前值小于key,向右子树查找cur = cur->_right;}else if (cur->_key > 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) { // 当前值小于key,向右子树查找parent = cur;cur = cur->_right;}else if (cur->_key > key) { // 当前值大于key,向左子树查找parent = cur;cur = cur->_left;}else { // 找到要删除的节点// 情况1和2:节点有0或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;}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 { // 情况3:节点有2个子节点(替换法删除)// 找到右子树的最小节点(中序后继)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; // 根节点指针
};
7. 二叉搜索树key
和key/value
使用场景
-
key
搜索场景只有key作为关键码:结构中只需要存储key即可,关键码即为需要搜索到的值,搜索场景只需要判断key在不在。key的搜索场景实现的二叉搜索树支持增删查,但不支持修改,因为修改key会破坏搜索树结构。
应用场景示例:
- 小区无人值守车库:小区车库仅允许购买了车位的业主车辆进入,物业会将已购车位业主的车牌号录入后台系统。车辆进入时扫描车牌,若在系统中则抬杆放行,否则提示“非本小区车辆,无法进入”。
- 英文拼写检查:将词库中所有单词存入二叉搜索树,读取文章中的单词并查找是否在树中。若不存在,则用波浪线标红提示拼写错误。
-
key/value
搜索场景在二叉搜索树的键值对(key/value)存储结构中,每个关键码(key)都对应一个任意类型的值(value)。树的结点除了存储key外还需存储对应的value,增删查操作均以key为关键字按照二叉搜索树的规则进行比较,从而快速定位key对应的value。这种结构支持修改value但不允许修改key,因为修改key会破坏搜索树的性质。
应用场景示例:
- 中英字典:结点存储key(英文)和value(中文),输入英文即可检索对应中文;
- 无人车库管理:入场时记录车牌(key)和入场时间(value),出场时通过车牌查询入场时间,计算停车时长和费用;
- 单词统计:读取单词时,若未存在则插入(单词,1),若已存在则递增其计数(value)。
-
key/value
二叉搜索树代码实现// 二叉搜索树(BST)节点模板类 // K 表示键(key)类型,V 表示值(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){} };// 二叉搜索树(BST)模板类 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 {// 找到要删除的节点,分三种情况处理// 情况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;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 << 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; // 树的根节点 };// 主函数1:字典应用示例 int main() {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;}}return 0; }// 主函数2:统计水果出现次数示例 int main() {string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };BSTree<string, int> countTree;for (const auto& str : arr) {auto ret = countTree.Find(str);if (ret == NULL) {countTree.Insert(str, 1); // 第一次出现的水果}else {ret->_value++; // 已存在的水果,计数增加}}countTree.InOrder(); // 输出统计结果return 0; }