C++ 二叉搜索树(BST)完全指南:从概念原理、核心操作到底层实现

C++ 二叉搜索树(BST)完全指南:从概念原理、核心操作到底层实现
在这里插入图片描述

🔥草莓熊Lotso:个人主页
❄️个人专栏: 《C++知识分享》《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!


🎬 博主简介:

在这里插入图片描述

文章目录


前言:

在数据结构中, 二叉搜索树(Binary Search Tree,简称 BST) 是一种兼具 “有序性” 与 “高效操作” 的树形结构,它通过特定的节点值规则,让增删查操作的时间复杂度在理想情况下达到O(log₂N)但是平均来看还是O(N)。本文将从二叉搜索树的核心概念入手,结合实战代码,逐步拆解其插入、查找、删除三大核心操作,同时分析性能特点与实际应用场景,帮你彻底掌握这一基础数据结构。

一. 二叉搜索树的核心概念:什么是 BST?

二叉搜索树又称二叉排序树,它要么是空树,要么是满足以下值分布规则的二叉树:

  • 若左子树不为空,则左子树中所有节点的值 ≤ 根节点的值;
  • 若右子树不为空,则右子树中所有节点的值 ≥ 根节点的值;
  • 左、右子树也分别是二叉搜索树(递归定义)。
  • 二叉搜索树可以支持插入相等的值,也可以不支持插入相等的值,具体看使用场景定义,后续我们学习map/set/multimap/multiset系列容器底层就是二叉搜索树,其中map/set不支持插入相等值,multimap/multiset支持插入相等值

关键特性中序遍历为有序序列
二叉搜索树的核心价值在于 “中序遍历结果是升序序列”。例如,下图 BST 的中序遍历结果为1 3 4 6 7 8 10 13 14,天然具备 “排序” 属性,这也是其 “二叉排序树” 名称的由来。

在这里插入图片描述


关于 “相等值” 的约定
BST 对相等值的处理可灵活定义,具体取决于场景:
不支持相等值插入(如map/set底层):插入时若值已存在,直接返回失败;
支持相等值插入(如multimap/multiset底层):相等值需统一插入左子树或右子树(保持逻辑一致,避免后续查找混乱)。
本文实现的 BST 默认不支持相等值插入。


二. 二叉搜索树的性能分析:理想与最差情况

BST 的操作效率直接取决于树的 “高度”,而高度由节点插入顺序决定,存在两种极端情况:

场景树的形态高度增删查时间复杂度典型插入顺序核心影响因素
理想情况完全二叉树(接近平衡) log ⁡ 2 N \log_2 N log2N O ( log ⁡ 2 N ) O(\log_2 N) O(log2N)随机插入(如8,3,10,1,6)插入顺序无序,节点均匀分布在左右子树
最差情况单支树(退化为链表) N N N O ( N ) O(N) O(N)有序插入(如1,3,6,8,10)插入顺序严格递增/递减,节点仅向单侧延伸

综合来看二叉搜索树增删查改时间复杂度

在这里插入图片描述


与 “二分查找” 的对比
二分查找虽也能实现O(log₂N)的查找效率,但存在明显缺陷:

  • 依赖支持随机访问的结构(如数组),且需提前排序;
  • 插入 / 删除效率低:数组中插入 / 删除元素需挪动大量数据,时间复杂度为O(N)

而 BST 无需提前排序,且插入 / 删除时仅需修改节点指针,避免了数据挪动,这也是其在动态数据场景中更具优势的原因。


三. 二叉搜索树的实战实现:基于 BinarySearchTree.h

