二叉树进阶 --- 上

目录

1. 二叉搜索树的概念及结构

1.1. 二叉搜索树的概念

1.2. 二叉搜索树的结构样例

2. 二叉搜索树的实现

2.1. insert 的非递归实现

2.2. find 的非递归实现

2.3. erase 的非递归实现

2.3.1. 第一种情况:所删除的节点的左孩子为空

2.3.1.1. 错误的代码

2.3.1.2. 正确的代码

2.3.2. 第二种情况:所删除的节点的右孩子为空

2.3.2.1. 正确的代码

2.3.3. 第三种情况:所删除的节点有两个非空节点 && 找右子树的最左节点

2.3.3.1. 有错误的代码

2.3.3.2. 正确的代码

2.3.4. erase的完整实现如下


1. 二叉搜索树的概念及结构

学习二叉搜索树的一些原因:

  • map 和 set 特性需要先铺垫二叉搜索树,而二叉搜索树也是一种树形结构;
  • 二叉搜索树的特性了解,有助于更好的理解 map 和 set 的特性。

1.1. 二叉搜索树的概念

二叉搜索树(Binary Search Tree,简称BST) 又名为二叉排序树或者是二叉查找树。它可能是一棵空树,或者是满足下面性质的二叉树:

  • 如果它的左子树不为空,那么左子树上的所有节点的值都要小于根节点的值;
  • 如果它的右子树不为空,那么右子树上的所有节点的值都要大于根节点的值;
  • 它的左右子树也是一颗二叉搜索树。

对于一颗二叉搜索树,它的中序遍历可以得到有序的数据;

需要注意的是,二叉搜索树要求每个节点的值都唯一,如果存在重复的值,可以在节点中添加计数器来解决。

1.2. 二叉搜索树的结构样例

一棵树是否是一颗二叉搜索树,必须要符合二叉搜索树的性质。

 为了更好地理解二叉搜索树,我们需要其进行模拟实现:

2. 二叉搜索树的实现

2.1. insert 的非递归实现

对于二叉搜索树的插入,我们需要满足插入后的二叉树仍旧是一颗二叉搜索树,也就是说,插入的元素必须要被插入到特定的位置,以维持二叉搜索树的结构。如上图所示,如果要插入14,那么它的位置是确定的,如下图所示: 

因此 insert 的具体实现我们可以分解为两个过程:

  • 第一步:找到要插入元素的位置;
  • 第二步:插入元素,完成连接关系。 

注意:在这里实现的二叉搜索树的每个值具有唯一性,相同值不插入。

bool insert(const T& key)
{// 1. 如果是空树,直接对_root赋值即可,插入成功并返回trueif (_root == nullptr){_root = new Node(key);return true;}else{// step 1: 先找目标位置Node* cur = _root;// 为了更好的连接新节点, 因此记录父节点Node* parent = nullptr;while (cur){// 如果当前节点的Key大于目标Key// 当前节点应该向左子树走if (cur->_key > key){parent = cur;cur = cur->_left;}// 如果当前节点的Key小于目标Key// 当前节点应该向右子树走else if (cur->_key < key){parent = cur;cur = cur->_right;}else{// 找到了相同的 key, 在这里不插入return false;}}// cur 走到了空, 即 cur 就是合适的位置cur = new Node(key);// 我们需要判断cur是parent的左节点还是右节点// 如果key小于parent的key,那么插入左节点if (key < parent->_key)parent->_left = cur;// 反之连接到右节点elseparent->_right = cur;return true;}
}

2.2. find 的非递归实现

find 就很简单了,没什么要说的,根据传递的 key 进行判断,大于当前节点,那么当前节点向左走,反之向右走,如果相等,返回true,循环结束,则说明没有这个key,实现如下:

bool find(const T& key)
{// 1. 从根节点开始Node* cur = _root;while (cur){// 2. 如果当前关键字大于目标关键字,那么向左子树走if (cur->_key > key)cur = cur->_left;// 3. 如果小于目标关键字,那么向右子树走else if (cur->_key < key)cur = cur->_right;// 4. 相等,就返回trueelsereturn true;}// 5. 循环结束,说明没找到, 返回falsereturn false;
}

