【算法思想·二叉搜索树】基操篇

news2024/11/24 14:38:16

本文参考labuladong算法笔记[二叉搜索树心法(基操篇) | labuladong 的算法笔记]

1、概述

我们前文 东哥带你刷二叉搜索树(特性篇) 介绍了 BST 的基本特性,还利用二叉搜索树「中序遍历有序」的特性来解决了几道题目,本文来实现 BST 的基础操作:判断 BST 的合法性、增、删、查。其中「删」和「判断合法性」略微复杂。

BST 的基础操作主要依赖「左小右大」的特性,可以在二叉树中做类似二分搜索的操作,寻找一个元素的效率很高。

对于 BST 相关的问题,你可能会经常看到类似下面这样的代码逻辑:

def BST(root: TreeNode, target: int) -> None:
    if root.val == target:
        # 找到目标,做点什么
    if root.val < target:
        BST(root.right, target)
    if root.val > target:
        BST(root.left, target)

这个代码框架其实和二叉树的遍历框架差不多,无非就是利用了 BST 左小右大的特性而已。接下来看下 BST 这种结构的基础操作是如何实现的。

2、判断 BST 的合法性

力扣98 .「验证二叉搜索树」

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

有效 二叉搜索树定义如下:

  • 节点的左子树只包含 小于 当前节点的数。
  • 节点的右子树只包含 大于 当前节点的数。
  • 所有左子树和右子树自身必须也是二叉搜索树。

示例 1:

输入:root = [2,1,3]
输出:true

示例 2:

输入:root = [5,1,4,null,null,3,6]
输出:false
解释:根节点的值是 5 ,但是右子节点的值是 4 。

提示:

  • 树中节点数目范围在[1, 104] 内
  • -231 <= Node.val <= 231 - 1

【思路】

注意,这里是有坑的哦。按照 BST 左小右大的特性,每个节点想要判断自己是否是合法的 BST 节点,要做的事不就是比较自己和左右孩子吗?感觉应该这样写代码:

def isValidBST(root: TreeNode) -> bool:
    if root is None:
        return True
    # root 的左边应该更小
    if root.left is not None and root.left.val >= root.val:
        return False
    # root 的右边应该更大
    if root.right is not None and root.right.val <= root.val:
        return False

    return isValidBST(root.left) and isValidBST(root.right)

 但是这个算法出现了错误,BST 的每个节点应该要小于右边子树的所有节点,下面这个二叉树显然不是 BST,因为节点 10 的右子树中有一个节点 6,但是我们的算法会把它判定为合法 BST:

错误的原因在于,对于每一个节点 root,代码值检查了它的左右孩子节点是否符合左小右大的原则;但是根据 BST 的定义,root 的整个左子树都要小于 root.val,整个右子树都要大于 root.val

问题是,对于某一个节点 root,他只能管得了自己的左右子节点,怎么把 root 的约束传递给左右子树呢?请看正确的代码:

【python】

class Solution:
    def isValidBST(self, root: TreeNode) -> bool:
        return self._isValidBST(root, None, None)

    # 定义:该函数返回 root 为根的子树的所有节点是否满足 max.val > root.val > min.val
    def _isValidBST(self, root: TreeNode, min: TreeNode, max: TreeNode) -> bool:
        # base case
        if root is None:
            return True
        # 若 root.val 不符合 max 和 min 的限制,说明不是合法 BST
        if min is not None and root.val <= min.val:
            return False
        if max is not None and root.val >= max.val:
            return False
        # 根据定义,限定左子树的最大值是 root.val,右子树的最小值是 root.val
        return self._isValidBST(root.left, min, root) and self._isValidBST(root.right, root, max)

3、在 BST 中搜索元素

700 .「二叉搜索树中的搜索」

给定二叉搜索树(BST)的根节点 root 和一个整数值 val

你需要在 BST 中找到节点值等于 val 的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 null 。

示例 1:

输入:root = [4,2,7,1,3], val = 2
输出:[2,1,3]

