Leetcode刷题笔记21:二叉树7(二叉搜索树专题)

223 阅读6分钟

导语

leetcode刷题笔记记录,本篇博客记录二叉搜索树部分的题目,主要题目包括:

  • 700 二叉搜索树中的搜索
  • 98 验证二叉搜索树
  • 530 二叉搜索树的最小绝对差
  • 501 二叉搜索树中的众数

知识点

二叉搜索树

二叉搜索树(Binary Search Tree,简称 BST) 是一种特殊的二叉树,它具有以下性质:

  1. 节点的键值唯一:树中每个节点的键值都是唯一的,不允许有重复的键值。
  2. 左子树的键值小于其父节点:对于树中的任意节点 N,其左子树中的所有节点的键值都小于 N 的键值。
  3. 右子树的键值大于其父节点:对于树中的任意节点 N,其右子树中的所有节点的键值都大于 N 的键值。
  4. 左右子树也都是二叉搜索树:这意味着定义是递归的。

基于上述性质,二叉搜索树有以下优势:

  • 有效的查找操作:可以高效地在 BST 中进行查找、添加和删除操作,平均时间复杂度为 O(log n),但在最坏的情况下(例如,树完全不平衡,类似于链表),这些操作的时间复杂度可能会达到 O(n)。
  • 中序遍历得到有序数组:对 BST 进行中序遍历会得到一个键值递增的有序序列,这使得 BST 成为从小到大或从大到小排序元素的一个很好的选择。
  • 范围查询简单高效:例如,查找所有在 K1 和 K2 之间的元素非常高效。
  • 灵活性:BST 可以用于构建一系列派生的数据结构,如平衡二叉搜索树、红黑树和树堆。

然而,二叉搜索树也存在缺点,其中最主要的是它可能会变得不平衡。例如,当连续插入已排序的数据时,它可能会退化为链表。为了解决这个问题,研究者们设计了多种自平衡二叉搜索树,如 AVL 树、红黑树等,它们可以在插入和删除操作后自动保持树的平衡。

Leetcode 700 二叉搜索树中的搜索

题目描述

给定二叉搜索树(BST)的根节点 root 和一个整数值 val。你需要在 BST 中找到节点值等于 val 的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 null 。   示例 1:

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

解法

二叉搜索树由于具有良好的排序特性,因此,本题在解答时需要充分利用,使用递归和层次遍历的方式解法都很直观,具体代码如下:

层次遍历

class Solution:
    def searchBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
        while root:
            if root.val < val:
                root = root.right
            elif root.val > val:
                root = root.left
            else:
                return root

递归

class Solution:
    def searchBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
        if root is None or root.val == val:
            return root
        elif root.val < val:
            return self.searchBST(root.right, val)
        else:
            return self.searchBST(root.left, val)

Leetcode 98 验证二叉搜索树

题目描述

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

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

示例 1:

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

解法

中序遍历+数组循环

由于二叉搜索树的中序遍历得到的一定是单调递增的数组,我们可以利用这个特性来判断一棵树是否为二叉搜索树。

class Solution:
    def isValidBST(self, root: Optional[TreeNode]) -> bool:
        nums = self.inorderTraversal(root)
        for i in range(len(nums)-1):
            if nums[i] >= nums[i+1]:
                return False
        return True

    def inorderTraversal(self, root):
        if root is None:
            return []
        left = self.inorderTraversal(root.left)
        right = self.inorderTraversal(root.right)

        return left + [root.val] + right

递归(+双指针)

同时,也可以写出双指针的递归解法,这里需要定义一个pre指针,用于记录当前递归的上一个遍历节点,因为要判断单调递增特性,所以需要使用中序遍历。

class Solution:
    def __init__(self):
        self.pre = None

    def isValidBST(self, root: Optional[TreeNode]) -> bool:
        if root is None:                          # 终止条件
            return True 
        left_valid = self.isValidBST(root.left)   # 左-处理逻辑
        if self.pre is not None and self.pre.val >= root.val:  # 中-处理逻辑
            return False
        self.pre = root
        right_valid = self.isValidBST(root.right) # 右-处理逻辑
        return left_valid and right_valid

易错点

最开始解答时,我给出的答案是这样的:

class Solution:
    def isValidBST(self, root: Optional[TreeNode]) -> bool:
        if root.left is None and root.right is None:
            return True
        if root.left is None:
            return root.val < root.right.val
        if root.right is None:
            return root.val > root.left.val
        if root.left.val >= root.val or root.val >= root.right.val:
            return False
        
        return self.isValidBST(root.left) and self.isValidBST(root.right)