2.3. erase 的非递归实现

对于搜索二叉树来说,真正有一些难度的是删除,对于删除我们可以分解为不同的情况,根据对应的情况,以特点方式解决。

在这里我们分为三种情况:

  • 所删除的节点的左孩子为空:托孤法删除;
  • 所删除的节点的右孩子为空:托孤法删除;
  • 所删除的节点的有两个非空孩子:替代法删除。

注意:对于叶子结点的处理可以归为第一类情况或者第二类情况。

为了可以更好的理解上面的三种情况,我们用图来说话:

2.3.1. 第一种情况:所删除的节点的左孩子为空

如图所示:假如现在我们要删除的节点是15节点,可以发现它的左孩子为空,那么如何删除呢?

 我们的方法是托孤法删除,什么叫托孤法删除呢?

就是将15的非空孩子(在这里就是19)交给它的父亲节点(在这里就是8),如图所示:

注意:在这里一定是父亲节点的右孩子指向被删除的节点的非空孩子吗?

答案是,不一定,我们需要根据被删除节点和父亲节点的关系判断:

  • 如果被删除节点是父亲节点的右孩子,那么在这里就是父亲节点的右孩子指向被删除节点的非空节点;
  • 如果被删除节点是父亲节点的左孩子,那么在这里就是父亲节点的左孩子指向被删除节点的非空节点。

代码如下:

2.3.1.1. 错误的代码
// 第一种情况: 所删除的节点的左孩子为空
if (del->_left == nullptr)
{if (del_parent->_left == del){del_parent->_left = del->_right;}else{del_parent->_right = del->_right;}delete del;del = nullptr;
}

可能我们认为这段代码没问题,但是如果是下面这种情况呢?  

如果我此时要删除8,而8是这棵树的根节点,它是没有父节点的,那么此时上面的代码就会崩溃;

为了解决这个隐患,我们的方案就是,如果被删除节点是根,且它的左子树为空树,那么我们更新根节点即可,在这里就是让15做根节点。

2.3.1.2. 正确的代码
//第一种情况:所删除的节点的左孩子为空
if (del->_left == nullptr)
{// 如果被删除节点是根,那么更新根即可if (del == _root){Node* newroot = del->_right;delete _root;_root = newroot;}// 被删除节点是非根节点else{if (del_parent->_left == del){del_parent->_left = del->_right;}else{del_parent->_right = del->_right;}delete del;}
}

2.3.2. 第二种情况:所删除的节点的右孩子为空

如图所示:假如现在我们要删除的节点是6节点,可以发现它的右孩子为空,那么如何删除呢?

方案依旧是托孤法删除,在这里就是将6(被删除节点)的5(非空孩子节点)交给4(父亲节点) ,如下:

 处理细节,和第一种情况大同小异。

需要注意的就是:最后父亲节点连接非空孩子节点的时候,要根据被删除节点是父亲节点的左孩子还是右孩子来判断。

第二种情况和第一种情况大同小异,也需要对根节点进行特殊处理:

代码如下:

2.3.2.1. 正确的代码
//第二种情况:所删除的节点的右孩子为空
else if (del->_right == nullptr)
{// 当被删除节点为根节点if (del == _root){Node* newroot = del->_left;delete del;_root = newroot;}//当被删除节点为非根节点else{if (del_parent->_left == del){del_parent->_left = del->_left;}else{del_parent->_right = del->_left;}delete del;del = nullptr;}
}

2.3.3. 第三种情况:所删除的节点有两个非空节点 && 找右子树的最左节点

较为复杂的就是第三种情况了,由于被删除的节点有两个孩子,因此无法托孤,因为父亲节点至多只能管理两个孩子,所以我们又提出了新的解决方案:替代法删除

如图所示:

假如现在我们要删除4所在的节点,可以发现,4所在的节点有两个孩子,因此无法托孤,那么我们需要采用替代法删除,替代法删除就是在左子树或者右子树找一个"合适节点",将4所在的节点的key进行覆盖,将删除4所在的节点转化为删除我们找的这个"合适节点"。