采用 C++ 模板实现,支持泛型K(,核心包含节点结构定义BST 类的三大操作(插入、查找、删除),同时提供中序遍历接口验证有序性。

3.1 节点结构定义:BSTreeNode

BST 的节点需存储 “值” 与 “左右子树指针”,模板化设计使其可适配 int、string 等多种类型:

#include<iostream>usingnamespace std;template<classK>structBSTreeNode{ BSTreeNode<K>* _left;// 左子树指针 BSTreeNode<K>* _right;// 右子树指针 K _key;// 节点键值// 构造函数:初始化指针为空,键值为传入值BSTreeNode(const K& key):_left(nullptr),_right(nullptr),_key(key){}};

3.2 BST 类核心操作:Insert、Find、Erase

BST 类封装了树的根节点_root,并通过私有辅助函数_InOrder实现中序遍历。以下是三大核心操作的详细实现与解析:

3.2.1 插入操作(Insert)

插入的核心逻辑是 “按 BST 规则找到空位置,创建新节点并链接”,步骤如下:

  1. 若树为空(_root == nullptr),直接创建根节点;
  2. 树非空时,用cur指针遍历树:
    • cur->_key < 插入值 :向右子树移动(cur = cur->_right);
    • cur->_key > 插入值:向左子树移动(cur = cur->_left);
    • 若值相等(不支持插入),返回false

找到空位置后,通过parent指针(记录cur的父节点)将新节点链接到树中。

代码实现(BinarySearchTree.h)

template<classK>classBSTree{typedef BSTreeNode<K> Node;public:boolInsert(const K& key){// 情况1:树为空,直接创建根节点if(_root ==nullptr){ _root =newNode(key);returntrue;}// 情况2:树非空,遍历找插入位置 Node* parent =nullptr;// 记录cur的父节点(用于后续链接新节点) Node* cur = _root;while(cur){if(cur->_key < key){ parent = cur; cur = cur->_right;// 比当前节点大,向右走}elseif(cur->_key > key){ parent = cur; cur = cur->_left;// 比当前节点小,向左走}else{// 键值已存在,不支持插入,返回falsereturnfalse;}}// 创建新节点,并链接到parent的左/右孩子 cur =newNode(key);if(parent->_key < key){ parent->_right = cur;// 插入值比parent大,作为右孩子}else{ parent->_left = cur;// 插入值比parent小,作为左孩子}returntrue;}// 中序遍历:验证BST的有序性voidInOrder(){_InOrder(_root); cout << endl;}private:void_InOrder(Node* root){if(root ==nullptr)return;_InOrder(root->_left);// 遍历左子树 cout << root->_key <<" ";// 访问当前节点_InOrder(root->_right);// 遍历右子树} Node* _root =nullptr;// 树的根节点,初始为空};
在这里插入图片描述

3.2.2 查找操作(Find)

查找的逻辑与插入类似,按 BST 规则遍历树,步骤如下:

  1. 从根节点_root开始,用cur指针遍历;
  2. cur->_key < 目标值:向右走;若 cur->_key > 目标值:向左走;
  3. 找到目标值返回true,遍历到空节点(未找到)返回false

代码实现(BinarySearchTree.h)

boolFind(const K& key){ Node* cur = _root;while(cur){if(cur->_key < key){ cur = cur->_right;// 目标值大,向右找}elseif(cur->_key > key){ cur = cur->_left;// 目标值小,向左找}else{// 找到目标值,返回truereturntrue;}}// 遍历到空,未找到returnfalse;}

如果支持插入相等的值,意味着有多个x存在,一般要求查找中序的第⼀个x。如下图,查找3,要找到1的右孩子的那个3返回

在这里插入图片描述

3.2.3 删除操作(Erase):最复杂的核心操作

删除的难点在于 “删除节点后,需保持 BST 的规则不变”。根据删除节点(记为cur)的子节点数量,分为 4 种情况,其中前 3 种可合并处理,第 4 种需用 “替换法” 删除:

情况子节点状态处理方案关键注意事项
1左右子树均为空(叶子节点)直接删除cur节点,将parent指向cur的孩子指针置空(可归为情况2或3统一处理)需判断cur是否为根节点(若为根,直接将_root置空,无需处理parent
2左子树为空,右子树非空parent指向cur的孩子指针,修改为指向cur->_right,随后删除cur节点cur是根节点,直接让_root = cur->_right,跳过parent判断
3右子树为空,左子树非空parent指向cur的孩子指针,修改为指向cur->_left,随后删除cur节点与情况2对称,根节点处理逻辑为_root = cur->_left
4左右子树均非空1. 找cur右子树的“最小节点”(最左节点)或左子树的“最大节点”(最右节点);
2. 将替换节点的键值(及值,若为key-value模型)赋给cur
3. 删除替换节点(替换节点满足情况2或3,直接处理)
替换节点的父节点指针需正确修改(如替换节点是父节点左孩子,需将父节点左指针指向替换节点的右子树)

代码实现(BinarySearchTree.h)

boolErase(const K& key){ Node* parent =nullptr; Node* cur = _root;// 第一步:找到要删除的节点curwhile(cur){if(cur->_key < key){ parent = cur; cur = cur->_right;}elseif(cur->_key > key){ parent = cur; cur = cur->_left;}else{// 第二步:找到节点,按子节点情况处理删除// 情况2:左子树为空,右子树非空if(cur->_left ==nullptr){// 若cur是根节点,直接让根指向右子树if(cur == _root){ _root = cur->_right;}else{// 判断cur是parent的左/右孩子,链接对应子树if(cur == parent->_left) parent->_left = cur->_right;else parent->_right = cur->_right;}delete cur;// 释放节点内存returntrue;}// 情况3:右子树为空,左子树非空elseif(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;returntrue;}// 情况4:左右子树均非空(替换法删除)else{// 找cur右子树的最小节点(最左节点)作为替换节点// 还可以找左子树的最大节点(最右节点)// 这里是找右子树最左节点 Node* replaceParent = cur;// 替换节点的父节点 Node* replace = cur->_right;while(replace->_left)// 一直向左走,直到左子树为空{ replaceParent = replace; replace = replace->_left;}// 替换:将replace的键值赋给cur(值替换,指针不变) cur->_key = replace->_key;// 删除replace节点(replace的左子树为空,符合情况2)if(replaceParent->_left == replace) replaceParent->_left = replace->_right;else replaceParent->_right = replace->_right;delete replace;returntrue;}}}// 未找到要删除的节点returnfalse;}

关键说明:情况 4 中选择 “右子树最小节点” 作为替换节点,是因为该节点的值是cur右子树中最小的,替换后仍满足 BST 规则(左子树≤根≤右子树);同理,选择 “左子树最大节点” 也可,逻辑对称

在这里插入图片描述

四. 实战测试:基于实战代码验证 BST 操作

test.cpp通过引入BinarySearchTree.h,实现 BST 的插入、删除与中序遍历验证,代码如下:

#include"BinarySearchTree.h"intmain(){// 测试数据:插入序列int a[]={8,3,1,10,6,4,7,14,13}; BSTree<int> t;// 1. 插入所有元素for(auto& e : a){ t.Insert(e);} cout <<"插入后中序遍历(应有序):"; t.InOrder();// 输出:1 3 4 6 7 8 10 13 14// 2. 删除测试:逐步删除节点,验证有序性 t.Erase(3);// 删除左子树非空、右子树非空的节点(情况4) cout <<"删除3后中序遍历:"; t.InOrder();// 输出:1 4 6 7 8 10 13 14 t.Erase(8);// 删除根节点(左右子树非空,情况4) cout <<"删除8后中序遍历:"; t.InOrder();// 输出:1 4 6 7 10 13 14 t.Erase(1);// 删除叶子节点(左右子树为空,情况1) cout <<"删除1后中序遍历:"; t.InOrder();// 输出:4 6 7 10 13 14 t.Erase(10);// 删除右子树非空、左子树非空的节点(情况4) cout <<"删除10后中序遍历:"; t.InOrder();// 输出:4 6 7 13 14// 3. 清空树(删除所有元素)for(auto& e : a){ t.Erase(e);} cout <<"清空后中序遍历(空行):"; t.InOrder();// 输出空行return0;}

结果符合预期,证明 BST 的插入、删除操作均保持了 “中序遍历有序” 的核心特性。

在这里插入图片描述

五. BST 的扩展:key/value 模型(支持映射场景)

上述实现是 “key 模型”(仅存储键值,用于判断 “存在性”,不能修改),但实际场景中常需 “key-value 模型”(键值对应数据,如字典、统计次数,可以修改value)。BinarySearchTree.h可扩展为模板template<class K, class V>,节点同时存储_key_value

在这里插入图片描述

5.1 key-value 模型节点与类实现

// key-value模型节点template<classK,classV>structBSTreeNode{ K _key; V _value; BSTreeNode<K, V>* _left; BSTreeNode<K, V>* _right;BSTreeNode(const K& key,const V& value):_key(key),_value(value),_left(nullptr),_right(nullptr){}};// key-value模型BST类template<classK,classV>classBSTree{typedef BSTreeNode<K, V> Node;public:// 插入:需传入key和valueboolInsert(const K& key,const V& value){if(_root ==nullptr){ _root =newNode(key, value);returntrue;} Node* parent =nullptr; Node* cur = _root;while(cur){if(cur->_key < key){ parent = cur; cur = cur->_right;}elseif(cur->_key > key){ parent = cur; cur = cur->_left;}else{returnfalse;// 不支持重复key}} cur =newNode(key, value);if(parent->_key < key) parent->_right = cur;else parent->_left = cur;returntrue;}// 查找:返回节点指针,可通过节点访问value Node*Find(const K& key){ Node* cur = _root;while(cur){if(cur->_key < key) cur = cur->_right;elseif(cur->_key > key) cur = cur->_left;elsereturn cur;// 返回节点,后续可操作value}returnnullptr;}//erase跟上面没区别,这里就不展示了// 中序遍历:输出key和valuevoidInOrder(){_InOrder(_root); cout << endl;}private:void_InOrder(Node* root){if(root ==nullptr)return;_InOrder(root->_left); cout << root->_key <<":"<< root->_value <<" ";_InOrder(root->_right);} Node* _root =nullptr;};

5.2 key-value 模型实战场景

场景 1:简单字典(中英互译)

intmain(){//简单字典 key_value::BSTree<string, string> dict; dict.Insert("sort","排序"); dict.Insert("string","字符串"); dict.Insert("insert","插入"); dict.Insert("erase","删除"); dict.Insert("move","移动"); dict.Insert("tree","树"); dict.Insert("tree","树*****");//插入失败,可以看看插入的逻辑,主要是key判断// 内置类型转换成类类型 -> 构造函数// 类类型转换成内置类型 -> operator 内置类型 string str;int i =0;//while ((cin >> str).operator bool())while(cin>>str){auto* node = dict.Find(str);if(node){ cout <<"->"<< node->_value << endl;}else{ cout <<"无此单词,请重新输入"<< endl;}}return0;}
在这里插入图片描述

场景 2:单词统计(统计水果出现次数)

intmain(){ string arr[]={"苹果","西瓜","苹果","西瓜","苹果","苹果","西瓜","苹果","香蕉","苹果","香蕉"}; key_value::BSTree<string,int> CountTree;for(auto& str : arr){//BSTreeNode<string, int>* ret = countTree.Find(str);auto ret = CountTree.Find(str);// 第一次出现,插入<水果, 1>if(ret ==nullptr){ CountTree.Insert(str,1);}else{// 已出现,次数+1 ret->_value++;}}// 中序遍历:按水果名称升序输出次数 CountTree.InOrder();return0;}
在这里插入图片描述

结尾:

🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点: 👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长 ❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量 ⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用 💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑 🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解 技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标! 

结语:二叉搜索树(BST)以 “左小右大” 的规则,实现了动态数据的有序管理,中序遍历的有序性与指针操作的灵活性是其核心优势。插入、查找逻辑直观,删除操作的场景化处理(尤其是替换法)则是掌握关键,但其性能受插入顺序影响大,单支树退化问题也为后续平衡树(AVL、红黑树)的学习埋下伏笔。作为基础树形结构,BST 是理解复杂数据结构设计逻辑的重要基石。

✨把这些内容吃透超牛的!放松下吧✨ʕ˘ᴥ˘ʔづきらど

Read more

安装 启动 使用 Neo4j的超详细教程

安装 启动 使用 Neo4j的超详细教程

最近在做一个基于知识图谱的智能生成项目。需要用到Neo4j图数据库。写这篇文章记录一下Neo4j的安装及其使用。 一.Neo4j的安装 1.首先安装JDK,配环境变量。(参照网上教程,很多) Neo4j是基于Java的图形数据库,运行Neo4j需要启动JVM进程,因此必须安装JAVA SE的JDK。从Oracle官方网站下载 Java SE JDK。我使用的版本是JDK1.8 2.官网上安装neo4j。 官方网址:https://neo4j.com/deployment-center/  在官网上下载对应版本。Neo4j应用程序有如下主要的目录结构: bin目录:用于存储Neo4j的可执行程序; conf目录:用于控制Neo4j启动的配置文件; data目录:用于存储核心数据库文件; plugins目录:用于存储Neo4j的插件; 3.配置环境变量 创建主目录环境变量NEO4J_HOME,并把主目录设置为变量值。复制具体的neo4j文件地址作为变量值。 配置文档存储在conf目录下,Neo4j通过配置文件neo4j.conf控制服务器的工作。默认情况下,不需

企业微信群机器人Webhook配置全攻略:从创建到发送消息的完整流程

企业微信群机器人Webhook配置全攻略:从创建到发送消息的完整流程 在数字化办公日益普及的今天,企业微信作为国内领先的企业级通讯工具,其群机器人功能为团队协作带来了极大的便利。本文将手把手教你如何从零开始配置企业微信群机器人Webhook,实现自动化消息推送,提升团队沟通效率。 1. 准备工作与环境配置 在开始创建机器人之前,需要确保满足以下基本条件: * 企业微信账号:拥有有效的企业微信管理员或成员账号 * 群聊条件:至少包含3名成员的群聊(这是创建机器人的最低人数要求) * 网络环境:能够正常访问企业微信服务器 提示:如果是企业管理员,建议先在"企业微信管理后台"确认机器人功能是否已对企业开放。某些企业可能出于安全考虑会限制此功能。 2. 创建群机器人 2.1 添加机器人到群聊 1. 打开企业微信客户端,进入目标群聊 2. 点击右上角的群菜单按钮(通常显示为"..."或"⋮") 3. 选择"添加群机器人"选项 4.

Flowise物联网融合:与智能家居设备联动的应用设想

Flowise物联网融合:与智能家居设备联动的应用设想 1. Flowise:让AI工作流变得像搭积木一样简单 Flowise 是一个真正把“AI平民化”落地的工具。它不像传统开发那样需要写几十行 LangChain 代码、配置向量库、调试提示词模板,而是把所有这些能力打包成一个个可拖拽的节点——就像小时候玩乐高,你不需要懂塑料怎么合成,只要知道哪块该拼在哪,就能搭出一座城堡。 它诞生于2023年,短短一年就收获了45.6k GitHub Stars,MIT协议开源,意味着你可以放心把它用在公司内部系统里,甚至嵌入到客户交付的产品中,完全不用担心授权问题。最打动人的不是它的技术多炫酷,而是它真的“不挑人”:产品经理能搭出知识库问答机器人,运营同学能配出自动抓取竞品文案的Agent,连刚学Python两周的实习生,也能在5分钟内跑通一个本地大模型的RAG流程。 它的核心逻辑很朴素:把LangChain里那些抽象概念——比如LLM调用、文档切分、向量检索、工具调用——变成画布上看得见、摸得着的方块。你拖一个“Ollama LLM”节点,再拖一个“Chroma Vector

OpenClaw配置Bot接入飞书机器人+Kimi2.5

OpenClaw配置Bot接入飞书机器人+Kimi2.5

上一篇文章写了Ubuntu_24.04下安装OpenClaw的过程,这篇文档记录一下接入飞书机器+Kimi2.5。 准备工作 飞书 创建飞书机器人 访问飞书开放平台:https://open.feishu.cn/app,点击创建应用: 填写应用名称和描述后就直接创建: 复制App ID 和 App Secret 创建成功后,在“凭证与基础信息”中找到 App ID 和 App Secret,把这2个信息复制记录下来,后面需要配置到openclaw中 配置权限 点击【权限管理】→【开通权限】 或使用【批量导入/导出权限】,选择导入,输入以下内容,如下图 点击【下一步,确认新增权限】即可开通所需要的权限。 配置事件与回调 说明:这一步的配置需要先讲AppId和AppSecret配置到openclaw成功之后再设置订阅方式,