初学二叉搜索树踩坑多?C++ 从原理到代码,搞定增删查全流程

初学二叉搜索树踩坑多?C++ 从原理到代码,搞定增删查全流程

个人主页



🎬 个人主页Vect个人主页

🎬 GitHubVect的代码仓库
🔥 个人专栏: 《数据结构与算法》《C++学习之旅》《计算机基础

⛺️Per aspera ad astra.



文章目录

1. 二叉搜索树相关概念

如下图所示,二叉搜索树(binary search tree)满足下列条件:

  1. 对于根节点,左子树中所有节点的值<根节点的值<右子树中所有节点的值
  2. 任意节点的左、右字数也是二叉搜索树,同样满足条件1
在这里插入图片描述

而下图就不是二叉搜索树:

在这里插入图片描述

2. 二叉搜索树的操作

我们将二叉搜索树封装为一个类BSTree,并声明一个_root变量,指向树的根节点

// 节点类template<classK>structBSTNode{ BSTNode<K>* _left; BSTNode<K>* _right; K _key;// 构造BSTNode(const K& val):_left(nullptr),_right(nullptr),_key(val){}};// 二叉搜索树类template<classK>classBSTree{public:typedef BSTNode<K> Node;private: Node* _root;};

2.1. 查找节点

给定目标val,可以声明一个节点cur,从二叉搜索树的根节点_root出发,循环比较cur._keyval之间的大小关系:

  • val < cur._key,说明目标节点在cur的左子树,执行cur = cur._left
  • val > cur._key,说明目标节点在cur的右子树,执行cur = cur._right
  • val == cur._key,说明找到目标节点,跳出循环并返回该节点

与二分查找原理一致,每轮排除一半的情况,当二叉树平衡时,循环次数最多为二叉树的高度, O ( l o g n ) O(logn) O(logn)

// 查找节点  Node*find(const K& val){ Node* cur = _root;while(cur){if(val < cur->_key) cur = cur->_left;elseif(val > cur->_key) cur = cur->_right;elsebreak;}return cur;}

2.2. 插入节点

给定一个待插入元素val,为了保持左子树<根节点<右子树的性质,插入流程如下:

  1. 查找插入位置: 与查找操作相同逻辑,从根节点出发,根据当前节点值和val的大小关系循环向下搜索,直到越过叶子节点(遍历到None)跳出循环
  2. 在该位置插入节点:new一个新节点,将新节点置于None的位置

注意:

  • 二叉搜索树不允许存在重复节点,否则将违反其定义。因此,若待插入节点在树中已存在,则不执行插入,直接返回。
  • 为了实现插入节点,我们需要借助节点pre 保存上一轮循环的节点。这样在遍历至 None时,我们可以获取到其父节点,从而完成节点插入操作。
