代码随想录算法训练营|Day 17

Day 17

第六章 二叉树 part05

详细布置

654.最大二叉树

又是构造二叉树,昨天大家刚刚做完 中序后序确定二叉树,今天做这个 应该会容易一些, 先看视频,好好体会一下 为什么构造二叉树都是 前序遍历

题目链接/文章讲解:https://programmercarl.com/0654.最大二叉树.html
视频讲解:https://www.bilibili.com/video/BV1MG411G7ox

写出来了,很简单

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def constructMaximumBinaryTree(self, nums: List[int]) -> Optional[TreeNode]:
        if not nums:
            return None
        root_val = max(nums)
        root = TreeNode(root_val)

        separator_idx = nums.index(root_val)

        nums_left = nums[:separator_idx]
        nums_right = nums[separator_idx+1:]

        root.left = self.constructMaximumBinaryTree(nums_left)
        root.right = self.constructMaximumBinaryTree(nums_right)

        return root

构造二叉树题目一定要用前序,先构造出根结点,再递归地去构造左子树和右子树

617.合并二叉树

这次是一起操作两个二叉树了, 估计大家也没一起操作过两个二叉树,也不知道该如何一起操作,可以看视频先理解一下。 优先掌握递归。

题目链接/文章讲解:https://programmercarl.com/0617.合并二叉树.html
视频讲解:https://www.bilibili.com/video/BV1m14y1Y7JK

按照前序来。中左右

递归法

1.确定递归函数参数和返回值

返回合并树的根结点,参数是tree1和tree2

2.处理终止条件

tree1遇到空节点,返回tree2对应位置的节点
-> if (tree1 == Null) return tree2;
-> if (tree2 == Null) return tree1;
这两句暗含了tree1和tree2同时为空的情况

3.确定单层递归的逻辑

不去定义新的二叉树,而去修改tree1的结构

新的tree1的左子树和右子树应该分别是新的合并两个tree的过程

class Solution:
    def mergeTrees(self, root1: TreeNode, root2: TreeNode) -> TreeNode:
        # 递归终止条件: 
        #  但凡有一个节点为空, 就立刻返回另外一个. 如果另外一个也为None就直接返回None. 
        if not root1: 
            return root2
        if not root2: 
            return root1
        # 上面的递归终止条件保证了代码执行到这里root1, root2都非空. 
        root1.val += root2.val # 中
        root1.left = self.mergeTrees(root1.left, root2.left) #左
        root1.right = self.mergeTrees(root1.right, root2.right) # 右
        
        return root1 # ⚠️ 注意: 本题我们重复使用了题目给出的节点而不是创建新节点. 节省时间, 空间. 

同样是递归,但是创建新树

class Solution:
    def mergeTrees(self, root1: TreeNode, root2: TreeNode) -> TreeNode:
        # 递归终止条件: 
        #  但凡有一个节点为空, 就立刻返回另外一个. 如果另外一个也为None就直接返回None. 
        if not root1: 
            return root2
        if not root2: 
            return root1
        # 上面的递归终止条件保证了代码执行到这里root1, root2都非空. 
        root = TreeNode() # 创建新节点
        root.val += root1.val + root2.val# 中
        root.left = self.mergeTrees(root1.left, root2.left) #左
        root.right = self.mergeTrees(root1.right, root2.right) # 右
        
        return root # ⚠️ 注意: 本题我们创建了新节点. 

迭代法

class Solution:
    def mergeTrees(self, root1: TreeNode, root2: TreeNode) -> TreeNode:
        if not root1: 
            return root2
        if not root2: 
            return root1

        queue = deque()
        queue.append(root1)
        queue.append(root2)

        while queue: 
            node1 = queue.popleft()
            node2 = queue.popleft()
            # 更新queue
            # 只有两个节点都有左节点时, 再往queue里面放.
            if node1.left and node2.left: 
                queue.append(node1.left)
                queue.append(node2.left)
            # 只有两个节点都有右节点时, 再往queue里面放.
            if node1.right and node2.right: 
                queue.append(node1.right)
                queue.append(node2.right)

            # 更新当前节点. 同时改变当前节点的左右孩子. 
            node1.val += node2.val
            if not node1.left and node2.left: 
                node1.left = node2.left
            if not node1.right and node2.right: 
                node1.right = node2.right

        return root1

