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

Day 16

第六章 二叉树 part04

找树左下角的值

本题递归偏难,反而迭代简单属于模板题, 两种方法掌握一下

题目链接/文章讲解/视频讲解:https://programmercarl.com/0513.找树左下角的值.html

本题使用前序/中序/后序都可以

前序:中左右
中序:左中右
后序:左右中

本题没有中节点的处理逻辑,只需先遍历左

强调左在右的前面
优先遍历左节点,只要得到了深度最大的节点,得到的就是最后一行最靠左的节点

最靠左侧的节点不一定就是左孩子

深度最大的节点里的值放进result
depth->遍历当前的深度
maxDepth->记录所有遍历深度中的最大深度

终止条件:遍历到叶子结点

递归法 明确回溯


class Solution:
    def findBottomLeftValue(self, root: TreeNode) -> int:
        self.max_depth = float('-inf')
        self.result = None
        self.traversal(root, 0)
        return self.result
    
    def traversal(self, node, depth):
        if not node.left and not node.right:
            if depth > self.max_depth:
                self.max_depth = depth
                self.result = node.val
            return
        
        if node.left:
            depth += 1
            self.traversal(node.left, depth)
            depth -= 1
        if node.right:
            depth += 1
            self.traversal(node.right, depth)
            depth -= 1

  1. 这是不是前序遍历 (pre‑order traversal)?
    严格来说,这段代码并不是在每个节点(包括内部节点)都做“访问”(visit)操作,所以不能算是完整的 pre‑order(“先访问 node,再递归 left,再递归 right”)。

    • 你在 traversal只在叶子节点(leaf) 时做处理(也就是比较深度并可能更新 result),
    • 在内部节点只是负责“决定往左还是往右走”,而没有把它当作一个“访问点”来处理。
      因此它更准确地说是一种 深度优先搜索 (Depth‑First Search, DFS),且你写的是“先左后右”的遍历顺序(left‑first),但处理(processing)仅限叶子节点。
  2. 为什么这样能拿到“树的左下角”那个节点?

    • 你维护了一个全局的 max_depth 和对应的 result

    • 每次到达一个叶子节点,就用当前 depthmax_depth 比较:

      if depth > self.max_depth:
          self.max_depth = depth
          self.result    = node.val
      
    • 关键是你是 先递归遍历左子树 (left subtree),再遍历右子树 (right subtree)。

    • 因此,当你第一次“达到”某个新的更大深度时,一定是该深度 最左边 的叶子节点,你就把它的值记录下来了。

    • 整棵树遍历完成后,self.result 保存的正是 最大深度(bottom)里最先被发现的节点,也就是最左边(leftmost)的叶子节点值

下面用一个示例来说明,假设我们有如下这棵二叉树(root 深度记为 0):

        1
       / \
      2   3
     /   / \
    4   5   6
       / \
      7   8
  • 叶子节点 (leaf nodes):4(深度 2)、7(深度 3)、8(深度 3)、6(深度 2)。
  • 最底层 deepest level 是深度 3,位于节点 7 和 8 所在的那一层。
  • 这层最左的叶子节点就是 7。

下面看我们的遍历(DFS,先左再右)以及 max_depth/result 如何变化:

访问节点 深度 depth 是否叶子 更新前 max_depth 更新后 max_depth 更新后 result
1 0 –∞ –∞
2 1 –∞ –∞
4 2 –∞ 2 4
(返回到 2,再返回到 1) 2 2 4
3 1 2 2 4
5 2 2 2 4
7 3 2 3 7
8 3 3 3 (不变) 7 (不变)
6 2 3 3 7
  • 初次遇到叶子 4(depth=2),max_depth 从 –∞ 更新到 2,result=4
  • 接着在右子树先遇到叶子 7(depth=3),3>2,于是更新为 max_depth=3, result=7
  • 后续遇到叶子 8 和 6 时,它们的深度都不大于当前 max_depth (3),因此不再更新。

最终 result=7,即最底层(bottom)最左边(leftmost)的叶子节点值。
这样就验证了:

  1. 虽然这段代码不是严格意义上对所有节点都做“访问”的前序遍历(pre‑order),但它是“先左后右”的 DFS,
  2. 借助“只在叶子节点处理 + 先遍历左子树”的策略,保证了第一次到达最深层时,拿到的就是那一层最左边的节点值。

递归法 精简版

不需要depth++后再depth--,而是直接把depth+1传递给函数

这样做,没有改变depth本身的值,因此不需要depth--来恢复深度再traverse右边

