当前位置: 首页 > news >正文

二叉搜索树完全指南:接口完善与搜索场景实战

目录

一、二叉搜索树接口代码

1.1 交换函数

1.2 中序遍历

1.3 默认构造与拷贝构造

1.4 赋值重载

1.5 析构函数

二、key搜索场景/key-value搜索场景

2.1 key 搜索场景

2.2 key/value 搜索场景

2.3 key/value 搜索场景代码


一、二叉搜索树接口代码

1.1 交换函数

先给大家看一个很实用的小接口,交换函数,代码写起来特别简单:

void swap(SBTree& other) { std::swap(_root, other._root); }

别看这代码只有一行,里面其实藏着两个值得一提的小知识点:

第一个知识点,为什么只交换 _root 就够了?

因为二叉搜索树的所有节点结构都挂在根节点下面,所以交换两个树的根指针,本质上就把两棵树的 “所有权” 给彻底互换了,原来属于 A 树的所有节点,现在全归 B 树管,反之亦然。更妙的是,这么做效率极高,就是 O (1) 的时间复杂度,完全不用去遍历或者复制任何节点。

第二个知识点,std::swap是C++标准库提供的通用交换函数,专门用来安全地交换两个对象的值。

这里用它来交换两个指针,既省去了咱们自己写临时变量中转的麻烦,又能保证代码的简洁和规范。

1.2 中序遍历

接下来是二叉搜索树里特别核心的一个接口,中序遍历。代码分成了两层设计,先看给外部调用的公有接口:

void Inorder() { inorder(_root); cout << endl; }

这个公有函数很省心,外部用的时候不用传任何参数,它内部直接把根节点_root塞给私有的递归函数,等遍历完了还顺手加个换行,让输出结果整整齐齐的。

然后是真正干活的私有递归实现:

void inorder(Node* root) { if (root == nullptr) { return; } inorder(root->left); cout << root->_key << " " << root->_value << endl; inorder(root->right); }

递归逻辑也很直白:先判断当前节点是不是空,是空就直接返回,这是递归的终止条件;然后严格按“左 - 根 - 右”的顺序来:先递归扎进左子树,接着打印当前节点的 _key 和 _value,最后再递归遍历右子树。

这里必须提一个二叉搜索树的关键知识点:二叉搜索树的中序遍历结果一定是升序的。原因很简单,它的规则就是“左小右大”,所以按“左 - 根 - 右”走一遍,刚好把所有节点从小到大排了个序 ,这也是中序遍历最实用的地方,能直接用来验证树的结构有没有乱。

1.3 默认构造与拷贝构造

接下来是两个和对象创建相关的核心接口,默认构造函数拷贝构造函数,咱们一个个来看。

先看默认构造函数,咱们直接写了SBTree() = default;。

这里藏着个C++11的实用知识点:= default到底是干嘛的?

其实就是明确告诉编译器:“别给我整复杂的,就生成一个最标准的默认构造函数就行”。这么写既省得我们自己写空函数体,又能保证编译器会乖乖把 _root 初始化成 nullptr(毕竟我们在类里给了缺省值),轻轻松松搞定空树的初始化。

然后是拷贝构造函数,当我们想基于一棵现成的树,完完整整 “克隆” 出一棵独立的新树时,就全靠它了:

SBTree(const SBTree& tree) { _root = Copy(tree._root); }

它的逻辑很直白,就是调用私有的Copy函数,把传入的 tree 的根节点塞进去,让 Copy 去递归复制所有节点,最后把新生成的根节点赋给当前树的 _root,这样两棵树就彻底分离开了。

重点来了,这个Copy函数是怎么做到递归复制整棵树的呢?咱们看代码:

Node* Copy(Node* root) { if (root == nullptr) { return nullptr; } Node* newnode = new Node(root->_key, root->_value); newnode->left = Copy(root->left); newnode->right = Copy(root->right); return newnode; }

递归的思路其实很好理解,也是分三步走:

  • 第一步,先判断当前节点是不是空,如果是空,直接返回 nullptr—— 这是递归的终止条件,碰到叶子节点的空孩子就立刻停手。
  • 第二步,要是当前节点不为空,那就先 new 一个新节点 newnode,把当前节点的 _key 和 _value 都复制过去 —— 这一步相当于 “先把自己复制好”。
  • 第三步,接着递归处理左子树和右子树:让 newnode->left 接住 Copy(root->left) 的返回值(也就是复制好的左子树的根),右子树同理。最后把 newnode 返回去,让上一层的节点接住。

这里必须敲黑板提一个关键知识点:为什么要这么费劲地递归复制?直接把根指针赋值过去不行吗?

答案是绝对不行。如果只复制根指针,那两棵树会共享同一片节点内存,你在A树里删了个节点,B树里对应的节点也会凭空消失,这就是危险的 “浅拷贝”。

而我们现在这种逐个节点递归复制的方式,叫“深拷贝”,能保证两棵树完全独立,你改你的、我改我的,互不干扰,这才是安全的拷贝方式。