这段代码用的是迭代的宽度优先搜索(BFS)来合并两棵二叉树,思路如下:

  1. 处理空树

    • 如果 root1 是空,就直接返回 root2
    • 如果 root2 是空,就直接返回 root1
  2. 初始化队列
    用一个双端队列 queue,每次存放一对要合并的节点 (node1, node2)

    queue = deque()
    queue.append(root1)
    queue.append(root2)
    
  3. BFS 合并
    当队列非空时,每次从队头取出一对节点 node1, node2

    node1 = queue.popleft()
    node2 = queue.popleft()
    
    • 合并节点值node1.val += node2.val

    • 处理左子节点

      • 如果两棵树在这一位置都存在左子节点,就把这一对子节点继续入队,留待后面合并;
      • 如果只有 node2.left 存在,就把它直接挂到 node1.left(不用再进一步合并);
    • 处理右子节点 同理。

  4. 返回结果
    队列遍历结束后,root1 上的每个节点都已经累加了 root2 对应节点的值,子节点也已正确挂接,最后返回 root1


关键点

  • 队列里“成对”存节点,保证每次处理的都是同一位置上要合并的两节点。

  • 先合并值,再决定怎么处理子节点

    • 对于“同在”的子节点,入队合并;
    • 对于“只有一边”的子节点,直接接过去。
  • 时空复杂度 都是 O(N),N 为两棵树中节点总数上限。

迭代法 代码优化

from collections import deque

class Solution:
    def mergeTrees(self, root1: TreeNode, root2: TreeNode) -> TreeNode:
        if not root1:
            return root2
        if not root2:
            return root1

        queue = deque()
        queue.append((root1, root2))

        while queue:
            node1, node2 = queue.popleft()
            node1.val += node2.val

            if node1.left and node2.left:
                queue.append((node1.left, node2.left))
            elif not node1.left:
                node1.left = node2.left

            if node1.right and node2.right:
                queue.append((node1.right, node2.right))
            elif not node1.right:
                node1.right = node2.right

        return root1

700.二叉搜索树中的搜索

递归和迭代 都可以掌握以下,因为本题比较简单, 了解一下 二叉搜索树的特性

题目链接/文章讲解: https://programmercarl.com/0700.二叉搜索树中的搜索.html
视频讲解:https://www.bilibili.com/video/BV1wG411g7sF

递归法

class Solution:
    def searchBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
        if not root:
            return None

        #也可以和return root合并
        #if not root or root.val == val:
        #    return root
        if root.val > val:
            return self.searchBST(root.left, val)
        elif root.val < val:
            return self.searchBST(root.right, val)
        else:
            return root

        

迭代法

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

98.验证二叉搜索树

遇到 搜索树,一定想着中序遍历,这样才能利用上特性。

但本题是有陷阱的,可以自己先做一做,然后在看题解,看看自己是不是掉陷阱里了。这样理解的更深刻。

题目链接/文章讲解:https://programmercarl.com/0098.验证二叉搜索树.html
视频讲解:https://www.bilibili.com/video/BV18P411n7Q4

由于二叉搜索树的特性,如果是中序遍历,可以得到有序数组

陷阱:不止要判断一个小三角。BST要求根结点大于左子树每个节点值,小于右子树每个节点值

即:中节点的值,大于左子树最大值,小于右子树最小值

img

递归法 转换成数组

class Solution:
    def __init__(self):
        self.vec = []

    def traversal(self, root):
        if root is None:
            return
        self.traversal(root.left)
        self.vec.append(root.val)  # 将二叉搜索树转换为有序数组
        self.traversal(root.right)

    def isValidBST(self, root):
        self.vec = []  # 清空数组
        self.traversal(root)
        for i in range(1, len(self.vec)):
            # 注意要小于等于,搜索树里不能有相同元素
            if self.vec[i] <= self.vec[i - 1]:
                return False
        return True

递归法 设定极小值,进行比较

class Solution:
    def __init__(self):
        self.maxVal = float('-inf')  # 因为后台测试数据中有int最小值

    def isValidBST(self, root):
        if root is None:
            return True

        left = self.isValidBST(root.left)
        # 中序遍历,验证遍历的元素是不是从小到大
        if self.maxVal < root.val:
            self.maxVal = root.val
        else:
            return False
        right = self.isValidBST(root.right)

        return left and right

