【C++指南】一文总结C++二叉搜索树

【C++指南】一文总结C++二叉搜索树


🌟 各位看官好,我是egoist2023

🌍 种一棵树最好是十年前,其次是现在!


🚀 今天来学习C++二叉搜索树的实现。

👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享给更多人哦

目录

何为二叉搜索树

性能分析 

搜索树的结构

插入和查找(非冗余版本)

中序遍历

删除

key/value

使用场景

代码实现


何为二叉搜索树

二叉搜索(排序)树具有如下特征:

若它的左子树不为空,则左子树上所有结点的值都小于等于根结点的值;若它的右⼦树不为空,则右子树上所有结点的值都大于等于根结点的值;它的左右⼦树也分别为二叉搜索树;二叉搜索树的中序遍历是有序的;了解二叉搜索树是为了之后为map/set/multimap/multiset的关联式容器打下基础。特别地,二叉搜索树分为冗余和不冗余版本。(冗余即有相等值也可插入)

在冗余版本的二叉搜索树中,会出现如下两种情况,那这两都是正确的吗?

是的,并不影响搜索二叉树的性质。

性能分析 

可以发现在该二叉搜索树较为平衡的情况下

即在最优情况下,找到指定结点的情况下,只需要访问高度k次,即高度为: log2 N;

但在最差情况下,高度会达到N。(在红黑树章节会有详解如何平衡高度差)



综合来讲,二叉搜索树增删查改时间复杂度为: O(N)。



二分查找也能达到 
log2 N 级别的查找效率,为何不使用二分查找呢?二分查找有以下缺陷:需要存储在⽀持下标随机访问的结构中,并且有序。插入和删除数据效率很低,因为存储在下标随机访问的结构中,插入和删除数据一般需要挪动数据。二叉搜索树在最差情况下能达到O(N)级别,那它能起到什么作用呢?二叉搜索树的引入是为了后续为平衡二叉树AVL树和红⿊树作铺垫,之后引入了旋转的概念,能够好维持二叉树的结构。

搜索树的结构

template<class K> struct BSTreeNode { K _key; BSTreeNode<K>* _left; BSTreeNode<K>* _right; BSTreeNode(const K& key) :_key(key) ,_left(nullptr) ,_right(nullptr) {} };

插入和查找(非冗余版本)

 插入时分为以下情况:

  1. 树为空时,申请一个新结点赋值给根结点_root;
  2. 树不为空时,插⼊值⽐当前结点⼤往右⾛,插⼊值⽐当前结点⼩往左⾛,找到空位置,插⼊新结点。(如若支持冗余,则可往左走,也可往右走,确保逻辑一致性)
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->_left = cur; } else { parent->_right = cur; } return true; }
  1. 查找指定结点时,和插入结点的逻辑是类似的,新插入结点的值比当前结点大则往右走,反之往左走,找到后返回即可 。
  2. 那如果是冗余版本的二叉搜索树呢?意味着存在多个相等的值,而我们一般要查找中序遍历后的第一个相等的值(疑惑1),如何解决呢?(疑惑2)

疑惑1:找到第一个相等的值,可以方便我们知道有多少个相等的值,也能从头到尾打印。迭代器也可以确认其相等的值的范围等等。

疑惑2:

非冗余版本 

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; }

中序遍历

这样设计的目的是让我们调用InOrder的时候不需要传_root根节点(根本原因是在类外不能访问private私有的_root,只能在类里访问)

void InOrder() { _InOrder(_root); cout << endl; } void _InOrder(Node* root) { if (root == nullptr) { return; } _InOrder(root->_left); cout << root->_key << " "; _InOrder(root->_right); }

删除

在二叉搜索树中删除的逻辑最为致命,它分为多种情况,需要考虑很多细节问题。

  1. 删除结点无左右孩子
  2. 删除结点有右孩子
  3. 删除结点有左孩子
  4. 删除结点有左右孩子

在情况1当中,把该结点的⽗亲对应孩⼦指针指向空,直接删除该结点,并不需要考虑删除结点是在左在右的情况。(情况1其实可以归类为情况2或情况3之中,本章节归类为情况2)

在情况2中:把N结点的⽗亲对应孩⼦指针指向N的右孩⼦,直接删除N结点

需要额外判断parent是左指针还是右指针指向cur的右孩子。

 

在情况3中: 把N结点的⽗亲对应孩⼦指针指向N的左孩⼦,直接删除N结点,依旧处理parent指向的问题。

在情况4中: 不能直接删除该结点且parent会不道指向cur的左还是右,这种情况需要找另一个结点来替代。为了保持二叉搜索树的性质,因此替代的结点必须是当前结点左子树的最大结点或者右子树的最小结点,交换值后,释放替代的结点资源。(本章节以找右子树最小结点为例)

这里埋下了一个坑,如果删除的是根结点,不能让parentreplace开始置为nullptr,否则会出现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 (cur == _root) { _root = cur->_right; } else { if (parent->_left == cur) { parent->_left = cur->_right; } else { parent->_right = cur->_right; } } delete cur; } //右节点为空 else if (cur->_right == nullptr) { if (cur == _root) { _root = cur->_left; } else { if (parent->_right == cur) { parent->_right = cur->_left; } else { parent->_left = cur->_left; } } delete cur; } //有双亲节点 else { Node* parentreplace = cur; Node* replace = cur->_right; while (replace->_left) { parentreplace = replace; replace = replace->_left; } swap(replace->_key, cur->_key); if (parentreplace->_left == replace) { parentreplace->_left = replace->_right; } else { parentreplace->_right = replace->_right; } delete replace; } return true; } } return false; }

key/value

使用场景

在现实生活中会遇到很多key/value场景。

如:1.简单中英互译字典,树的结构中(结点)存储key(英⽂)和vlaue(中⽂),搜索时输⼊英⽂,则同时 查找到了英⽂对应的中⽂。

2.统计⼀篇⽂章中单词出现的次数,读取⼀个单词,查找单词是否存在,不存在这个说明第⼀次出现,(单词,1),单词存在,则++单词对应的次数。

代码实现

其实key/value的实现和key时类似的,需要注意的是key/value场景下的value是可以冗余的,因为是以key为主体进行的。(注意:std库中key和value放在了pair的结构体当中,是为了达到返回多个值的目的)

 template<class K,class V> struct BSTreeNode { 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) {} }; template<class K,class V> class BSTree { typedef BSTreeNode<K,V> Node; public: 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->_left = cur; } else { parent->_right = cur; } return true; } BSTreeNode<K,V>* 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 (cur == _root) { _root = cur->_right; } else { if (parent->_left == cur) { parent->_left = cur->_right; } else { parent->_right = cur->_right; } } delete cur; } //右节点为空 else if (cur->_right == nullptr) { if (cur == _root) { _root = cur->_left; } else { if (parent->_right == cur) { parent->_right = cur->_left; } else { parent->_left = cur->_left; } } delete cur; } //有双亲节点 else { Node* parentreplace = cur; Node* replace = cur->_right; while (replace->_left) { parentreplace = replace; replace = replace->_left; } swap(replace->_key, cur->_key); swap(replace->_value, cur->_value); if (parentreplace->_left == replace) { parentreplace->_left = replace->_right; } else { parentreplace->_right = replace->_right; } delete replace; } return true; } } return false; } //析构函数 ~BSTree() { Destroy(_root); _root = nullptr; } void Destroy(Node* root) { if (root == nullptr) { return; } Destroy(root->_left); Destroy(root->_right); delete root; } private: Node* _root = nullptr; };

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成功之后再设置订阅方式,