示例 2:

输入:root = [4,2,7,1,3], val = 5
输出:[]

提示:

  • 树中节点数在 [1, 5000] 范围内
  • 1 <= Node.val <= 107
  • root 是二叉搜索树
  • 1 <= val <= 107

如果是在一棵普通的二叉树中寻找,可以这样写代码:

def searchBST(root, target):
    if not root:
        return None
    if root.val == target:
        return root
    # 当前节点没找到就递归地去左右子树寻找
    left = searchBST(root.left, target)
    right = searchBST(root.right, target)

    return left if left else right

这样写完全正确,但这段代码相当于穷举了所有节点,适用于所有二叉树。那么应该如何充分利用 BST 的特殊性,把「左小右大」的特性用上?

很简单,其实不需要递归地搜索两边,类似二分查找思想,根据 target 和 root.val 的大小比较,就能排除一边。我们把上面的思路稍稍改动:

def searchBST(root: TreeNode, target: int) -> TreeNode:
    # 如果二叉树为空,直接返回
    if not root:
        return None
    # 去左子树搜索
    if root.val > target:
        return searchBST(root.left, target)
    # 去右子树搜索
    if root.val < target:
        return searchBST(root.right, target)
    # 当前节点就是目标值
    return root

4、在 BST 中插入一个数

对数据结构的操作无非遍历 + 访问,遍历就是「找」,访问就是「改」。具体到这个问题,插入一个数,就是先找到插入位置,然后进行插入操作。

因为 BST 一般不会存在值重复的节点,所以我们一般不会在 BST 中插入已存在的值。下面的代码都默认不会向 BST 中插入已存在的值

上一个问题,我们总结了 BST 中的遍历框架,就是「找」的问题。直接套框架,加上「改」的操作即可。

一旦涉及「改」,就类似二叉树的构造问题,函数要返回 TreeNode 类型,并且要对递归调用的返回值进行接收

701. 「二叉搜索树中的插入操作」

给定二叉搜索树(BST)的根节点 root 和要插入树中的值 value ,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 ,新值和原始二叉搜索树中的任意节点值都不同。

注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回 任意有效的结果 。

示例 1:

输入:root = [4,2,7,1,3], val = 5

输出:[4,2,7,1,3,5]
解释:另一个满足题目要求可以通过的树是:

示例 2:

输入:root = [40,20,60,10,30,50,70], val = 25
输出:[40,20,60,10,30,50,70,null,null,25]

示例 3:

输入:root = [4,2,7,1,3,null,null,null,null,null,null], val = 5
输出:[4,2,7,1,3,5]

提示:

  • 树中的节点数将在 [0, 104]的范围内。
  • -108 <= Node.val <= 108
  • 所有值 Node.val 是 独一无二 的。
  • -108 <= val <= 108
  • 保证 val 在原始BST中不存在。

【python】

class Solution:
    def insertIntoBST(self, root: TreeNode, val: int) -> TreeNode:
        if not root:
            # 找到空位置插入新节点
            return TreeNode(val)
        # 去右子树找插入位置
        if root.val < val:
            root.right = self.insertIntoBST(root.right, val)
        # 去左子树找插入位置
        if root.val > val:
            root.left = self.insertIntoBST(root.left, val)
        # 返回 root,上层递归会接收返回值作为子节点
        return root

5、在 BST 中删除一个数

450. 删除二叉搜索树中的节点 | 力扣  

给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。

一般来说,删除节点可分为两个步骤:

  1. 首先找到需要删除的节点;
  2. 如果找到了,删除它。

示例 1:

输入:root = [5,3,6,2,4,null,7], key = 3
输出:[5,4,6,2,null,null,7]
解释:给定需要删除的节点值是 3,所以我们首先找到 3 这个节点,然后删除它。
一个正确的答案是 [5,4,6,2,null,null,7], 如下图所示。
另一个正确答案是 [5,2,6,null,4,null,7]。

示例 2:

输入: root = [5,3,6,2,4,null,7], key = 0
输出: [5,3,6,2,4,null,7]
解释: 二叉树不包含值为 0 的节点

示例 3:

输入: root = [], key = 0
输出: []

提示:

  • 节点数的范围 [0, 104].
  • -105 <= Node.val <= 105
  • 节点值唯一
  • root 是合法的二叉搜索树
  • -105 <= key <= 105

进阶: 要求算法时间复杂度为 O(h),h 为树的高度。

【思路】

这个问题稍微复杂,跟插入操作类似,先「找」再「改」,先把框架写出来再说:

def deleteNode(root: TreeNode, key: int) -> TreeNode:
    if root.val == key:
        # 找到啦,进行删除
    elif root.val > key:
        # 去左子树找
        root.left = deleteNode(root.left, key)
    elif root.val < key:
        # 去右子树找
        root.right = deleteNode(root.right, key)
    return root

找到目标节点了,比方说是节点 A,如何删除这个节点,这是难点。因为删除节点的同时不能破坏 BST 的性质。有三种情况,用图片来说明。

情况 1A 恰好是末端节点,两个子节点都为空,那么它可以当场去世了。

if (root.left == null && root.right == null)
    return null;

情况 2A 只有一个非空子节点,那么它要让这个孩子接替自己的位置。

// 排除了情况 1 之后
if (root.left == null) return root.right;
if (root.right == null) return root.left;

情况 3A 有两个子节点,麻烦了,为了不破坏 BST 的性质,A 必须找到左子树中最大的那个节点,或者右子树中最小的那个节点来接替自己。我们以第二种方式讲解。

if (root.left != null && root.right != null) {
    // 找到右子树的最小节点
    TreeNode minNode = getMin(root.right);
    // 把 root 改成 minNode
    root.val = minNode.val;
    // 转而去删除 minNode
    root.right = deleteNode(root.right, minNode.val);
}

三种情况分析完毕,填入框架,简化一下代码:

class Solution:
    '''
    思路:
        1、考虑每个节点应该做什么,应该用递归思路求解
        2、对于单个节点,分为节点值 大于/小于/等于 key三种情况讨论
        3、节点值大于或小于key时,直接递归调用 deleteNode 函数,重塑该节点
        4、节点值等于key时,考虑被删节点处的三种结构,无子节点、有一个子节点、有两个子节点
        5、若有两个子节点,可找到左子树最大节点或右子树最小节点将其删除,并对node做替换
        6、min_node.left = root.left, min_node.right = root.right
    '''
    def deleteNode(self, root: TreeNode, key: int) -> TreeNode:
        if root == None:
            return None
        if root.val == key:
            # 这两个 if 把情况 1 和 2 都正确处理了
            if root.left == None:
                return root.right
            if root.right == None:
                return root.left
            # 处理情况 3
            # 获得右子树最小的节点
            minNode = self.getMin(root.right)
            # 删除右子树最小的节点
            root.right = self.deleteNode(root.right, minNode.val)
            # 用右子树最小的节点替换 root 节点
            minNode.left = root.left
            minNode.right = root.right
            root = minNode
        elif root.val > key:
            root.left = self.deleteNode(root.left, key)
        elif root.val < key:
            root.right = self.deleteNode(root.right, key)
        return root

    def getMin(self, node: TreeNode) -> TreeNode:
        # BST 最左边的就是最小的
        while node.left != None:
            node = node.left
        return node

6、总结

1、如果当前节点会对下面的子节点有整体影响,可以通过辅助函数增长参数列表,借助参数传递信息。

2、掌握 BST 的增删查改方法。

3、递归修改数据结构时,需要对递归调用的返回值进行接收,并返回修改后的节点。

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

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

相关文章

OpenAI的o1模型与Transformer的无限潜力:数学证明推理算力无上限

近期&#xff0c;斯隆奖得主马腾宇和Google Brain推理团队创始人Denny Zhou合作&#xff0c;提出了一项引人注目的数学证明&#xff1a;只要思维链&#xff08;CoT&#xff09;足够长&#xff0c;Transformer就有能力解决各种复杂问题。这一发现引发了广泛关注&#xff0c;因为…