1.4 赋值重载

最后给大家看一个设计得特别巧妙的接口,赋值重载,代码短得惊人,但里面藏着C++里非常经典的技巧:

SBTree& operator= (const SBTree tree) { swap(tree); return *this; }

别看这代码只有三行,这可是大名鼎鼎的Copy-and-Swap(拷贝并交换)技巧,咱们来拆解一下它的妙处:

首先看参数,这里故意用了值传递而不是引用传递,这是第一个关键点。当你用值传递的时候,编译器会自动调用咱们前面写的拷贝构造函数,帮你生成一个tree的副本,相当于把深拷贝的活儿直接甩给了拷贝构造函数,咱们自己不用再写一遍重复的逻辑。

然后是 swap(tree):把当前对象的 _root 和这个临时副本的 _root 交换一下。这么一弄,当前对象就顺理成章地拿到了副本里的所有新节点;而副本原来揣着的、当前对象旧的那些节点,会在函数结束的时候,随着临时副本 tree 的析构自动被释放掉 —— 连内存泄漏的问题都一起解决了,简直一举两得。

最后是 return *this:返回当前对象的引用,这是为了支持链式赋值,比如 a = b = c 这种写法,符合 C++ 的常规操作习惯。

这里必须提一下 Copy-and-Swap 的核心知识点:它除了代码简洁之外,还有一个巨大的优点,异常安全。因为深拷贝是在swap之前完成的,如果拷贝构造函数不小心抛了异常,当前对象的状态完全不会被破坏,还是原来的样子,不会出现 “半吊子” 的赋值结果,非常稳健。

1.5 析构函数

最后是负责 “善后” 的核心接口,析构函数,专门用来清理树里的所有节点,避免内存泄漏,代码同样分成了两层:

先看公有的析构函数:

~SBTree() { destory(_root); _root = nullptr; }

它的逻辑很清晰:当树对象的生命周期结束时(比如函数返回、程序退出),会自动调用这个析构函数。它先让私有的 destory 去递归清理所有节点,等清理完了,还顺手把 _root 置为 nullptr—— 这一步是个好习惯,能防止后面有人误操作_root变成野指针。

然后是真正干活的私有递归函数 destory:

void destory(Node* root) { if (root == nullptr) { return; } destory(root->left); destory(root->right); delete(root); }

这里必须敲黑板:它用的是后序遍历,也就是严格的“左 - 右 - 根”顺序。为什么非得这样?

因为如果先删了父节点,那左孩子和右孩子的指针就直接丢了,再也找不到它们,内存就彻底泄漏了。所以得先递归扎进左子树删干净,再递归右子树删干净,最后才能放心地 delete 当前的 root 节点。

这里补充两个关键知识点:第一个,后序遍历是二叉树析构的唯一正确选择。核心原因就是“必须先释放孩子,才能释放父亲”,顺序乱了就会出问题。第二个,析构后置空根指针是个防御性编程的好习惯—— 虽然对象已经要销毁了,置空看起来多此一举,但万一后面有代码不小心访问了旧的_root,置空后至少能快速触发空指针错误,比野指针的随机崩溃好排查多了。

二、key搜索场景/key-value搜索场景

2.1 key 搜索场景

在 key 搜索场景下,我们只把key作为唯一的关键码,树的结构里也只需要存 key 就行。搜索的目标很单纯,就是判断这个 key 在不在树里。这种场景下实现的二叉搜索树,支持增、删、查,但绝对不支持修改 key,因为 key 是维持树结构的核心,一改就把 “左小右大” 的规矩给破坏了。

给大家举两个很常见的 key 搜索场景例子:第一个是小区无人值守车库:物业会把买了车位的业主车牌号录入系统,车辆进小区时扫一下车牌,查一下在不在系统里,在就抬杆,不在就提示非本小区车辆,不让进。第二个是英文单词拼写检查:把词库里所有单词都放进二叉搜索树,然后读取文章里的单词,挨个查在不在树里,不在的话就用波浪线标红提示拼写错误。

2.2 key/value 搜索场景

key/value场景就比纯 key场景多了一层关联:每一个关键码 key,都对应着一个 value,这个value可以是任意类型的对象所以树的节点里,除了存key,还得存对应的value。增、删、查的逻辑还是以 key 为核心,按二叉搜索树的规矩来,但好处是能通过 key 快速找到它对应的 value。

这种场景下的二叉搜索树,支持修改 value,但依然不支持修改 key——key 一动树结构就乱了,但 value 随便改,不影响结构。

key/value 的场景就更丰富了,比如:第一个是简单的中英互译字典:节点里存 key(英文)和 value(中文),搜索时输入英文,就能直接找到对应的中文释义。第二个是商场无人值守车库:入口扫车牌,把车牌(key)和入场时间(value)存进树里;出口再扫车牌,找到对应的入场时间,用当前时间一减算出停车时长,再算停车费,缴完费就抬杆放行。第三个是统计文章单词词频:读一个单词,先查在不在树里,不在就说明是第一次出现,存进去(单词,1);要是已经存在,就把对应的次数 ++ 就行。

