中序遍历 二叉搜索树🌳

2,352 阅读13分钟

二叉搜索树

什么是二叉搜索树

二叉搜索树(Binary Search Tree,BST)是一种常用的数据结构,它是一棵二叉树,其中每个节点的值都大于其左子树中任意节点的值,小于其右子树中任意节点的值。

也就是说,对于一棵 BST,任何一个节点的左子树都比它小,任何一个节点的右子树都比它大。因此,如果我们对 BST 进行中序遍历,得到的就是一个有序的序列。

二叉搜索树的优点是:

  1. 支持快速的查找、插入、删除操作,时间复杂度为 O(logN)。
  2. 中序遍历的结果是有序的,可以进行快速的查找最大值、最小值、第 K 小值等操作。
  3. 由于是一种基于指针的数据结构,可以支持动态的插入、删除操作。

二叉搜索树的缺点是:

  1. 当 BST 的节点极度不平衡时,查找、插入、删除操作的时间复杂度可能会退化到 O(N),失去了二叉搜索树的优点。
  2. 如果使用 BST 存储一些特殊的数据,例如退化成链表的情况,它的性能可能会比其他数据结构差。

因此,在实际应用中,我们需要根据具体的需求选择合适的数据结构。

解法

二叉搜索树问题的解法有很多种,下面介绍几种常见的解法:

  1. 中序遍历

由于二叉搜索树的中序遍历结果是一个升序序列,因此可以通过中序遍历来解决一些问题。例如,验证二叉搜索树、二叉搜索树中第K小的元素等问题,都可以通过中序遍历来解决。

  1. 递归

二叉搜索树问题常常可以使用递归解决,因为二叉搜索树的左子树和右子树也是二叉搜索树,可以用相同的方法处理。例如,二叉搜索树的搜索、插入、删除等问题,都可以使用递归解决。

  1. 迭代

二叉搜索树问题也可以使用迭代解决,使用迭代时需要借助栈或队列等数据结构。例如,二叉搜索树的搜索、插入、删除等问题,都可以使用迭代解决。

  1. Morris 遍历

Morris 遍历是一种不需要栈和队列的二叉树遍历算法,可以在 O(1) 的空间复杂度下完成中序遍历。这种遍历方法基于线索二叉树的思想,可以在遍历时动态修改树的结构,非常高效。例如,二叉搜索树中第K小的元素问题,可以使用 Morris 遍历解决。

  1. 双指针

对于二叉搜索树中查找两个节点之间的距离等问题,可以使用双指针技巧解决。双指针的思想是,定义两个指针指向树中的节点,然后通过移动指针来计算它们之间的距离或关系。例如,二叉搜索树中两个节点的最近公共祖先问题,可以使用双指针解决。

总之,二叉搜索树问题的解法多种多样,需要根据具体问题来选择合适的方法。但是需要注意的是,二叉搜索树的插入、删除操作需要保证树的平衡性,因此在实现时需要格外小心。

二叉树中序遍历

这两个代码都是求二叉树的中序遍历,第一个是非递归写法,第二个是递归写法。

class Solution:
    def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        stack = []
        res = []
        while root or stack:
            while root:
                stack.append(root)
                root = root.left
            root = stack.pop()
            res.append(root.val)
            root = root.right
        return res

非递归写法使用了栈来模拟递归的过程,每次将左子树中的节点全部压入栈中,然后弹出栈顶元素,将其值加入结果数组中,再将右子树赋值给根节点,继续遍历右子树,直到栈为空。

class Solution:
    def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        res = []
        def inorder(root):
            if not root:
                return
            inorder(root.left)
            res.append(root.val)
            inorder(root.right)
        inorder(root)
        return res

递归写法则是典型的深度优先遍历,先遍历左子树,然后将根节点的值加入结果数组中,再遍历右子树。

这两种方法本质上是相同的,只是实现方式不同。在实际应用中,应根据具体的情况选择合适的方法。如果递归层数太深可能会导致栈溢出,而非递归方法则需要手动维护栈,增加了实现难度。

二叉搜索树和中序遍历之间存在着密切的关系。

二叉搜索树(Binary Search Tree,BST)是一种特殊的二叉树,它的每个节点的值都大于其左子树中的所有节点的值,小于其右子树中的所有节点的值。也就是说,如果对 BST 进行中序遍历,得到的序列是一个递增的有序序列。

因此,中序遍历是判断一个二叉树是否为 BST 的有效方法之一。我们只需要按照中序遍历的顺序依次比较相邻的节点,如果当前节点的值大于等于前一个节点的值,则这棵树是 BST。

