Day15 第六章 二叉树Part3 初见回溯(二叉树相关)
任务
110.平衡二叉树
给定一个二叉树,判断它是否是 平衡二叉树
思路
典型的树形DP,每个节点向它的左右孩子收集信息,然后利用收集到的信息判断当前节点,最后再将信息传给上层。对于本题,每个节点要判断以自己为根的树是否是平衡二叉树,需要判断3个条件:
- 自己的左子树是否平衡
- 自己的右子树是否平衡
- 自己的左右高度是否相差<=1
只有满足这三个条件,以当前节点为根的二叉树才平衡,返回给上层。
class Solution: def isBalanced(self, root: Optional[TreeNode]) -> bool: height,balanced = self.isNodeBalanced(root) return balanced def isNodeBalanced(self,node): if not node: return 0,True leftHeight,isLeftB = self.isNodeBalanced(node.left) rightHeight,isRighttB = self.isNodeBalanced(node.right) curNodeHeight = max(leftHeight,rightHeight)+1 res = abs(leftHeight - rightHeight) <= 1 and isLeftB and isRighttB return curNodeHeight,res
257. 二叉树的所有路径
给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。
思路
采用遍历的思路:
- 对于任意节点,每次首次遍历到节点就将其加入到path中。
- 对于任意节点,递归遍历其左右节点
- 遇到叶子节点后,将之前收集到的路径加入到paths中。
- 从任意节点退出后,需要回溯path。
class Solution: def binaryTreePaths(self, root: Optional[TreeNode]) -> List[str]: if not root: return [] paths =[] path = [] self.dfs(path,root,paths) return paths #解法1:当前节点退出时,回溯处理,元素出栈 def dfs(self,path,node,paths): if not node: return path.append(node.val) # 第一次访问时添加 if (not node.left and not node.right): paths.append('->'.join(map(str,path))) else: self.dfs(path,node.left,paths) self.dfs(path,node.right,paths) path.pop() # 第三次访问时移除
代码随想录与我的思路有些许不同,他的思路是任意节点的子节点返回时回溯,特别的,对于叶子节点,因为左右都为空,所以直接返回。细微之处需要细细体会,代码如下:
#解法2: 当前节点的左右孩子返回时,回溯处理,元素出栈 def dfs(self,path,node,paths): path.append(node.val) #当前节点加入到路径 if (not node.left and not node.right): # 作为叶子节点的处理 paths.append('->'.join(map(str,path))) return else: if node.left: self.dfs(path,node.left,paths) path.pop() if node.right: self.dfs(path,node.right,paths) path.pop() #处理完后当前节点从路径中移除
此外,还可以用隐形回溯,即使用当前层递归函数的局部变量记录路径,从下一层返回时就可以不用显示的调用pop,局部变量就会从之前的函数栈中弹出而保证正确性了。
def dfs(self,node,path,paths): if not node: return new_path = path[:] + [node.val] if not node.left and not node.right: paathStr = '->'.join(map(str,new_path)) paths.append(paathStr) self.dfs(node.left,new_path,paths) self.dfs(node.right,new_path,paths)
404. 左叶子之和
给定二叉树的根节点 root ,返回所有左叶子之和。
思路
由于int类型为不可变类型,所以用成员变量统计sum,也是遍历的思路,递归遍历左右,到达左叶子时处理信息(修改sum)。
class Solution: def __init__(self): self.sum = 0 def sumOfLeftLeaves(self, root: Optional[TreeNode]) -> int: self.dfs(root) return self.sum def dfs(self,node): if not node: return 0 self.dfs(node.left) self.dfs(node.right) if node.left: if not node.left.left and not node.left.right: # 左叶子 self.sum += node.left.val
个人觉得代码随想录的方法在理解上比较难,不是很直观,其代码如下:
class Solution: def sumOfLeftLeaves(self, root): if root is None: return 0 if root.left is None and root.right is None: return 0 leftValue = self.sumOfLeftLeaves(root.left) # 左 if root.left and not root.left.left and not root.left.right: # 左子树是左叶子的情况 leftValue = root.left.val rightValue = self.sumOfLeftLeaves(root.right) # 右 sum_val = leftValue + rightValue # 中 return sum_val
513.找树左下角的值
给定一个二叉树的 根节点 root,请找出该二叉树的 最底层 最左边 节点的值。
假设二叉树中至少有一个节点。
思路
遍历思路,每次遇到叶子节点,判断其是否是最底层的。
实际编码中,使用depth记录当前节点的深度,使用maxDepth记录最大深度。
由于depth>self.maxDepth,所以每次最底层碰到的第一个节点就会被记录,而同层其他的节点的深度等于该节点,所以不会被更新。
class Solution: def __init__(self): self.maxDepth = float('-inf') self.res = 0 def findBottomLeftValue(self, root: Optional[TreeNode]) -> int: self.dfs(root,0) return self.res def dfs(self,node,depth): if not node: return if not node.left and not node.right: # 叶子节点 if depth > self.maxDepth: self.maxDepth = depth self.res = node.val self.dfs(node.left,depth+1) self.dfs(node.right,depth+1)
222. 完全二叉树的节点个数
思路
找到子树为满二叉树的节点,公式计算。
如何判断满二叉树?(由于题目是完全二叉树),所以只遍历左链和右链即可判断。
class Solution: def countNodes(self, root: Optional[TreeNode]) -> int: return self.dfs(root) def dfs(self,node): if not node: return 0 left = node.left right = node.right leftDepth,rightDepth = 0,0 while(left): leftDepth+=1 left = left.left while(right): leftDepth+=1 right = right.right if leftDepth == rightDepth: return (2<<leftDepth)-1 lnum = self.dfs(node.left) rnum = self.dfs(node.right) return lnum + rnum+1
心得体会
更新了对二叉树章节中递归的思考。
- 上节课中树形DP的思路,一般只考虑单层递归逻辑和递归基,如我这个节点和我左右孩子要做什么(局部),我左右子树要做什么(递归)
- 本节的二叉树的所有路径,左叶子之和以及找树左下角的值都是考虑遍历的思路,也就是在思考上,需要我们考虑,我们是用递归序在遍历这棵树,判断我们在遍历过程中需要收集的信息,放入全局变量,然后再考虑单层逻辑,特殊节点逻辑等。特别的,单层逻辑中,需要考虑相对全局变量的回溯,或者利用局部变量隐形回溯。(注:这里的全局变量指的是比当前局部作用域更大的变量,如可变类型的参数,类的成员等)单层逻辑中一般考虑该节点怎么收集,离开该节点时怎么处理。
也就是说,随着更多的题目,思路上不再是之前完全不考虑遍历过程的逻辑。
- 判断性质的,计算以某节点为根的某个问题等,一般用树形DP等传统递归思考方式。
- 处理特殊的节点,路径相关的(处理全部节点)等,一般用遍历(递归序)的思考方式。
重点例子
实际上,下面的例子可以很好的用这两种不同思路解决:
求一个二叉树的节点数量
- 可以用树形DP的思考方式,这种理解上很简单,以当前节点为根的树,节点数量是多少呢? 就是我左子树的数量加我右子树的数量加我自己(1)。
class Solution: def countNodes(self, root: Optional[TreeNode]) -> int: return self.dfs(root) def dfs(self,node): if not node: return 0 lnum = self.dfs(node.left) rnum = self.dfs(node.right) return lnum + rnum +1
- 可以用遍历的思考方式,我要遍历整棵树,可以在每次第一次访问节点时将数量加一即可。第二次,第三次访问到该节点时啥都不做。像这个问题中,为什么不需要回溯呢?因为要求的是节点的数量,每次经过节点第一次访问时处理一次(即将节点数量加1),从左右孩子返回时并不需要做什么,所以不需要回溯。而之前路径的题目,它在到达子节点后,是路径中添加了一个节点,返回其父节点时(第三次访问自己后)要把它去掉,所以才有了回溯。后面要做的路径之和,同样要消除子节点的影响,所以同样需要回溯。再
class Solution: def __init__(self): self.num = 0 def countNodes(self, root: Optional[TreeNode]) -> int: self.dfs(root) return self.num def dfs(self,node): if not node: return self.num +=1 # 第一次访问时记录 self.dfs(node.left) self.dfs(node.right)
求深度的例子也可以按这两种方式来思考
此外,递归过程中的局部变量(包含本身的局部变量和传入的不可变变量,因为修改后实际是创建新的对象)肯定是和递归函数一致的,即天然会回溯,而为了实现某些功能使用全局变量时,为了保证全局变量(如参数为列表,修改的是列表内容时)记录时与递归函数中保持一致,才引入了回溯。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步