而这个"合适节点"通常只有两个:

  • 其一:以被删除的节点所在的二叉树开始,左子树的最大节点,即左子树的最右(大)节点;
  • 其二:以被删除的节点所在的二叉树开始,右子树的最小节点,即右子树的最左(小)节点。

而我们在这里就找右子树的最左(小)节点,注意,要从被删除的节点开始,在这里就是5;

当找到这个 "合适节点" 后,交换它与要删除节点的 Key;

此时就将删除目标节点转换为删除这个 "合适节点" 了;

因为此时这个 "合适节点" 只会有两种情况:

  • 第一种:它没有孩子,即左右子树为空;
  • 第二种:它只有一个孩子,且只可能是右孩子,因为这个 "合适节点" 是右子树的最左节点;

如果是第一种,即没有孩子,我们也可以将其归类为第二种情况,即右孩子不为空 && 左孩子为空,因此,可以用统一的方式处理,即托孤法删除 (所删除的节点的左孩子为空)。

故我们的大致步骤如下:

  1. 找合适节点;
  2. 交换合适节点和删除节点的 Key;
  3. 托孤发删除合适节点。

如图所示:

2.3.3.1. 有错误的代码
// 第三种情况:所删除的节点有两个非空节点
else
{// 从被删除结点开始, 找右子树的最小(左)节点Node* right_min = del->_right;// 并记录这个节点的父亲节点// 有可能这里我们会习惯的从nullptr开始,但是对于某些特殊情况会崩溃Node* right_min_parent = nullptr;while (right_min->_left){right_min_parent = right_min;right_min = right_min->_left;}// 交换这个节点和要删除节点的 keystd::swap(del->_key, right_min->_key);// 将删除 del 转化为删除 right_min (托孤法删除)if (right_min_parent->_left == right_min)right_min_parent->_left = right_min->_right;elseright_min_parent->_right = right_min->_right;delete right_min;right_min = nullptr;
}

如果我们将"合适节点"的父节点初始值设为nullptr,那么在下面的场景会发生崩溃:

由于此时,这个 "合适节点" 正好是 del->_right,不会进入循环,那么 right_min_parent 就是空,那么后面的操作就会对空指针进行解引用,非法操作,进程崩溃。

因此这里的 right_min_parent 的初始值可以从 del 开始,不可以将初始值设为空。

同时,我们发现,最后进行托孤法删除时,我们也进行了判断,这样的原因是因为这个"合适节点"既可能是父节点的左孩子,也可能是父节点的右孩子,因此必须判断。

2.3.3.2. 正确的代码
// 第三种情况:所删除的节点有两个非空节点
else
{// 从被删除结点开始, 找右子树的最小(左)节点Node* right_min = del->_right;// 并记录这个节点的父亲节点, 让其从del开始Node* right_min_parent = del;while (right_min->_left){right_min_parent = right_min;right_min = right_min->_left;}// 交换这个节点和要删除节点的 keystd::swap(del->_key, right_min->_key);// 将删除 del 转化为删除 right_min (托孤法删除)if (right_min_parent->_left == right_min)right_min_parent->_left = right_min->_right;elseright_min_parent->_right = right_min->_right;delete right_min;right_min = nullptr;
}

以此类推,我们也可以找左子树的最大(右)节点,代码如下: 

else
{// 从被删除节点开始, 找左子树的最大(右)节点Node* left_max = del->_left;// 并记录这个节点的父亲节点, 让其从del开始Node* left_max_parent = del;while (left_max->_right){left_max_parent = left_max;left_max = left_max->_right;}// 交换这个节点和要删除节点的 keystd::swap(del->_key, left_max->_key);// 将删除 del 转化为删除 left_max (托孤法删除)if (left_max_parent->_left == left_max)left_max_parent->_left = left_max->_left;elseleft_max_parent->_right = left_max->_left;delete left_max;left_max = nullptr;
}

最后,我们将第三种情况汇总,第一种是找右子树的最左节点,第二种是找左子树的最右节点,如下:

