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

Day 21

第六章 二叉树part08

669. 修剪二叉搜索树

这道题目比较难,比 添加增加和删除节点难的多,建议先看视频理解。

题目链接/文章讲解: https://programmercarl.com/0669.修剪二叉搜索树.html
视频讲解: https://www.bilibili.com/video/BV17P41177ud

按照第二十天的删节点思路仿写

class Solution:
    def trimBST(self, root: Optional[TreeNode], low: int, high: int) -> Optional[TreeNode]:
        if root is None:
            return root
        if low <= root.val <= high:
            root.left = self.trimBST(root.left, low, high)
            root.right = self.trimBST(root.right, low, high)
            return root #这一句开始没写一直报错
        else:
            if root.left is None and root.right is None:
                return None
            elif root.left is None:
                return self.trimBST(root.right, low, high)
            elif root.right is None:
                return self.trimBST(root.left,low,high)
            else:
                cur = root.right
                while cur.left:
                    cur = cur.left
                cur.left = root.left
                return self.trimBST(root.right, low,high)    

更有逻辑的思路

递归法

img

img

img

class Solution:
    def trimBST(self, root: TreeNode, low: int, high: int) -> TreeNode:
        if root is None:
            return None
        if root.val > high:
            return self.trimBST(root.left, low, high)
        if root.val < low:
            return self.trimBST(root.right, low, high)
        root.left = self.trimBST(root.left, low, high)
        root.right = self.trimBST(root.right, low, high)
        return root

迭代法

class Solution:
    def trimBST(self, root: TreeNode, L: int, R: int) -> TreeNode:
        if not root:
            return None
        
        # 处理头结点,让root移动到[L, R] 范围内,注意是左闭右闭
        while root and (root.val < L or root.val > R):
            if root.val < L:
                root = root.right  # 小于L往右走
            else:
                root = root.left  # 大于R往左走
        
        cur = root
        
        # 此时root已经在[L, R] 范围内,处理左孩子元素小于L的情况
        while cur:
            while cur.left and cur.left.val < L:
                cur.left = cur.left.right
            cur = cur.left
        
        cur = root
        
        # 此时root已经在[L, R] 范围内,处理右孩子大于R的情况
        while cur:
            while cur.right and cur.right.val > R:
                cur.right = cur.right.left
            cur = cur.right
        
        return root

好,这个写法是 迭代版的 trimBST,思路和递归是一样的,只不过不用函数栈,而是用 while 循环一步步把指针调整过来。我们拆开来看:


1. 处理根节点不在区间 [L, R] 的情况

while root and (root.val < L or root.val > R):
    if root.val < L:
        root = root.right  # 小于L,丢掉左子树,去右子树
    else:
        root = root.left   # 大于R,丢掉右子树,去左子树
  • 因为 BST 的性质:

    • 如果 root.val < L,整棵左子树都 < L,不可能有合法节点 → 根往右走。
    • 如果 root.val > R,整棵右子树都 > R,不可能有合法节点 → 根往左走。
  • 这个循环保证最终停下来的 root 一定在 [L, R] 内(或者是 None)。


2. 修剪左子树里过小的节点

cur = root
while cur:
    while cur.left and cur.left.val < L:
        cur.left = cur.left.right
    cur = cur.left

逻辑:

  • 从根开始,一路向左。

  • 每次检查当前节点的左孩子:

    • 如果 cur.left.val < L,说明这个左孩子和它的整个左子树都比 L 小 → 都要删掉。
    • 但是它的右子树可能还有合法节点(值可能 ≥ L),所以直接把 cur.left 替换为 cur.left.right
  • 然后继续往下走,直到走到最左端。


3. 修剪右子树里过大的节点

cur = root
while cur:
    while cur.right and cur.right.val > R:
        cur.right = cur.right.left
    cur = cur.right

逻辑类似:

  • 从根开始,一路向右。

  • 每次检查当前节点的右孩子:

    • 如果 cur.right.val > R,说明这个右孩子和它的整个右子树都比 R 大 → 都要删掉。
    • 但是它的左子树可能还有合法节点(值可能 ≤ R),所以直接把 cur.right 替换为 cur.right.left
  • 然后继续往右端走。


4. 返回结果

最后返回调整过的 root,它就是修剪后的合法 BST。


