文章目录
leetcode 树相关:67题(有重复)
模板总结
- 最常见3种遍历:递归和迭代
- 函数直接递归;
- 定义DFS函数,进一步进行递归;
- 类型:
- 是否型False/True;
- 返回值型 int/float;
- 返回值型 list
- 要不要重新定义函数:
- 递归参数是否可以复用? 如果不同,重新定义。
- 递归返回值类型是否相同? 如果不同,必须重新定义;
- 是不是有全局变量需要设置? 如果有全局变量,则新定义;
- 使用方法:
- 层次遍历+queue
- DFS:前序、中序、后续
- 迭代:前序、中序、后续
- DFS+回溯:前序、中序、后续
二叉树算法的设计的总路线:明确一个节点要做的事情,然后剩下的事抛给框架。
def traverse(TreeNode root):
// root 需要做什么?在这做。
1) 处理顺序至关重要, 处理好可以简化很多操作;
// 其他的不用 root 操心,抛给框架
traverse(root.left)
traverse(root.right)
- 层次遍历模板:
queue = [root]
while queue:
# 每层节点数量;
queue_len = len(queue)
# 队列中每个元素向四周扩展;
for i in range(queue_len):
# 记住: 一定一定, 队列pop(0)!!!!!
curNode = queue.pop(0)
# 当前节点进一步处理: 1) 记录; 2) 满足性; 3) 终点判断、异常判断;
pass
# 当前元素向四周扩展; 有时候需要根据题目判断满足性;
queue.append(curNode.left)
queue.append(curNode.right)
- 非递归, 迭代方式;
三种非递归遍历
二叉树的遍历都可以借助栈结构使用DFS算法完成。
首先是最简单的先序遍历,**父>左>右。**见144题 。
每次入栈前先将父节点加入结果列表,然后左节点入栈。
当左子树遍历完后,再遍历右子树。
- 先序遍历,父>左>右
# 先序遍历,父>左>右
class Solution:
def preorderTraversal(self, root: TreeNode) -> List[int]:
res = [] #结果列表
stack = [] #辅助栈
cur = root #当前节点
while stack or cur: # cur 和 stack 一个满足就继续;
while cur: #一直遍历到最后一层
res.append(cur.val)
stack.append(cur)
cur = cur.left
top = stack.pop() #此时该节点的左子树已经全部遍历完
cur = top.right #对右子树遍历
return res
- 后序遍历,左>右>父
后序遍历,左>右>父。见145题 。
能不能借助先序遍历的思路来呢,我们将上面的顺序翻转过来得到,父>右>左。
所以现在可以按照之前的方法遍历,最后把结果翻转一下。
# 后序遍历,左>右>父
class Solution:
def postorderTraversal(self, root: TreeNode) -> List[int]:
res = []
stack = []
cur = root
while stack or cur:
while cur:
res.append(cur.val)
stack.append(cur)
cur = cur.right #先将右节点压栈
top = stack.pop() #此时该节点的右子树已经全部遍历完
cur = top.left #对左子树遍历
return res[::-1] #结果翻转
- 中序遍历, 左>父>右
中序遍历, 左>父>右。见94题 。
与先序遍历不同的是,出栈时才将结果写入列表。
class Solution:
def inorderTraversal(self, root: TreeNode) -> List[int]:
res = []
stack = []
cur = root
while stack or cur:
while cur:
stack.append(cur)
cur = cur.left
top = stack.pop() #此时左子树遍历完成
res.append(top.val) #将父节点加入列表
cur = top.right #遍历右子树
return res
三种递归遍历
# 先序遍历
def preorderTraversal(self, root: TreeNode) -> List[int]:
if not root:
return None
print(root.val)
self.preorderTraversal(root.left)
self.preorderTraversal(root.right)
# 中序遍历
def midOrder(root):
if not root:
return None
self.midOrder(root.left)
print(root.val)
self.midOrder(root.right)
# 后序遍历
def postOrder(root):
if not root:
return None
self.postOrder(root.left)
self.postOrder(root.right)
print(root.val)
python实现树形DP,一般都是后序遍历,理由上述
class Solution:
def rob(self, root: TreeNode) -> int:
return max(self.dp(root))
def dp(curRoot):
if not curRoot:
return [0, 0]
# 后序遍历
left = self.dp(curNode.left)
right = self.dp(curNode.right)
dp = [0, 0]
# dp[0]:以当前 node 为根结点的子树能够偷取的最大价值,规定 node 结点不偷
# dp[1]:以当前 node 为根结点的子树能够偷取的最大价值,规定 node 结点偷
dp[0] = max(left[0], left[1]) + max(right[0], right[1])
dp[1] = curRoot.val + left[0] + right[0]
模仿递归三种遍历
新思路
递归的本质就是压栈,了解递归本质后就完全可以按照递归的思路来迭代。
怎么压,压什么?压的当然是待执行的内容,后面的语句先进栈,所以进栈顺序就决定了前中后序。
我们需要一个标志区分每个递归调用栈,这里使用nullptr来表示。
具体直接看注释,可以参考文章最后“和递归写法的对比”。先序遍历看懂了,中序和后序也就秒懂。
- 先序遍历
# 先序遍历: 自己未看
def preorderTraversal(self, root: TreeNode) -> List[int]:
if root is None: return [] # 首先介入root节点
result = []
stack = [root]
while stack:
p = stack.pop() # 访问过节点弹出
if p is None:
p = stack.pop()
result.append(p.val)
else:
if p.right: # 右节点先压栈,最后处理
stack.append(p.right) # 先append的最后访问
if p.left:
stack.append(p.left)
stack.append(p) # 当前节点重新压栈(留着以后处理),因为先序遍历所以最后压栈
stack.append(None) # 在当前节点之前加入一个空节点表示已经访问过了
return result
- 中序遍历
# 中序遍历
else:
if p.right:
stack.append(p.right)
stack.append(p)
stack.append(None)
if p.left:
stack.append(p.left)
- 后序遍历
else:
stack.append(p)
stack.append(None)
if p.right:
stack.append(p.right)
if p.left:
stack.append(p.left)
⭐LeetCode刷题总结-树篇(上)
在LeetCode的标签分类题库中,和树有关的标签有:树(123道题)、字典树(17道题)、线段树(11道题)、树状数组(6道题)。对于这些题,作者在粗略刷过一遍后,对其中的考点进行了总结,并归纳为以下四大类:
- 树的自身特性
- 树的类型
- 子树问题
- 新概念定义问题
一、 树基本特性问题
Offer 32 - I. 从上到下打印二叉树-中等-层次遍历
- 层次遍历思路:
class Solution:
def levelOrder(self, root: TreeNode) -> List[int]:
# 思路: 二叉树的层次遍历;
res = []
queue = [root]
while queue:
queue_len = len(queue)
for i in range(queue_len):
curNode = queue.pop(0)
# 当前元素加入
if curNode:
res.append(curNode.val)
queue.append(curNode.left)
queue.append(curNode.right)
return res
Offer 32 - II. 从上到下打印二叉树 II
- 层次遍历
class Solution:
def levelOrder(self, root: TreeNode) -> List[List[int]]:
res = []
queue = [root]
while queue:
lines = []
queue_len = len(queue)
for i in range(queue_len):
curNode = queue.pop(0)
if curNode:
lines.append(curNode.val)
queue.append(curNode.left)
queue.append(curNode.right)
if lines:
res.append(lines)
return res
32-III.从上到下打印二叉树III-之字型
- 层次遍历
class Solution:
def levelOrder(self, root: TreeNode) -> List[List[int]]:
res = []
queue = [root]
count = 0
while queue:
count += 1
lines = []
queue_len = len(queue)
for i in range(queue_len):
curNode = queue.pop(0)
if curNode:
lines.append(curNode.val)
queue.append(curNode.left)
queue.append(curNode.right)
if count%2 == 0:
lines = lines[::-1]
if lines:
res.append(lines)
return res
Offer28. 对称的二叉树-lc101
leetcode链接
101-对称二叉树-简单
- 思路1: 递归思路 : 深度优先遍历: 100
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def isSymmetric(self, root: TreeNode) -> bool:
# 类型: 1) 是否型; 2)重新定义: 自身不满足; ; 3) 递归;
def isMirror(root1, root2):
'''
判断两个二叉树是否镜像;
'''
# 框架:
# 步骤1: root需要做什么? 1) 满足条件判断; 2) 不满足条件判断: a:剪枝; b:退出;
# 步骤2:其他y不用root操心, 交给框架;
# root做什么?
# 两种写法: True / False类型; 1)哪种情况满足; 2) 哪种情况不满足;
# 两个都是空节点: 必定相等;
if not root1 and not root2:
return True
# 一个空,一个非空,必定不等;
if not root1 or not root2:
return False
# 交给框架: 1) val相等; 2) 左; 3) 右;
return root1.val == root2.val and isMirror(root1.left, root2.right) and isMirror(root1.right, root2.left)
# 可以省略;
'''
# 异常判断;
if not root:
return False
'''
return isMirror(root, root)
- 思路2:迭代思路: 层次遍历
class Solution:
def isSymmetric(self, root: TreeNode) -> bool:
# 思路2: 层次遍历: 镜像特点: 沿中间到两半对称;
queue = [root]
while queue:
queue_len = len(queue)
row_res = []
# 队列每个元素向四周扩展;
for i in range(queue_len):
# 记住了: 一定一定一定 pop(0)
curNode = queue.pop(0)
# 每行元素记录:
if curNode:
row_res.append(curNode.val)
else:
row_res.append('null')
if curNode:
# 当前元素四周扩展; **可能添加了: null
queue.append(curNode.left)
queue.append(curNode.right)
# while queue一次循环: 遍历一行结果;
# 满足性质判断;
reverse_row_res = row_res[::-1]
if reverse_row_res != row_res:
return False
return True
❌971.翻转二叉树以匹配前序遍历-中等
❌655.出二叉树-中等-不做了,求坐标就行
class Solution:
def printTree(self, root: TreeNode) -> List[List[str]]:
# 思路: 直接层次遍历,完了再来一遍进行转换?
# 思考难点: 1) 二叉树深度不知道 -> 数组 m, n不清楚; n = 2^m -1;
# 2) 层级关系填充,想考察: 节点坐标关系?
def BFS(root):
queue = [root]
while queue:
row_res = []
queue_len = len(queue)
# 队列中元素向四周扩展;
for i in range(queue_len):
# pop(0) !!!
curNode = queue.pop(0)
if curNode:
row_res.append(curNode.val)
else:
row_res.append('null')
if curNode:
queue.append(curNode.left)
queue.append(curNode.right)
res.append(row_res)
if not root:
return []
res = []
BFS(root)
print(res)
没做完----
617-合并二叉树-简单
- 深度优先搜索
class Solution:
def mergeTrees(self, t1: TreeNode, t2: TreeNode) -> TreeNode:
# 类型: 1) 非是否型; 2) 自身调用; 3) 递归;
# 框架:
# 步骤1: root做点什么?
# 步骤2: 框架左右
# 步骤3: 返回
# 步骤1: 根节点值相加;
if not t1:
return t2
if not t2:
return t1
t1.val = t1.val + t2.val
'''
这块自己第一次i写错了,顺序没有考虑对,!!!
!!!处理好顺序,可能更容易;
elif t1 and not t2:
t1.val = t1.val
elif not t1 and t2:
t1.val = t2.val
else:
return None
'''
# 步骤2:左右框架;
left = self.mergeTrees(t1.left, t2.left)
right = self.mergeTrees(t1.right, t2.right)
# 返回结果处理
t1.left = left
t1.right = right
# 返回
return t1
814.二叉树剪枝-中等-后序遍历
- 大佬后序遍历: 剪枝方法;
class Solution:
def pruneTree(self, root: TreeNode) -> TreeNode:
# 明确问题:二叉树剪支,减掉,值为0且无子树的节点,值为0且子树的节点值均为0
# 解题思路:后序遍历,判断当前节点值是否为0,左右子节点,是否为空,均是则减掉
if not root:
return None
root.left = self.pruneTree(root.left)
root.right = self.pruneTree(root.right)
# 剪枝: 自底向上, 去掉叶子节点, 且为0;
# 放在此处: 后序遍历: 先处理左右,完了再处理当前;
if root.val == 0 and not root.left and not root.right:
return None
return root
- 自己实现
class Solution:
def pruneTree(self, root: TreeNode) -> TreeNode:
# 思路: 深度优先搜索, 记录对应子树是否包含1;
# 递归返回的值: 0 或者 1; 自己做时候没想明白这个;
def prune(root):
# 空节点;
if not root:
return 0
# 非空节点;
if not root.left and not root.right:
return root.val
left = prune(root.left)
right = prune(root.right)
if left == 0:
root.left = None
if right == 0:
root.right = None
return root.val or left or right
prune(root)
# 这种傻逼题目....
if root.val == 0 and not root.left and not root.right:
return None
return root
199.二叉树的右视图-中等
- 解题思路:
层次遍历的实际应用。只需依次保存每层最右边的一个节点即可。 - 具体代码:
class Solution:
def rightSideView(self, root: TreeNode) -> List[int]:
# 思路: 层次遍历;
res = []
queue = [root]
while queue:
line = []
queue_len = len(queue)
for _ in range(queue_len):
curNode = queue.pop(0)
if curNode:
line.append(curNode.val)
queue.append(curNode.left)
queue.append(curNode.right)
if line:
res.append(line[-1])
return res
111.二叉树的最小深度-简单
- 层次遍历思路:
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def minDepth(self, root: TreeNode) -> int:
# BFS: 时间复杂度相对DFS较低, 但是空间复杂度O(N) > DFS:O(log N)
if not root:
return 0
queue = [root] # BFS使用队列
depth = 1 # 初始化高度/深度; 可以随后面调整;
while queue:
# 记录队列中元素数量: 二叉树每一层node数量
queue_size = len(queue)
# 遍历二叉树一层node, 并将周围节点加入 队列
for i in range(queue_size):
# 取最先进入队列元素
curNode = queue.pop(0)
# 判断是否到达终点
if not curNode.left and not curNode.right:
return depth
# 将当前节点周围node加入队列
if curNode.left:
queue.append(curNode.left)
if curNode.right:
queue.append(curNode.right)
# 遍历一层, 深度 +1
depth += 1
-------------------------------------------------------------------------------------------------
class Solution:
def minDepth(self, root: TreeNode) -> int:
# 思路1: 层次遍历
if not root:
return 0
queue = [root]
res = 0
while queue:
res += 1
queue_len = len(queue)
for _ in range(queue_len):
curNode = queue.pop(0)
if curNode:
queue.append(curNode.left)
queue.append(curNode.right)
# 必须满足左右子树都为空才可以是叶子节点;
if not curNode.left and not curNode.right:
return res
- 深度优先搜索思路
class Solution:
def minDepth(self, root: TreeNode) -> int:
# 思路:深度优先DFS; 后续遍历;
if not root:
return 0
left = self.minDepth(root.left)
right = self.minDepth(root.right)
# 计算最小深度: 坑;
# 深度定义必须到叶子节点,所以中间节点不算;
if root.left and root.right:
return min(left, right) + 1
else:
return 1 + left + right
Offer 55 - I. 二叉树的深度
104. 二叉树的最大深度
- 递归思路:
class Solution:
def maxDepth(self, root: TreeNode) -> int:
# 思路: 后续遍历;
if not root:
return 0
left = self.maxDepth(root.left)
right = self.maxDepth(root.right)
return max(left, right) + 1
- 层次遍历思路: 累加
662.二叉树的最大宽度-中等
- BFS思路, 难点在于:下标计算和表示;
class Solution:
def widthOfBinaryTree(self, root: TreeNode) -> int:
# 思路: BFS, 记录对应节点下标即可;
# 坐标对应: left=2*1+1, right = 2*i+2;
if not root:
return 0
queue = [(0, root)]
res = 0
while queue:
queue_len = len(queue)
res = max(res, queue[-1][0] - queue[0][0]+1)
for i in range(queue_len):
i, curNode = queue.pop(0)
# 当前元素向四周扩散;
if curNode.left:
queue.append((2*i+1, curNode.left))
if curNode.right:
queue.append((2*i+2, curNode.right))
return res
二、构造二叉树
889.依据前序和后序遍历构造二叉树-中等
- 递归思路
class Solution:
def constructFromPrePost(self, pre: List[int], post: List[int]) -> TreeNode:
# 前序+后序 构造二叉树数量很大, 题目要求返回一个即可; 按照常见思路思考即可;
# 思路: 1) 返回TreeNode; 2) 无需全局变量; 3)自身调用;
# 框架1: root做点什么?
if not pre or not post:
return None
# 根据题目调试,pre[1]越界添加;
if len(pre) == 1:
return TreeNode(pre[0])
root = TreeNode(pre[0])
left_root = pre[1]
index = post.index(left_root)
# 框架2: 左右子树;
left = self.constructFromPrePost(pre[1:index+2], post[:index+1])
right = self.constructFromPrePost(pre[index+2:], post[index+1:-1])
# 框架3:后序遍历:返回值进一步处理;
root.left = left
root.right = right
# 框架4:根节点返回;
return root
1008. 先序遍历构造二叉树-中等
- 先序+中序:递归思路
class Solution:
def bstFromPreorder(self, preorder: List[int]) -> TreeNode:
# 二叉搜索树: 中序遍历=排序; 相当于 先序+中序;
# 思路:1) 返回TreeNode; 2) 无需全局变量; 3)重新定义;
inorder = sorted(preorder)
def buildTree(preorder, inorder):
# 框架1: root做点什么?
if not preorder or not inorder:
return None
root = preorder[0]
index =inorder.index(root)
root = TreeNode(root)
# 框架2: 左右子树;
left = buildTree(preorder[1:index+1], inorder[:index])
right = buildTree(preorder[index+1:], inorder[index+1:])
# 框架3: 后序遍历: 返回结果处理
root.left = left
root.right = right
# 框架4: 返回子树根节点;
return root
return buildTree(preorder, inorder)
1028.从先序遍历还原二叉树-困难-迭代模型回溯
- 栈模型:迭代+回溯过程;
class Solution:
def recoverFromPreorder(self, S: str) -> TreeNode:
# 思路: 1)返回TreeNode; 2)需全局变量; 3) 不是递归调用;
# 全局变量path, 用来模拟回溯过程;
path = []
# 全局变量pos, 记录字符串中的位置;
pos = 0
while pos < len(S):
# 获取高度;
level = 0
while S[pos] == '-':
level += 1
pos += 1
# 获取当前节点;
value = 0
# pos<len(S):防止越界;
while pos < len(S) and S[pos].isdigit():
value = value*10 + int(S[pos])
pos += 1
root = TreeNode(value)
# 当前节点连接到父 前节点;
# 左子树;
if level == len(path):
if path:
path[-1].left = root
# 右子树;
else:
# 模拟回溯过程: path[:level]找到当前父节点、右子树;
path = path[:level]
path[-1].right = root
path.append(root)
return path[0]
105. 从前序与中序遍历序列构造二叉树-中等
- 后序遍历, 递归思路
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
# 步骤1: 思路: 1)返回TreeNode; 2)无需全局变量; 3) 自身调用;
# 异常判断: 可有可无;
if len(preorder) != len(inorder):
return None
# 框架1: root做点什么?
if not preorder or not inorder:
return None
root = preorder[0]
index = inorder.index(root)
root = TreeNode(root)
# 框架2: 左右子树
left = self.buildTree(preorder[1:index+1], inorder[:index])
right = self.buildTree(preorder[index+1:], inorder[index+1:])
# 框架3: 后序:递归结果进一步处理;
root.left = left
root.right = right
# 框架4: 返回当前根节点;
return root
106. 从中序与后序遍历序列构造二叉树-中等
- 后序遍历,递归思路
class Solution:
def buildTree(self, inorder: List[int], postorder: List[int]) -> TreeNode:
# 思路: 1) 返回TreeNode; 2)无需全局变量; 3)自身调用;
# 框架1: root做点什么
if not inorder or not postorder:
return None
root = postorder[-1]
index = inorder.index(root)
root = TreeNode(root)
# 框架2: 左右子树;
left = self.buildTree(inorder[:index], postorder[:index])
right = self.buildTree(inorder[index+1:], postorder[index:-1])
# 框架3: 后序遍历结果处理
root.left = left
root.right = right
# 框架4: 返回当前子树根节点;
return root
三、节点问题
Offer 68 - I. 二叉搜索树的最近公共祖先
Offer 68 - II. 二叉树的最近公共祖先
236-二叉树的最近公共祖先-中等
- 思路: 直接递归
- 直接递归和使用定义dfs区别:
class Solution:
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
# 类型: 1) 返回TreeNode; 2)后序遍历; 3)自身调用; 4)无全局变量;
# 框架1: root做点什么?
if not root:
return None
if root == p or root == q:
return root
# 框架2: 左右子树;
left = self.lowestCommonAncestor(root.left, p, q)
right = self.lowestCommonAncestor(root.right, p, q)
# 框架3/4: 后序遍历:返回值进一步处理;
if left and right:
return root
if left:
return left
if right:
return right
return None
337.打家劫舍 III-中等
class Solution:
def rob(self, root: TreeNode) -> int:
# 类型: 1) 返回int; 2无需全局变量; 3)重新定义; 4)后序遍历;
def maxRob(root):
# 框架1: root做点什么?
if not root:
return [0, 0]
# 框架2: 左右子树;
left = maxRob(root.left)
right = maxRob(root.right)
# 框架3:后序遍历返回结果处理
dp = [0, 0] # [当前节点偷, 当前节点不偷]
# case1:当前节点不偷;
dp[0] = max(left[0], left[1]) + max(right[0], right[1])
# case2: 当前节点偷;
dp[1] = left[0] + right[0] + root.val
# 框架4: 返回
return dp
return max(maxRob(root))
❌623.在二叉树中增加一行-中等
863.二叉树中所有距离为K的节点-中等
- DFS:每个节点加入父亲节点; 转换为图; 然后进行BFS,查找距离为1节点;
class Solution:
def distanceK(self, root: TreeNode, target: TreeNode, K: int) -> List[int]:
# 思路:1.添加parentPointer构建有向图; 2.BFS:向四周遍历K步;
# 类型: 1) 返回list; 2) 无需全局变量; 3) 非递归; 4) 层次遍历;
def dfs(root, par= None):
# 框架1: root做点什么?
if not root:
return None
root.parent = par
# 框架2: 左右子树;
dfs(root.left, root)
dfs(root.right, root)
dfs(root)
# BFS进行层次遍历,查找K步;
queue = [(target, 0)]
# visited: 防止父节点重复访问;
visited = [target]
while queue:
# queue.pop(0) 之前对queue内元素进行满足性判断;
if queue[0][1] == K:
return [node.val for node, dist in queue]
queue_len = len(queue)
# 队列元素向四周扩撒;
for _ in range(queue_len):
curNode, dist = queue.pop(0)
# 当前元素向四周扩撒;
for direction in (curNode.left, curNode.right, curNode.parent):
# 满足性条件检查;
if direction and direction not in visited:
queue.append((direction, dist+1))
visited.append(direction)
return []
968.监控二叉树-困难⭐
-
如果左右子节点都无法被监控到 则当前放置一个摄像头。
-
如果左右子节点能够被监控到,则当前返回无法被监控
-
如果左右子节点有一个有摄像头,则当前返回可以被监控到。
-
叶子节点始终返回无法被监控到。 叶子节点不能放摄像头
-
递归+贪心
class Solution:
def minCameraCover(self, root: TreeNode) -> int:
# 类型: 1)返回int; 2)需全局变量; 3)重新定义; 4) 后序遍历;
# 思路: 贪心 + 后序遍历;
# 定义三种状态:
# 0:未被覆盖(当前节点未被照到);
# 1:已被覆盖(摄像头照到该节点);
# 2: 需要放置摄像头;
# 如果左右节点都无法监控到:放置摄像头;
# 如果左右节点都可以被监控到: 当前节点无法监控;
# 左右节点有一个摄像头:返回当前节点可以被监控;
# 叶子节点返回始终被监控:叶子节点不能放置摄像头;
def dfs(root):
# return:对于每个节点返回3种状态
# 框架1: root做点什么?
if not root:
return 1
# 框架2: 左右子树
left = dfs(root.left)
right = dfs(root.right)
# 框架3/4: 后序遍历结果处理;
# case1: 左右有一个未被监控,则放置摄像头;
if left == 0 or right == 0:
self.res += 1
return 2
# case2:左右都已覆盖,则当前节点未覆盖;
if left == 1 and right == 1:
return 0
# case3: 左右一个覆盖、至少一个摄像头(至少), 则当前节点覆盖;
if left + right >= 3:
return 1
self.res = 0
tmp = dfs(root)
# 递归最后:对于最顶层节点,如果返回0,说明顶层节点没有被覆盖, +1摄像头;
return self.res + 1 if tmp == 0 else self.res
1145-二叉树着色游戏-中等-不做了
四、路径问题
- 模板:
# 确定类型:
def dfs():
1.root
2. left - right
3. 后序遍历进一步处理
4. 返回
112. 路径总和
路径定义: 根节点到子节点; 所以递归找到子节点再判断;
class Solution:
def hasPathSum(self, root: TreeNode, sum: int) -> bool:
def dfs(root, target):
'''
递归找目标值
'''
# dfs-3步骤
# 其实已经判断, 不必要再判断
if not root:
return False
# 满足条件判断: 1) 找到目标值; 2)必须是叶子节点;
if sum == target and not root.left and not root.right:
return True
# 右子树找满足条件
if root.right:
if dfs(root.right, target+root.right.val):
return True
# 左子树找满足条件;
if root.left:
if dfs(root.left, target+root.left.val):
return True
return False
if not root:
return False
return dfs(root, root.val)
- target设置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 hasPathSum(self, root: TreeNode, sum: int) -> bool:
# 思路: 递归dfs求路径和, 判断路径和是不是等于 sum;
def dfs(root, target):
'''
计算根节点到当前节点的路径和;
'''
if not root:
return False
# 满足条件判断;
target = target + root.val
# 1) 等于目标和; 2)叶子节点;
if target == sum and not root.left and not root.right:
return True
# 左子树是否满足条件;
if root.left:
if dfs(root.left, target):
return True
# 右子树是否满足条件;
if root.right:
if dfs(root.right, target):
return True
# 如果左右都False;
return False
if not root:
return False
return dfs(root, 0)
---------------------------------------------------------------------------------------------
# 推荐
class Solution:
def hasPathSum(self, root: TreeNode, sum: int) -> bool:
# 类型: 1) bool; 2)无需全局变量; 3)重新定义; 4) 后序遍历;
def dfs(root, target):
# 框架1: root做点什么?
if not root:
return False
# 满足条件判断
target = target + root.val
if sum == target and not root.left and not root.right:
return True
# 框架2: 左右子树
left = dfs(root.left, target)
right = dfs(root.right, target)
# 框架3/4: 后序遍历结果处理;
return left or right
return dfs(root, 0)
- 同113-Offer 34. 二叉树中和为某一值的路径
leetcode链接
113. 路径总和 II
- 仍然套回溯模板;
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def pathSum(self, root: TreeNode, sum: int) -> List[List[int]]:
# 思路: 回溯记录path; 回溯和dfs思路和实现一致,但是要对于路径回溯;
def dfs(root, target):
# 3步骤;
# 满足条件判断
target = target + root.val
# 1) 等于目标和; 2)
if target == sum and not root.left and not root.right:
res.append(path[:])
# 最好判断: root.left 否则可能: root.left.val 不存在;
if root.left:
path.append(root.left.val)
dfs(root.left, target)
path.pop()
if root.right:
path.append(root.right.val)
dfs(root.right, target)
path.pop()
if not root:
return []
res = []
path = [root.val]
dfs(root, 0)
return res
---------------------------------------------------------------------------------------------
# 推荐
class Solution:
def pathSum(self, root: TreeNode, sum: int) -> List[List[int]]:
# 类型: 1) 返回list; 2)全局变量; 3)重新定义; 4)后序遍历;
def dfs(root, target):
# 框架1: root做点什么?
if not root:
return []
target = target + root.val
path.append(root.val)
# 满足条件判断;
if sum == target and not root.left and not root.right:
res.append(path[:])
# 框架2: 左右子树
left = dfs(root.left, target)
right = dfs(root.right, target)
path.pop()
# 框架3/4:后序遍历,进一步处理
res = []
path = []
dfs(root, 0)
return res
437. 路径总和 III
- 继续套模板;
- 局部变量、全局变量、可变变量、不可变变量: 继续看看
class Solution:
def pathSum(self, root: TreeNode, sum: int) -> int:
# 1. 不需要保存路径,不用对path回溯;
# 2. 不限制起点, BFS全部试一遍;
# 3. 不限制终点, 不必判断是否叶子节点;
# 类型: 1) 返回int; 2)全局变量; 3)重新定义; 4) 后序遍历;
def dfs(root, target):
# 框架1: root做点什么?
if not root:
return
target = target + root.val
# 满足条件判断
if sum == target:
self.res += 1
# 框架2: 左右子树;
left = dfs(root.left, target)
right = dfs(root.right, target)
if not root:
return 0
# 起点没有限制,所有节点遍历一遍;
# BFS遍历:不用dfs, 思考简单;
queue = [root]
self.res = 0
while queue:
queue_len = len(queue)
for _ in range(queue_len):
curNode = queue.pop(0)
if curNode:
dfs(curNode, 0)
queue.append(curNode.left)
queue.append(curNode.right)
return self.res
257. 二叉树的所有路径
- 套模板
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def binaryTreePaths(self, root: TreeNode) -> List[str]:
# 思路: 回溯
def dfs(root):
if not root:
return
if not root.left and not root.right:
res.append('->'.join([str(i) for i in path]))
return
# 左子树; 判断存在性: root.left.val 存在性;
if root.left:
path.append(root.left.val)
dfs(root.left)
path.pop()
# 右子树
if root.right:
path.append(root.right.val)
dfs(root.right)
path.pop()
res = []
if not root:
return []
path = [root.val]
dfs(root)
return res
-------------------------------------------------------------
# 推荐
class Solution:
def binaryTreePaths(self, root: TreeNode) -> List[str]:
# 类型: 1)返回list; 2) path全局变量; 3) 重新定义; 4) 后序遍历;
def dfs(root):
# 无用返回值
# 框架1: root做点什么?
if not root:
return []
# 满足条件判断
path.append(root.val)
if not root.left and not root.right:
res.append('->'.join([str(num) for num in path]))
path.pop() # 特别之处;
return
# 框架2: 左右子树;
left = dfs(root.left)
right = dfs(root.right)
path.pop()
res = []
path = []
dfs(root)
return res
124.二叉树中的最大路径和-困难
class Solution:
def maxPathSum(self, root: TreeNode) -> int:
# 路径和定义: 起点任意, 终点任意;
# 路径: 对于当前节点如果走其父节点, 则max(left, right)
# 如果当前为根节点, left + right + root.val
# 类型: 1)返回int; 2)全局变量; 3)重新定义; 4) 后序遍历;
def dfs(root):
# 框架1: root做点什么?
if not root:
return 0
# 框架2: 左右子树;
left = dfs(root.left)
right = dfs(root.right)
# 框架3: 后序遍历结果处理
curMax = max(left, right, 0) + root.val
# 更新全局变量;
# 1)最大值不更新,当前节点不经过;
# 2)路过该节点,选择左、右一个;
# 3)当前节点为根节点;
self.max_path_sum = max(self.max_path_sum, left+right+root.val, curMax)
# 框架4: 返回当前子树根节点条件下最大值;
return curMax
self.max_path_sum = float('-inf')
dfs(root)
return self.max_path_sum
543.二叉树的直径
- 类似于:二叉树最大路径和124思路;
class Solution:
def diameterOfBinaryTree(self, root: TreeNode) -> int:
# 和最大路径和一样,区别: 直径全为正;
# 类型: 1)返回int; 2)全局变量; 3)重新定义; 4)后序遍历;
def dfs(root):
# 框架1:root做点什么?
if not root:
return 0
# 框架2:左右子树;
left = dfs(root.left)
right = dfs(root.right)
# 框架3: 后序遍历结果进一步处理; left+right+1 > curMax
curMax = max(left, right) + 1
self.res = max(self.res, left+right+1)
# 框架4: 返回结果;
return curMax
if not root:
return 0
self.res = 0
dfs(root)
return self.res-1
687. 最长同值路径
- 类似124最大路径和;
class Solution:
def longestUnivaluePath(self, root: TreeNode) -> int:
def dfs(root, par=None):
# 框架1: root做点什么?
if not root:
return 0
# 框架2: 左右子树
left = dfs(root.left, root)
right = dfs(root.right, root)
# 框架3: 后序遍历结果处理
curValue = 0
if par.val == root.val:
curValue = max(left, right) + 1
# 不用+1: 求边数;
self.res = max(self.res, left+right)
# 框架4: 返回
return curValue
self.res = 0
dfs(root, root)
return self.res
979.二叉树中分配硬币-中等
- 类似124思路;
class Solution:
def distributeCoins(self, root: TreeNode) -> int:
# 类型: 1) 返回int; 2)全局变量; 3) 重新定义; 4) 后序遍历;
# 思路: 自己没有想明白移动次数怎么表示, 尤其是在递归过程中怎么计算;
# 1) 使用 过载量 的概念来表示,当前节点向上父节点,或者父节点到自己移动次数;
# 2) 全局变量: 每个节点都计算其左右的移动次数求和;
def dfs(root):
# 框架: root做点什么?
if not root:
return 0
# 框架2: 左右子树;
left = dfs(root.left)
right = dfs(root.right)
# 框架3: 后序遍历:结果处理;
# 全局移动次数: 每个节点为根节点子树的之间(不涉及向父节点)移动次数;
self.res += abs(left) + abs(right)
# 框架4: 返回当前节点的过载量;
return root.val + left + right - 1
self.res = 0
dfs(root)
return self.res
987.二叉树的垂序遍历-中等
- 太牛逼了! DFS记录坐标,然后排序:
class Solution:
def verticalTraversal(self, root: TreeNode) -> List[List[int]]:
# 思路: DFS记录坐标进行排序
# 类型: 1) 返回list; 2)全局变量:记录访问路径; 3)重新定义; 4)先序遍历;
lst = []
def dfs(root, x= 0, y=0):
# 框架1: root做点什么?
if not root:
return
lst.append((x, y, root.val))
# 框架2: 左右子树
left = dfs(root.left, x-1, y-1)
right = dfs(root.right, x+1, y-1)
# 框架3/4
dfs(root, 0, 0)
# 结果进一步排序; -x[1]:y坐标逆序;
lst = sorted(lst, key= lambda x:(x[0], -x[1], x[2]))
res = [[lst[0][2]]]
for i in range(1, len(lst)):
if lst[i][0] == lst[i-1][0]:
res[-1].append(lst[i][2])
else:
res.append([lst[i][2]])
return res
1457. 二叉树中的伪回文路径
- DFS记录路径+回文判断;
class Solution:
def pseudoPalindromicPaths (self, root: TreeNode) -> int:
# 类型: 1)返回int; 2)全局变量res:统计数目; 3)重新定义; 4)先序遍历;
def isPath(path):
# 判断回文路径;
cnt = 0
dst = {}
for pth in path:
if pth not in dst:
dst[pth] = 0
dst[pth] += 1
for key, value in dst.items():
if value%2 != 0:
cnt += 1
if cnt > 1:
return False
return True
def dfs(root):
# 框架1: root做点什么?
if not root:
return None
# 终止条件;
path.append(root.val)
if not root.left and not root.right:
if isPath(path):
self.res += 1
# 框架2:左右子树;
dfs(root.left)
dfs(root.right)
# 框架3/4
path.pop()
if not root:
return 0
path = []
self.res = 0
dfs(root)
return self.res
1372. 二叉树中的最长交错路径
- 不会-直接抄答案了;
class Solution:
def longestZigZag(self, root: TreeNode) -> int:
# 思路: 对于每个节点维护, 根节点到当前节点为止的 交错路径;
# 类型: 1) 返回int; 2)全局变量; 3)重新定义; 3)先序遍历;
def dfs(root, left, right):
# root: 当前节点;
# left:当前节点作为左路径最长;
# rihgt:当前节点作为右路径最长;
# 框架1: root做点什么?
if not root:
return None
# 结果判断;
self.res = max(self.res, left, right)
# 框架2: 左右子树;
dfs(root.left, right+1, 0)
dfs(root.right, 0, left+1)
# 框架3/4
if not root:
return -1
self.res = 0
dfs(root, 0, 0)
return self.res
字节搜索面试:最大路径总和
通过对以上问题的总结,对于二叉树的路径问题有个 屡试不爽 的框架,所有题目都可由该框架求得。可先浏览一下该框架,后面习题代码再对比一下。基本上题解就是填这个框架。
class Solution(object):
def maxPath(self, root, sum):
def dfs(head,path,target):
'''
dfs传入的head必须为非None
dfs由三部分组成 :
1、满足要求的处理
2、左右子树的处理
3、返回结果
'''
if not head.right and not head.left and ###:
# 满足要求做处理
return
if head.left:
# 对左子树dfs
if head.right:
# 对右子树dfs
return ###
####保证传入非None
if not root:
return ###
####调用dfs
dfs(root,[head.val],root.val)
return ###
⭐LeetCode刷题总结-树篇(中)
二、平衡二叉树
- Offer 55 - II. 平衡二叉树
110.平衡二叉树-简单
- 自己写代码时候问题:
class Solution:
def isBalanced(self, root: TreeNode) -> bool:
# 思路: 递归: 判断左右子树高度;
# 类型: 1) 返回bool还是高度呢? 2) 自身调用还是重写? 3)不用全局变量
def isBTree(root, high)
if not root:
return [True, 0]
left = isBTree(root.left, high)
right = isBTree(root.right, high)
return
- 自顶向下:从上到下,每一个节点进行高度计算: 时间复杂度: O(NlogN)
class Solution:
def isBalanced(self, root: TreeNode) -> bool:
# 类型: 1) bool; 2)全局变量; 3)重新定义; 4) 后序遍历;
# 思路: 定义去全局变量: flag, 递归判断每个节点左右子树的高度;
self.res = True
def dfs(root):
# 计算当前节点的最大深度;
# 框架1: root做点什么?
if not root:
return 0
# 框架2: 左右子树;
left = dfs(root.left)
right = dfs(root.right)
# 框架3: 后序遍历结果处理; 左右树高高度不大于1;
if abs(left-right) > 1:
self.res = False
return max(left, right) + 1
dfs(root)
return self.res
三、满二叉树
894.所有可能的满二叉树-中等
- 直接抄的,大概率不会考,遇到就抄:
class Solution:
memo = {1: [TreeNode(0)]}
def allPossibleFBT(self, N: int) -> List[TreeNode]:
if N in self.memo:
return self.memo[N]
res = []
for i in range(1, N, 2):
for left in self.allPossibleFBT(i):
for right in self.allPossibleFBT(N - i - 1):
root = TreeNode(0)
root.left = left
root.right = right
res.append(root)
self.memo[N] = res
return self.memo[N]
四、完全二叉树
222.完全二叉树的节点个数-中等(考察统计节点个数)
- 二分查找思路1:利用满二叉树,肯定有完全二叉树,进行剪枝;
class Solution:
def countNodes(self, root: TreeNode) -> int:
# 思路:利用完全二叉树节点数目计算对于 暴力遍历进行剪枝;
# 类型: 1) 返回int; 2)无全局变量; 3)自身调用; 4)后序遍历;
# 框架1: root做点什么?
if not root:
return 0
left_height = 0
left_node = root
right_height = 0
right_node = root
# 统计左右子树高度;
while left_node:
left_node = left_node.left
left_height += 1
while right_node:
right_node = right_node.right
right_height += 1
if left_height == right_height:
return pow(2, left_height) - 1
# 框架2: 左右子树;
left = self.countNodes(root.left)
right = self.countNodes(root.right)
# 框架3/4
return 1 + left + right
958.二叉树的完全性检验-中等(考察检测是否为完全二叉树)
- 思路:层次遍历: 当当前节点不为空,而前一节点为空时,不是完全二叉树。
class Solution:
def isCompleteTree(self, root: TreeNode) -> bool:
# 思路: 当 当前节点不为空, 而前一节点为空时, 不是完全二叉树;
if not root:
return False
queue = [root]
# pre: 记录前一个节点;
pre = root
# 队列模拟
while queue:
curNode = queue.pop(0)
# 结束条件判断;
if not pre and curNode:
return False
if curNode:
queue.append(curNode.left)
queue.append(curNode.right)
pre = curNode
return True
五、线段树
六、字典树
208.实现Trie(前缀树)-中等(考察创建字典树)
- 通过字典构建:一个单词的结束标注有多种,本文: 如果单词最后一次字符的 字典value中含有# ,则算结束;
class Trie:
def __init__(self):
self.lookup = {}
def insert(self, word: str) -> None:
tree = self.lookup
for a in word:
if a not in tree:
tree[a] = {}
# 最核心一步;
tree = tree[a]
# 单词结束标志
tree["#"] = "#"
print(self.lookup)
def search(self, word: str) -> bool:
tree = self.lookup
for a in word:
if a not in tree:
return False
# 最核心一步;
tree = tree[a]
# 单词结束标志;
if "#" in tree:
return True
return False
def startsWith(self, prefix: str) -> bool:
tree = self.lookup
for a in prefix:
if a not in tree:
return False
# 最核心一步;
tree = tree[a]
return True
212.单词搜索II-困难(考察单词搜索)
- Trie树实现:
class Solution:
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
# 字典树构造; 将word进行组织, 加速搜索: 原来 m*n次搜索,
# 每次遍历所有word, 现在word搜索都在一起组织,不用处理;
trie = {}
for word in words:
node = trie
for char in word:
# 1) 新加; 2) 延续
if char not in node:
node[char] = {}
node = node[char]
node['#'] = True
# 遍历方向
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
# (i,j)当前坐标; node:当前trie节点;
# pre:trie树已经访问字符串; visited: board已经访问坐标;
def search(i, j, node, pre, visited):
# 网格DFS找到Trie树中叶子节点;
if '#' in node:
res.append(pre)
# 没找到,沿着4方向搜索;
for (di, dj) in directions:
new_i, new_j = i+di, j+dj
# 当前搜索节点可满足性判断;
# 1) 不越界; 2)board中字符在trie树; 3)当前DFS路径未访问;
if 0<=new_i<h and 0<=new_j<w and board[new_i][new_j] in node and (new_i, new_j) not in visited:
# visited是回溯过程; 不是全局过程;
# 参数中调整, 模拟了回溯过程, visited也是回溯;
search(new_i, new_j, node[board[new_i][new_j]], pre+board[new_i][new_j], visited|{(new_i, new_j)})
res = []
h, w = len(board), len(board[0])
for i in range(h):
for j in range(w):
if board[i][j] in trie:
search(i, j, trie[board[i][j]], board[i][j], {(i,j)})
return list(set(res))
七、树状数组
⭐LeetCode刷题总结-树篇(下)
一、新概念定义问题
117.填充每个节点的下一个右侧节点指针II-中等
- Offer 37. 序列化二叉树-困难-lc297
leetcode链接
297.二叉树的序列化与反序列化-困难
-
上图用: 队列pop(0) 和 append演示过程;
-
BFS思路:
class Codec:
def serialize(self, root):
"""Encodes a tree to a single string.
:type root: TreeNode
:rtype: str
"""
if not root:
return []
q = []
q.append(root)
res = ''
while q:
node = q.pop(0)
if node != None:
res += str(node.val) + ','
q.append(node.left)
q.append(node.right)
else:
res += 'X,'
return res
def deserialize(self, data):
"""Decodes your encoded data to tree.
:type data: str
:rtype: TreeNode
"""
if not data:
return None
data = data.split(',')
root = TreeNode(data.pop(0))
q = [root]
while q:
node = q.pop(0)
if data:
val = data.pop(0)
if val != 'X':
node.left = TreeNode(val)
q.append(node.left)
if data:
val = data.pop(0)
if val != 'X':
node.right = TreeNode(val)
q.append(node.right)
return root
- DFS: 序列化
class Codec:
def serialize(self, root):
# 先序遍历; None用#代替;
if not root:
# , 至关重要;
return '#,'
left = self.serialize(root.left)
right = self.serialize(root.right)
return str(root.val) + ',' + left + right
def deserialize(self, data):
# 递归构建;
data = data.split(',')
print(data)
root = self.buildTree(data)
return root
def buildTree(self, data):
# 序列--> 结构化;
val = data.pop(0)
if val == '#':
return None
root = TreeNode(val)
# 递归序列化;
# 框架: 左右子树;
left = self.buildTree(data)
right = self.buildTree(data)
root.left = left
root.right = right
# 框架:返回当前子树;
return root
114.二叉树展开为链表-中等
- 前序遍历存储,再生成;
- 递归思路
class Solution:
def flatten(self, root: TreeNode) -> None:
# 递归思路: 1) 先序遍历存储; 2) 结果生成;
# 递归存储;
preOrder = []
def preOrderTraversal(root):
if not root:
return None
preOrder.append(root)
preOrderTraversal(root.left)
preOrderTraversal(root.right)
preOrderTraversal(root)
# 生成
size = len(preOrder)
for i in range(1, size):
prevNode, curNode = preOrder[i-1], preOrder[i]
prevNode.left = None
prevNode.right = curNode
- 迭代思路
class Solution:
def flatten(self, root: TreeNode) -> None:
preOrder = []
stack = []
curNode = root
while stack or curNode:
while curNode:
preOrder.append(curNode)
stack.append(curNode)
curNode = curNode.left
curNode = stack.pop()
curNode = curNode.right
# 生成
size = len(preOrder)
for i in range(1, size):
prevNode, curNode = preOrder[i-1], preOrder[i]
prevNode.left = None
prevNode.right = curNode
- 时间复杂度:O(n)
- 空间复杂度:O(1)
class Solution:
def flatten(self, root: TreeNode) -> None:
curNode = root
while curNode:
# 如果存在左节点, 进行指针转换;
if curNode.left:
# pre指针: curNode的左子树的最右边叶子;
# nxt指针: curNode的左子树的根节点;
preNode = nxtNode = curNode.left
# 找到左子树中最右边叶子节点;
while preNode.right:
preNode = preNode.right
# 转换:
preNode.right = curNode.right
curNode.left = None
curNode.right = nxtNode
# 转换之后,更新curNode;
curNode = curNode.right
998.最大二叉树II-中等-不做
834.树中距离之和-困难-不会、抄的
- 实现
class Solution(object):
def sumOfDistancesInTree(self, N, edges):
graph = collections.defaultdict(set)
for u, v in edges:
graph[u].add(v)
graph[v].add(u)
count = [1] * N
ans = [0] * N
def dfs(node = 0, parent = None):
for child in graph[node]:
if child != parent:
dfs(child, node)
count[node] += count[child]
ans[node] += ans[child] + count[child]
def dfs2(node = 0, parent = None):
for child in graph[node]:
if child != parent:
ans[child] = ans[node] - count[child] + N - count[child]
dfs2(child, node)
dfs()
dfs2()
return ans
二、子树问题,匹配问题
- 模板:
# 类型: 1) bool类型; 2) 一般都重新定义; 3)一般不需要全局变量;
# 主函数:
def main(root1, root2):
if not root1 and not root2:
return False
# 递归调用判断;
return def() and/or main(root1.left, root2.left / right) and/or mina()
def dfs(root1, root2):
# root判断
# true判断: 出现匹配完;
if not t1 / not t2 / not t1 and not t2:
return True
if not ...:
return False
# 进一步递归子树判断;
return t1.val == t2.val and dfs(root1..., root2...) and dfs(root1..., root2...)
508.出现次数最多的子树元素和-中等-不懂题
101-对称二叉树-简单
- 思路1: 递归思路 : 深度优先遍历: 100
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def isSymmetric(self, root: TreeNode) -> bool:
# 类型: 1) 是否型; 2)重新定义: 自身不满足; ; 3) 递归;
def isMirror(root1, root2):
'''
判断两个二叉树是否镜像;
'''
# 框架:
# 步骤1: root需要做什么? 1) 满足条件判断; 2) 不满足条件判断: a:剪枝; b:退出;
# 步骤2:其他y不用root操心, 交给框架;
# root做什么?
# 两种写法: True / False类型; 1)哪种情况满足; 2) 哪种情况不满足;
# 两个都是空节点: 必定相等;
if not root1 and not root2:
return True
# 一个空,一个非空,必定不等;
if not root1 or not root2:
return False
# 交给框架: 1) val相等; 2) 左; 3) 右;
return root1.val == root2.val and isMirror(root1.left, root2.right) and isMirror(root1.right, root2.left)
# 可以省略;
'''
# 异常判断;
if not root:
return False
'''
return isMirror(root, root)
面试题 04.10. 检查子树-中等
- 模板实现:
class Solution:
def checkSubTree(self, t1: TreeNode, t2: TreeNode) -> bool:
# 类型: 1)返回bool; 2)无全局变量; 3)重新定义; 4)先序遍历;
# 思路: 递归判断两个树的结构;
def dfs(root1, root2):
# 框架1: root做点什么?
if not root2:
return True
if not root1:
return False
# 框架2/3/4
return root1.val == root2.val and dfs(root1.left, root2.left) and dfs(root1.right, root2.right)
if not t1 or not t2:
return False
return dfs(t1, t2) or self.checkSubTree(t1.left, t2) or self.checkSubTree(t1.right, t2)
572. 另一个树的子树-简单
- 调用模板
class Solution:
def isSubtree(self, s: TreeNode, t: TreeNode) -> bool:
# 类型: 1) 返回bool; 2)无全局变量; 3)重新定义; 4)先序遍历;
# 思想: 对s每个节点进行递归判断; 要求必须子树(到叶子节点,而非部分)
def dfs(root1, root2):
# 框架1: root做点什么?
if not root1 and not root2:
return True
if not root1 or not root2:
return False
# 框架2/3/4
return root1.val == root2.val and dfs(root1.left, root2.left) and dfs(root1.right, root2.right)
if not s or not t:
return False
return dfs(s, t) or self.isSubtree(s.left, t) or self.isSubtree(s.right, t)
652.寻找重复的子树-中等
- python实现:
# 时间复杂度:
# 主要是受字符串拼接影响,假设 root.val 的长度为 a,root.left 的长度为 b,root.right 的长度为 c,
# 每次字符串拼接所需时间是 a + b + c <= N,而一共有 N 个节点,所以是 O(N^2)。
# 空间复杂度:主要是 Map 的 key 占的空间。
class Solution(object):
def findDuplicateSubtrees(self, root):
# 类型: 1) 返回list; 2) 全局变量; 3)重新定义; 4)先序遍历;
count = {}
res = []
def dfs(root):
# 框架1:root做点什么?
if not root:
return '#'
# 框架2:左右子树
serial = '{},{},{}'.format(root.val, dfs(root.left), dfs(root.right))
# 框架3: 后序遍历结果处理;
if serial not in count:
count[serial] = 0
count[serial] += 1
if count[serial] == 2:
# 注意: 返回子树根节点;
res.append(root)
# 框架4: 返回结果;
return serial
dfs(root)
return res
865.具有所有最深结点的最小子树-中等
1110.删点成林-中等
# 一遍DFS:如果当前结点是要被删除的,那么向上返回None,如果不需要被删除,
# 则返回原结点,上层遍历左子树和右子树后,直接赋给这个返回值。
# 还有一个优化就是把to_delete这个列表转换成集合,增加查询速度。
class Solution:
def delNodes(self, root: TreeNode, to_delete: List[int]) -> List[TreeNode]:
# 思路: 找到删除节点; 然后断开,分别访问;
# 类型: 1) 返回值; 2) 全局变量; 3)重新定义; 4)先序遍历;
dct = set(to_delete)
res = []
def dfs(root, flag):
'''
先序遍历; 存储结果;
root: 当前节点;
flag: 父节点是否删除标志;
'''
# 框架1: root做点什么? 空节点向上返回None;
if not root:
return None
# flag: 记录父节点是否删除;
deleted = root.val in dct
# 判断以root为根节点的子树是否加入森林;
# 父节点要被删除, 当前节点不删除时: root作为子树根节点加入;
if flag and not deleted:
res.append(root)
# 框架2: 左右子树;
left = dfs(root.left, deleted)
right = dfs(root.right, deleted)
root.left = left
root.right = right
# 框架3/4: 1) root需要删除,返回None; 2)root不需要删除,返回root;
return None if deleted else root
dfs(root, True)
return res
⭐二叉搜索树相关
- leetcode中常见的二叉树相关的知识点和题目总结;
- 面经中的二叉树题目;
大佬笔记-二叉搜索树操作集锦
大佬笔记-二叉搜索树操作集锦
相关题目:
leetcode-100.相同的树
leetcode-450.删除二叉搜索树中的节点
leetcode-701.二叉搜索树中的插入操作
leetcode-700.二叉搜索树中的搜索
leetcode-98.验证二叉搜索树
二叉树算法的设计的总路线:明确一个节点要做的事情,然后剩下的事抛给框架。
void traverse(TreeNode root) {
// root 需要做什么?在这做。
// 其他的不用 root 操心,抛给框架
traverse(root.left);
traverse(root.right);
}
- 如何把二叉树所有的节点中的值加一?
# python
def plusOne(TreeNode root):
if root == None:
return None
root.val += 1
plusOne(root.left)
plusOne(root.right)
- 如何判断两棵二叉树是否完全相同?
leetcode-100. 相同的树-简单
100. 相同的树
给定两个二叉树,编写一个函数来检验它们是否相同。
如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
示例 1:
输入: 1 1
/ \ / \
2 3 2 3
[1,2,3], [1,2,3]
输出: true
python题解
# python
def isSameTree(TreeNode root1, TreeNode root2):
# 如果都为空, 现然相同
if root1 == None and root2 == None:
return True
# 一个为空, 一个非空, 显然不同
if root1 == None or root2 == None:
return False
# 两个都非空, 但是 val不同,现然不同
if root1.val != root2.val:
return False
# root1, root2 该比的都比完了
return isSameTree(root1.left, root2.left) and isSameTree(root1.right, root2.right)
借助框架,上面这两个例子不难理解吧?如果可以理解,那么所有二叉树算法你都能解决。
二叉搜索树(Binary Search Tree,简称 BST)是一种很常用的的二叉树。它的定义是:一个二叉树中,任意节点的值要大于等于左子树所有节点的值,且要小于等于右边子树的所有节点的值。
如下就是一个符合定义的 BST:
下面实现 BST 的基础操作:判断 BST 的合法性、增、删、查。其中“删”和“判断合法性”略微复杂。
0-判断 BST 的合法性
leetcode-98. 验证二叉搜索树-中等
98. 验证二叉搜索树
给定一个二叉树,判断其是否是一个有效的二叉搜索树。
假设一个二叉搜索树具有如下特征:
节点的左子树只包含小于当前节点的数。
节点的右子树只包含大于当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
示例 1:
输入:
2
/ \
1 3
输出: true
这里是有坑的, 按照刚才的思路, 每个节点自己要做的不就是比较自己和左右孩子嘛? 看起来代码:
def isVailsBST(TreeNode root):
# root 需要做什么?在这做。
if root == None:
return False
# 若: 根值 <= 左子树,返回False
if root.left and root.val <= root.left.val:
return False
# 若: 根值 >= 右子树, 返回False
if root.right and root.val >= root.right.val:
return False
# 其他的不用 root 操心,抛给框架
return isValidBST(root.left) and isValidBST(root.right)
!!! 但是这个算法出现了错误, BST的每个节点应该要小于 右边子树的所有节点,下面的二叉树现然不是 BST,但上述算法会误认为是。
出现错误,不要慌张,框架没有错,一定是某个细节问题没注意到。我们重新看一下 BST 的定义,root 需要做的,不仅仅是和左右子节点比较,而是要和左子树和右子树的所有节点比较。怎么办,鞭长莫及啊!
这种情况,我们可以使用辅助函数,增加函数参数列表,在参数中携带额外信息,请看正确的代码:
python 最终AC版本代码
class Solution:
def isValidBST(self, root: TreeNode) -> bool:
# 画图可知: 1): 右走: max不更新, min = parent; 2) 左走: max = parent, min不更新;
# 思路: 递归;
# 类型: 1) 是否型; 2) 重新定义函数; 3) 无需全局变量;
def isBST(root, minValue, maxValue):
'''
判断当前是否为 二叉搜索树;
root: 当前节点
minValue: 当前子树的最小值;
maxValue: 当前子树的最大值;
'''
# 步骤1: root做点什么? 空节点必定为二叉搜索树;
if not root:
return True
# 判断minValue存在性:你和刚开始根节点;
if minValue and root.val <= minValue.val:
return False
if maxValue and root.val >= maxValue.val:
return False
# 框架: 左子树
left = isBST(root.left, minValue, root)
right = isBST(root.right, root, maxValue)
# 框架: 返回结果;
return left and right
return isBST(root, None, None)
这样,root 可以对整棵左子树和右子树进行约束,根据定义,root 才真正完成了它该做的事,所以这个算法是正确的。
一、在 BST 中查找一个数是否存在
700. 二叉搜索树中的搜索-简单
700. 二叉搜索树中的搜索
给定二叉搜索树(BST)的根节点和一个值。 你需要在BST中找到节点值等于给定值的节点。
返回以该节点为根的子树。 如果节点不存在,则返回 NULL。
例如,
给定二叉搜索树:
4
/ \
2 7
/ \
1 3
和值: 2
你应该返回如下子树:
2
/ \
1 3
根据我们的指导思想,可以这样写代码:
class Solution:
def searchBST(self, root: TreeNode, val: int) -> TreeNode:
# root做的事情
if not root or not val:
return None
if root.val == val:
return root
left = self.searchBST(root.left, val)
right = self.searchBST(root.right, val)
if left:
return left
if right:
return right
python实现: 注意利用 BST性质进行加速搜索
class Solution:
def searchBST(self, root: TreeNode, val: int) -> TreeNode:
# 类型: 1) 返回值; 2) 自身递归; 3)不用全局变量;
# root做点什么?
if not root or not val:
return None
if root.val == val:
return root
# 框架: 左子树;
# BST加速
if root.val > val:
left = self.searchBST(root.left, val)
return left
# 框架: 右子树;
# BST加速
if root.val < val:
right = self.searchBST(root.right, val)
return right
二、在 BST 中插入一个数
701. 二叉搜索树中的插入操作-中等
701. 二叉搜索树中的插入操
给定二叉搜索树(BST)的根节点和要插入树中的值,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。
保证原始二叉搜索树中不存在新值。
注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回任意有效的结果。
例如,
给定二叉搜索树:
4
/ \
2 7
/ \
1 3
和 插入的值: 5
你可以返回这个二叉搜索树:
4
/ \
2 7
/ \ /
1 3 5
或者这个树也是有效的:
5
/ \
2 7
/ \
1 3
\
4
对数据结构的操作无非 遍历 + 访问, 遍历就是‘找’, 访问就是‘改’。 具体到这个问题, 插入一个数, 就是先找到插入位置,然后进行插入操作。
上一个问题,总结了BST中的遍历框架, 就是 “找” 的问题。 直接 套上框架, 加上”改“的操作即可。 一旦涉及”改“, 函数就要返回TreeNode 类型, 并且对 递归调用的返回值进行接收。 (一定一定记住了。 )
python实现
class Solution:
def insertIntoBST(self, root: TreeNode, val: int) -> TreeNode:
# 思路: 数据结构操作: 遍历+访问; 找+改;
# 改: 返回 TreeNode类型;
# 框架: root做点什么?
if not root:
return TreeNode(val)
# 框架: 右边看看;
if root.val < val:
root.right = self.insertIntoBST(root.right, val)
# 框架:右边看看;
if root.val > val:
root.left = self.insertIntoBST(root.left, val)
return root
三、在 BST 中删除一个数
450. 删除二叉搜索树中的节点-中等
450. 删除二叉搜索树中的节点
给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,
并保证二叉搜索树的性质不变。
返回二叉搜索树(有可能被更新)的根节点的引用。
一般来说,删除节点可分为两个步骤:
首先找到需要删除的节点;
如果找到了,删除它。
说明: 要求算法时间复杂度为 O(h),h 为树的高度。
示例:
root = [5,3,6,2,4,null,7]
key = 3
5
/ \
3 6
/ \ \
2 4 7
给定需要删除的节点值是 3,所以我们首先找到 3 这个节点,然后删除它。
一个正确的答案是 [5,4,6,2,null,null,7], 如下图所示。
5
/ \
4 6
/ \
2 7
另一个正确答案是 [5,2,6,null,4,null,7]。
5
/ \
2 6
\ \
4 7
python 初级版本实现
class Solution:
def getMin(self, root):
# 获取当前子树中的最小值;
while root.left:
root = root.left
return root.left
def deleteNode(self, root: TreeNode, key: int) -> TreeNode:
'''
re: TreeNode
'''
# 类型: 1) 返回值; 2) 自身递归; 3) 不用全局变量;
# 框架: root做什么?
if not root:
return None
# 框架: root.val == val
if root.val == key:
# 找到要删除节点, 然后对于BST进行调整;
# 情况1:左空,右返回;
if not root.left:
return root.right
# 情况2:右空, 左返回;
if not root.right:
return root.left
# 情况3:左右都非空;
# 处理: 1) 左边最大值填上去; 2) 右边最小值填上去;
minValue = self.getMin(root.right)
# 通过修改node值实现交换,实际中不会这种操作;
root.val = minValue
# 再去右子树中删除minValue节点:
self.deleteNode(root.right, minValue)
# 框架: root.val > val
elif root.val > key:
self.deleteNode(root.left, key)
# 框架:root.val < val
elif root.val < key:
self.deleteNode(root.right, key)
# 框架: 返回值;
return root
删除操作就完成了。 注意一下,这个删除操作并不完美,因为我们一般不会通过 root.val = minNode.val 修改节点内部的值来交换节点, 而是通过一些列略微复杂的链表操作交换 root 和 minNode 两个节点。 因为具体的应用中, val 域可能会很大,修改起来耗时, 而链表操作无非改一下指针,而不会去碰内部数据。
但这里忽略这个细节, 旨在突出BST基本操作的共性,以及借助框架逐层细化问题的思维方式。
四、二叉树搜索树 其他题目
96. 不同的二叉搜索树-中等
- python代码- 有点不像动态规划了
class Solution:
def numTrees(self, n: int) -> int:
if n<=1:
return 1
store = [1, 1]
for i in range(2, n+1):
s = i-1
count = 0
for j in range(i):
count += store[j]*store[s-j]
store.append(count)
print(i, count)
return store[n]
- 动态规划:转移方程有点奇怪
class Solution:
def numTrees(self, n: int) -> int:
if n<=1:
return 1
# dp[i] 代表什么含义呢?
# 以i 为结尾的
dp = [0 for _ in range(n+1)]
dp[0], dp[1] = 1, 1
for i in range(2, n+1):
for j in range(i):
dp[i] += dp[j]*dp[i-j-1]
return dp[n]
95. 不同的二叉搜索树 II-中等-未做
- 本题要求输出所有的: 二叉搜索数, 肯定不能像之前那样通过规律计算数字了;
- 回溯 + 记忆化(剪枝) 方法: 保存所有的可能结果;
class Solution:
def generateTrees(self, n: int) -> List[TreeNode]:
# 需要存储所有的二叉搜索树, 所以必须使用 回溯/递归 等保留结果,
# 而不是简单递推数字;
# 回溯+记忆化剪枝;
if n == 0:
return []
# 记忆化递归思路: 记忆化: memo[(i, j)]: i~j 构成的所有二叉搜索树;
# 记忆化词典: 避免重复计算; 如: (left, right)= (2, 3)可能计算多次;
memo = {}
# 递归函数: left~right范围的二叉搜索树的组织形式;
def left_right_BST(left, right):
# 返回 list形式;
if left > right:
return [None]
# 记忆化查询;
if (left, right) in memo:
return memo[(left, right)]
# 当前范围:left~right,所有结果存储;
# 注意: 函数内部变量, 仅仅参与当前left~right情况下的结果;
ret = []
# 2~3搜索二叉树组织形式, 取值: [2, 4)
for i in range(left, right+1):
# 节点i为根节点的左右子树分别计算;
left_res = left_right_BST(left, i-1)
right_res = left_right_BST(i+1, right)
# 左右结果进行组合;
for l_bst in left_res:
for r_bst in right_res:
# 对于当前节点构建左右子树, 生成树;
tmpTree = TreeNode(i)
tmpTree.left = l_bst
tmpTree.right = r_bst
ret.append(tmpTree)
# 记忆化存储;
memo[(left, right)] = ret
return ret
return left_right_BST(1, n)
- 动态规划思路: 自底向上推导-不会
99.恢复二叉搜索树-困难
- 空间复杂度: O(N), 可以进一步优化,但是不必了;
class Solution:
def recoverTree(self, root: TreeNode) -> None:
"""
Do not return anything, modify root in-place instead.
"""
# 中序遍历二叉树, 并将遍历结果保存到list中;
if not root:
return []
inOreder = []
def dfs(root):
if not root:
return []
dfs(root.left)
inOreder.append(root)
dfs(root.right)
dfs(root)
# 找到: 位置出错的2个节点;
preNode = None
postNode = None
pre = inOreder[0]
# 扫描遍历中序遍历结果,找到两个异常值;
for i in range(1, len(inOreder)):
if pre.val > inOreder[i].val:
# BST_inOrder后面的值: 偏小;
postNode = inOreder[i]
if not preNode:
# BST_inOrder前面的值:偏大;
preNode = pre
pre = inOreder[i]
# 如果preNode 和 postNode 不为空, 则交换这两个节点, 恢复BST;
if preNode and postNode:
preNode.val, postNode.val = postNode.val, preNode.val
108. 将有序数组转换为二叉搜索树-简单
- 递归实现: 代码很像二叉树重构、其实也算是重构;
class Solution:
def sortedArrayToBST(self, nums: List[int]) -> TreeNode:
# 思路: 递归; 使用二分: 将中点(考虑了奇、偶数)作root节点;
# 有序数组=BSD中序遍历结果; 不能确定一棵树;
# 平衡二叉树: h(left) ~ h(right) 差值在1以内;
# 类型: 1) 返回值; 2) 重新定义函数; 3) 不用全局变量;
def dfs(left, right):
'''
nums[left]~nums[right]组建BST;
'''
if left > right:
return None
# 选择中间位置左边的数字作为根节点;
mid = (left + right) // 2
root = TreeNode(nums[mid])
# 框架:左右子树;
left = dfs(left, mid-1)
right = dfs(mid+1, right)
root.left = left
root.right = right
# 向上返回当前子树;
return root
return dfs(0, len(nums)-1)
Offer 36. 二叉搜索树与双向链表-中等
- 复杂度分析:
- 时间复杂度 O(N): N 为二叉树的节点数,中序遍历需要访问所有节点。
- 空间复杂度 O(N): 最差情况下,即树退化为链表时,递归深度达到 N,系统使用 O(N) 栈空间。
- 排序:中序遍历; 循环双向链表:
class Solution:
def treeToDoublyList(self, root: 'Node') -> 'Node':
# 思路: 中序遍历: 增加left指针;
# 类型: 1) 返回值; 2)重新定义; 3)不虚全局变量;
def dfs(curNode):
'''
中序遍历结果+left\right指针;
'''
if not curNode:
return
# 框架: 左子树: 找到底;
dfs(curNode.left)
# 修改节点引用;
if self.preNode:
self.preNode.right, curNode.left = curNode, self.preNode
# 记录头节点;
else:
self.head = curNode
# 保存curNode;
self.preNode = curNode
# 框架: 右子树;
dfs(curNode.right)
if not root:
return None
self.preNode = None
self.head = None
dfs(root)
self.head.left, self.preNode.right = self.preNode, self.head
return self.head
Offer 54. 二叉搜索树的第k大节点-简单
- 中序遍历
class Solution:
def kthLargest(self, root: TreeNode, k: int) -> int:
# 思路: 中序遍历取topK?
res = []
def inOrder(root):
if not root:
return None
inOrder(root.left)
res.append(root.val)
inOrder(root.right)
inOrder(root)
return res[-k]
五、最后总结
通过这篇文章,你学会了如下几个技巧:
- 二叉树算法设计的总路线:把当前节点要做的事情做好,其他的交给框架,不用当前节点操心。
- 如果当前节点会对下面的子节点有整体影响,可以通过辅助函数增长参数列表,借助参数传递信息。
- 在二叉树框架之上,扩展出一套BST遍历框架:
def BST(TreeNode root, int target):
# 当前节点该做的事情
if root.val == target:
# 找到目标, 做点什么:
elif root.val < target:
BST(root.right, target)
elif root.val > target:
BST(root.left, target)
4.掌握BST的基本操作: 增、删、改、查。
二叉树题目补充
-
翻转二叉树,难度 Easy
-
填充二叉树节点的右侧指针,难度 Medium