class Solution:
    def findBottomLeftValue(self, root: TreeNode) -> int:
        self.max_depth = float('-inf')
        self.result = None
        self.traversal(root, 0)
        return self.result
    
    def traversal(self, node, depth):
        if not node.left and not node.right:
            if depth > self.max_depth:
                self.max_depth = depth
                self.result = node.val
            return
        
        if node.left:
            self.traversal(node.left, depth+1)
        if node.right:
            self.traversal(node.right, depth+1)

迭代法 层序遍历

# 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
from collections import deque
class Solution:
    def findBottomLeftValue(self, root):
        if root is None:
            return 0
        queue = deque()
        queue.append(root)
        result = 0
        while queue:
            size = len(queue)
            for i in range(size):
                node = queue.popleft()
                if i == 0:
                    result = node.val
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
        return result

路径总和

本题 又一次涉及到回溯的过程,而且回溯的过程隐藏的还挺深,建议先看视频来理解
优先掌握递归法

本题同样不涉及中节点的处理逻辑,因此前/中/后序均可

112. 路径总和

img

用累加然后判断是否等于目标和比较麻烦

可以初始化计数器count为目标和,然后每次减去路径节点上的数值

如果最后count==0,同时到了叶子结点的话,说明找到了目标和

如果遍历到了叶子结点,count不为0,就是没找到

递归法

class Solution:
    def traversal(self, cur: TreeNode, count: int) -> bool:
        if not cur.left and not cur.right and count == 0: # 遇到叶子节点,并且计数为0
            return True
        if not cur.left and not cur.right: # 遇到叶子节点直接返回
            return False
        
        if cur.left: # 左
            count -= cur.left.val
            if self.traversal(cur.left, count): # 递归,处理节点
                return True
            count += cur.left.val # 回溯,撤销处理结果
            
        if cur.right: # 右
            count -= cur.right.val
            if self.traversal(cur.right, count): # 递归,处理节点
                return True
            count += cur.right.val # 回溯,撤销处理结果
            
        return False
    
    def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
        if root is None:
            return False
        return self.traversal(root, targetSum - root.val)    

递归法 精简

class Solution:
    def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
        if not root:
            return False
        if not root.left and not root.right and targetSum == root.val:
            return True
        return self.hasPathSum(root.left, targetSum - root.val) or self.hasPathSum(root.right, targetSum - root.val)

迭代法

class Solution:
    def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
        if not root:
            return False
        # 此时栈里要放的是pair<节点指针,路径数值>
        st = [(root, root.val)]
        while st:
            node, path_sum = st.pop()
            # 如果该节点是叶子节点了,同时该节点的路径数值等于sum,那么就返回true
            if not node.left and not node.right and path_sum == targetSum:
                return True
            # 右节点,压进去一个节点的时候,将该节点的路径数值也记录下来
            if node.right:
                st.append((node.right, path_sum + node.right.val))
            # 左节点,压进去一个节点的时候,将该节点的路径数值也记录下来
            if node.left:
                st.append((node.left, path_sum + node.left.val))
        return False

这段代码用的是迭代版的深度优先搜索(DFS),用一个显式的栈来模拟递归遍历,同时在栈里把“走到当前节点时的路径和”也一起记录下来。

下面分几步来说明它的工作流程:

  1. 初始化栈

    st = [(root, root.val)]
    

    栈里每一项都是一个 (节点引用, 从根节点累加到这个节点的路径和)。一开始只有根节点和它自己的值。

  2. 循环遍历,直到栈空或找到答案

    while st:
        node, path_sum = st.pop()
        …
    

    每次从栈顶 pop 出当前要“访问”的节点和对应的 path_sum

  3. 遇到叶子节点就判断

    if not node.left and not node.right and path_sum == targetSum:
        return True
    
    • not node.left and not node.right 确认当前节点是叶子
    • path_sum == targetSum 则说明从根到这个叶子的那条路径和恰好等于题目给的 targetSum,直接返回 True
  4. 如果不是满足条件的叶子,就把子节点和更新后的路径和压栈

    if node.right:
        st.append((node.right, path_sum + node.right.val))
    if node.left:
        st.append((node.left,  path_sum + node.left.val))
    
    • 先把右子节点压栈,再把左子节点压栈,因为栈是后进先出 (LIFO),这样下次 pop 时就会先遍历“左子树”,保证与递归 dfs(node.left)…dfs(node.right) 的顺序一致。
  5. 遍历结束还没返回,就说明不存在这样的路径

    return False
    