此外,中序遍历还可以对 BST 进行排序,将树中的所有节点按照大小顺序排列。如果需要在 BST 中查找某个值,我们只需要对 BST 进行中序遍历,找到第一个大于等于目标值的节点即可。

因此,可以说二叉搜索树和中序遍历是密不可分的。二者相互依存,相互促进,为我们解决问题提供了很大的便利性。

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

给你一个二叉搜索树的根节点 root ,返回 树中任意两不同节点值之间的最小差值 。

差值是一个正数,其数值等于两值之差的绝对值。

 

示例 1:

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

示例 2:

输入: root = [1,0,48,null,null,12,49]
输出: 1

 

提示:

  • 树中节点的数目范围是 [2, 104]
  • 0 <= Node.val <= 105
class Solution:
    def getMinimumDifference(self, root: Optional[TreeNode]) -> int:
        stack = []
        num = []
        while root or stack:
            while root:
                stack.append(root)
                root = root.left
            root = stack.pop()
            num.append(root.val)
            root = root.right
        res = float('inf')
        for i in range(len(num)-1):
            res = min(abs(num[i+1]-num[i]),res)
        return res

这段代码是用来求二叉搜索树中任意两个节点值之差的最小值。

代码的实现过程为:首先用中序遍历得到 BST 中的所有节点值,并将其存储在数组 num 中;然后对数组 num 进行遍历,求出任意两个相邻节点值之差的最小值。

具体来说,我们可以通过以下步骤来理解代码:

  1. 定义一个栈 stack 和一个数组 num,用来存储中序遍历得到的节点值。

  2. 当 root 不为空或者栈 stack 不为空时,进行循环遍历。

  3. 在循环中,将 root 和其所有左子树节点依次入栈 stack 中,直到 root 为空。

  4. 弹出栈顶节点 root,并将其值加入数组 num 中。

  5. 将 root 指向其右子树,进行下一轮遍历。

  6. 遍历完成后,得到一个有序的节点值数组 num。

  7. 对数组 num 进行遍历,计算任意两个相邻节点值之差的最小值,返回结果。

由于是通过中序遍历得到的节点值数组,数组中的节点值是有序的。因此,我们可以通过遍历数组 num 来求出任意两个相邻节点值之差的最小值。

该算法的时间复杂度为 O(n),其中 n 表示 BST 中的节点数。空间复杂度为 O(n),主要用于存储节点值数组和栈的空间。

代码中可以优化掉那个res数组:

class Solution:
    def getMinimumDifference(self, root: Optional[TreeNode]) -> int:
        pre = float('-inf')
        minDiff = float('inf')
        stack = []
        while root or stack:
            while root:
                stack.append(root)
                root = root.left
            root = stack.pop()
            minDiff = min(minDiff, root.val-pre)
            pre = root.val
            root = root.right
        return minDiff
  1. 初始化pre为负无穷,minDiff为正无穷,stack为空。

  2. 当root非空或stack非空时,执行以下循环:

    a. 当root非空时,将root入栈,再将root的左子节点作为root继续执行循环,直到root为空。

    b. 当root为空时,从stack中弹出一个节点作为root。

    c. 计算当前节点值root.val与前一个节点值pre的差,更新minDiff的值为当前minDiff和root.val-pre的最小值。

    d. 将pre的值更新为root.val。

    e. 将root的右子节点作为新的root,继续执行循环。

  3. 返回minDiff的值。

230. 二叉搜索树中第K小的元素

给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 个最小元素(从 1 开始计数)。

 

示例 1:

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

示例 2:

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

 

 

提示:

  • 树中的节点数为 n 。
  • 1 <= k <= n <= 104
  • 0 <= Node.val <= 104

 

进阶: 如果二叉搜索树经常被修改(插入/删除操作)并且你需要频繁地查找第 k 小的值,你将如何优化算法?

class Solution:
    def kthSmallest(self, root: Optional[TreeNode], k: int) -> int:
        res = []
        stack = []
        while root or stack:
            while root:
                stack.append(root)
                root = root.left
            root = stack.pop()
            res.append(root.val)
            root = root.right
        return res[k-1]