🔑 总结一下思路

  • 第一步:把 root 移动到合法区间 [L, R] 内。
  • 第二步:从根往左走,修剪掉所有 < L 的左子树。
  • 第三步:从根往右走,修剪掉所有 > R 的右子树。
  • 利用 BST 性质:一次走就能处理掉整片无效子树。

好,我们用一个具体的小树来演示一下迭代法的过程。假设有一棵 BST:

        4
      /   \
     2     6
    / \   / \
   1   3 5   7

要修剪到区间 [3, 6]


第一步:找到合法的 root

while root and (root.val < L or root.val > R):
  • 初始 root = 44 ∈ [3,6],所以直接停下。
    ✔️ 最终 root 仍然是 4

第二步:修剪左子树里 < L 的部分

左子树是:

     2
    / \
   1   3

从根 cur = 4 往左走:

  1. cur = 4,检查 cur.left = 22 < 3 ❌,所以:

    cur.left = cur.left.right
    

    cur.left2 变成 3

    树变成:

        4
       / \
      3   6
         / \
        5   7
    
  2. 接下来 cur = 3,它的 left = None,循环结束。

✔️ 左子树修剪完成。


第三步:修剪右子树里 > R 的部分

右子树是:

     6
    / \
   5   7

从根 cur = 4 往右走:

  1. cur = 4,检查 cur.right = 66 ∈ [3,6] ✅,不用动,往下走。

  2. cur = 6,检查 cur.right = 77 > 6 ❌,所以:

    cur.right = cur.right.left
    

    7.left = None,所以直接把 cur.right 设为 None

    树变成:

        4
       / \
      3   6
         /
        5
    
  3. 接下来 cur = 6.right = None,循环结束。

✔️ 右子树修剪完成。


最终结果

修剪后的树就是:

       4
      / \
     3   6
        /
       5

其中所有节点值都在 [3,6] 范围内。


108.将有序数组转换为二叉搜索树

本题就简单一些,可以尝试先自己做做。

https://programmercarl.com/0108.将有序数组转换为二叉搜索树.html
视频讲解:https://www.bilibili.com/video/BV1uR4y1X7qL

递归法

class Solution:
    def sortedArrayToBST(self, nums: List[int]) -> Optional[TreeNode]:
        if not nums:
            return None
        mid = len(nums)//2
        root = TreeNode(nums[mid])
        left = nums[:mid]
        right = nums[mid+1:]
        root.left = self.sortedArrayToBST(left)
        root.right = self.sortedArrayToBST(right)
        return root

img

迭代法

存一下下次再看

from collections import deque

class Solution:
    def sortedArrayToBST(self, nums: List[int]) -> TreeNode:
        if len(nums) == 0:
            return None
        
        root = TreeNode(0)  # 初始根节点
        nodeQue = deque()   # 放遍历的节点
        leftQue = deque()   # 保存左区间下标
        rightQue = deque()  # 保存右区间下标
        
        nodeQue.append(root)               # 根节点入队列
        leftQue.append(0)                  # 0为左区间下标初始位置
        rightQue.append(len(nums) - 1)     # len(nums) - 1为右区间下标初始位置

        while nodeQue:
            curNode = nodeQue.popleft()
            left = leftQue.popleft()
            right = rightQue.popleft()
            mid = left + (right - left) // 2

            curNode.val = nums[mid]  # 将mid对应的元素给中间节点

            if left <= mid - 1:  # 处理左区间
                curNode.left = TreeNode(0)
                nodeQue.append(curNode.left)
                leftQue.append(left)
                rightQue.append(mid - 1)

            if right >= mid + 1:  # 处理右区间
                curNode.right = TreeNode(0)
                nodeQue.append(curNode.right)
                leftQue.append(mid + 1)
                rightQue.append(right)

        return root

deque回顾


1. deque 是什么?

  • 来自 collections 模块:from collections import deque
  • 双端队列(double-ended queue)
  • 支持在队列两端 O(1) 插入和删除
  • 常用在 BFS滑动窗口迭代树构造等场景

2. 常用方法

from collections import deque

q = deque()        # 初始化空队列

q.append(1)        # 在右端添加元素 -> [1]
q.append(2)        # [1, 2]
q.appendleft(0)    # 在左端添加 -> [0, 1, 2]