为什么这种方法能工作?

  • 显式栈模拟递归:递归版 DFS 每到一个节点都会把「剩下要处理的分支」压到调用栈里;这种写法把调用栈变成了一个手动维护的 st 列表。
  • 路径和累加:在遍历过程中,一旦你从父节点“走”到子节点,就把子节点的值加到前面的 path_sum 里,这样到达任何节点时,path_sum 都恰好等于「从根节点到它的这条路径的累加和」。
  • 叶子判断+早停:只在叶子节点才做等于判断,并且一旦满足就马上 return True,避免继续无意义的遍历。

复杂度

  • 时间复杂度:每个节点最多入栈和出栈各一次,都是 O(1) 操作,合计 O(N),N 是节点总数。
  • 空间复杂度:栈最坏情况下要同时保存一整条从根到最深(或最宽)的一条路径/所有兄弟节点,最坏是 O(N)。

错误解法

# 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 hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
        #要注意剪枝
        #前序 中左右
        if not root:
            return False
        if self.traversal(root, root.val, targetSum):
            return True
        return False

    def traversal(self,node, cur_sum,targetSum):
        if cur_sum > targetSum:
            return False
        if node.left or node.right:
            if cur_sum == targetSum:
                return False
            if node.left:
                if self.traversal(node.left, cur_sum+node.left.val,targetSum):
                    return True
            if node.right:
                if self.traversal(node.right, cur_sum+node.right.val,targetSum):
                    return True
            return False
        if not node.left and not node.right:
            if cur_sum == targetSum:
                return True
            return False

错误原因

去掉有风险的剪枝

你在开头写了:

if cur_sum > targetSum:
    return False

但题目并不保证所有节点值都是非负的——遇到负数你就会 误剪枝。除非你能确认树中 所有 val ≥ 0,否则应该去掉这条剪枝。

事实上,测试用例中有的节点值为0


一个更清晰的递归版本

下面是一个简洁又可靠的写法,和你的思路几乎一样,但把叶子判断放前面,并去掉了那个有风险的剪枝:

class Solution:
    def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
        if not root:
            return False
        return self.dfs(root, root.val, targetSum)

    def dfs(self, node: TreeNode, cur_sum: int, target: int) -> bool:
        # 1) 先判断叶子
        if not node.left and not node.right:
            return cur_sum == target

        # 2) 递归左、右子树
        if node.left:
            if self.dfs(node.left, cur_sum + node.left.val, target):
                return True
        if node.right:
            if self.dfs(node.right, cur_sum + node.right.val, target):
                return True

        return False

这样:

  1. 检测 not node.left and not node.right,一遇到叶子节点就直接比较并返回结果;
  2. 不会误剪枝,也不用在中间节点再去判断 cur_sum == target
  3. 逻辑直观:叶子判断在前,递归在后。

只要把你的叶子判断改成 and not node.right,或者全面重写成上面的样子,就能通过所有测试用例了。

113. 路径总和ii

题目链接/文章讲解/视频讲解:https://programmercarl.com/0112.路径总和.html

我的解法

class Solution:
    def pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]:
        if not root:
            return []
        res = []
        def find_path(node,path,targetSum, res):
            if not node.left and not node.right and sum(path) == targetSum:
                res.append(path)
                return 
            if node.left:
                find_path(node.left, path+[node.left.val],targetSum,res)
            if node.right:
                find_path(node.right, path+[node.right.val],targetSum,res)
        find_path(root, [root.val], targetSum, res)
        return res
        

递归法 体现回溯

# 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 __init__(self):
        self.result = []
        self.path = []

    def traversal(self, cur, count):
        if not cur.left and not cur.right and count == 0: # 遇到了叶子节点且找到了和为sum的路径
            self.result.append(self.path[:])
            return

        if not cur.left and not cur.right: # 遇到叶子节点而没有找到合适的边,直接返回
            return

        if cur.left: # 左 (空节点不遍历)
            self.path.append(cur.left.val)
            count -= cur.left.val
            self.traversal(cur.left, count) # 递归
            count += cur.left.val # 回溯
            self.path.pop() # 回溯

        if cur.right: #  右 (空节点不遍历)
            self.path.append(cur.right.val) 
            count -= cur.right.val
            self.traversal(cur.right, count) # 递归
            count += cur.right.val # 回溯
            self.path.pop() # 回溯

        return

    def pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]:
        self.result.clear()
        self.path.clear()
        if not root:
            return self.result
        self.path.append(root.val) # 把根节点放进路径
        self.traversal(root, targetSum - root.val)
        return self.result 

