【数据结构】二叉搜索树 C++ 简单实现:增删查改全攻略

【数据结构】二叉搜索树 C++ 简单实现:增删查改全攻略

目录

前言:

1、什么是二叉搜索树?

2、二叉搜索树性能分析

3、key类型二叉搜索树的实现

节点结构

类结构

3.1、插入

3.2、中序遍历

3.3、查找

3.4、删除

4、key_value类型二叉搜索树的实现

节点结构

类结构

4.1、构造函数

4.1.1 默认构造

4.1.2 拷贝构造

4.2、赋值重载

4.3、析构

4.4、插入

总结

前言:

今天这篇,我们就从‘0’开始搭一棵 C++ 二叉搜索树:先定义节点结构,再实现最核心的‘插入’和‘查找’,最后攻克最难的‘删除’(包括叶子节点、单孩子节点、双孩子节点三种情况)。全程不跳步、不埋坑,写完后你不仅能运行代码,更能说清‘每一步为什么这么做’。”

1、什么是二叉搜索树?

二叉搜索树又称二叉排序树,如图,它或者是一棵空树,或者是具有以下性质的二叉树:

左子树上所有节点的值都小于等于根结点的值;

右子树上所有节点的值都大于等于根结点的值;

它的左右子树也分别为二叉搜索树
注意:二叉搜索树中可以支持插入相等的值,也可以不支持插入相等的值,具体看使用场景定义,后续我 们学习map/set/multimap/multiset系列容器底层就是二叉搜索树,其中map/set不支持插入相等值,multimap/multiset支持插入相等值

2、二叉搜索树性能分析

在最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其高度为: log2 N

最差情况下,二叉搜索树退化为单支树(或者类似单支),其高度为: N

所以平均时间的时间复杂度为 O(log n),最差情况为 O(n)。

说明:虽然二分查找也可以实现 O(log2 N) 级别的查找效率,但是二分查找有两大缺陷

1. 需要存储在支持下标随机访问的结构中,并且有序。

2. 插入和删除数据效率很低,因为存储在下标随机访问的结构中,插入和删除数据一般需要挪动数 据。

3、key类型二叉搜索树的实现

为什么是key类型二叉搜索树呢?因为我们后面要讲的set/multiset/map/multimap容器的底层数据结构就红黑树(Red-Black Tree)—— 一种 “近似平衡” 的二叉搜索树(BST)。但不同的是,对于set/multiset容器,如下图,集合(Sets)是一种容器,它按照特定顺序存储唯一的元素。

节点结构

