您的位置:首页 > 娱乐 > 八卦 > 【算法思想·二叉搜索树】基操篇

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

2024/12/23 4:44:18 来源:https://blog.csdn.net/Captain823Jack/article/details/142319561  浏览:    关键词:【算法思想·二叉搜索树】基操篇

本文参考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 Falsereturn 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.valdef _isValidBST(self, root: TreeNode, min: TreeNode, max: TreeNode) -> bool:# base caseif root is None:return True# 若 root.val 不符合 max 和 min 的限制,说明不是合法 BSTif min is not None and root.val <= min.val:return Falseif max is not None and root.val >= max.val:return False# 根据定义,限定左子树的最大值是 root.val,右子树的最小值是 root.valreturn 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 Noneif 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 改成 minNoderoot.val = minNode.val;// 转而去删除 minNoderoot.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 Noneif root.val == key:# 这两个 if 把情况 1 和 2 都正确处理了if root.left == None:return root.rightif root.right == None:return root.left# 处理情况 3# 获得右子树最小的节点minNode = self.getMin(root.right)# 删除右子树最小的节点root.right = self.deleteNode(root.right, minNode.val)# 用右子树最小的节点替换 root 节点minNode.left = root.leftminNode.right = root.rightroot = minNodeelif root.val > key:root.left = self.deleteNode(root.left, key)elif root.val < key:root.right = self.deleteNode(root.right, key)return rootdef getMin(self, node: TreeNode) -> TreeNode:# BST 最左边的就是最小的while node.left != None:node = node.leftreturn node

6、总结

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

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

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

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com