这个解法利用了二叉搜索树(BST)的一个重要性质:中序遍历(in‑order traversal)会按从小到大的顺序访问节点。只要在中序遍历的过程中,发现当前访问的节点值不大于上一个访问的节点值,就能断定这棵树不是合法的 BST。


1. 核心思路

  1. 中序遍历顺序

    • 对一棵 BST 做“先遍历左子树 → 访问根节点 → 再遍历右子树”的中序遍历,会得到一个严格递增的序列。
  2. 跟踪上一个访问的值

    • self.maxVal 记录“上一次访问(上一个节点)的值”。
    • 初始设置为 -∞float('-inf')),这样任意整数第一次比较时都能通过。
  3. 比较与更新

    • 每访问到一个节点,就检查 node.val 是否严格大于 self.maxVal

      • 如果是,就更新 self.maxVal = node.val,继续遍历;
      • 如果不是,立刻返回 False,表示不满足 BST 的有序性。

2. 代码分步解析

class Solution:
    def __init__(self):
        # 记录中序遍历时“上一个”节点的值,初始设为负无穷
        self.maxVal = float('-inf')

    def isValidBST(self, root):
        # 空树也算合法 BST
        if root is None:
            return True

        # 1) 递归验证左子树
        left = self.isValidBST(root.left)

        # 如果左子树已经不是 BST,直接返回 False
        if not left:
            return False

        # 2) “访问”当前节点,检查顺序是否严格递增
        if root.val > self.maxVal:
            # 合法则更新 maxVal 为当前节点值
            self.maxVal = root.val
        else:
            # 遇到不满足“严格递增”时,立刻判定失败
            return False

        # 3) 递归验证右子树,并且只有左子树和当前节点都合法时,才继续检查右子树
        return self.isValidBST(root.right)
  • __init__
    用来初始化状态 —— self.maxVal 保存了“上一次访问节点的值”。

  • if root is None: return True
    空节点看作合法,递归到叶子孩子时就返回 True

  • 递归左子树 self.isValidBST(root.left)
    先保证左子树本身也是合法的 BST,如果左子树不合法,直接向上层返回 False,无需再看当前节点或右子树。

  • 检查当前节点

    • 比较 root.valself.maxVal
    • 如果 root.val <= self.maxVal,说明中序遍历序列没有保持严格递增,整个树肯定不是 BST。
  • 更新 self.maxVal
    root.val 合法时,把它记作新的“上一次访问值”,然后去遍历右子树。

  • 递归右子树
    右子树的所有节点都必须比当前节点大,同时右子树本身也要满足 BST 条件,最终结果是左子树合法 当前节点合法 右子树合法。


3. 为什么可行?

  • 全局唯一的 self.maxVal
    因为中序遍历是深度优先的「左→根→右」顺序,在遍历完一个节点的左子树后,self.maxVal 恰好是左子树中最大的节点值;接着比较根节点值,就能保证根节点大于左子树所有值。
  • 递归保证
    每一步都先验证“左子树合法”再验证“当前节点顺序正确”再验证“右子树合法”,逻辑严密,不会漏掉任何子树或分支。

总结:这是一种「中序遍历 + 追踪上一个节点值」的巧妙做法,用 O(N) 时间、O(H) 递归栈空间(H 是树高)就能完成 BST 验证。

递归法 直接取该树最小值

class Solution:
    def __init__(self):
        self.pre = None  # 用来记录前一个节点

    def isValidBST(self, root):
        if root is None:
            return True

        left = self.isValidBST(root.left)

        if self.pre is not None and self.pre.val >= root.val:
            return False
        self.pre = root  # 记录前一个节点

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

这一版依然是基于 中序遍历(in‑order traversal)的验证,只不过不再用一个“全局最大值”数值做比较,而是用一个 指向「上一次访问节点」的指针 self.pre

核心流程

