================================================================
在复习总览部分,尽可能文字写思路,梳理过程,尽可能是力扣最好的解法。
随想录+力扣题解的基础上,包括hot100的二叉树题目。
================================================================
二叉树基础
满二叉树
除了最后一层无任何子节点外,每一层上的所有节点都有两个子节点。
完全二叉树
若二叉树的深度为 h,除第 h 层外,其它各层的结点数都达到最大个数,第 h 层所有的叶子结点都连续集中在最左边,这就是完全二叉树。
二叉搜索树
左子树上的所有节点都小于它的根节点,右子树上的所有节点大于它的根节点。
二叉搜索树上一个有序树。
平衡二叉搜索树(AVL树,adelson-velsky and landis)
左右两个子树的高度差不超过1的二叉搜索树。
226. 翻转二叉树 【HOT100】
把每个节点的左右节点交换就好了。左右节点交换时,它们的子树自然会一起被带着交换。
class Solution:
def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
if not root:
return
root.left, root.right = root.right, root.left
self.invertTree(root.left)
self.invertTree(root.right)
return root
Python 的“多重赋值”机制允许直接交换两个变量而不需要额外的临时变量。
树的遍历
深度优先遍历:先往深走,遇到叶子节点再往回走。
- 前序遍历(中左右)
- 中序遍历(左中右)
- 后序遍历(左右中)
94. 二叉树的中序遍历【HOT100】
class Solution:
def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
if not root:
return []
return self.inorderTraversal(root.left) + [root.val] + self.inorderTraversal(root.right)
比想象中的难写,一定要确定自己能写出来而不只是懂原理。
广度优先遍历:一层一层遍历。
102. 二叉树的层序遍历 【HOT100】
class Solution:
def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
if not root:
return []
# 用双端队列,先进先出
next_level = collections.deque([root])
result = []
while next_level:
level = []
for _ in range(len(next_level)):
# 处理本节点
cur = next_level.popleft()
level.append(cur.val)
# 如果有子节点,把子节点压入nextlevel
if cur.left:
next_level.append(cur.left)
if cur.right:
next_level.append(cur.right)
result.append(level)
return result
重新整理过代码和注释。层序遍历,其实二叉树基本只看了递归,所以对层序遍历不是很熟。
通常实现层序遍历时,会使用deque双端队列,因为它可以popleft,满足二叉树层序遍历所需要的先进先出。
==-*——
在Python里我们会使用deque双端队列来处理层序遍历,因为它可以popleft,实现先进先出。先进先出就可以自然地逐层地访问每一层的节点。
我们需要一个while循环,只要双端队列next_level不为空,就代表还有下一层的节点需要处理。
具体上是用len(next_level)次的For循环处理目前next_level里的每个节点,把这一层的节点取出加入到列表level,把节点的子节点,全部放到next_level。
每一次while循环的结尾就是说明处理完了一整层,把level列表加入到result列表。
最后返回result列表就可以了。
==-*——
这一题是要从最下面一层往最上面一层遍历。那么在102的基础上,return result[::-1]就可以了。
199. 二叉树的右视图 【HOT100】
在102的基础上,每层只把level.pop(),也就是每层的最后一个加入到result就好。
105. 从前序与中序遍历序列构造二叉树【HOT100】
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
index = {x: i for i, x in enumerate(inorder)}
def dfs(pre_l: int, pre_r: int, in_l: int, in_r: int) -> Optional[TreeNode]:
if pre_l == pre_r: # 空节点
return None
left_size = index[preorder[pre_l]] - in_l # 左子树的大小
left = dfs(pre_l + 1, pre_l + 1 + left_size, in_l, in_l + left_size)
right = dfs(pre_l + 1 + left_size, pre_r, in_l + 1 + left_size, in_r)
return TreeNode(preorder[pre_l], left, right)
return dfs(0, len(preorder), 0, len(inorder)) # 左闭右开区间
高度深度相关
104. 二叉树的最大深度【HOT100】
深度:任意一个节点到根节点的距离(根节点的深度为1)
高度:任意一个节点到最远的叶子结点的距离(根节点的高度为层数)
求高度解法:
class Solution:
def maxDepth(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
leftheight = self.maxDepth(root.left)
rightheight = self.maxDepth(root.right)
height = 1 + max(leftheight, rightheight)
return height
==-*——
最大深度就是根节点的高度。所以这道题转换为求高度会更简单。
核心思路:一个节点的高度,就是左边子树的高度和右边子树的高度中大的那个再+1。
==-*——
直接求深度解法:
class Solution:
def maxDepth(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
if root.left and not root.right:
return 1 + self.maxDepth(root.left)
if root.right and not root.left:
return 1 + self.maxDepth(root.right)
return 1 + max(self.maxDepth(root.left), self.maxDepth(root.right))
==-*——
相同核心思路:一个节点的高度,就是左边子树的高度和右边子树的高度中大的那个再+1。
111.二叉树的最小深度,答案就是这个直接求深度解法,把max换成min。所以两个都要掌握。
==-*——
101. 对称二叉树【HOT100】
我记得这一题,是先比较内侧的节点是否一致,root.left.right == root.right.left
然后比较外侧的节点是否一致,root.left.left == root.right.right
class Solution:
def isSymmetric(self, root: Optional[TreeNode]) -> bool:
def isMirror(left: Optional[TreeNode], right: Optional[TreeNode]) -> bool:
# 若两边都为空,对称
if not left and not right:
return True
# 若一边为空一边不为空,不对称
if not left or not right:
return False
# 两边的值相同,且继续判断子节点是否镜像
return (left.val == right.val) and isMirror(left.left, right.right) and isMirror(left.right, right.left)
# 根节点为空,返回 True;否则调用 isMirror 判断左右子树是否镜像
return isMirror(root.left, root.right) if root else True
必须使用辅助函数的原因在于:对称性检查本质上是二叉树的左右子树的递归比较,需要同时传入左右两个子树的节点进行镜像比较,而主函数 isSymmetric
本身只接收根节点 root
。因此,辅助函数 isMirror(left, right)
用于分别传入左右子树进行递归的镜像对比判断。
==-*——
因为检查对称性需要传入两个节点作为参数,所以新开一个需要输入两个节点的isMirror辅助函数可以让逻辑更清晰,符合“单一责任原则”。
对于每对节点,首先判断高度相同,然后比较val值是否相同,最后继续判断子节点是否镜像。
==-*——
平衡二叉树:左右两个子树的高度差最多是1。
class Solution:
def isBalanced(self, root: Optional[TreeNode]) -> bool:
return False if self.getHeight(root) == -1 else True
def getHeight(self,node) -> int:
if not node:
return 0
lefth = self.getHeight(node.left)
righth = self.getHeight(node.right)
if lefth == -1 or righth == -1:
return -1
if abs(lefth - righth) > 1:
return -1
return 1 + max(lefth,righth)
==-*——
因为需要每次记录当前节点的高度,所以重新开一个辅助函数,每次返回int变量节点高度。如果左右不平衡了,就直接返回-1。
每一次递归的逻辑:
如果节点为空,返回0。
如果节点不为空,获取节点左子树的高度,右子树的高度。①如果左子树或右子树的高度为-1,直接返回-1。②如果一个节点的左右子树高度差大于1,返回-1。③否则,返回1+子树里最高的高度。
==-*——
543. 二叉树的直径【HOT100】
class Solution:
def diameterOfBinaryTree(self, root: TreeNode) -> int:
self.max_diameter = 0
def depth(node):
if not node:
return 0
left_depth = depth(node.left)
right_depth = depth(node.right)
# 更新最大直径
self.max_diameter = max(self.max_diameter, left_depth + right_depth)
# 返回节点的最大深度
return max(left_depth, right_depth) + 1
depth(root)
return self.max_diameter
543 题 关注的是 每个节点的最大左右子树深度之和,通过递归遍历所有节点的左右子树来更新最大直径。
路径相关
比之前写换了个思路,这个是力扣排名最快的。这题稍微有点难。
class Solution:
def binaryTreePaths(self, root: Optional[TreeNode]) -> List[str]:
# 初始化结果列表
res = []
# 深度优先搜索递归函数
def dfs(node, path):
# 将当前节点的值加入路径中
path.append(str(node.val))
# 如果当前节点是叶子节点,将路径添加到结果列表
if node.left is None and node.right is None:
res.append("->".join(path))
else:
# 递归遍历左子节点和右子节点
if node.left:
dfs(node.left, path)
if node.right:
dfs(node.right, path)
# 回溯,移除路径中的当前节点
path.pop()
# 特殊情况:根节点为空
if root:
dfs(root, [])
return res
回溯模板
递归结束条件:当前节点是叶子节点,那就有一条路径了
递归过程:递归左右子节点
path.pop()就是回溯的过程。
class Solution:
def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
if not root:
return False
# 叶子节点,且总和等于目标值
if not root.left and not root.right and targetSum == root.val:
return True
# 不是叶子结点或还没找到目标值,检查它的子节点
left_has_path = self.hasPathSum(root.left, targetSum - root.val)
right_has_path = self.hasPathSum(root.right, targetSum - root.val)
return left_has_path or right_has_path
上一题是求所有路径,这道题是求是否有一条路径的总和为目标值。题目看起来很像,所以放在一起看。但是他们的解题思路差别很大。
- 257题 需要遍历所有路径,故采用 回溯,记录路径以获取所有可能的路径。
- 112题 只需找到一条符合条件的路径,因此采用 递归/DFS 即可,无需回溯,因为一旦找到目标路径就可以直接返回。
437. 路径总和 III【HOT100】
这题是求该二叉树里节点值之和等于 targetSum
的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
本题的前置题目,560. 和为 K 的子数组,是本题在数组上的情况。本题做法和 560 题是一样的,前缀和+哈希表。
class Solution:
def pathSum(self, root: Optional[TreeNode], targetSum: int) -> int:
ans = 0
cnt = defaultdict(int)
cnt[0] = 1
def dfs(node: Optional[TreeNode], s: int) -> None:
if node is None:
return
nonlocal ans
s += node.val
ans += cnt[s - targetSum]
cnt[s] += 1
dfs(node.left, s)
dfs(node.right, s)
cnt[s] -= 1 # 恢复现场
dfs(root, 0)
return ans
-=- 目前到day17 -=-
补充二叉树题目
层序遍历相关
凡是每一层要做什么的,都先考虑BFS能不能解决。
class Solution:
def averageOfLevels(self, root: Optional[TreeNode]) -> List[float]:
ans = []
queue = deque([root])
while queue:
s = 0
length = len(queue)
for _ in range(length):
node = queue.popleft()
s += node.val
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
ans.append(s / length)
return ans
在102的基础上,在每一行,把每个节点的值累加。在每次while循环的结尾,把累加值除于这行节点个数得到平均值,加入到结果列表。
# 如果有子节点,把子节点压入nextlevel
for child in cur.children:
next_level.append(child)
N叉树和二叉树的区别,只有处理子节点时不一样。
只是把if cur.right/ if cur.left的部分改成了for child in cur.children。这个cur.children是这题里面定义的数据结构。
简单递归
class Solution:
def sumOfLeftLeaves(self, root: Optional[TreeNode]) -> int:
if root is None:
return 0
value = 0
if root.left and not root.left.left and not root.left.right:
value = root.left.val
return value + self.sumOfLeftLeaves(root.left) + self.sumOfLeftLeaves(root.right)
左叶子要通过父节点判断是左子节点,然后判断是叶子节点。
每次递归要先初始化value = 0,因为return的时候可能需要传这个0。没有初始化的话,会因为找不到变量报错。
==-*——
左叶子之和,就是每次判断当前节点、当前节点的左右子节点是否为左叶子,是的话就把值累加。
==-*——
class Solution:
def countNodes(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
return 1 + self.countNodes(root.left) + self.countNodes(root.right)
所有二叉树的节点个数都可以这样算。
先进先出列表
class Solution:
def findBottomLeftValue(self, root: Optional[TreeNode]) -> int:
# 先进先出列表
q = deque([root])
while q:
node = q.popleft()
# 先放了右子节点
if node.right:
q.append(node.right)
if node.left:
q.append(node.left)
# 最后留下的肯定是最底层最左边的节点
return node.val
==-*——
用了一个先进先出列表。在while循环里,只要列表不为空,每次把最前面的节点取出,然后把它的右子节点加到列表里,然后把它的左子节点加到列表里。这样处理,while循环结束后,最后留下的肯定是最底层最左边的节点。
==-*——