template<class K> struct BSTNode { K _key; // 存储数据 BSTNode<K>* _left; // 指向左节点的指针 BSTNode<K>* _right;// 指向右节点的指针 BSTNode(const K& key = 0) //默认构造 :_key(key) ,_left(nullptr) ,_right(nullptr) { } };

类结构

template<class K> class BSTree { //typedef BSTNode<K> Node; // typedef对节点对象重命名 using Node = BSTNode<K>; // 使用using重命名 public: // ... private: Node* _root = nullptr; // 成员变量 };
说明:下面我们实现过程中默认为值不重复的情形

3.1、插入

思路:

(1)树为空,则直接新增结点,赋值给root指针

(2)树不空,按二叉搜索树性质,用cur指针遍历二叉搜索树,用一个parent指针记下父亲节点,要插入值key比当前节点大往右走,插入值比当前结点小往左走,直到找到空位置,插入新结点。

(4)确定要插入的节点在父亲节点的左边还是右边,即当_key < parent->_key则将新节点插入到parent的左边,反之则插入到右边。

(5)如果支持插入相等的值,插入值跟当前结点相等的值可以往右走,也可以往左走,找到空位置,插入新结点。但是,要确定相同的值要么都往左边走或者都往右边走。

//-------------------------------代码实现------------------------------------------------- bool Insert(const K& key) { Node* node = new Node(key); // 处理空树 if (_root == nullptr) { _root = node; return true; } Node* parent = nullptr; // 记下父亲节点的指针,方便最后连接新节点 Node* cur = _root; // 遍历二叉搜索树 while (cur) { if (key < cur->_key) { parent = cur; cur = cur->_left; } else if (key > cur->_key) { parent = cur; cur = cur->_right; } else { return false; } } // 确定新节点最后的插入位置 if (key < parent->_key) { parent->_left = node; } else { parent->_right = node; } }

3.2、中序遍历

根据二叉搜索树的特性,其中序遍历的结果恰好为一个有序序列(升序)

为了解决函数调用传参的问题,因为中序遍历的参数_root是成员变量,在类外面调用无法访问成员变量而无法传参,所以,用子函数方式来实现。
// 中序遍历 void MidOrder() { _MidOrder(_root); } // 子函数 void _MidOrder(Node* _root) { if (_root == nullptr) { return; } _MidOrder(_root->_left); cout << _root->_key << " "; _MidOrder(_root->_right); }

3.3、查找

思路:

(1)从根开始比较,查找val,val比根的值大则往右边走查找,val比根值小则往左边走查找。

(2)最多查找高度次,走到到空,还没找到,这个值不存在,返回false。

(3)如果不支持插入相等的值,找到x即可返回。

bool Find(const K& val) { Node* cur = _root; // 遍历查找 while (cur) { if (val < cur->_key) { cur = cur->_left; } else if (val > cur->_key) { cur = cur->_right; } else if (val == cur->_key) { return true; } else { return false; } } }

3.4、删除

思路:

(1)要删除节点N的位置有四种情况

a. 结点N左右孩子均为空 ;b. 结点N左孩子为空,右孩子结点不为空 ;c. 结点N右孩子位空,左孩子结点不为空 ;d. 结点N左右孩子结点均不为空。

(2)对于左右孩子都为空的情况,我们可以将其放在左孩子为空和右孩子为空的情况中处理。即将上面四种情况重新分为:左为空;右为空;左右都不为空三种。

(3)对于左为空,要删除节点N可能为根结点,则让该节点的右节点作为新的根结点;或者是父节点的左节点,则让父节点的_left指针指向节点N的右边;或者父节点的右节点,则让父节点的_right指针指向节点N的左边,右为空类似。

(4)对于左右都不为空,无法直接删除N结点,因为N的两个孩子无处安放,只能用替换法删除,找N左子树的值最大结点 R(最右结点)或者N右子树的值最小结点R(最左结点)替代N,因为这两个结点中任意一个,放到N的位置,都满足二叉搜索树的规则。替代N的意思就是N和R的两个结点的值交换,转而变成删除R结点,R结点符合情况2或情况3,可以直接删除。

bool Earse(const K& val) { Node* cur = _root; Node* parent = cur; // 遍历先找到N节点 while (cur) { if (val < cur->_key) { parent = cur; cur = cur->_left; } else if (val > cur->_key) { parent = cur; cur = cur->_right; } // 找到N节点 else { // 左为空 if (cur->_left == nullptr) { // 根结点 if (_root->_key == val) { _root = _root->_right; } else if (cur == parent->_left) parent->_left = cur->_right; else parent->_right = cur->_right; delete cur; } // 右为空 else if (cur->_right == nullptr) { // 根结点 if (_root->_key == val) { _root = _root->_left; } else if (cur == parent->_right) parent->_right = cur->_left; else parent->_left = cur->_left; delete cur; } // 左右孩子均不为空 else { Node* replaceparent = cur; Node* replace = cur->_right; // 找左子树中最小值的节点R while (replace->_left) { replaceparent = replace; replace = replace->_left; } // 替换节点N与节点R的值 cur->_key = replace->_key; if (replace == replaceparent->_left) { replaceparent->_left = replace->_right; } else { cur->_right = replace->_right; } delete replace; } return true; } } return false; }

4、key_value类型二叉搜索树的实现

为什么还有key_value类型二叉搜索树呢?对于map/multimap容器,如下图,映射(Maps)是一种关联容器,它按照特定顺序存储由 “键值(key value)” 和 “映射值(mapped value)” 组合而成的元素。

节点结构

与key类型相比,只是多了个_value。

template<class K, class V> struct BSTNode { K _key; V _value; BSTNode<K, V>* _left; BSTNode<K, V>* _right; BSTNode(const K& key = 0, const V& value = 0) :_key(key) , _value(value) , _left(nullptr) , _right(nullptr) {} };

类结构

template<class K, class V> class BSTree { using Node = BSTNode<K, V>; public: // ... private: Node* _root = nullptr; };
说明:对于key_value类型我们实现一下其构造,析构等,除了插入操作与key类型有一点不同外,其他操作基本相同,所以我们不再重复解释。

4.1、构造函数

4.1.1 默认构造
// C++11强制生成默认构造 BSTree() = default;
4.1.2 拷贝构造
// 拷贝构造(深拷贝) BSTree(const BSTree& tree) { _root = copy(tree._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; }

4.2、赋值重载

// 赋值重载(现代写法) BSTree& operator=(BSTree tree) { swap(_root, tree._root); return *this; }

4.3、析构

// 析构函数 ~BSTree() { Destry(_root); _root = nullptr; } void Destry(Node* root) { if (root == nullptr) { return; } // 后序遍历:防止根结点被释放而找不到左右孩子节点 Destry(root->_left); Destry(root->_right); delete root; }

4.4、插入

bool Insert(const K& key, const V& value) { Node* node = new Node(key, value); if (_root == nullptr) { _root = node; return true; } Node* parent = nullptr; Node* cur = _root; while (cur) { if (key < cur->_key) { parent = cur; cur = cur->_left; } else if (key > cur->_key) { parent = cur; cur = cur->_right; } else { return false; } } if (key < parent->_key) { parent->_left = node; } else { parent->_right = node; } return true; }

总结

本文主要是简单实现了一下二叉搜索树的最基础的版本,与set/multiset/map/multimap底层的数据结构——平衡二叉搜索树有所不同,并没有过多关注底层实现细节,比如insert,Find返回值等。只是为了能够加深大家对二叉搜索树的理解,方便后面对平衡二叉树的理解。

Read more

Flutter for OpenHarmony: Flutter 三方库 google_maps 在鸿蒙应用中嵌入全球地图服务的架构实践(跨平台地图方案库)

Flutter for OpenHarmony: Flutter 三方库 google_maps 在鸿蒙应用中嵌入全球地图服务的架构实践(跨平台地图方案库)

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 在进行 OpenHarmony 的全球化应用开发时,地图服务是出海项目绕不开的核心组件。对于已经在海外市场成熟运行、深度依赖 Google 地图生态的 Flutter 应用,如何将现有的地图逻辑迁移或适配到鸿蒙平台,是许多出海大企关注的焦点。 虽然鸿蒙在国内市场主要使用高德或百度地图,但在处理“全球一张图”需求时,google_maps 相关的 Flutter 插件及其底层的 Dart 模型定义,依然是定义地理围栏、标记点(Marker)和轨迹绘制的标准参考。本篇将探讨如何在鸿蒙跨平台架构中,平衡 Google 地图的通用逻辑与鸿蒙的原生渲染。 一、跨平台地图适配架构 在鸿蒙适配中,我们通常采用“统一接口层,分平台实现”的策略。 模型转换 适配层 Flutter 业务层 (Dart) 地图抽象层

By Ne0inhk
Flutter 组件 csv2json 适配鸿蒙 HarmonyOS 实战:高性能异构数据转换,构建 CSV 流式解析与全栈式数据映射架构

Flutter 组件 csv2json 适配鸿蒙 HarmonyOS 实战:高性能异构数据转换,构建 CSV 流式解析与全栈式数据映射架构

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 csv2json 适配鸿蒙 HarmonyOS 实战:高性能异构数据转换,构建 CSV 流式解析与全栈式数据映射架构 前言 在鸿蒙(OpenHarmony)生态迈向工业数字化、涉及海量历史报表同步、离线数据采集及跨系统异构数据对齐的背景下,如何实现一种既能处理超大规模文本、又能保障转换极速且具备“非阻塞”特性的数据清洗方案,已成为决定应用数据吞吐能力与内存稳健性的核心因素。在鸿蒙设备这类强调 AOT 极致性能与受限内存足迹的环境下,如果应用依然采用原始的循环分割或同步全量加载 CSV,由于由于数据规模的膨胀,极易由于由于“内存瞬时爆表”导致鸿蒙应用的任务栈卡死。 我们需要一种能够流式处理(Streaming)、支持自动化字段映射(Auto-mapping)且具备零样板代码特性的转换方案。 csv2json 为 Flutter 开发者引入了“数据流变幻”范式。它将结构松散的 CSV 文本精确轰击为高维度的 JSON

By Ne0inhk
EnvPilot:一款基于 Rust 的跨平台环境变量神器,一键搞定 Windows/Linux 环境配置!

EnvPilot:一款基于 Rust 的跨平台环境变量神器,一键搞定 Windows/Linux 环境配置!

文章目录 * 1. 项目介绍🎯 * 1.1. 什么是 EnvPilot? * 1.2. 为什么选择 EnvPilot? * 2. 核心优势:四大痛点全部解决!💪 * ✅ 痛点一:添加不生效?已修复! * ✅ 痛点二:删除删不掉?已修复! * ✅ 痛点三:PATH 清理失效?已修复! * ✅ 痛点四:误操作无法恢复?已解决! * 3. 支持的开发环境🛠️ * 4. 详细使用教程📖 * 4.1. Windows 平台使用教程 * 1️⃣ 下载安装 * 2️⃣ 配置环境变量 * 3️⃣ 清除环境变量 * 4.2. Linux 平台使用教程 * 1️⃣ 从源码编译 * 2️⃣ 配置环境变量 * 3️

By Ne0inhk
Spring Boot 机制三: ApplicationContext 生命周期与事件机制源码解析

Spring Boot 机制三: ApplicationContext 生命周期与事件机制源码解析

博主社群介绍: ① 群内初中生、高中生、本科生、研究生、博士生遍布,可互相学习,交流困惑。 ② 热榜top10的常客也在群里,也有数不清的万粉大佬,可以交流写作技巧,上榜经验,涨粉秘籍。 ③ 群内也有职场精英,大厂大佬,跨国企业主管,可交流技术、面试、找工作的经验。 进群免费赠送写作秘籍一份,助你由写作小白晋升为创作大佬,进群赠送ZEEKLOG评论防封脚本,送真活跃粉丝,助你提升文章热度。 群公告里还有全网大赛约稿汇总/博客提效工具集/ZEEKLOG自动化运营脚本 有兴趣的加文末联系方式,备注自己的ZEEKLOG昵称,拉你进群,互相学习共同进步。 文章目录 * Spring Boot 机制三: ApplicationContext 生命周期与事件机制源码解析 * 目录 * 1. Spring Boot 启动概览 * 2. ApplicationContext 生命周期概览 * 3. BeanFactoryPostProcessor 与 BeanPostProcessor 调用顺序

By Ne0inhk