代码随想录算法训练营第十八天|513.找树左下角的值、112. 路径总和、113.路径总和ii、106.从中序与后序遍历序列构造二叉树、105.从前序与中序遍历序列构造二叉树

513.找树左下角的值

思路:

本题要找出树的最后一行的最左边的值。此时大家应该想起用层序遍历是非常简单的了,反而用递归的话会比较难一点。

1.递归法

如果使用递归法,如何判断是最后一行呢,其实就是深度最大的叶子节点一定是最后一行。

那么如何找最左边的呢?可以使用前序遍历(当然中序,后序都可以,因为本题没有 中间节点的处理逻辑,只要左优先就行),保证优先左边搜索,然后记录深度最大的叶子节点,此时就是树的最后一行最左边的值。 

递归三部曲:

1.确定递归函数的参数和返回值:参数必须有要遍历的树的根节点,还有就是一个int型的变量用来记录最长深度。 这里就不需要返回值了,所以递归函数的返回类型为void。本题还需要类里的两个全局变量,maxLen用来记录最大深度,result记录最大深度最左节点的数值。

2.确定终止条件:当遇到叶子节点的时候,就需要统计一下最大的深度了,所以需要遇到叶子节点来更新最大深度。

3.确定单层递归的逻辑:在找最大深度的时候,递归的过程中依然要使用回溯

2. 迭代法

本题使用层序遍历再合适不过了,比递归要好理解得多!

只需要记录最后一行第一个节点的数值就可以了。

代码:

递归法

class Solution:
    def findBottomLeftValue(self, root: Optional[TreeNode]) -> int:
        self.max_depth = float('-inf') # 初始化一个类变量max_depth,用来记录目前找到的最深深度,初始化为负无穷大
        self.result = None # 初始化一个类变量result,用来记录最深深度下最左边的节点的值,初始化为None
        self.traversal(root, 0) # 调用traversal方法,开始从根节点遍历二叉树,并将深度初始化为0
        return self.result # 返回result,即最底层最左边的节点的值
    
    def traversal(self, node, depth): # 定义一个递归辅助方法traversal,输入是当前节点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) # 递归调用traversal方法处理左子节点
            depth -= 1 # 返回上一级节点时,将深度减一(回溯)

        if node.right: # 如果当前节点有右子节点
            depth += 1 # 增加深度
            self.traversal(node.right, depth) # 递归调用traversal方法处理右子节点
            depth -= 1 # 返回上一级节点时,将深度减一

        return # 处理完当前节点的左右子节点,返回

时间复杂度:O(n),其中 n是二叉树的节点数目。需要遍历 n个节点。

空间复杂度:O(n)。递归栈需要占用 O(n)的空间。

补充:在这段代码中,traversal 方法对二叉树进行了类似前序遍历的遍历(即先处理当前节点,然后遍历左子树,最后遍历右子树),但有一个关键的区别:在遍历过程中,它记录了遍历到的最大深度,并据此更新最底层最左边节点的值。

迭代法 

from collections import deque # 从collections模块导入deque,这是一个双端队列,可以高效地在队列的两端进行添加和删除操作。

class Solution:
    def findBottomLeftValue(self, root: Optional[TreeNode]) -> int:
        if root is None: # 判断根节点是否为空,如果为空,则返回0
            return 0
        queue = deque() # 初始化一个空的双端队列
        queue.append(root) # 将根节点添加到队列中
        result = 0 # 初始化result变量为0,用于存储最底层最左边节点的值

        while queue: # 当队列不为空时,继续循环
            size = len(queue) # 获取当前队列的长度,即当前层的节点数
            for i in range(size): # 遍历当前层的所有节点
                node = queue.popleft() # 从队列的左侧(前端)弹出一个节点
                if i == 0: # 判断当前节点是否是当前层的第一个节点
                    result = node.val # 如果是当前层的第一个节点,更新result为当前节点的值
                if node.left: # 判断当前节点是否有左子节点
                    queue.append(node.left) # 如果有左子节点,将左子节点添加到队列中
                if node.right: # 判断当前节点是否有右子节点
                    queue.append(node.right) # 如果有右子节点,将右子节点添加到队列中

        return result # 返回最底层最左边节点的值

时间复杂度:O(n),其中 n是二叉树的节点数目。

空间复杂度:O(n)。如果二叉树是满完全二叉树,那么队列最多保存n/2个节点。

112. 路径总和

思路:

可以使用深度优先遍历的方式(本题前中后序都可以,无所谓,因为中节点也没有处理逻辑)来遍历二叉树 

