写这一系列的笔记,是为了分类整理LeetCode里面的算法题目,以及涉及到的算法的知识点。
树天然就是一种递归结构,所以与树相关的问题,通常都是以下三步:1、明确递归停止的条件(何时return);2、递归的本质是栈式调用,明确从树的叶子节点到根节点的主要的代码逻辑;3、二叉树分左右两棵子树递归。多叉树for循环递归。在Python中,为了减少参数个数,一般树的递归或者dfs通常会和闭包混合使用,即在解题函数中套用一个backtrack的辅助函数。
以LeetCode热题Hot100为例,来复习一下求解树类问题的套路:
树的遍历方式分为前序、中序、后序、层序,前面三种指的是根节点的排放位置,例如中序遍历就是递归按照左子树-根节点-右子树的方式进行遍历。于是按照之前的三步走则为:1、递归停止条件,如果当前节点为空,则返回对应的res列表。2、主体代码逻辑:res首先加上左子树中序遍历的列表,再加上根节点的值,最后加上右子树中序遍历的列表。3、左右递归,实际上已经在第2步中完成了。代码如下
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def inorderTraversal(self, root: TreeNode) -> List[int]:
res = []
if not root:
return res
res += self.inorderTraversal(root.left)
res.append(root.val)
res += self.inorderTraversal(root.right)
return res
这道题目的递归相对来说要复杂一些,因为二叉搜索树右子树的最小值不一定就是root.right.val,而有可能是root.right.left.let..val,对于左子树亦然。所以我们需要维护两个临时变量,来兜住当前树的最大值和最小值。假如当前树是一棵左子树,那么其最小值可以认为是-inf,最大值认为是root.val。如果当前树是右子树,那么最大值是inf,最小值是root.val。所以继续三步走:1、递归终止条件:当前节点为空,则return True。2、主逻辑,判断当前节点的值是否落在最小最大值的区间里面,如不符合则返回False。3、左右子树递归,更换相应的最小最大值,如不满足则返回False。代码如下:
class Solution:
def isValidBST(self, root: TreeNode) -> bool:
return self.helper(root)
def helper(self,node,lower=float('-inf'),upper=float('inf')):
if not node:
return True
val = node.val
if val <= lower or val>= upper:
return False
if not self.helper(node.right,val,upper):
return False
if not self.helper(node.left,lower,val):
return False
return True
大多数和树相关的问题,都没有前序遍历、中序遍历、后序遍历那么简单。因为大多数需要对参数进行改变,例如上一题的二叉搜索树的验证,所以需要设计辅助函数,这一道题的子问题是左右子树是否是镜像对称的。所以很自然地想到需要加一个辅助函数,参数是两个node,以判断这两棵树是否是镜像对称的。
递归肯定是针对辅助函数进行递归,那么继续三步走:1、递归终止条件:两棵树的当前节点均为空,则返回True;两棵树一个当前节点为空,一个不为空,则返回False。2、主要逻辑:如果两颗树的当前节点值相同,且node1.left、node2.right互为镜像,且node1.right、node2.left互为镜像,则返回为True。代码如下:
class Solution:
def isSymmetric(self, root: TreeNode) -> bool:
return self.helper(root,root)
def helper(self, node1:TreeNode, node2:TreeNode)->bool:
if not node1 and not node2:
return True
if not node1 or not node2:
return False
return node1.val==node2.val and self.helper(node1.left,node2.right) and self.helper(node1.right, node2.left)
这个题目没啥特别好说的,树的基本遍历方式之一,甚至不需要用到递归,直接两层for循环就解决了,代码如下:
class Solution:
def levelOrder(self, root: TreeNode) -> List[List[int]]:
res,cur_layer = [],[]
if not root:
return res
cur_layer.append(root)
while cur_layer:
cur_temp,cur_val = [],[]
for node in cur_layer:
if node.left:
cur_temp.append(node.left)
if node.right:
cur_temp.append(node.right)
cur_val.append(node.val)
res.append(cur_val)
cur_layer = cur_temp
return res
这个题目属于简单题,直接三步走吧。1、递归结束条件:来到叶子节点,直接返回为0。2、主要代码逻辑,二叉树的最大深度为max(左子树最大深度、右子树最大深度)+1。代码如下:
class Solution:
def maxDepth(self, root: TreeNode) -> int:
if not root:
return 0
return max(self.maxDepth(root.left), self.maxDepth(root.right))+1
这道题目可以先把什么递归啊遍历啊先抛开都不管,当自己什么思路都没有的时候,就先试试自己走一个例子,然后从特殊到一般具体看要用什么方法解题。树是这样,动态规划确定动态空间、base、与动态转移方程也是这样。ok,那先看看具体的题目:
前序遍历特点是先获取根节点的元素值,中序遍历是以左子树-根节点-右子树的方式去遍历。所以,由前序遍历的第一个节点,节后中序遍历,可以得出左子树的元素个数为1,根节点元素为3,右子树的元素个数为3,。于是根据preorder[-3:]与inorder[:-3]继续走重构的套路,所以很明显是一个递归的套路。于是考虑之前的三步走:1、递归终止条件,如果preorder的长度为0,则直接返回None;如果长度为1,则返回根节点元素为preorder[0]的TreeNode。2.主体逻辑,首先排除掉根节点元素,root.left = 左半部分元素继续重构。root.right = 右半部分元素继续重构。代码如下:
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
if not preorder:
return None
if len(preorder) == 1:
return TreeNode(preorder[0])
root_index = inorder.index(preorder[0])
root = TreeNode(preorder[0])
root.left = self.buildTree(preorder[1 : root_index + 1], inorder[:root_index])
root.right = self.buildTree(preorder[root_index + 1:], inorder[root_index + 1:])
return root
这道题难度较大,先读题目:
如果不考虑题目中要求的原地两个字,那就直接gg了,害,不就是前序遍历再重构嘛,拿到前序遍历的val列表,然后list[0].right = list[1],list[1].right =list[2]..如此这般题目就解决了,当然这样好像也是原地的一种,就是显得有点low。其实在面对树的问题时,很多时候可以先将树通过前序、中序、后序或者层序遍历转换为数组(列表),再来进行求解,不管白猫黑猫抓到耗子就是好猫。ok,先试试这一种方法吧。需要注意的是,前序遍历得到的是val列表,不是TreeNode列表,如果是TreeNode列表的话需要先遍历把TreeNode的left 和right都置为None,再进行拼接。该思路代码如下:
class Solution:
def flatten(self, root: TreeNode) -> None:
preorder = self.preorder(root)
n = len(preorder)
if n <= 1:
return
root.left = None
root.right = TreeNode(preorder[1])
cur = root.right
for i in range(2, n):
cur.right = TreeNode(preorder[i])
cur = cur.right
def preorder(self, root:TreeNode):
if not root:
return []
res = []
res.append(root.val)
res += self.preorder(root.left)
res += self.preorder(root.right)
return res
思路二:这道题,出题人的原本意图肯定不是希望我们用前序遍历再拼接去解题的,面试官看到这种答题方式只会说你个poor guy。所以考虑别的方法,其实简单分解一下步骤是这样的,1、先把左子树拉平,变成2.right = 3,3.right=4的这样的一棵子树,2、再把右子树拉平,题目给出的测试例中右子树已经是平的了。3、找到左子树的最右叶子节点,将右子树拼接到最右叶子结点之后。4、此时右子树为空,将左子树移到右子树即可,结束。其中每一个子树都是这样的处理过程,所以采用递归栈式调用。
代码如下:
class Solution:
def flatten(self, root: TreeNode) -> None:
"""
Do not return anything, modify root in-place instead.
"""
if root is None:
return
self.flatten(root.left)
self.flatten(root.right)
if root.left:
pre = root.left
while pre.right :
pre = pre.right
pre.right = root.right
root.right = root.left
root.left = None
这题目和之前判断是否是对称二叉树一致。还是一样的,终止条件+主体对换代码+两路递归结束,没啥特别需要注意的点。
class Solution:
def invertTree(self, root: TreeNode) -> TreeNode:
if not root:
return
root.left,root.right = root.right, root.left
self.invertTree(root.left)
self.invertTree(root.right)
return root
个人觉得这道题的难点在于路径的起点和终点都是不确定的,这道题直接能够想到的就是肯定是dfs来确定当前走过的列表的尾节点,然后遍历求值,如果相等则进行计数;另外应该涉及回溯,遍历左节点之后再遍历右节点。右节点的值应该会覆盖左节点原本的值。
class Solution:
def __init__(self):
self.sum = 0
def pathSum(self, root: TreeNode, sum: int) -> int:
self.sum = sum
lyst = [0 for _ in range(1000)]
return self.getSum(root, lyst, 0)
def getSum(self, root: TreeNode, vals: [], layer):
if not root:
return 0
vals[layer] = root.val
cur, temp = 0, 0
for i in range(layer, -1, -1):
temp += vals[i]
if temp == self.sum:
cur += 1
return cur + self.getSum(root.left, vals, layer + 1) + self.getSum(root.right, vals, layer + 1)
class Solution:
def __init__(self):
self.total = 0
def convertBST(self, root: TreeNode) -> TreeNode:
if not root:
return
self.convertBST(root.right)
self.total += root.val
root.val = self.total
self.convertBST(root.left)
return root
这道题目很明显是需要辅助函数的,如果要求直径必须经过根节点的话,那么只要对104题二叉树的最大深度稍加改动,求出L+R+1即可。但是这道题,L+R+1仅仅用于递归中间的比较过程,而不作为递归返回的结果,递归返回的是以当前节点为根节点的最大节点深度(但是仅仅是左子树或者右子树)。
class Solution:
def diameterOfBinaryTree(self, root: TreeNode) -> int:
res = 1
def backtrack(node):
nonlocal res
if not node:
return 0
L = backtrack(node.left)
R = backtrack(node.right)
res = max(L + R + 1, res)
return max(L, R) + 1
backtrack(root)
return res - 1
把合并后的结果都放到t1上去,代码如下:
class Solution:
def mergeTrees(self, t1: TreeNode, t2: TreeNode) -> TreeNode:
if not t1:
return t2
if not t2:
return t1
t1.val += t2.val
t1.left = self.mergeTrees(t1.left, t2.left)
t1.right = self.mergeTrees(t1.right, t2.right)
return t1
与543题类似,只不过一个是加1一个是加节点的值,代码如下:
class Solution:
def maxPathSum(self, root: TreeNode) -> int:
def max_gain(node):
nonlocal max_sum
if not node:
return 0
left_gain = max(max_gain(node.left), 0)
right_gain = max(max_gain(node.right), 0)
price_newpath = node.val + left_gain + right_gain
max_sum = max(max_sum, price_newpath)
return node.val + max(left_gain, right_gain)
max_sum = float("-inf")
max_gain(root)
return max_sum