驱动器磁盘未格式化难题:深度剖析与恢复实践

驱动器磁盘未格式化的深层探索 在数据存储与管理的日常中&#xff0c;驱动器作为我们数字生活的基石&#xff0c;其稳定性直接关系到数据的安全与可用性。然而&#xff0c;当屏幕上赫然出现“驱动器中的磁盘未被格式化”的提示时&#xff0c;许多用户往往感到手足无措&#xf…

把设计模式用起来!(3)用不好模式?之时机不对

上一篇&#xff1a;《把设计模式用起来&#xff08;2&#xff09;——用不好&#xff1f;之实践不足》 本篇继续讲设计模式用不好的常见原因&#xff0c;这是第二个&#xff1a;使用设计模式的时机不对。 二、时机不对 这里说的时机并不是单纯指软件研发周期中的时间阶段&…

C++11新增特性:lambda表达式、function包装器、bind绑定

一、lambda表达式 1&#xff09;、为啥需要引入lambda&#xff1f; 在c98中&#xff0c;我们使用sort对一段自定义类型进行排序的时候&#xff0c;每次都需要传一个仿函数&#xff0c;即手写一个完整的类。甚至有时需要同时实现排升序和降序&#xff0c;就需要各自手写一个类&…

基于SSM的社区爱心捐赠管理系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、SSM项目源码 系统展示 【2025最新】基于JavaSSMVueMySQL的社区爱…

任嘉伦新剧《流水迢迢》:卫昭多层人设引关注

近日&#xff0c;由晋江文学城同名小说改编的武侠古装爱情传奇剧《流水迢迢》即将开播&#xff0c;这部由任嘉伦主演的新剧&#xff0c;在原著和阵容的双双加持下热度直线上涨&#xff0c;宣传阶段就已备受网友期待&#xff0c;预约人数截止9月13日已达到206万&#xff0c;上升…

通信工程学习:什么是GPON吉比特无源光网络

GPON&#xff1a;吉比特无源光网络 GPON&#xff08;Gigabit-Capable Passive Optical Network&#xff0c;吉比特无源光网络&#xff09;是一种基于ITU-T G.984.x标准的最新一代宽带无源光综合接入技术。该技术以其高带宽、高效率、大覆盖范围和用户接口丰富等特点&#xff0c…

ubuntu服务器版NVIDIA驱动失效解决方案

ubuntu服务器版NVIDIA驱动失效解决方案 1. 问题描述2. 解决方法--卸载并重新安装最新版显卡驱动cudacudnn2.1 卸载显卡驱动2.2 重新安装最新版显卡驱动cudacudnn2.2.1 显卡驱动2.2.2 cuda2.2.3 cuda安装cudnn 1. 问题描述 在终端输入nvidia-smi&#xff0c;输出如下&#xff1…

Leetcode—移除元素

移除元素 题目描述 思路 思路&#xff1a;定义两个指针变量指向数组第一个位置&#xff0c;判断nums[scr]是否等于val case1:相等&#xff0c;scr; case2:不相等&#xff0c;nums[dst]nums[scr],scr,dst; 时间复杂度&#xff1a;O&#xff08;n&#xff09;&#xff1b;空间复杂…

微信支付开发-后台统计工厂实现