else 
{// 从被删除节点开始, 找右子树的最小(左)节点 || 找左子树的最大(右)节点if (del->_right)_erase_right_min_node(del);else_erase_left_max_node(del);
}void _erase_right_min_node(Node* del) 
{// 从被删除结点开始, 找右子树的最小(左)节点Node* right_min = del->_right;// 并记录这个节点的父亲节点, 让其从del开始Node* right_min_parent = del;while (right_min->_left){right_min_parent = right_min;right_min = right_min->_left;}// 交换这个节点和要删除节点的 keystd::swap(del->_key, right_min->_key);// 将删除 del 转化为删除 right_min (托孤法删除)if (right_min_parent->_left == right_min)right_min_parent->_left = right_min->_right;elseright_min_parent->_right = right_min->_right;delete right_min;right_min = nullptr;
}void _erase_left_max_node(Node* del)
{// 从被删除节点开始, 找左子树的最大(右)节点Node* left_max = del->_left;// 并记录这个节点的父亲节点, 让其从del开始Node* left_max_parent = del;while (left_max->_right){left_max_parent = left_max;left_max = left_max->_right;}// 交换这个节点和要删除节点的 keystd::swap(del->_key, left_max->_key);// 将删除 del 转化为删除 left_max (托孤法删除)if (left_max_parent->_left == left_max)left_max_parent->_left = left_max->_left;elseleft_max_parent->_right = left_max->_left;delete left_max;left_max = nullptr;
}

2.3.4. erase的完整实现如下

bool erase(const T& key)
{// 先找要删除的节点Node* del = _root;Node* del_parent = nullptr;while (del){if (del->_key < key){del_parent = del;del = del->_right;}else if (del->_key > key){del_parent = del;del = del->_left;}else{// 锁定了要删除的节点// 分三种情况:// case 1: 左子树为空if (del->_left == nullptr){// 如果要删除的节点是根if (del == _root){Node* newroot = del->_right;delete _root;_root = newroot;}else{// 托孤法删除if (del_parent->_left == del)del_parent->_left = del->_right;elsedel_parent->_right = del->_right;delete del;del = nullptr;}}// case 2: 右子树为空else if (del->_right == nullptr){if (_root == del){Node* newroot = del->_left;delete _root;_root = newroot;}else{if (del_parent->_left == del)del_parent->_left = del->_left;elsedel_parent->_right = del->_left;delete del;del = nullptr;}}// case 3: 左右子树都不为空else {// 从被删除节点开始, 找右子树的最小(左)节点 || 找左子树的最大(右)节点if (del->_right)_erase_right_min_node(del);else_erase_left_max_node(del);}return true;}}return false;
}void _erase_right_min_node(Node* del) 
{// 从被删除结点开始, 找右子树的最小(左)节点Node* right_min = del->_right;// 并记录这个节点的父亲节点, 让其从del开始Node* right_min_parent = del;while (right_min->_left){right_min_parent = right_min;right_min = right_min->_left;}// 交换这个节点和要删除节点的 keystd::swap(del->_key, right_min->_key);// 将删除 del 转化为删除 right_min (托孤法删除)if (right_min_parent->_left == right_min)right_min_parent->_left = right_min->_right;elseright_min_parent->_right = right_min->_right;delete right_min;right_min = nullptr;
}
void _erase_left_max_node(Node* del)
{// 从被删除节点开始, 找左子树的最大(右)节点Node* left_max = del->_left;// 并记录这个节点的父亲节点, 让其从del开始Node* left_max_parent = del;while (left_max->_right){left_max_parent = left_max;left_max = left_max->_right;}// 交换这个节点和要删除节点的 keystd::swap(del->_key, left_max->_key);// 将删除 del 转化为删除 left_max (托孤法删除)if (left_max_parent->_left == left_max)left_max_parent->_left = left_max->_left;elseleft_max_parent->_right = left_max->_left;delete left_max;left_max = nullptr;
}

二叉树进阶 --- 上,至此结束。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/10488.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基本QinQ