x = q.pop()        # 从右端取出并删除 -> 返回2, q = [0, 1]
y = q.popleft()    # 从左端取出并删除 -> 返回0, q = [1]

print(q)           # deque([1])
  • append() → 右边入队
  • appendleft() → 左边入队
  • pop() → 右边出队
  • popleft() → 左边出队

3. 为什么用 deque 而不是 list?

Python 的 list 做 左端删除pop(0))是 O(n),因为要整体移动元素。
deque 专门优化了两端操作,popleft() 是 O(1)
👉 所以 BFS、层序遍历、迭代建树时都推荐 deque


4. 在你那段迭代建树代码里的作用

回到你 sortedArrayToBST 的迭代解法:

nodeQue = deque()
leftQue = deque()
rightQue = deque()

这 3 个队列是一一对应的:

  • nodeQue:当前要赋值的节点
  • leftQuerightQue:该节点对应的数组区间

运行时,每次循环:

curNode = nodeQue.popleft()
left = leftQue.popleft()
right = rightQue.popleft()

👉 从队列左边取出“一组任务”。

然后处理完后(确定中点、建立左右子节点),再用 append() 把子任务丢到队尾,等待后续处理。
这就是典型的 BFS 队列调度


5. 小练习帮你熟悉

你可以自己跑一下这个小例子:

from collections import deque

q = deque()
q.append("root")      # 加入root任务
q.append("left")      # 加入left任务
q.append("right")     # 加入right任务

print(q)              # deque(['root', 'left', 'right'])

print(q.popleft())    # 出队 -> root
print(q)              # deque(['left', 'right'])

q.append("new")       # 加入新任务
print(q)              # deque(['left', 'right', 'new'])

你会发现:

  • 它就是一个普通队列:先进先出(FIFO)。
  • 我们只是在 BFS 建树里多用了几条平行队列(保存节点对象、保存左右区间)。

这个写法其实就是把递归过程手动展开成了 BFS(层序)迭代。我们一步步看:


🌱 递归版回顾

递归版的思路是:

  1. 找中点 mid,建立根节点。
  2. 递归处理左半部分 → 左子树。
  3. 递归处理右半部分 → 右子树。

迭代法要做的,就是用队列自己管理这些“待处理的区间”。


🌳 迭代版的三个队列

代码里用了三个队列:

  • nodeQue:保存正在等待赋值的节点
  • leftQuerightQue:保存该节点对应的子数组区间范围 [left, right]

这样就把“递归里传下去的参数”显式存在队列里了。


🔄 核心循环

while nodeQue:
    curNode = nodeQue.popleft()
    left = leftQue.popleft()
    right = rightQue.popleft()
    mid = left + (right - left) // 2

    curNode.val = nums[mid]
  • 从队列里拿出一个“任务”:一个节点 + 区间 [left, right]
  • 找到该区间的中点 mid,把值 nums[mid] 赋给当前节点。
  • 这一步等价于递归里的“确定根节点”。

👈 处理左半部分

if left <= mid - 1:
    curNode.left = TreeNode(0)   # 占位节点
    nodeQue.append(curNode.left)
    leftQue.append(left)
    rightQue.append(mid - 1)
  • 如果左区间 [left, mid-1] 非空,就说明有左子树。
  • 建一个占位 TreeNode(0) 挂上去。
  • 把它和对应的区间放进队列 → 等待后续处理。

👉 处理右半部分

if right >= mid + 1:
    curNode.right = TreeNode(0)
    nodeQue.append(curNode.right)
    leftQue.append(mid + 1)
    rightQue.append(right)

逻辑同理,对右区间 [mid+1, right] 建立右子树。


🏁 循环终止

当所有区间都处理完,队列为空,树就构建好了。


⚖️ 举个例子

假设 nums = [-10, -3, 0, 5, 9]

  1. 初始:root(0) + 区间 [0, 4] 入队。

  2. 出队 (root, 0, 4)mid=2root.val = 0

    • 左子区间 [0,1] → 建立左孩子。
    • 右子区间 [3,4] → 建立右孩子。
  3. 出队 (root.left, 0, 1)mid=0,值 -10

    • 右子区间 [1,1] → 建立右孩子 -3
  4. 出队 (root.right, 3, 4)mid=3,值 5

    • 右子区间 [4,4] → 建立右孩子 9
  5. 队列空,完成。