这段代码是用来寻找二叉搜索树中第 k 小的节点值的。它的实现过程为:

  1. 定义一个空列表 res 和一个栈 stack,用于存储二叉搜索树的节点值;

  2. 如果当前节点存在或者栈不为空,则进行循环遍历;

  3. 在循环中,将当前节点及其左子树中所有的节点入栈 stack 中,直到当前节点为空;

  4. 弹出栈顶元素,将其节点值加入列表 res 中;

  5. 将当前节点指向其右子树;

  6. 遍历完成后,根据列表 res 中的顺序,返回第 k 小的节点值 res[k-1]。

由于二叉搜索树的中序遍历得到的结果是一个有序的序列,因此,通过中序遍历可以得到二叉搜索树的节点值的有序序列。这样就可以根据有序序列的特性,很方便地寻找第 k 小的节点值。

该算法的时间复杂度为 O(n),其中 n 表示二叉搜索树的节点数。因为需要遍历整个二叉搜索树并将节点值存入列表中,所以空间复杂度为 O(n),其中 n 表示二叉搜索树的节点数。

当然其实根本没必要使用额外的那个res

class Solution:
    def kthSmallest(self, root: Optional[TreeNode], k: int) -> int:
        stack = []
        while root or stack:
            while root:
                stack.append(root)
                root = root.left
            root = stack.pop()
            k -= 1
            if k == 0:
                return root.val
            root = root.right

这段代码是用来查找二叉搜索树中第 k 小的元素。具体的实现过程为:

  1. 初始化一个栈 stack,用于存储遍历过的节点。

  2. 进入循环,当根节点不为空或栈不为空时,进行如下操作:

    1. 不断将当前节点的左子节点入栈,直到左子节点为空。

    2. 弹出栈顶节点,并将 k 减1。

    3. 如果 k 减到0,则返回当前节点的值。

    4. 将当前节点指向右子节点,进入下一轮循环。

  3. 如果未找到第 k 小的元素,则返回 None。

时间复杂度为 O(H + k),其中 H 为树的高度,空间复杂度为 O(H)。这里的空间复杂度是指栈空间的使用。

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
class Solution:
    def isValidBST(self, root: Optional[TreeNode]) -> bool:
        res = []
        stack = []
        while root or stack:
            while root:
                stack.append(root)
                root = root.left
            root = stack.pop()
            res.append(root.val)
            root = root.right
        for i in range(len(res)-1):
            if res[i] >= res[i+1]:
                return False
        return True

这段代码的功能是判断给定的二叉树是否为二叉搜索树(BST)。算法的主要思路是通过中序遍历得到二叉树的节点值序列,然后判断序列是否有序。

具体来说,代码的实现过程为:

  1. 定义一个栈和一个数组,分别用于存储节点和节点值;

  2. 当二叉树不为空或者栈不为空时,进行循环遍历;

  3. 在循环中,将二叉树的所有左子树节点依次入栈,直到遇到叶子节点;

  4. 弹出栈顶节点,将其值存入数组;

  5. 将二叉树的指针指向其右子树,进行下一轮遍历;

  6. 遍历完成后,得到一个有序的节点值数组;

  7. 对节点值数组进行遍历,判断序列是否有序,如果不是则返回 False,否则返回 True。

该算法的时间复杂度为 O(n),其中 n 表示二叉树的节点数,主要用于中序遍历的过程。空间复杂度也为 O(n),主要用于存储栈和节点值数组的空间。

当然这段代码也可以优化掉那个res数组

class Solution:
    def isValidBST(self, root: TreeNode) -> bool:
        stack = []
        pre = float('-inf')
        while stack or root:
            while root:
                stack.append(root)
                root = root.left
            root = stack.pop()
            if root.val <= pre:
                return False
            pre = root.val
            root = root.right
        return True

算法思路:

采用迭代的方式进行中序遍历,由于中序遍历得到的结果是一个递增的序列,因此只需要判断当前节点是否大于前一个节点即可。

具体实现:

  1. 初始化一个栈stack和一个pre变量,用于记录前一个遍历的节点的值,初始值为负无穷。

  2. 当栈不为空或当前节点不为空时,执行下面的操作:

    • 当前节点存在,将其左子树的所有节点入栈。

    • 取出栈顶元素,如果其值小于或等于pre,说明不满足二叉搜索树的定义,返回False。

    • 将当前节点的值赋值给pre,遍历其右子树。

    • 如果循环结束,仍未返回False,说明遍历完成,返回True。

时间复杂度为O(n),空间复杂度为O(n)。

173. 二叉搜索树迭代器