1.确定递归函数的参数和返回类型:参数:需要二叉树的根节点,还需要一个计数器,这个计数器用来计算二叉树的一条边之和是否正好是目标和,计数器为int型。递归函数需要返回值,可以用bool类型表示

2.确定终止条件:不要去累加然后判断是否等于目标和,那么代码比较麻烦,可以用递减,让计数器count初始为目标和,然后每次减去遍历路径节点上的数值。如果最后count == 0,同时到了叶子节点的话,说明找到了目标和。如果遍历到了叶子节点,count不为0,就是没找到。

3.确定单层递归的逻辑:因为终止条件是判断叶子节点,所以递归的过程中就不要让空节点进入递归了。递归函数是有返回值的,如果递归函数返回true,说明找到了合适的路径,应该立刻返回。

113. 路径总和 II

思路:

113.路径总和ii要遍历整个树,找到所有路径,所以递归函数不要返回值!

代码:

路径总和

class Solution:
    def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
        if root is None: # 如果根节点为空,说明二叉树为空,返回False
            return False
        return self.traversal(root, targetSum - root.val) # 调用traversal方法,开始从根节点遍历二叉树,传入targetSum - root.val作为count的初始值,即减去根节点的值

    def traversal(self, cur: TreeNode, count: int) -> bool: # 定义traversal方法,它接收一个当前节点cur和一个计数器count作为参数,返回一个布尔值
        if not cur.left and not cur.right and count == 0: # 如果当前节点是叶子节点(即没有左子节点和右子节点),并且count为0,说明从根节点到该叶子节点的路径上节点值的和等于目标和targetSum
            return True # 返回True,表示找到了符合条件的路径
        if not cur.left and not cur.right: # 如果当前节点是叶子节点,但是count不为0,说明该路径不符合条件,返回False
            return False
        
        if cur.left: # 如果当前节点有左子节点
            count -= cur.left.val # 更新count,减去左子节点的值
            if self.traversal(cur.left, count): # 递归调用traversal方法,继续遍历左子树
                return True # 如果左子树中存在符合条件的路径,则返回True
            count += cur.left.val # 如果左子树中不存在符合条件的路径,回溯到当前节点,恢复count的值
            
        if cur.right: # 如果当前节点有右子节点
            count -= cur.right.val # 更新count,减去右子节点的值
            if self.traversal(cur.right, count): # 递归调用traversal方法,继续遍历右子树
                return True # 如果右子树中存在符合条件的路径,则返回True
            count += cur.right.val # 如果右子树中不存在符合条件的路径,回溯到当前节点,恢复count的值
            
        return False # 如果当前节点的左子树和右子树都不存在符合条件的路径,则返回False 

时间复杂度 O(n)

空间复杂度 O(n) 

补充:

这段代码中的遍历方式类似于前序遍历,但带有额外的逻辑来处理路径和。

前序遍历的标准步骤是“根-左-右”,即先访问根节点,然后遍历左子树,最后遍历右子树。在这段代码中,当遍历到一个节点时,首先会检查当前节点是否是叶子节点并且路径和是否等于目标 sum。如果不是叶子节点,则会递归地先遍历左子树,再遍历右子树。

路径总和II

class Solution:
    def pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]:
        path=[] # 初始化一个空列表用于存储路径上的节点值
        result=[] # 初始化一个空列表用于存储所有满足条件的路径
        if not root: # 如果根节点为空,则没有路径可走,直接返回空的结果列表
            return result
        path.append(root.val) # 将根节点的值加入当前路径
        self.traversal(root, targetSum - root.val, path, result) # 调用traversal方法递归遍历树,并将目标和减去根节点的值作为新的目标和传入
        return result # 返回所有满足条件的路径列表
    
    def traversal(self, cur, count, path, result): # 输入参数为当前节点cur、目标和count、当前路径path和结果列表result
        if not cur.left and not cur.right and count == 0: # 如果当前节点是叶子节点,且路径上的节点值之和等于目标和
            result.append(path[:]) # 将当前路径的副本添加到结果列表中
            return # 当前路径已经处理完毕,直接返回
        if not cur.left and not cur.right: # 如果当前节点是叶子节点,但路径上的节点值之和不等于目标和
            return # 则说明这条路径不满足条件,直接返回

        if cur.left: # 如果当前节点有左子节点
            path.append(cur.left.val) # 将左子节点的值加入当前路径
            count -= cur.left.val # 更新目标和,减去左子节点的值
            self.traversal(cur.left, count, path, result) # 递归遍历左子树
            count += cur.left.val # 回溯,更新目标和,加上左子节点的值,恢复之前的状态
            path.pop() # 回溯,将左子节点的值从路径中移除

        if cur.right: # 如果当前节点有右子节点
            path.append(cur.right.val) # 将右子节点的值加入当前路径
            count -= cur.right.val # 更新目标和,减去右子节点的值
            self.traversal(cur.right, count, path, result) # 递归遍历右子树
            count += cur.right.val # 回溯,更新目标和,加上右子节点的值,恢复之前的状态
            path.pop() # 回溯,将右子节点的值从路径中移除