拓扑图 配置 开启LLDP功能&#xff0c;查看是否能通过QinQ隧道透传 sysname AR1 # lldp enable # interface GigabitEthernet0/0/0.10dot1q termination vid 10ip address 12.1.1.1 255.255.255.0 arp broadcast enable # sysname AR2 # lldp enable # interface GigabitE…

天童教育:孩子是难以改变的,除非他感觉到爱

在家长眼里&#xff0c;明明对孩子上了很多心&#xff0c;但是孩子就是很顽固&#xff0c;软硬皆施都收效甚微&#xff0c;家长们束手无策。都说孩子是最容易接受知识的熏陶的年纪&#xff0c;为什么看起来这样难以改变呢&#xff1f; 孩子有问题&#xff0c;往往因为内在的渴…

地磁暴红色预警来袭,普通人该如何应对?绝绝子的防护指南来了

近日&#xff0c;国家空间天气监测预警中心发布了一则令人瞩目的消息——地磁暴红色预警。这一预警不仅提醒我们地磁暴即将影响我国的电离层和低轨卫星&#xff0c;更让我们深刻认识到地球空间环境的脆弱性和复杂性。对于普通公众而言&#xff0c;地磁暴的概念可能相对陌生&…

随手集☞springboot知识盘点

概述 Spring Boot是一个开源的Java框架&#xff0c;由Pivotal团队开发&#xff0c;旨在简化和加速基于Java的应用程序的开发过程。它提供了一套开发工具和约定&#xff0c;使得构建独立、可执行的、生产级别的Spring应用变得更加容易。Spring Boot的主要目标是减少开发者在项目…

【每日刷题】Day37

【每日刷题】Day37 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;每日刷题&#x1f34d; &#x1f33c;文章目录&#x1f33c; 1. 2391. 收集垃圾的最少总时间 - 力扣&#xff08;LeetCode&#xff09; 2. 1614. 括号的最大嵌套深度…

你可能喜欢但也许还不知道的好用网站-搜嗖工具箱

在线工具 https://www.zxgj.cn/ 作为一个工作生活好帮手&#xff0c;在线咨询网站提供了丰富的实用功能&#xff0c;从工作中的图表制作、图片修改到生活中的各种测试、健康、娱乐、学习、理财等等涵盖面很广。 在线工具网站从界面和操作上来看对用户也很友好&#xff0c;页面…

论文研读 An Image Is Worth 16x16 Words: Transformers For Image Recognition At Scale

完整翻译 《An Image is Worth 16x16 Words》完整版翻译_an image is worth 16*16words-CSDN博客 大神讲解 Vision Transformer详解-CSDN博客 视频讲解 11.1 Vision Transformer(vit)网络详解_哔哩哔哩_bilibili 学习整理 简要概述&#xff1a;Vision Transformer&#xff…

Typescript高级: 深入理解断言

概述 关于断言&#xff0c;就是TS 类型断言&#xff0c;即把两种能有重叠关系的数据类型进行相互转换的一种 TS 语法把其中的一种数据类型转换成另外一种数据类型类型断言和类型转换产生的效果一样&#xff0c;但语法格式不同TS 类型断言语法格式&#xff1a;A 数据类型的变量…

在 Kubernetes 上运行 Apache Spark 进行大规模数据处理的实践

在刚刚结束的 Kubernetes Community Day 上海站&#xff0c;亚马逊云科技在云原生分论坛分享的“在 Kunernets 上运行 Apache Spark 进行大规模数据处理实践”引起了现场参与者的关注。开发者告诉我们&#xff0c;为了充分利用 Kubernetes 的高可用设计、弹性&#xff0c;在越来…

AIGC (AI-Generated Content) 技术深度探索:现状、挑战与未来愿景

&#x1f525; 个人主页&#xff1a;空白诗 文章目录 &#x1f916; AIGC技术&#xff1a;塑造未来的创意与内容革命 &#x1f31f;引言 &#x1f680;AIGC技术发展现状 &#x1f4c8;核心技术驱动 &#x1f4a1;应用领域拓展 &#x1f310; 面临的挑战 ❌真实性与伦理考量 &am…