最终树结构:

        0
      /   \
   -10      5
      \       \
      -3       9

✅ 总结

  • 递归版:函数栈帮我们保存“子区间”任务。

  • 迭代版:用三个队列手动保存“任务”:

    • 节点本身
    • 对应区间 [left, right]
  • 每次循环处理一个任务,逐层扩展子树。

这样做的好处:

  • 不会因为递归太深导致栈溢出。
  • 思路清晰:和 BFS 构建树类似。

538.把二叉搜索树转换为累加树

本题也不难,在 求二叉搜索树的最小绝对差 和 众数 那两道题目 都讲过了 双指针法,思路是一样的。

https://programmercarl.com/0538.把二叉搜索树转换为累加树.html
视频讲解:https://www.bilibili.com/video/BV1d44y1f7wP

尝试做

class Solution:
    def convertBST(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
        #对于一个节点
        # 身为左树 且 >=其值 的之和的:本身 + 已构造新树的其父节点的值+其右树的值
        # 身为右树 且 >=其值 的之和的:本身 + 其右树的值
        #思路:先构造右树,再构造左树
        #遍历顺序:右中左
        if root.right is not None:
            root.right = self.convertBST(root.right)
            root.val += root.right.val
        if root.left is not None:
            root.left = self.convertBST(root.left)
            root.left.val += root.val
        return root

你这题的关键是:不要把“该加多少”寄存在子节点的 val 里,然后往上拼。正确做法是用右→中→左(reverse in-order)遍历,并维护一个全局/闭包的累加和 acc:走到当前节点时,它右边的节点都已经访问过,acc 就是“所有 ≥ 它的值的和(包含它自己之前的和)”。于是:

  • 先遍历右子树,累加更大的值到 acc
  • 再把当前节点的值更新为 acc += 原值
  • 最后去遍历左子树

你这版为什么不对?

你写的是:

if root.right is not None:
    root.right = self.convertBST(root.right)
    root.val += root.right.val          # 用右子树根的 val 作为“右边之和”
if root.left is not None:
    root.left = self.convertBST(root.left)
    root.left.val += root.val           # 只把父节点的值加到“左孩子根”

两个核心问题:

  1. root.val += root.right.val 错因:root.right.val 在转换后表示“≥ 右孩子原值的和”,**并不等于“整棵右子树所有原值之和”**的稳定载体(逻辑上容易混淆,特别是有更深层结构或重复值时就不正确)。

  2. root.left.val += root.val 只给“左子树的根”加了父节点的贡献,左子树更深层的节点完全没得到来自父节点与其右侧的贡献,导致左子树内部的很多节点都偏小。

反例(你这版会错)

    5
   /
  2
 /
1

正确答案(≥ 节点值的和)应为:

  • 5→5
  • 2→7(=2+5)
  • 1→8(=1+2+5)

你的流程会把 1 只加到 3(=1+2),没有加上来自根 5 的贡献。


小结

  • 反向中序遍历(Right→Node→Left)保证访问顺序是从大到小;
  • 用一个 acc 把“已经处理过的更大节点之和”带着走;
  • 更新时:acc += node.val; node.val = acc
  • 不要尝试用“子树根的值”来代替右侧总和,也不要只给“左孩子根”加父节点,深层节点会漏算。

递归法

class Solution:
    def convertBST(self, root: TreeNode) -> TreeNode:
        self.pre = 0  # 记录前一个节点的数值
        self.traversal(root)
        return root
    def traversal(self, cur):
        if cur is None:
            return        
        self.traversal(cur.right)
        cur.val += self.pre
        self.pre = cur.val
        self.traversal(cur.left)

迭代法

class Solution:
    def convertBST(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
        if not root: 
            return root
        stack = []
        cur = root
        pre = 0
        while cur or stack:
            if cur:
                stack.append(cur)
                cur = cur.right
            else: 
                cur = stack.pop()
                cur.val+= pre
                pre = cur.val
                cur =cur.left
        return root

总结篇

好了,二叉树大家就这样刷完了,做一个总结吧

https://programmercarl.com/二叉树总结篇.html

posted @ 2025-08-27 16:05  ForeverEver333  阅读(10)  评论(0)    收藏  举报