boolinsert(const K& val){// 空树 直接插入if(_root ==nullptr){ Node* newNode =newNode(val); _root = newNode;returntrue;}// 非空树 先找待插入节点的位置 Node* cur = _root; Node* curPre =nullptr;// 保存cur的父节点 while(cur){if(val == cur->_key)returnfalse;// 重复值 不插入elseif(val < cur->_key){// 小于键值 往左走 同时更新cur的父节点 curPre = cur; cur = cur->_left;}else{// 大于键值 往右走 同时更新cur的父节点 curPre = cur; cur = cur->_right;}}// cur的位置是待插入位置 Node* newNode =newNode(val);// 判断cur在curPre的左边还是右边 确定新节点位置if(curPre->_left == cur) curPre->_left = newNode;else curPre->_right = newNode;returntrue;}

2.3. 删除节点

先在二叉树中查找到目标节点,再将其删除。与插入节点一样,我们需要保证在删除操作完成后,保证二叉搜索树的“左子树 < 根节点 < 右子树”的性质不变

根据待删除节点的孩子数量,分0、1和2三种情况

  • 待删除结点是叶子节点,直接删除
在这里插入图片描述
  • 待删除节点有一个孩子,将待删除节点的子节点和其替换即可
在这里插入图片描述
  • 待删除节点2个孩子,不能直接删除,需要用一个节点替换该节点,保持“左子树 < 根节点 < 右子树”的性质不变,可以选择右子树最小节点或左子树最大节点

假设我们选择右子树最小节点,如下流程:

  1. 找到待删除节点cur的右子树最小节点rightMin
  2. rightMin的值覆盖cur,并删除rightMin
在这里插入图片描述

总共有如下几种情况:

cur 有两个孩子;rightMin 在更下方;rightMin 只有右孩子(B1)触发:cur->_right有左孩子,沿左链找到 rightMin,且 rightMin->_right != nullptr操作:cur->_key = rightMin->_key;令 rightMinPre->_left = rightMin->_right;删除 rightMin。示意:

cur(•) \ ... / rightMin \ X 

cur 有两个孩子;rightMin 在更下方;rightMin 无右孩子(B0)触发:cur->_right有左孩子,沿左链找到 rightMin,且 rightMin->_right == nullptr操作:cur->_key = rightMin->_key;令 rightMinPre->_left = nullptr;删除 rightMin。示意:

cur(•) \ ... / rightMin 

cur 有两个孩子;rightMin 就是 cur->_rightrightMin 只有右孩子(A1)触发:cur->_right没有左孩子,且 cur->_right->_right != nullptr操作:cur->_key = rightMin->_key;令 cur->_right = rightMin->_right;删除 rightMin。示意:

cur(•) / \ (...) rightMin \ X 

cur 有两个孩子;右子树最小节点 rightMin 就是 cur->_rightrightMin 无右孩子(A0)触发:cur->_right没有左孩子,且 cur->_right->_right == nullptr操作:cur->_key = rightMin->_key;令 cur->_right = nullptr;删除 rightMin。示意:

cur(•) / \ (...) rightMin 
boolerase(const K& val){// 空树无法删除if(_root ==nullptr)returnfalse;// 查找待删除节点 Node* cur = _root; Node* curPre =nullptr;while(cur){if(val < cur->_key){// 向左找 curPre = cur; cur = cur->_left;}elseif(val > cur->_key){// 向右找 curPre = cur; cur = cur->_right;}elsebreak;// 命中}// cur为空 无法删除if(cur ==nullptr)returnfalse;// 找到cur了// 孩子0/1个的情况if(cur->_left ==nullptr|| cur->_right ==nullptr){// 找到待删除节点的孩子 Node* curNext = cur->_left ==nullptr? cur->_right : cur->_left;if(cur != _root){// 删除的不是根节点if(curPre->_left == cur) curPre->_left = curNext;else curPre->_right = curNext;}else{// 删除的是根节点 _root = curNext;}delete cur;}else{// 2个孩子// 1. 找右子树最小节点 Node* rightMin = cur->_right; Node* rightMinPre = cur;while(rightMin->_left){ rightMinPre = rightMin; rightMin = rightMin->_left;}// 2. 用rightMin的值覆盖待删除节点的值 cur->_key = rightMin->_key;// 3. 删除rightMin 要处理rightMin的孩子 Node* rightMinNext = rightMin->_right;if(rightMinPre->_left == rightMin) rightMinPre->_left = rightMinNext;else rightMinPre->_right = rightMinNext;// 右子树最小节点是cur->_right的情况delete rightMin;}returntrue;}

再次总结:

查找阶段

  • cur 定位待删,curPre 记录父。
  • 终止条件:命中或走到空。
  • 注意:若支持自定义比较器,等价判断应为“!(val<key) && !(key<val)”。

0/1 个孩子:真正移除 cur 节点

  • curNext = (cur->_left ? cur->_left : cur->_right)
  • cur != _root:把 curPre 的相应孩子指向 curNext
  • cur == _root_root = curNext
  • delete cur
  • 要点:这是必须对“根”特判的地方(根没有父指针可改)。

2 个孩子:值覆盖 + 删后继(不移动 cur 本体)

  • 覆盖键值:cur->_key = rightMin->_key;(若有 value,一并拷/搬移)。
  • 要点:这里不需要根特判,因为根节点对象没被删除,只是“值被改写”。

删除 rightMin

Node* rightMinNext = rightMin->_right; // 只可能有右孩子或空 if (rightMinPre->_left == rightMin) rightMinPre->_left = rightMinNext; else rightMinPre->_right = rightMinNext; // 当 rightMin 就是 cur->_right delete rightMin; 

找右子树最小:

Node* rightMin = cur->_right; Node* rightMinPre = cur; while (rightMin->_left) { rightMinPre = rightMin; rightMin = rightMin->_left; } 

3. 二叉搜索树的实现

#pragmaonce#include<iostream>#include<vector>usingnamespace std;namespace BSTKey {template<classK>structBSTNode{ BSTNode<K>* _left; BSTNode<K>* _right; K _key;// 构造BSTNode(const K& val):_left(nullptr),_right(nullptr),_key(val){}};template<classK>classBSTree{public:typedef BSTNode<K> Node;BSTree()=default;// 析构 需要辅助析构函数~BSTree(){destroy(_root); _root =nullptr;}// 查找节点  Node*find(const K& val){ Node* cur = _root;while(cur){if(val < cur->_key) cur = cur->_left;elseif(val > cur->_key) cur = cur->_right;elsebreak;}return cur;}boolinsert(const K& val){// 空树 直接插入if(_root ==nullptr){ Node* newNode =newNode(val); _root = newNode;returntrue;}// 非空树 先找待插入节点的位置 Node* cur = _root; Node* curPre =nullptr;// 保存cur的父节点 while(cur){if(val == cur->_key)returnfalse;// 重复值 不插入elseif(val < cur->_key){// 小于键值 往左走 同时更新cur的父节点 curPre = cur; cur = cur->_left;}else{// 大于键值 往右走 同时更新cur的父节点 curPre = cur; cur = cur->_right;}}// cur的位置是待插入位置 Node* newNode =newNode(val);// 判断cur在curPre的左边还是右边 确定新节点位置if(curPre->_left == cur) curPre->_left = newNode;else curPre->_right = newNode;returntrue;}boolerase(const K& val){// 空树无法删除if(_root ==nullptr)returnfalse;// 查找待删除节点 Node* cur = _root; Node* curPre =nullptr;while(cur){if(val < cur->_key){// 向左找 curPre = cur; cur = cur->_left;}elseif(val > cur->_key){// 向右找 curPre = cur; cur = cur->_right;}elsebreak;// 命中}// cur为空 无法删除if(cur ==nullptr)returnfalse;// 找到cur了// 孩子0/1个的情况if(cur->_left ==nullptr|| cur->_right ==nullptr){// 找到待删除节点的孩子 Node* curNext = cur->_left ==nullptr? cur->_right : cur->_left;if(cur != _root){// 删除的不是根节点if(curPre->_left == cur) curPre->_left = curNext;else curPre->_right = curNext;}else{// 删除的是根节点 _root = curNext;}delete cur;}else{// 2个孩子// 1. 找右子树最小节点 Node* rightMin = cur->_right; Node* rightMinPre = cur;while(rightMin->_left){ rightMinPre = rightMin; rightMin = rightMin->_left;}// 2. 用rightMin的值覆盖待删除节点的值 cur->_key = rightMin->_key;// 3. 删除rightMin 要处理rightMin的孩子 Node* rightMinNext = rightMin->_right;if(rightMinPre->_left == rightMin) rightMinPre->_left = rightMinNext;else rightMinPre->_right = rightMinNext;// 右子树最小节点是cur->_right的情况delete rightMin;}returntrue;}voidinOrder(){_inOrder(_root); cout << endl;}private: Node* _root;// 后续析构voiddestroy(Node* root){if(root ==nullptr)return;destroy(root->_left);destroy(root->_right);delete root;}void_inOrder(Node* root){if(root ==nullptr)return;_inOrder(root->_left); cout << root->_key <<" ";_inOrder(root->_right);}};voidtestBST(){ BSTree<int> bst; vector<int> arr ={8,3,10,1,6,14,5,7,13}; cout <<"=== 1. 创建二叉搜索树 ===\n"; cout <<"初始数据:-> ";for(constauto& e : arr) cout << e <<" "; cout <<"\n创建完成数据:-> ";for(constauto& e : arr) bst.insert(e); bst.inOrder(); cout <<"=== 2. 查找数据 ===\n"; cout <<"查找不存在数据:28 查找结果:"<< bst.find(28)<< endl; cout <<"查找存在数据:14 查找结果:"<< bst.find(14)<< endl; cout <<"\n=== 3. 删除数据 ===\n"; cout <<"删除前的序列:->"; bst.inOrder(); cout <<"删除节点6后的序列:->"; bst.erase(6); bst.inOrder(); cout <<"删除不存在的节点86后的序列:->"; bst.erase(86); bst.inOrder(); cout <<"\n销毁二叉树:->"; bst.~BSTree(); bst.inOrder();}}

4. 二叉搜索树的应用

4.1. K模型

只有key作为关键码(需要搜索到的值),结构中只存储key即可

例如:给一个单词word,判断该单词是否正确

  • 以词库中所有单词集合中的每个单词作为key,构建一颗二叉搜索树
  • 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误

又比如门禁系统,只关注对象在不在

第三节实现的就是K模型

4.2. KV模型

每个关键码key,都有与之对应的值value,即<key,value>的键值对,例如:

  • 简单英汉词典中英文和中文是相互对应的关系,通过英文可快速找到其对应的中文,例如:<book,书>就是一种键值对
  • 车库收费系统,一个键值记录车牌号,另一个键值记录停留时间,随后交给收费系统处理缴费事宜
  • 统计单词出现次数,统计成功之后,给定单词就可快速找到其出现的次数,单词与其出现的次数就是<word,count>一种键值对

我们将K模型改造成KV模型

template<classK,classV>structBSTNode{ K _key; V _value; BSTNode<K, V>* _left; BSTNode<K, V>* _right;BSTNode(const K& k,const V& v):_key(k),_value(v),_left(nullptr),_right(nullptr){}};template<classK,classV>classBSTree{public:typedef BSTNode<K, V> Node;// ... 接口实现private: Node* _root =nullptr;// ...辅助接口实现}};
// 输入中文 输出对应英文voiddictionary(){ BSTree<string, string> dict; dict.insert("书包","bag"); dict.insert("水果","fruit"); dict.insert("钢笔","pen"); dict.insert("书","book"); dict.insert("树","tree");// 插入词库中所有单词 string str;while(cin >> str){auto ret = dict.find(str);if(ret ==nullptr) cout <<"输入错误或词库中未包含该词"<< str << endl;else cout << str <<"英文单词:->"<< ret->_value << endl;}}// 计数voidcount(){ string arr[]={"苹果","西瓜","苹果","西瓜","苹果","苹果","西瓜","苹果","香蕉","苹果","香蕉"}; BSTree<string,int> cntTree;for(constauto& s : arr){// 先查找水果在不在搜索树中// 1. 不在,说明水果第一次出现,则插入<水果, 1>// 2. 在,则查找到的节点中水果对应的次数++auto ret = cntTree.find(s);if(ret ==nullptr) cntTree.insert(s,1);else ret->_value++;} cntTree.inOrder();}

完整代码请详见此链接:二叉搜索树
二叉搜索树的核心价值,在于用 “左子树 < 根节点 < 右子树” 的简单规则,换来了查找、插入、删除操作的高效性 —— 平衡状态下$ O (logn)$ 的时间复杂度,让它成为处理动态数据查找场景的基础选择。无论是仅需判断 “存在性” 的 K 模型,还是需键值关联的 KV 模型,都能通过其核心操作快速适配,覆盖词典查询、计数统计等常见需求。

但需注意,二叉搜索树的性能依赖结构平衡:若插入数据有序,会退化为单链表,此时操作复杂度飙升至 O ( n ) O(n) O(n)。这也催生了 AVL 树、红黑树等平衡二叉搜索树 —— 它们通过自平衡机制维持树的高度,兼顾了二叉搜索树的简洁性与稳定高效的性能,是后续深入学习的重要方向。

Read more

在 macOS 下升级 Python 几种常见的方法

在 macOS 下升级 Python 几种常见的方法

在 macOS 下升级 Python 有几种常见的方法,具体取决于你最初是如何安装 Python 的。了解你的安装方式是关键。 首先,你需要知道你当前 Python 版本以及它的安装路径。 1. 检查 Python 版本: python --version# 可能指向 Python 2.x python3 --version# 通常指向 Python 3.x 2. 检查 Python 路径: which python which python3 根据你 which 命令的输出,我们可以推断出安装方式。常见的安装方式有: * macOS 系统自带 Python: 通常在 /usr/bin/python。不建议直接修改或升级系统自带的

By Ne0inhk
Python智慧农业信息化服务平台农产品商城系统 小程序

Python智慧农业信息化服务平台农产品商城系统 小程序

文章目录 * 技术架构设计 * 核心功能模块 * 物联网数据整合 * 性能优化策略 * 安全防护措施 * 部署与监控 * 系统设计与实现的思路 * 主要技术与实现手段 * 源码lw获取/同行可拿货,招校园代理 :文章底部获取博主联系方式! 技术架构设计 * 前端框架:采用微信小程序原生框架+WXML/WXSS,结合Vant Weapp组件库快速搭建UI界面。 * 后端服务:基于Django REST Framework构建API,支持JWT身份认证与RBAC权限控制。 * 数据库:MySQL存储业务数据,Redis缓存高频访问数据(如商品详情、用户会话)。 * 消息队列:使用RabbitMQ处理异步任务(如订单状态更新、消息推送)。 核心功能模块 * 用户系统:OpenID自动注册/登录,农户与消费者角色分离,个人中心集成实名认证模块。 * 商品管理:支持多级分类、动态SKU、溯源信息(区块链哈希值存储)。 * 订单系统:微信支付接口对接,物流状态实时同步(调用快递鸟API)。 智能推荐:

By Ne0inhk
【 C/C++ 算法】入门动态规划-----一维动态规划基础(以练代学式)

【 C/C++ 算法】入门动态规划-----一维动态规划基础(以练代学式)

每日激励:“不设限和自我肯定的心态:I can do all things。 — Stephen Curry” 绪论 : 本章是动态规划算法的基础入门篇,我将通过三道简单题 + 一道中等难度的一维动态规划题来带你对动态规划有个初认识,并基本了解动态规划的最基本常见的写法,只有将基本写法了解了,对后续的难的题目自然也不会毫无头绪,后续还将持续更新更多相关的动规算法,敬请期待~🙃 ———————— 早关注不迷路,话不多说安全带系好,发车啦(建议电脑观看)。 👻动态规划🌥️ 这里通过大量练习得出下面动态规划做题步骤 简单的说动态规划理解成:某种状态的公式 + 提前求出来值的容器 求出当前位置的值然后放到容器中后后续使用 因为最开始的值一般是会看见的所以就能有初始值,从而启动动态规划 从上中可以主要提炼出: * 状态 * 容器的重要性 * 公式,可以换种说法:状态转移方程 这样严格😈的说:动态规划 = 状态定义 + 状态转移方程 + 初始条件 + 状态存储(容器) 下述步骤是通过写完下述四道题后的总结,所以同样需要道友🗡️大量的练习沉淀最终就能对动态规划的题目

By Ne0inhk
Python从0到100(九十八):融合选择性卷积与残差结构的SKResNet架构详解

Python从0到100(九十八):融合选择性卷积与残差结构的SKResNet架构详解

前言:零基础学Python:Python从0到100最新最全教程。 想做这件事情很久了,这次我更新了自己所写过的所有博客,汇集成了Python从0到100,共一百节课,帮助大家一个月时间里从零基础到学习Python基础语法、Python爬虫、Web开发、 计算机视觉、机器学习、神经网络以及人工智能相关知识,成为学业升学和工作就业的先行者! 【优惠信息】 • 新专栏订阅前500名享9.9元优惠 • 订阅量破500后价格上涨至19.9元 • 订阅本专栏可免费加入粉丝福利群,享受: - 所有问题解答 -专属福利领取 欢迎大家订阅专栏:零基础学Python:Python从0到100最新最全教程! 本文目录: * 一、SKResNet的理论基础与创新点 * 1. 传统卷积神经网络的局限性 * 2. SKResNet的核心创新 * 3. 技术优势分析 * 二、SKResNet架构设计详解 * 1. 整体架构概览 * 2. SKBlock:选择核模块详解 * 2.1 多尺度卷积核设计 * 2.2 注意力机制实现 * 2.

By Ne0inhk