SAP-CentralFinance - 会计核算中的组织要素 - 学习心得1

1. 定义SAP组织架构和理解各组织架构含义 组织结构遍布SAP 系统的所有重要功能范围。FI 中最重要的组织要素是公司代码。它是“财务会计”中的最小组织单位,可以为其编制自主式完整科目集供外部报告使用。其他重要的组织要素是利润中心业务范围和段。您可以为各个利润中…

大模型微调之 在亚马逊AWS上实战LlaMA案例(十)

大模型微调之 在亚马逊AWS上实战LlaMA案例&#xff08;十&#xff09; 训练数据集格式 SageMaker JumpStart 目前支持域适应格式和指令调整格式的数据集。在本节中&#xff0c;我们指定两种格式的示例数据集。有关更多详细信息&#xff0c;请参阅附录中的数据集格式化部分。 …

Vue进阶(贰零捌)浏览器地址栏URL防篡改攻略

文章目录 一、前言二、动态路由三、Params 参数四、在 URL 中使用加密参数五、参数转码六、综述七、拓展阅读 一、前言 在应用 vue.js 开发前端项目时&#xff0c;浏览器中url 地址栏参数的处理是一个很基础但却很重要的问题。在很多情况下&#xff0c;我们需要从 url 中获取参…

采购管理软件:采购自动化提高效率的5种方式

在采购领域&#xff0c;手动数据输入和耗时的文书工作的时代已经落后了。采购自动化正在改变游戏规则&#xff0c;使企业能够简化流程、降低成本并提高效率。 以下是采购自动化帮助企业提高效率的5种方法。 采购管理软件,采购自动化管理,8Manage SRM,高亚科技 减少手动流程和…

安卓手机APP开发__屏幕兼容性概述

安卓手机APP开发__屏幕兼容性概述 目录 概述 屏幕大小 灵活的布局 备选的布局 可改写的图像 像素的深度 深度依赖 备选的位图 向量图形 概述 安卓运行各种各样的设备&#xff0c;它们有不同的屏幕大小和屏幕的像素深度。 系统执行基本的缩放和变形来适配在不同的屏幕…

iview(viewUI) span-method 表格实现将指定列的值相同的行合并单元格

效果图是上面这样的&#xff0c;将第一列的名字一样的合并在一起&#xff1b; <template><div class"table-wrap"><Table stripe :columns"columns" :data"data" :span-method"handleSpan"></Table></div&…

HDFS- DataNode磁盘扩缩容

HDFS- DataNode磁盘扩缩容 背景: 缩减/增加节点磁盘 方案介绍: 采用hdfs dfsadmin -reconfig 动态刷新配置实现,不停服扩缩容。 注意事项: 请在进行缩容之前,务必了解实际的数据量,并确保磁盘有足够的空间来容纳这些数据。还需要考虑未来的使用需求,要预留一定数量的空间…

如何成为一个优秀的程序员

1 衡量一个程序员是否优秀&#xff0c;不是看程序员懂多少编程语言和编程技能&#xff0c;也不是看程序员做过多少项目&#xff0c;写过多少行代码&#xff0c;发表多少篇技术文章&#xff0c;甚至也不是看程序员的工作年限。 衡量程序员&#xff0c;第一看薪资&#xff0c;第…

java+vue3+iclientol实现警务地理信息系统实践

警务地理信息系统&#xff08;Police Geographic Information System, PGIS&#xff09;是一种专为警务工作设计的地理信息系统&#xff0c;它结合了地理信息技术、数据库技术、网络技术和现代警务理念&#xff0c;旨在提升公安机关的空间数据分析、决策支持、指挥调度、案件管…

SpringMVC的WebMvcConfigurer及返回

由于很久很久没有做过纯springmvc的代码了&#xff0c;好多东西都遗忘&#xff0c;最近接手了一个古早项目springmvc的。记录一下&#xff1a; 1、WebMvcConfigurer 是 Spring Framework 中的一个接口&#xff0c;它提供了一种扩展 Spring MVC 配置的方式。通过实现 WebMvcConf…