这样的解答是错误的,原因在于主要考虑了每个节点与其直接的左、右子节点的关系,但没有考虑整体的范围限制。

  1. 只检查了当前节点与其子节点的关系:虽然代码确实检查了当前节点与其左右子节点的大小关系,但这并不足以确保整棵子树都是一个二叉搜索树。例如,示例2中,尽管5大于其左子节点1且小于其右子节点4,但右子树中的3却小于根节点5
  2. 没有对整个子树的值范围进行检查:即使某个节点满足上述条件,其子树中可能还有其他节点不满足整个BST的条件。例如,一个节点可能有一个右子节点,该子节点的左子节点的值小于当前节点。

Leetcode 530 二叉搜索树的最小绝对差

题目描述

给你一个二叉搜索树的根节点 root ,返回 树中任意两不同节点值之间的最小差值 。差值是一个正数,其数值等于两值之差的绝对值。

示例 1:

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

解法

中序遍历+数组循环

最直观的解法即直接中序遍历得到单调递增数组,然后记录相邻元素的最小差值即可,代码如下:

class Solution:
    def getMinimumDifference(self, root: Optional[TreeNode]) -> int:
        nums = self.inorderTraversal(root)
        min_sub = float("inf")
        for i in range(len(nums)-1):
            if nums[i+1] - nums[i] < min_sub:
                min_sub = nums[i+1] - nums[i]
                
        return min_sub

    def inorderTraversal(self, root):
        if root is None:
            return []
        return self.inorderTraversal(root.left) + [root.val] + self.inorderTraversal(root.right)

递归(+双指针)

同样的,依旧可以通过维护一个当前节点的前一个遍历节点的指针来完成一次遍历比较:

class Solution:
    def __init__(self):
        self.result = float('inf')
        self.pre = None

    def getMinimumDifference(self, root: Optional[TreeNode]) -> int:
        self.inorderTraversal(root)

        return self.result

    def inorderTraversal(self, root):
        if root is None:
            return 
        if root.left is not None: self.inorderTraversal(root.left)
        if self.pre is not None:
            self.result = min(root.val - self.pre.val, self.result)
        self.pre = root
        if root.right is not None: self.inorderTraversal(root.right)
        return 

Leetcode 501 二叉搜索树中的众数

题目描述

给你一个含重复值的二叉搜索树(BST)的根节点 root ,找出并返回 BST 中的所有 众数(即,出现频率最高的元素)。如果树中有不止一个众数,可以按 任意顺序 返回。假定 BST 满足如下定义:

  • 结点左子树中所含节点的值 小于等于 当前节点的值
  • 结点右子树中所含节点的值 大于等于 当前节点的值
  • 左子树和右子树都是二叉搜索树

示例 1:

输入: root = [1,null,2,2]
输出: [2]

解法

中序遍历+哈希

最直观的解法是我们在遍历的同时记录下每个元素的出现频次,然后直接找到频次最大的那个(些)值即可,对应的代码如下:

class Solution:
    def __init__(self):
        self.item2count = defaultdict(int)

    def findMode(self, root: Optional[TreeNode]) -> List[int]:
        self.inorderTraversal(root)
        # 找到最大的计数值
        max_count = max(self.item2count.values())

        # 返回所有计数值与最大计数值相等的元素
        return [num for num, count in self.item2count.items() if count == max_count]

    def inorderTraversal(self, root):
        if root is None:
            return
        self.item2count[root.val] += 1
        if root.left: self.inorderTraversal(root.left)
        if root.right: self.inorderTraversal(root.right)

        return

递归(+双指针)

这道题目同样也可以使用双指针的方法,我们需要维护最大频次max_num和最终的结果。双指针pre指向当前遍历节点root的上一个节点,由于是二叉搜索树,若是相同的数值,那么中序遍历过程中必定是挨着的。

所以我们每次在root和pre都指向一个值时给count++,最后判断如果count大于max_num后,清空结果result,添加最新的最大频率元素。

这样之后,直接返回result数组就是要找的众数。

class Solution:
    def __init__(self):
        self.max_num = 0
        self.count = 0
        self.result = []
        self.pre = None

    def findMode(self, root: Optional[TreeNode]) -> List[int]:
        self.inorderTraversal(root)

        return self.result
    
    def inorderTraversal(self, root):
        if root is None:
            return
        self.inorderTraversal(root.left)
        if self.pre is None:
            self.count = 1
        elif self.pre.val == root.val:
            self.count += 1
        else:
            self.count = 1
        self.pre = root
        if self.count == self.max_num:
            self.result.append(root.val)
        if self.count > self.max_num:
            self.max_num = self.count
            self.result.clear()
            self.result.append(root.val)
        self.inorderTraversal(root.right)
        
        return