2.3 key/value 搜索场景代码

我之前带着大家写的是纯 key 搜索场景的代码,key/value 场景的代码其实不用大改,只要在原来的基础上稍作调整就行。

namespace key_value{ template <class K, class V> class SBTnode{ public: K _key;V _value; SBTnode<K, V>* left; SBTnode<K, V>* right; SBTnode(const K& key, const V& value) :_key(key) , _value(value) , left(nullptr) , right(nullptr) {} }; template <class K, class V> class SBTree{ using Node = SBTnode<K, V>; public: void swap(SBTree& other){std::swap(_root, other._root);} void Inorder(){ inorder(_root); cout << endl; } void inorder(Node* root){ if (root == nullptr) return; inorder(root->left); cout << root->_key << " " << root->_value << endl; inorder(root->right); } SBTree() = default; SBTree(const SBTree& tree){_root = Copy(tree._root);} Node* Copy(Node* root){ if (root == nullptr) return nullptr; Node* newnode = new Node(root->_key, root->_value); newnode->left = Copy(root->left); newnode->right = Copy(root->right); return newnode; } SBTree& operator= (const SBTree tree){ swap(tree); return *this; } ~SBTree(){ destory(_root); _root = nullptr; } void destory(Node* root){ if (root == nullptr) return; destory(root->left); destory(root->right); delete(root); } 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 Insert(const K& key, const V& value){ Node* cur = _root; Node* parent = nullptr; if (_root == nullptr){ _root = new Node(key, value); return true; } 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 if (parent->_key < key) parent->right = cur; return true; } 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 if (parent->right == cur) parent->right = cur->right; } } else if (cur->right == nullptr){ if (cur == _root) _root = cur->left; else{ if (parent->left == cur) parent->left = cur->left; else if (parent->right == cur) parent->right = cur->left; } } else{ Node* replaceparent = cur; Node* replace = cur->right; while (replace->left){ replaceparent = replace; replace = replace->left; } cur->_key = replace->_key; if (replaceparent->left == replace) replaceparent->left = replace->right; else if (replaceparent->right == replace) replaceparent->right = replace->right; cur = replace; } delete (cur); return true; } } return false; } protected: Node* _root=nullptr; }; }
http://www.jsqmd.com/news/812896/

相关文章:

  • 2026年4月行业内比较好的制粒机源头厂家推荐,精炼剂专用制粒机/炒灰剂专用制粒机,制粒机机构口碑推荐 - 品牌推荐师
  • OpenCLI技能框架:让命令行工具拥有自然语言交互与自动化能力
  • 氛围驱动开发:量化开发者体验与团队效能的工程化实践
  • 五分钟 熟悉所有Claude Code指令
  • 移动端AI编程助手AnyClaw:双引擎架构与本地化部署实践
  • ChatTTS开源对话语音合成模型:从原理到工程实践全解析
  • AI代码变更查看器:透视Claude Code修改过程,提升开发协作效率
  • Android / IoT 面试复盘总结:从 MQTT、TLS 到 JWT 权限体系(标准答案 + 工程理解 + 延伸知识链)
  • AI提示词工程化实践:从模块化到自动化的工作流构建
  • Agent-Harness:为AI编码助手套上“缰绳”的工程化框架
  • SQL数据分析实战:电商新品高流量低转化问题
  • 半导体制造中的金属填充技术:原理与应用
  • 别再用默认设置了!手把手教你调校Intel RealSense D435/D435i,让深度图质量翻倍
  • AI研究工具性能评估实战:基于Autoresearch基准的AdaL与Claude Code对比
  • 基于MCP协议构建AI工具桥接器:从原理到MySQL适配器开发实战
  • DistroAV for macOS:为什么这是OBS用户必备的3步网络视频传输解决方案
  • WordPress开发利器:clawwp工具库提升PHP开发效率与代码质量
  • 使用 Let’s Encrypt 免费申请泛域名 SSL 证书,并实现自动续期
  • shell 脚本中注释的正确写法是什么?
  • 招募Kiro大使!会员权益、内测资格等重磅福利等你领!
  • RAG:解锁大语言模型新能力,告别幻觉与知识陈旧!
  • 为AI智能体设计网站体验:AX设计原则与落地实践指南
  • 别再乱用multicycle约束了!一个真实案例带你搞懂ASIC/FPGA时序收敛中的-start与-end参数
  • 魔兽争霸III地图编辑器革命:HiveWE如何让地图制作效率提升5倍
  • Arm技术文档体系与合规使用指南
  • AI智能体架构实战:从规划、记忆到工具调用的核心组件解析
  • OpenCrab:面向中文开发者的开源项目导航与协作平台架构实践
  • 2026年比较好的母婴用品锂电池用户口碑推荐厂家 - 行业平台推荐
  • 基于MCP协议构建AI智能体工具网关:Orbis-mcp实战指南
  • AI都能直接生成代码了,程序员还有必要深究框架源码吗?