本篇全部集中在二叉树相关问题上,是参考东哥的思路进行的练习和思考。东哥有《labuladong 的算法小抄》以及宝藏微信公众号 labuladong,github 也有项目,自来水推荐购买和关注。
重要的事情念 3 遍,重在遍历遍历遍历~
二叉树的遍历本身就仅仅是递归而已,无论是否有前序中序后序,这个递归都在那里,做它自己的事情。前序中序后序遍历只是在递归过程不同的时间点做操作而已,如下图。
如果你想要通过遍历二叉树来寻求某些问题的解,需要想明白两个问题:
二叉树相关的算法可以分为两大类:
有关上面两大类算法的一个典型题目是求一棵二叉树的深度。
一棵二叉树的前序遍历的结果为 [3,9,20,15,7] ,中序遍历的结果为 [9,3,15,20,7] ,请你画出该二叉树。
一棵二叉树的后序遍历的结果为[9,15,20,7,3] ,中序遍历的结果为[9,3,15,20,7] ,请你画出该二叉树。
思路一:通过遍历二叉树的方式,在不同时刻做操作来求取二叉树深度。
def maxDepth(self, root: TreeNode) -> int:
self.depth = 0
self.res = 0
def helper(root):
if not root:
return
self.depth += 1
self.res = max(self.res, self.depth)
helper(root.left)
helper(root.right)
self.depth -= 1
return
helper(root)
return self.res
思路二:通过分治的思想,将问题规模缩小为求左子树和右子树的深度。
def maxDepth(self, root: TreeNode) -> int:
if not root:
return 0
return max(self.maxDepth(root.left), self.maxDepth(root.right)) + 1
思路一:与上问题如出一辙,通过遍历树来求取。
def maxDepth(self, root: 'Node') -> int:
self.depth = 0
self.res = 0
def helper(root):
if root is None:
return
self.depth += 1
self.res = max(self.res, self.depth)
for child in root.children:
helper(child)
self.depth -= 1
return
helper(root)
return self.res
思路二:通过分治求取子树深度继而求取整棵树的深度。
def maxDepth(self, root: 'Node') -> int:
if not root:
return 0
return max([0] + [self.maxDepth(child) for child in root.children]) + 1
思路一:通过遍历二叉树
def minDepth(self, root: TreeNode) -> int:
# 与最大深度如出一辙
self.res = float('Inf')
self.depth = 0
def helper(root):
if not root:
return
self.depth += 1
# 当且仅当叶子节点时更新 res
if not root.left and not root.right:
self.res = min(self.res, self.depth)
helper(root.left)
helper(root.right)
self.depth -= 1
return
if not root:
return 0
helper(root)
return self.res
思路二:分治
def minDepth(self, root: TreeNode) -> int:
if not root:
return 0
# 比最大深度问题多了一点需要额外考虑的,就是左子树为空或者右子树为空的时候
if not root.left:
return self.minDepth(root.right) + 1
elif not root.right:
return self.minDepth(root.left) + 1
return min(self.minDepth(root.left), self.minDepth(root.right)) + 1
版本一:维护一个全局变量,来表示是否存在目标和,仅当叶子节点时做判断。缺点:未剪枝,必须全部遍历完。
def hasPathSum(self, root: TreeNode, targetSum: int) -> bool:
self.sum = 0
self.exist = False
def helper(root):
if not root:
return
self.sum += root.val
if not root.left and not root.right and self.sum == targetSum:
self.exist = True
helper(root.left)
helper(root.right)
self.sum -= root.val
return
helper(root)
return self.exist
思路二: 递归分治
def hasPathSum(self, root: TreeNode, targetSum: int) -> bool:
# 本函数输入为一棵树的根节点(指针),返回该树是否有和为 targetSum 的路径
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)
def pathSum(self, root: TreeNode, targetSum: int) -> List[List[int]]:
# 维护变量
self.sum = 0
self.res = []
track = []
def helper(root, track):
if not root:
return
# 进入节点,更新变量
track.append(root.val)
self.sum += root.val
# 判断叶子节点以及路径和
if not root.left and not root.right and self.sum == targetSum:
self.res.append(track[:])
helper(root.left, track)
helper(root.right, track)
# 撤销更新
track.pop()
self.sum -= root.val
return
helper(root, track)
return self.res
def invertTree(self, root: TreeNode) -> TreeNode:
def helper(root):
if not root:
return root
temp = root.right
root.right = helper(root.left)
root.left = helper(temp)
return root
return helper(root)
打印十进制只需要把上述代码 for 循环中的 2 改为 10 即可,但是此时打印高位会为 0,比如数字 9 会作为 09 打印。
python 里面 in 方法在数据规模很大的时候会很耗时,但这里暂定认为 nums 大小比较小,所以直接用 in 方法。
def permute(self, nums: List[int]) -> List[List[int]]:
n = len(nums)
track = []
self.res = []
def dfs(track):
if len(track) == n:
self.res.append(track[:])
return
for i in range(n):
if nums[i] in track:
continue
track.append(nums[i])
dfs(track)
track.pop()
return
dfs(track)
return self.res
二叉树问题里面遍历方式“做选择和撤销选择” 都在 for 循环外面,比如 N 叉树深度问题;但是回溯算法比如全排列或者生成 10 进制数,“做选择和撤销选择”都在 for 循环里面。因为这两种情况不一样?比如二叉树深度问题 depth 确实是对当前节点做的,而不是它的子节点;而回溯全排列 track 是对下一个位置填空,所以要对它的“子节点”(选择列表)进行“做选择和撤销选择”?
上述这条应该这么理解,回溯模板写在 for 循环里面,就“抛弃”了 root,但其实 root 在回溯这里是无用的,root 下面的分支才是你可以做的“选择”。
因为集中用递归的方式,所以迭代都略过。