def isValidBST(self, root):
    if root is None:
        return True

    # —— 1)先遍历左子树
    if not self.isValidBST(root.left):
        return False

    # —— 2)访问当前节点:用 self.pre 保存「前一个」节点
    #     对比前一个节点的值和当前节点的值,必须严格递增
    if self.pre is not None and self.pre.val >= root.val:
        return False

    # —— 3)更新 self.pre,表示「上一次访问」已经移动到当前节点
    self.pre = root

    # —— 4)再遍历右子树
    return self.isValidBST(root.right)
  1. 初始化
    构造函数里把 self.pre = None,表示“还没访问过任何节点”。

  2. 左 → 中 → 右
    按照 BST 的中序顺序,先递归走到最左,然后一路“回升”到父节点,最后遍历右子树。
    **

  3. 严格递增判断

    • 中序遍历时,前一次访问的节点一定是「比当前节点值小的最大那个值」。
    • self.pre.val < root.val 来验证这一点。
    • 一旦发现 self.pre.val >= root.val,就能立即断定不是合法 BST。
  4. 状态维护

    • 每次“访问”完一个节点(也就是中序遍历的“中”),都要把 self.pre 指向它,才能给下一次比较做准备。
  5. 递归短路

    • 左子树如果已经不合法,直接 return False,不会再去比较当前或右子树。
    • 同样,比较失败也立即返回,不用继续往下。

为什么这样可行?

  • 中序遍历 BST → 节点值严格递增:这是验证二叉搜索树最常用也最直观的性质。

  • self.pre 追踪上一个节点:相比用 float('-inf') 这种“哨兵值”,用指针更语义化,也不用担心节点值可能等于最小整数的问题。

  • 时间/空间复杂度

    • 时间 O(N),每个节点访问一次;
    • 空间 O(H),递归栈深度最多为树的高度 H。

小贴士:如果在同一个 Solution 实例上多次调用 isValidBST,记得 每次调用前 都把 self.pre = None 重置一遍,否则上一次的状态会影响下一次的判断。

迭代法

class Solution:
    def isValidBST(self, root):
        stack = []
        cur = root
        pre = None  # 记录前一个节点
        while cur is not None or len(stack) > 0:
            if cur is not None:
                stack.append(cur)
                cur = cur.left  # 左
            else:
                cur = stack.pop()  # 中
                if pre is not None and cur.val <= pre.val:
                    return False
                pre = cur  # 保存前一个访问的结点
                cur = cur.right  # 右
        return True

这一版用的还是 中序遍历 验证 BST,但把递归改成了显式的栈来实现——即所谓的「迭代中序遍历」。流程如下:

  1. 初始化

    • stack = []:用来模拟系统调用栈,保存「待访问」的节点。
    • cur = root:当前指针从根开始。
    • pre = None:记录上一次“访问”(中序中的“中”)的节点。
  2. 左→中→右 的迭代实现

    while cur is not None or stack:
        if cur is not None:
            # 一直往左走,把沿途节点都 push 进 stack
            stack.append(cur)
            cur = cur.left
        else:
            # 左子树走完了,pop 出栈顶节点当“中”
            cur = stack.pop()
            # —— 访问节点:检查和上一次访问值的大小关系
            if pre is not None and cur.val <= pre.val:
                return False
            pre = cur           # 更新“前一个节点”
            # 然后转向右子树,继续下一轮
            cur = cur.right
    
    • 压左:只要 cur 不为空,就把它压栈并继续 cur = cur.left,这样能先把所有左孩子都入栈。
    • 弹中:一旦 curNone,说明左子树走到底了,就 pop() 最近那个节点,把它当作“中序的中间节点”来“访问”。
    • 访问逻辑:中序访问时,保证前一次访问的节点 pre 的值一定小于当前节点值,若不满足就立刻 return False
    • 转右:访问完一个节点,就转向它的右子树——即 cur = cur.right,下次循环再开始把右子树的左链一路压栈。
  3. 结束条件

    • curNonestack 空了,说明所有节点都访问完,没有发现逆序情况,就返回 True

为什么可行?

  • 中序遍历 BST → 严格递增序列:这条性质同递归版一致。
  • 显式栈:你把「递归框架」拆成两部分——往左走(压栈)和回溯访问(弹栈)——手动维护了调用顺序。
  • pre 指针:记录上一个中序访问的节点,用来和当前节点比较。

复杂度

  • 时间 O(N):每个节点最多压入和弹出各一次。
  • 空间 O(H)stack 最多保存从根到最深左链上的节点数,H 是树高。

这样就实现了和递归版本完全等价的验证逻辑,但避免了函数调用开销,也能更灵活地控制遍历过程。

posted @ 2025-07-20 22:45  ForeverEver333  阅读(15)  评论(0)    收藏  举报