1、与深度相关的递归问题不同于深度优先遍历的前、中、后序遍历,该类问题在递归调用时带有返回值,但它们的核心难点还是在于递归三要素中的提取重复的逻辑,缩小问题规模,即递归函数内部的操作
2、在做了“110. 平衡二叉树”之后,会发现本次总结的题型全部都是自底向顶的递归,这种的逻辑还是有点难想的
104. 二叉树的最大深度
思路1、层序遍历:一般 层序遍历的基础上,输出最大高度值
思路2、递归:重复逻辑:二叉树节点的最大深度 等于 左右子树的最大深度+1
'''
104. 二叉树的最大深度
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
示例 1:
输入:[3,9,20,null,null,15,7]
输出:3
思路1、层序遍历:一般 层序遍历的基础上,输出最大高度值
思路2、递归:重复逻辑:二叉树节点的最大深度 等于 左右子树的最大深度+1
'''
class Solution:
def maxDepth(self, root: Optional[TreeNode]) -> int:
# 情况1、树为空
if root == None:
return 0
# 情况2、树不为空
result = 0
que = deque()
que.append(root)
while len(que) > 0:
size = len(que)
for _ in range(size):
node = que.popleft()
if node.left != None:
que.append(node.left)
if node.right != None:
que.append(node.right)
result += 1
return result
# 重复逻辑:二叉树节点的最大深度 等于 左右子树的最大深度+1
def maxDepthRecursive(self, root: Optional[TreeNode]) -> int:
# 1、确定函数参数和返回值
def maxDepth(node: Optional[TreeNode]) -> int:
# 2、终止条件
if node == None:
return 0
else: # 3、单次递归的操作
leftN = maxDepth(node.left)
rightN = maxDepth(node.right)
return max(leftN, rightN) + 1
return maxDepth(root)
111. 二叉树的最小深度
思路1、层序遍历:一般 层序遍历的基础上,输出最小高度值(第一次遍历到叶子节点的时候)
思路2、递归:重复逻辑:二叉树节点的最小深度 等于 左右子树的最小深度+1,当左右子树不同时存在需要分类讨论
'''
111. 二叉树的最小深度
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明:叶子节点是指没有子节点的节点。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:2
思路1、层序遍历:一般 层序遍历的基础上,输出最小高度值(第一次遍历到叶子节点的时候)
思路2、递归:重复逻辑:二叉树节点的最小深度 等于 左右子树的最小深度+1,当左右子树不同时存在需要分类讨论
'''
class Solution:
def minDepth(self, root: Optional[TreeNode]) -> int:
# 情况1、树为空
if root == None:
return 0
# 情况2、树不为空
result = 0
que = deque()
que.append(root)
while len(que) > 0:
size = len(que)
for _ in range(size):
node = que.popleft()
if node.left == None and node.right == None:
return result + 1
if node.left != None:
que.append(node.left)
if node.right != None:
que.append(node.right)
result += 1
return result
# 重复逻辑:二叉树节点的最小深度 等于 左右子树的最小深度+1,当左右子树不同时存在需要分类讨论
def minDepthRecursive(self, root: Optional[TreeNode]) -> int:
# 1、确定函数参数和返回值
def minD(node: Optional[TreeNode]) -> int:
# 2、终止条件
if node == None:
return 0
else: # 3、单次递归的操作
leftN = minD(node.left)
rightN = minD(node.right)
if node.left == None and node.right != None: # 当一个左子树为空,右不为空,最低高度取决于右子树
return rightN + 1
elif node.left != None and node.right == None: # 当一个右子树为空,左不为空,最低高度取决于左子树
return leftN + 1
else:
return min(leftN, rightN) + 1 # 包含了叶子节点及左右子树都不为空的两种情况
return minD(root)
559. N 叉树的最大深度
思路1、层序遍历:一般 层序遍历的基础上,输出最大高度值
思路2、递归:重复逻辑:N叉树节点的最大深度 等于 所有子树的最大深度+1
from typing import List, Optional, Union
from collections import deque
'''
559. N 叉树的最大深度
给定一个 N 叉树,找到其最大深度。
最大深度是指从根节点到最远叶子节点的最长路径上的节点总数。
N 叉树输入按层序遍历序列化表示,每组子节点由空值分隔(请参见示例)。
示例 1:
输入:root = [1,null,3,2,4,null,5,6]
输出:3
题眼:二叉树的层序遍历
思路1、层序遍历:一般 层序遍历的基础上,输出最大高度值
思路2、递归:重复逻辑:N叉树节点的最大深度 等于 所有子树的最大深度+1
'''
class Solution:
def maxDepth(self, root: 'Node') -> int: # 递归解法
if root == None: # 简单情况
return 0
else: # 重复逻辑:节点的最大高度 等于所有孩子的最大高度+1
depth = 0
for child in root.children:
depth = max(depth, self.maxDepth(child))
return depth + 1
def maxDepthIteration(self, root: Optional[Node]) -> int:
# 情况1、树为空
if root == None:
return 0
# 情况2、树不为空
result = 0
que = deque()
que.append(root)
while len(que) > 0:
size = len(que)
for _ in range(size):
node = que.popleft()
if node.children != None:
for child in node.children:
que.append(child)
result += 1
return result
222. 完全二叉树的节点个数
本题如果按照普通二叉树来处理,则它的递归解法和“104. 二叉树的最大深度”题目极为类似,连续练习几道递归解法解题之后,递归函数的内部实现和黑盒函数调用区分体会更佳明显了
'''
222. 完全二叉树的节点个数
给你一棵 完全二叉树 的根节点 root ,求出该树的节点个数。
完全二叉树 的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。
若最底层为第 h 层,则该层包含 1~2h个节点。
示例 1:
输入:root = [1,2,3,4,5,6]
输出:6
思路1、按照普通二叉树求解,“104. 二叉树的最大深度”的相似题目,重复逻辑:二叉树的节点个数等于左右子树的节点个数+1
思路2、利用完全二叉树的性质,判断每个二叉树是否为满二叉树,满二叉树的节点个数等于2^深度-1,不是满二叉树就按照普通二叉树进行处理
'''
class Solution:
# 思路1、按照普通二叉树求解:迭代法-简单的层序遍历
def countNodesIteration(self, root: Optional[TreeNode]) -> int:
if root == None:
return 0
result = 0
dq = deque()
dq.append(root)
while len(dq) > 0:
node = dq.popleft()
result += 1 # 在每次弹出节点后面记录节点个数
if node.left != None:
dq.append(node.left)
if node.right != None:
dq.append(node.right)
return result
# 思路1、按照普通二叉树求解:递归法,重复逻辑:二叉树的节点个数等于左右子树的节点个数+1
def countNodes(self, root: Optional[TreeNode]) -> int:
if root == None: # 终止条件:简单情况
return 0
else: # 重复逻辑
leftN = self.countNodes(root.left)
rightN = self.countNodes(root.right)
return leftN + rightN + 1
# 思路2、利用完全二叉树的性质:迭代法-简单的层序遍历
def countNodesIteration2(self, root: Optional[TreeNode]) -> int:
if root == None:
return 0
result = 0
dq = deque()
dq.append(root)
while len(dq) > 0:
node = dq.popleft()
# 判断node是否是满二叉树:分别求解左右两侧的高度是否相等
leftN, rightN = 0, 0
node1, node2 = node, node
while node1 != None:
leftN += 1
node1 = node1.left
while node2 != None:
rightN += 1
node2 = node2.right
if leftN == rightN: # 满二叉树
result += 2 ** leftN - 1
else: # 非满二叉树
result += 1
if node.left != None:
dq.append(node.left)
if node.right != None:
dq.append(node.right)
return result
# 思路2、利用完全二叉树的性质:递归法,重复逻辑:判断每个二叉树是否为满二叉树,满二叉树的节点个数等于2^深度-1,不是满二叉树就按照普通二叉树进行处理
def countNodes2(self, root: Optional[TreeNode]) -> int:
if root == None: # 终止条件:简单情况
return 0
else: # 重复逻辑
leftN, rightN = 0, 0
node1, node2 = root, root
while node1 != None:
leftN += 1
node1 = node1.left
while node2 != None:
rightN += 1
node2 = node2.right
if leftN == rightN: # 满二叉树
return 2 ** leftN - 1
else: # 非满二叉树
leftN = self.countNodes2(root.left)
rightN = self.countNodes2(root.right)
return leftN + rightN + 1
110. 平衡二叉树
思路1、一般 层序遍历的基础上,分别获取 每个节点的 两颗子树的高度值,根据平衡二叉树的定义判断——容易写但代码量大
思路2、思路1的递归写法,自顶向底的递归,重复逻辑:重复判断每个二叉树的本身、左右子是否是平衡二叉树,需要把求高度差的函数在每个节点上都运行一遍——复杂度高
思路3、自底向顶的递归,重复逻辑:不断求解左右子树的高度,是平衡二叉树就返回高度,不是平衡二叉树就返回标志-1——这样每个节点的计算高度和判断是否是平衡二叉树都只需要执行一次,算法复杂度为O(N)
class Solution:
# 思路1、一般 层序遍历的基础上,分别获取 每个节点的 两颗子树的高度值,根据平衡二叉树的定义判断——容易写但代码量大
def isBalanced(self, root: Optional[TreeNode]) -> bool:
def getHeight(root: Optional[TreeNode]) -> int:
if root == None:
return 0
dq = deque()
dq.append(root)
result = 0
while len(dq) > 0:
size = len(dq)
result += 1
for _ in range(size):
cur = dq.popleft()
if cur.left:
dq.append(cur.left)
if cur.right:
dq.append(cur.right)
return result
if root == None:
return True
# 前序遍历root
stack = []
stack.append(root)
while len(stack) > 0:
cur = stack.pop()
leftHeight = getHeight(cur.left)
rightHeight = getHeight(cur.right)
if abs(leftHeight - rightHeight) > 1:
return False
if cur.right:
stack.append(cur.right)
if cur.left:
stack.append(cur.left)
return True
# 思路2、思路1的递归写法,自顶向底的递归,重复逻辑:重复判断每个二叉树的本身、左右子是否是平衡二叉树
# 需要把求高度差的函数在每个节点上都运行一遍——复杂度高
def isBalancedRecursive(self, root: Optional[TreeNode]) -> bool:
# 求解二叉树的高度
# 1、确定函数参数和返回值
def getHeight(node: Optional[TreeNode]) -> int:
# 2、终止条件
if node == None:
return 0
else: # 3、单次递归的操作
leftN = getHeight(node.left)
rightN = getHeight(node.right)
return max(leftN, rightN) + 1
# 重复逻辑:判断每个二叉树的本身、左右子是否是平衡二叉树
# 简单情况
if root == None:
return True
else: # 重复逻辑
leftH = getHeight(root.left)
rightH = getHeight(root.right)
if abs(leftH - rightH) > 1:
return False
else:
return self.isBalancedRecursive(root.left) and self.isBalancedRecursive(root.right)
# 思路3、自底向顶的递归,重复逻辑:不断求解左右子树的高度,是平衡二叉树就返回高度,不是平衡二叉树就返回标志-1——这样每个节点的计算高度和判断是否是平衡二叉树
# 都只需要执行一次,算法复杂度为O(N)
def isBalancedRecursive2(self, root: Optional[TreeNode]) -> bool:
if root == None:
return True
# 1、函数参数和返回值
def getHeight(node: Optional[TreeNode]) -> int:
# 2、终止条件
if node == None:
return 0
# 3、确定单次递归的操作
leftH = getHeight(node.left) # 左
if leftH == -1:
return -1
rightH = getHeight(node.right) # 右
if rightH == -1:
return -1
if abs(leftH - rightH) > 1:
return -1
else:
return 1 + max(leftH, rightH)
return getHeight(root) != -1
236. 二叉树的最近公共祖先
这道题可能不是很符合本次总结的“与深度相关”,但在实现上是一种“自底向顶的递归”,和“110. 平衡二叉树”的递归也有点类似的,就是很难想到这种解法:特别是简单情况都不太好罗列,需要先把重复逻辑想清楚:不断在二叉树的左右子树中寻找并返回p、q节点
'''
236. 二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
示例 1:
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3 。
思路:这道题太难了!自底向顶的递归:重复逻辑:不断在二叉树的左右子树中寻找并返回p、q节点
'''
class Solution:
def lowestCommonAncestor(self, root: Optional[TreeNode], p: Optional[TreeNode], q: Optional[TreeNode]) -> Optional[TreeNode]:
# 简单情况
if root == None:
return None
elif root == p: # 因为p、q一定是root中的二叉树节点,因此可以直接进行root与p、q相等的判断,一般是比较它们的节点值
return p
elif root == q:
return q
else: # 重复逻辑:不断在二叉树的左右子树中寻找并返回p、q节点
left = self.lowestCommonAncestor(root.left, p, q)
right = self.lowestCommonAncestor(root.right, p, q)
if left != None and right != None: # p、q刚好在以root为根节点的二叉树的左右子树上,此时返回值left、right对应p、q
return root
elif left != None and right == None: # p、q刚好在以root为根节点的二叉树的左子树上,此时不为None的返回值left一定是p、q之一
return left
elif left == None and right != None: # p、q刚好在以root为根节点的二叉树的右子树上,此时不为None的返回值right一定是p、q之一
return right
elif left == None and right == None: # 按照题意来说,这种情况是不存在的,可以把这种情况删除掉
return None