实现一个二叉搜索树迭代器类BSTIterator ,表示一个按中序遍历二叉搜索树(BST)的迭代器:

  • BSTIterator(TreeNode root) 初始化 BSTIterator 类的一个对象。BST 的根节点 root 会作为构造函数的一部分给出。指针应初始化为一个不存在于 BST 中的数字,且该数字小于 BST 中的任何元素。
  • boolean hasNext() 如果向指针右侧遍历存在数字,则返回 true ;否则返回 false 。
  • int next()将指针向右移动,然后返回指针处的数字。

注意,指针初始化为一个不存在于 BST 中的数字,所以对 next() 的首次调用将返回 BST 中的最小元素。

你可以假设 next() 调用总是有效的,也就是说,当调用 next() 时,BST 的中序遍历中至少存在一个下一个数字。

 

示例:

输入
["BSTIterator", "next", "next", "hasNext", "next", "hasNext", "next", "hasNext", "next", "hasNext"]
[[[7, 3, 15, null, null, 9, 20]], [], [], [], [], [], [], [], [], []]
输出
[null, 3, 7, true, 9, true, 15, true, 20, false]

解释
BSTIterator bSTIterator = new BSTIterator([7, 3, 15, null, null, 9, 20]);
bSTIterator.next();    // 返回 3
bSTIterator.next();    // 返回 7
bSTIterator.hasNext(); // 返回 True
bSTIterator.next();    // 返回 9
bSTIterator.hasNext(); // 返回 True
bSTIterator.next();    // 返回 15
bSTIterator.hasNext(); // 返回 True
bSTIterator.next();    // 返回 20
bSTIterator.hasNext(); // 返回 False

 

提示:

  • 树中节点的数目在范围 [1, 105] 内
  • 0 <= Node.val <= 106
  • 最多调用 105 次 hasNext 和 next 操作
class BSTIterator:

    def __init__(self, root: Optional[TreeNode]):
        self.queue = deque([root])
        self.iterator = []
        self.idx = 0
        self.inOrder(root)
        
    def inOrder(self,root):
        if not root:
            return
        self.inOrder(root.left)
        self.iterator.append(root.val)
        self.inOrder(root.right)

    def next(self) -> int:
        n = self.iterator[self.idx]
        self.idx += 1
        return n

    def hasNext(self) -> bool:
        if self.idx < len(self.iterator):
            return True
        else:
            return False

把inOrder改成这样也OK:

    def inOrder(self,root):
        stack = []
        while root or stack:
            while root:
                stack.append(root)
                root = root.left
            root = stack.pop()
            self.iterator.append(root.val)
            root = root.right

补充练习

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
class Solution:
    def searchBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
        if not root:
            return 
        if root.val == val:
            return root
        elif root.val < val:
            return self.searchBST(root.right,val)
        elif root.val > val:
            return self.searchBST(root.left,val)

这段代码的思路是利用二叉搜索树的特性,即左子树中的所有节点的值都小于根节点的值,右子树中的所有节点的值都大于根节点的值。因此,可以根据root的值和val的大小关系来判断要在哪个子树中查找。如果找到了节点,就返回该节点;否则返回None。

501. 二叉搜索树中的众数

给你一个含重复值的二叉搜索树(BST)的根节点 root ,找出并返回 BST 中的所有 众数(即,出现频率最高的元素)。

如果树中有不止一个众数,可以按 任意顺序 返回。

假定 BST 满足如下定义:

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

 

示例 1:

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

示例 2:

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

 

提示:

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

 

进阶: 你可以不使用额外的空间吗?(假设由递归产生的隐式调用栈的开销不被计算在内)

class Solution:
    def findMode(self, root):
        res = Counter()
        stack = []
        while root or stack:
            while root:
                stack.append(root)
                root = root.left
            root = stack.pop()
            res[root.val] += 1
            root = root.right
        m = max(res.values())
        ans = []
        for k,v in res.items():
            if v == m:
                ans.append(k)
        return ans
  1. 初始化一个 Counter 对象 res,用于存储树中每个值的频率。

  2. 初始化一个堆栈以跟踪树中的节点。

  3. 当根节点不为 None 或堆栈不为空时:

    • 将所有左子节点推入堆栈。
    • 从堆栈中弹出顶部节点。
    • 在 Counter res 中递增节点值的频率。
    • 将右子节点推入堆栈(如果存在)。
  4. 在 Counter res 中查找最大频率 m

  5. 创建一个空列表 ans

  6. 对于 Counter res 中的每个键值对 (k,v)

    • 如果值 v 等于最大频率 m,则将键 k 添加到列表 ans
  7. 返回包含树的众数的列表 ans