时间复杂度:O(N^2)

空间复杂度:O(N)

补充:

上述代码中的result.append(path[:])为什么不能写成result.append(path)?

在Python中,path[:]是一个切片操作,它创建了path列表的一个浅拷贝(shallow copy)。

具体来说:

  • path 是一个列表。
  • path[:] 表示取path从开始到结束的所有元素
  • 这个操作返回一个新的列表,它包含与path相同的元素,但是是两个独立的对象。对path[:]的修改不会影响原始的path列表,反之亦然。

在你提供的代码中,result.append(path[:]) 这行代码的目的是将当前路径path的一个拷贝添加到结果列表result中,而不是直接添加path的引用。这样做是为了避免在后续的递归调用中修改path时影响到已经添加到result中的路径。如果不使用path[:]而是直接result.append(path),那么result中存储的将是path的引用,而不是一个独立的副本。当path在后续的递归调用中被修改时,result中存储的所有路径都会受到影响,因为它们实际上指向的是同一个列表对象。

使用path[:]确保了result中存储的是每个路径在找到时的独立状态,而不是它们被最后修改时的状态。

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

思路:

首先回忆一下如何根据两个顺序构造一个唯一的二叉树,相信理论知识大家应该都清楚,就是以 后序数组的最后一个元素为切割点,先切中序数组,根据中序数组,反过来再切后序数组。一层一层切下去,每次后序数组最后一个元素就是节点元素。

说到一层一层切割,就应该想到了递归。

来看一下一共分几步:

  • 第一步:如果数组大小为零的话,说明是空节点了。

  • 第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。

  • 第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点

  • 第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)

  • 第五步:切割后序数组,切成后序左数组和后序右数组

  • 第六步:递归处理左区间和右区间 

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

本题和106是一样的道理。

前序和中序可以唯一确定一棵二叉树,后序和中序可以唯一确定一棵二叉树。

那么前序和后序可不可以唯一确定一棵二叉树呢?

前序和后序不能唯一确定一棵二叉树!,因为没有中序遍历无法确定左右部分,也就是无法分割。

 

代码:

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

# 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 buildTree(self, inorder: List[int], postorder: List[int]) -> Optional[TreeNode]:
        # 第一步: 特殊情况讨论。如果后序遍历的数组为空,说明树为空,直接返回None。
        if not postorder:
            return None
            
        # 第二步: 找到根节点。在后序遍历中,最后一个元素总是当前(子)树的根节点值。因此,我们创建了一个新的TreeNode对象作为当前(子)树的根节点。
        root_val = postorder[-1]
        root = TreeNode(root_val)

        # 第三步: 找到中序遍历中的根节点位置。使用index方法找到根节点值在中序遍历数组中的(下标)位置。
        separator_idx = inorder.index(root_val)

        # 第四步: 切割中序遍历数组。根据根节点的位置,将中序遍历数组切割为左子树和右子树的中序遍历数组。
        inorder_left = inorder[:separator_idx]
        inorder_right = inorder[separator_idx + 1:]

        # 第五步: 切割后序遍历数组。后序遍历数组也需要切割为左子树和右子树的后序遍历数组。注意,这里不包括后序遍历的最后一个元素(即当前子树的根节点),因为该元素已经在第二步中使用了。
        postorder_left = postorder[:len(inorder_left)]
        postorder_right = postorder[len(inorder_left): len(postorder) - 1]

        # 第六步: 递归构建左右子树。递归地调用buildTree方法来构建当前根节点的左子树和右子树。
        root.left = self.buildTree(inorder_left, postorder_left) # 传入左中序,左后序
        root.right = self.buildTree(inorder_right, postorder_right) # 传入右中序,右后序
        
        # 第七步: 返回答案。返回当前子树的根节点。
        return root

时间复杂度:O(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 buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
        # 第一步: 特殊情况讨论: 树为空. 或者说是递归终止条件
        if not preorder:
            return None

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

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

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

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

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

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值