一、数据库设计图 二、后端统计工厂逻辑 1、统计父抽象类 a、StatisticsHandle.php 2、统计工厂通道类 a、StatisticsFactory.php 3、查询实现类 a、答题统计(Answer.php) 三、后端统计工厂代码实现 1、统计父抽象类(StatisticsHandle.php) <?php /*** 统计父抽象类* Use…

基于密码的大模型安全治理的思考

文章目录 前言一、大模型发展现状1.1 大模型技术的发展历程1.2 大模型技术的产业发展二、大模型安全政策与标准现状2.1 国外大模型安全政策与标准2.2 我国大模型安全政策与标准前言 随着大模型技术的迅速发展和广泛应用,其安全性问题日益凸显。密码学作为网络空间安全的核心技…

Linux搭建邮箱服务器(简易版)

本章是上一文档的简易版本搭建方式更为快速简洁&#xff08;只需要两条命令即可搭建&#xff09;&#xff0c;如果想了解更详细一些可以看我上一文档 Linux接发邮件mailx_linux mailx o365-CSDN博客文章浏览阅读857次&#xff0c;点赞25次&#xff0c;收藏19次。本文详细描述了…

计算机组成原理-3.1储存系统

现代结构 1.储存器的层次结构 辅存的数据要调入主存后才能被CUP&#xff0c;与操作系统的进程进行联动 运行速度&#xff1a;CPU>寄存器>Cache>主存>磁盘>磁盘和光盘 主存-辅存:实现了虚拟系统&#xff0c;解决了主存容量不够的问题。 Cache-主存&#xff1a…

二叉树的前中后序遍历(递归法)( 含leetcode上三道【前中后序】遍历题目)

文章目录 深入理解递归思想递归三要素 leetcode上三道题目&#xff1a;144.二叉树的前序遍历145.二叉树的后序遍历94.二叉树的中序遍历 深入理解递归思想 这次我们要好好谈一谈递归&#xff0c;为什么很多同学看递归算法都是“一看就会&#xff0c;一写就废”。 主要是对递归…

宝塔部署python项目

宝塔部署-python项目文章浏览阅读559次&#xff0c;点赞11次&#xff0c;收藏9次。在添加项目后&#xff0c;选择项目所在的路径&#xff0c;然后命令行启动主py文件。具体先看项目日志&#xff0c;根据日志在环境管理处下载包。首先下载项目需要的python版本。_宝塔部署python…

Typora安装,使用,图片加载全流程!!!

文章目录 前言&#xff1a;安装&#xff1a;破解&#xff1a;使用typora&#xff1a;关于CSDN加载不出图片&#xff1a;创建OSS&#xff1a;设置PicGo&#xff1a; 前言&#xff1a; ​ Typora是一款非常流行的Markdown编辑器&#xff0c;简单来说就是可以方便我们写博客。拿我…

禁忌搜索算法(TS算法)求解实例---旅行商问题 (TSP)

目录 一、采用TS求解 TSP二、 旅行商问题2.1 实际例子&#xff1a;求解 6 个城市的 TSP2.2 **求解该问题的代码**2.3 代码运行过程截屏2.4 代码运行结果截屏&#xff08;后续和其他算法进行对比&#xff09; 三、 如何修改代码&#xff1f;3.1 减少城市坐标&#xff0c;如下&am…

论文阅读: SigLit | SigLip |Sigmoid Loss for Language Image Pre-Training

论文地址&#xff1a;https://arxiv.org/pdf/2303.15343 项目地址&#xff1a;https://github.com/google-research/big_vision 发表时间&#xff1a;2023年3月27日 我们提出了一种用于语言图像预训练&#xff08;SigLIP&#xff09;的简单成对 Sigmoid 损失。与使用 softmax …

Redis 篇-初步了解 Redis 持久化、Redis 主从集群、Redis 哨兵集群、Redis 分片集群

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 分布式缓存概述 2.0 Redis 持久化 2.1 RDB 持久化 2.1.1 RDB 的 fork 原理 2.2 AOF 持久化 2.3 RDB 与 AOF 之间的区别 3.0 Redis 主从集群 3.1 搭建主从集群 3.2…

new/delete和malloc/free到底有什么区别

new和malloc 文章目录 new和malloc前言一、属性上的区别二、使用上的区别三、内存位置的区别四、返回类型的区别五、分配失败的区别六、扩张内存的区别七、系统调度过程的区别总结 前言 new和malloc的知识点&#xff0c;作为一个嵌入式工程师是必须要了解清楚的。new和malloc的…