这段代码用的是深度优先搜索 + 回溯(DFS+backtracking)的思路,来枚举所有从根到叶子的路径,并找出那些路径和恰好等于 targetSum 的路径。下面分步来讲它是怎么做的:


1. 全局状态

  • self.result:用来收集所有满足条件的路径,最终返回
  • self.path:当前递归过程中,从根节点到“正在访问节点”这条路径上,节点值的列表
def __init__(self):
    self.result = []
    self.path = []

调用 pathSum(root, targetSum) 时,会先清空这两个列表,然后把根节点值压入 self.path,再启动递归:

self.result.clear()
self.path.clear()
if not root:
    return self.result
self.path.append(root.val)
self.traversal(root, targetSum - root.val)
return self.result

这里把 count = targetSum - root.val 看作“从下一个子节点开始,还需要凑齐多少和”。


2. 递归函数 traversal(cur, count)

def traversal(self, cur, count):
    # —— 1)到达叶子并且 count==0,就把当前 path 拷贝进 result
    if not cur.left and not cur.right and count == 0:
        self.result.append(self.path[:])
        return

    # —— 2)到达叶子但 count!=0,直接剪掉,返回
    if not cur.left and not cur.right:
        return

    # —— 3)如果有左子节点,就“走”向左
    if cur.left:
        self.path.append(cur.left.val)       # 加入左孩子到路径
        count -= cur.left.val               # 剩余的目标和减少
        self.traversal(cur.left, count)     # 递归
        count += cur.left.val               # 回溯:把 count 恢复
        self.path.pop()                     # 回溯:把路径末尾弹出

    # —— 4)同理,遍历右子节点
    if cur.right:
        self.path.append(cur.right.val)
        count -= cur.right.val
        self.traversal(cur.right, count)
        count += cur.right.val
        self.path.pop()

    return

核心要点

  1. 叶子节点检测

    • if not cur.left and not cur.right 判断当前节点是不是叶子
    • 如果是叶子,再看 count==0(说明从根到它的这条路的和正好等于 targetSum),就把 self.path 的一个拷贝加入 self.result
  2. 回溯机制

    • 每次“走”向一个子节点之前:

      • 把子节点的值 appendself.path
      • count 减去这个子节点的值
    • 递归返回后,要把这两步的操作逆向执行,即 pop()self.path 末尾,并把 count 加回来

    • 这样能够保证,把每一条路径都“干净”地探索一遍,互不干扰

  3. 剪枝

    • 当到叶子但 count!=0 时,直接 return,不往下走(事实上叶子没孩子,但这写法在叶子上做了两次判断,第二个判断就是无条件退出)

3. 举个小例子

假设树如下,targetSum = 22

      5
     / \
    4   8
   /   / \
 11   13  4
 /  \      \
7    2      1
  • 开始时:path = [5], count = 22−5=17
  • 向左走:path = [5,4], count = 17−4=13
  • 再向左:path = [5,4,11], count = 13−11=2
  • 向左叶子 7:path=[5,4,11,7], count=2−7=−5 → 叶子且 count!=0 → 返回,不存结果,回溯
  • 同理访问 2:path=[5,4,11,2], count=2−2=0 → 叶子且 count==0 → 把 [5,4,11,2] 加入 result
  • 再遍历右子树……最终会找到 [5,8,4,5](若存在)等路径

4. 复杂度

  • 时间:每个节点最多访问一次,且每条成功路径要做一次 path[:] 拷贝。最坏情况所有节点都被访问,O(N);如果所有路径都符合,还要额外拷贝,最坏 O(N·L),L 是平均路径长度。
  • 空间:递归栈 + self.path 最多保存一条从根到叶子的路径,都是 O(H),H 是树的高度(最坏 O(N))。

总结

  • 这是经典的「先序 DFS + 回溯」解法,
  • self.path 记录当前节点的路径,
  • count 跟踪还需凑齐的和,
  • 走到叶子时检查是否达标,
  • 回溯时要恢复状态,保证其它分支遍历时路径和 count 正确。这样就能枚举出所有满足 path sum == targetSum 的根—叶子路径了。

问题
如果说走完左子节点,需要回溯是因为还要走右子节点,那为什么右子节点也需要回溯呢?

不论是左子树还是右子树,每次把子节点的值加到 self.pathcount 里,走完那条分支以后,都必须把它们“弹”回去,才能恢复到「走这条分支之前」的状态,这样回到父节点的时候,self.pathcount 才不会带着刚才走过的那一部分信息去影响后续的遍历。


举个对比更清晰:

# 假设 cur 有两个孩子:left 和 right

