110. 平衡二叉树
一棵高度平衡二叉树定义为:一个二叉树每个节点的左右两个子树的高度差的绝对值不超过 1 。
暴力解法很容易想到,计算每个节点的高度,然后遍历所有节点,如果发现不平衡直接返回。
但是对于不平衡的输入会由很多不必要的复杂度(时间或者空间)。实际上在计算节点高度的时候,已经可以进行平衡判断了。
暴力解法
class Solution:
def height(self, node: Optional[TreeNode]) -> int:
if node == None:
return 0
left_height = self.height(node.left) # left
right_height = self.height(node.right) # right
return 1 + max(left_height, right_height) # middle
def isBalanced(self, root: Optional[TreeNode]) -> bool:
if root == None:
return True
left_height = self.height(root.left)
right_height = self.height(root.right)
if abs(left_height - right_height) > 1:
return False
return self.isBalanced(root.left) and self.isBalanced(root.right)
巧妙解法
class Solution:
def height(self, node: Optional[TreeNode]) -> int:
if node == None:
return 0
left_height = self.height(node.left) # left
right_height = self.height(node.right) # right
if left_height < 0 or right_height < 0:
return -1
if abs(left_height - right_height) > 1:
return -1
return 1 + max(left_height, right_height)
def isBalanced(self, root: Optional[TreeNode]) -> bool:
return self.height(root) != -1
257. 二叉树的所有路径
非常符合直觉的前序遍历,记录当前的 path,在遍历中不断更新,直到碰到叶子节点的时候将整条 path 添加到结果中。
但是与之前题目的区别在于,用于记录当前路径的 path 数组是会不断更新的,也就是说在执行完左子树的递归后,path 已经发生了改变,没办法直接再将其传入右子树的递归。
这里就需要用到回溯的思想。
每一次递归都要和回溯绑定使用,才能保证回溯有效!
暴力解法
朴素的前序遍历,通过递归时的输入传递当前节点的 path(不包括当前节点),最后返回结果。为了保证 path 中记录的有效性,递归时传入的是 path.copy()
。
要注意数组的复制。这里用的一维数组使用浅复制即可,如果是多层数组则需要用到深复制(copy.deepcopy(a)
)。
这个方法的缺点是需要不停进行数组的复制,空间复杂度有点高。
class Solution:
def __init__(self):
self.results = []
def path(self, node: TreeNode, path: List[str]) -> None:
if node != None: # do nothing over None node
path.append(str(node.val))
if node.left == None and node.right == None: # leaf node, add to results
self.results.append("->".join(path))
else:
self.path(node.left, path.copy())
self.path(node.right, path.copy())
def binaryTreePaths(self, root: Optional[TreeNode]) -> List[str]:
self.path(root, [])
return self.results
回溯解法
与之前的解法唯一的区别在于,递归时直接传入了 path 数组,并且在当前递归执行完毕后进行了一次 pop。这保证了在进行下一次递归(不管是当前的右子树,或者是)的时候,path 数组会保持进行当前递归前的状态(就像是当前数组的 copy 一样)。
重点:回溯和递归是一一对应的,有一个递归,就要有一个回溯。同时也只需要有一个回溯,因为可以保证每一次调用递归都进行了一层回溯,所以当前的递归结束后,每次深层递归导致的改动都被回溯了。
如下图所示:
class Solution:
def __init__(self):
self.results = []
def path(self, node: TreeNode, path: List[str]) -> None:
path.append(str(node.val))
if node.left == None and node.right == None: # leaf node, add to results
self.results.append("->".join(path))
else:
if node.left != None:
self.path(node.left, path)
path.pop()
if node.right != None:
self.path(node.right, path)
path.pop()
def binaryTreePaths(self, root: Optional[TreeNode]) -> List[str]:
if root == None:
return []
self.path(root, [])
return self.results
404. 左叶子之和
题目中“左叶子”的定义:一个叶子节点,同时是另一个节点的左节点。
很明显这是一个 bottom up 的过程,所以应该是后序遍历,从子节点收集数据然后回传给父节点。
需要注意的是递归的单层逻辑中,判断当前节点的左节点是否是叶子并不属于终止条件,而是决定了回传的值,属于是“中”的部分。
class Solution:
def sumOfLeftLeaves(self, root: Optional[TreeNode]) -> int:
if root == None:
return 0
count = 0
if root.left != None and root.left.left == None and root.left.right == None:
count += root.left.val
left_sub_sum = self.sumOfLeftLeaves(root.left)
right_sub_sum = self.sumOfLeftLeaves(root.right)
return left_sub_sum + right_sub_sum + count