# 先走左
self.path.append(cur.left.val)
count -= cur.left.val
self.traversal(cur.left, count)
# 【回溯】——弹出 left 的那一步
count += cur.left.val
self.path.pop()

# 然后走右
self.path.append(cur.right.val)
count -= cur.right.val
self.traversal(cur.right, count)
# 【回溯】——弹出 right 的那一步
count += cur.right.val
self.path.pop()

如果你 不在右子树遍历后也做回溯,那么当这一层的递归函数结束、要返回到更高一层时,self.path 里依然会留着右孩子的值,导致:

  1. 路径不干净:父节点或祖先节点再看 self.path 时,会多了本不该有的节点。
  2. 后续遍历错乱:祖先节点的其它分支(或者结束时收集的路径)就全乱了。

即使你在右子树走完之后,不再继续“同级别”的兄弟遍历,也必须把这次「压入/弹出」的过程完整做一次,才能保持全局状态一致。

递归法 精简回溯

class Solution:
    def pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]:
        
        result = []
        self.traversal(root, targetSum, [], result)
        return result
    def traversal(self,node, count, path, result):
            if not node:
                return
            path.append(node.val)
            count -= node.val
            if not node.left and not node.right and count == 0:
                result.append(list(path))
            self.traversal(node.left, count, path, result)
            self.traversal(node.right, count, path, result)
            path.pop()

迭代法

# 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 pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]:
        if not root:
            return []
        stack = [(root, [root.val])]
        res = []
        while stack:
            node, path = stack.pop()
            if not node.left and not node.right and sum(path) == targetSum:
                res.append(path)
            if node.right:
                stack.append((node.right, path + [node.right.val]))
            if node.left:
                stack.append((node.left, path + [node.left.val]))
        return res

从中序与后序遍历序列构造二叉树

本题算是比较难的二叉树题目了,大家先看视频来理解。

题目链接/文章讲解/视频讲解:https://programmercarl.com/0106.从中序与后序遍历序列构造二叉树.html

img

中序:左中右 ->能确定最左和最右节点

后序:左右中 ->最后一个元素一定是中间节点

img

切中序数组,是为了得到:左中序 右中序

拿中序数组的左区间的大小,可以去切后序数组的左区间,那么右区间就也得到了

切后序数组,得到:左后序,右后序

img

一定先切中序再切后序。后序的左右挨在一起,没办法切开

106.从中序与后序遍历序列构造二叉树

class Solution:
    def buildTree(self, inorder: List[int], postorder: List[int]) -> TreeNode:
        # 第一步: 特殊情况讨论: 树为空. (递归终止条件)
        if not postorder:
            return None

        # 第二步: 后序遍历的最后一个就是当前的中间节点.
        root_val = postorder[-1]
        root = TreeNode(root_val)

        # 第三步: 找切割点.
        separator_idx = inorder.index(root_val)

        # 第四步: 切割inorder数组. 得到inorder数组的左,右半边.
        inorder_left = inorder[:separator_idx]
        inorder_right = inorder[separator_idx + 1:]

        # 第五步: 切割postorder数组. 得到postorder数组的左,右半边.
        # ⭐️ 重点1: 中序数组大小一定跟后序数组大小是相同的.
        postorder_left = postorder[:len(inorder_left)]
        postorder_right = postorder[len(inorder_left): len(postorder) - 1]

        # 第六步: 递归
        root.left = self.buildTree(inorder_left, postorder_left)
        root.right = self.buildTree(inorder_right, postorder_right)
         # 第七步: 返回答案
        return root

105.从前序与中序遍历序列构造二叉树

class Solution:
    def buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
        #inorder:左中右
        #preorder:中左右
        if not preorder:
            return None

        root_val = preorder[0]
        root = TreeNode(root_val)

        separator_idx = inorder.index(root_val)


        inorder_left = inorder[:separator_idx]
        inorder_right = inorder[separator_idx+1:]

        preorder_left = preorder[1:len(inorder_left)+1]
        preorder_right = preorder[len(inorder_left)+1:]
        
        root.left = self.buildTree(preorder_left,inorder_left)
        root.right = self.buildTree(preorder_right,inorder_right)
        return root

中序和后序 或 中序和前序都可以确定一个唯一的二叉树

但前序和后序不行

前序和后序中,左右都是挨在一起的,分割点没有告诉我们。而中序从中间把左右区间隔开了

我们才能知道左右区间分别是什么

img

形如上图的二叉树,前后序一样

posted @ 2025-07-18 21:48  ForeverEver333  阅读(7)  评论(0)    收藏  举报