二叉树理论基础
二叉树的种类
解题过程中二叉树有两种主要的形式:满二叉树和完全二叉树。
- 满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。深度为k的满二叉树有
2^k-1
个节点。
证明:
树的深度 𝑘 意味着树有 𝑘+1 层节点。
等比数列求和公式: S n = a r n − 1 r − 1 S_n = a \frac{r^n - 1}{r - 1} Sn=ar−1rn−1,因此有 N ( k ) = 2 k − 1 2 − 1 = 2 k − 1 N(k) = \frac{2^k - 1}{2 - 1} = 2^k - 1 N(k)=2−12k−1=2k−1.
- 完全二叉树的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。除了最后一层,其他所有层的节点都是满的,即每一层都有
2
i
2^i
2i 个节点(其中𝑖是从根到该层的深度,从 0 开始计数)。若最底层为第 h 层(h从1开始),则该层包含
1~ 2^(h-1)
个节点(等比数列)。
优先级队列其实是一个堆,堆就是一棵完全二叉树,同时保证父子节点的顺序关系。 - 二叉搜索树
前面介绍的树,都没有数值的,而二叉搜索树是有数值的了,二叉搜索树是一个有序树。- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉排序树。
- 平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
二叉树的存储方式
二叉树可以链式存储,也可以顺序存储。链式存储方式就用指针, 顺序存储的方式就是用数组。顺序存储的元素在内存是连续分布的,而链式存储则是通过指针把分布在各个地址的节点串联一起。
用链式表示的二叉树,更有利于我们理解,所以一般我们都是用链式存储二叉树。用数组依然可以表示二叉树。如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1
,右孩子就是 i * 2 + 2
。
二叉树的遍历方式
二叉树主要有两种遍历方式:
- 深度优先遍历:先往深走,遇到叶子节点再往回走。
- 广度优先遍历:一层一层的去遍历。
这两种遍历是图论中最基本的两种遍历方式。
从深度优先遍历和广度优先遍历进一步拓展,才有如下遍历方式:
- 深度优先遍历
- 前序遍历(递归法,迭代法)
- 中序遍历(递归法,迭代法)
- 后序遍历(递归法,迭代法)
- 广度优先遍历
- 层次遍历(迭代法)
前中后序遍历的逻辑其实都是可以借助栈使用递归的方式来实现的,而广度优先遍历的实现一般使用队列来实现。
二叉树的定义
在现场面试的时候 面试官可能要求手写代码,所以数据结构的定义以及简单逻辑的代码一定要锻炼白纸写出来。因为我们在刷leetcode的时候,节点的定义默认都定义好了,真到面试的时候,需要自己写节点定义的时候,有时候会一脸懵逼!
链式存储的二叉树节点的定义方式:
class TreeNode:
def __init__(self, val, left=None, right=None);
self.val = val
self.left = left
self.right = right
二叉树的定义和链表是差不多的,相对于链表 ,二叉树的节点里多了一个指针, 有两个指针,指向左右孩子。
二叉树的题型
二叉树的递归遍历
递归算法的三个要素:
- 确定递归函数的参数和返回值:确定哪些参数是递归的过程中需要处理的
- 确定终止条件:如果递归没有终止,操作系统的内存栈必然就会溢出
(在递归的过程中,如何算是递归结束了呢,当然是当前遍历的节点是空了,那么本层递归就要结束了,所以如果当前遍历的这个节点是空,就直接return。) - 确定单层递归的逻辑:确定每一层递归需要处理的信息
二叉树前序遍历(递归):(代码随想录的代码有问题,少了dfs的调用)
用res
+ dfs
函数
class Solution:
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
res = []
def dfs(node):
if not node:
return
res.append(node.val)
dfs(node.left)
dfs(node.right)
dfs(root)
return res
运用python特性,直接返回递归的函数
class Solution:
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
if not root:
return []
return [root.val] + self.preorderTraversal(root.left) + self.preorderTraversal(root.right)
网上基本都是这个精简的代码版本,其实不建议大家照着这个来写,代码确实精简,但隐藏了一些内容,连遍历的顺序都看不出来,所以初学者建议学习版本一的代码,稳稳的打基础。积累自己的代码模板。
二叉树的中序遍历:
用res
+ dfs
函数
class Solution:
def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
res = []
def dfs(node):
if not node:
return
dfs(node.left)
res.append(node.val)
dfs(node.right)
dfs(root)
return res
二叉树的后序遍历:
运用python特性,直接返回递归的函数
class Solution:
def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
if not root:
return []
return self.postorderTraversal(root.left) + self.postorderTraversal(root.right) + [root.val]
二叉树的迭代遍历
为什么可以用迭代法(非递归的方式)来实现二叉树的前后中序遍历呢?递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。因此,用栈就可以实现二叉树的迭代遍历。
二叉树的前序遍历:
前序遍历是中左右,每次先处理的是中间节点,那么先将根节点放入栈中,然后将右孩子加入栈,再加入左孩子。
为什么要先加入 右孩子,再加入左孩子呢? 因为这样出栈的时候才是中左右的顺序。
class Solution:
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
if not root:
return []
stack = [root]
res = []
while stack:
node = stack.pop()
res.append(node.val)
if node.right: stack.append(node.right)
if node.left: stack.append(node.left)
return res
二叉树的中序遍历:
再用迭代法写中序遍历的时候,会发现套路又不一样了,目前的前序遍历的逻辑无法直接应用到中序遍历上。
因为:前序遍历要访问的元素和要处理的元素顺序是一致的,都是中间节点。中序遍历处理顺序和访问顺序是不一致的。
在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。
class Solution:
def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
stack = []
res = []
cur = root
while cur or stack:
if cur:
stack.append(cur)
cur = cur.left
else:
cur = stack.pop()
res.append(cur.val)
cur = cur.right # 不需要if cur.right: stack.append(cur.right)
return res
注意:
添加已进栈元素右孩子的时候,不需要if cur.right: stack.append(cur.right)
,只需要cur = cur.right
,因为已经用了指针cur来让元素进栈。
二叉树的后序遍历:
先序遍历是中左右,后续遍历是左右中,那么我们只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中了。
class Solution:
def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
if not root:
return []
stack = [root]
res = []
while stack:
node = stack.pop()
res.append(node.val)
if node.left: stack.append(node.left)
if node.right: stack.append(node.right)
return res[::-1]
二叉树的迭代遍历(统一前中后序风格)
好复杂,递归掌握了两种,迭代掌握一种就行了。统一风格的之后有空再刷。
二叉树层序遍历
层序遍历一个二叉树,需要借用一个辅助数据结构即队列来实现,队列先进先出,符合一层一层遍历的逻辑,而用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。
而这种层序遍历方式就是图论中的广度优先遍历,只不过我们应用在二叉树上。
参考hot100的解法,有两种:
普通list
和collections.deque
199. 二叉树的右视图
为什么我这样写是不对的:
class Solution:
def rightSideView(self, root: Optional[TreeNode]) -> List[int]:
if not root:
return []
queue, res = collections.deque([root]), []
while queue:
for i in range(len(queue)):
node = queue.popleft()
if i == len(queue)-1: res.append(node.val)
if node.left: queue.append(node.left)
if node.right: queue.append(node.right)
return res
因为没有把len(queue)
记录下来,queue.popleft()
之后,len(queue)
的长度就变了。
要把len(queue)
记录下来,让i==len(queue)-2
也是不对的。
class Solution:
def rightSideView(self, root: Optional[TreeNode]) -> List[int]:
if not root:
return []
queue, res = collections.deque([root]), []
while queue:
length = len(queue)
for i in range(length):
node = queue.popleft()
if i == length-1: res.append(node.val)
if node.left: queue.append(node.left)
if node.right: queue.append(node.right)
return res
429. N 叉树的层序遍历
打印之后发现N叉树的node.children是一个list,那就可以用下标来调用。
"""
# Definition for a Node.
class Node:
def __init__(self, val=None, children=None):
self.val = val
self.children = children
"""
class Solution:
def levelOrder(self, root: 'Node') -> List[List[int]]:
if not root:
return []
queue, res = collections.deque([root]), []
while queue:
level, length = [], len(queue)
for _ in range(length):
node = queue.popleft()
level.append(node.val)
for i in range(len(node.children)):
if node.children[i]: queue.append(node.children[i])
# 是不是不需要if node.children[i]
res.append(level)
return res
- python extend()与append的区别:extend与append方法的相似之处在于都是将新接收到参数放置到已有列表的后面。而extend方法只能接收list,且把这个list中的每个元素添加到原list中。
而append方法可以接收任意数据类型的参数,并且简单地追加到list尾部。
116. 填充每个节点的下一个右侧节点指针
本题依然是层序遍历,只不过在单层遍历的时候记录一下本层的头部节点,然后在遍历的时候让前一个节点指向本节点就可以了。
if prev:
prev.next = node
prev = node
和
if prev:
prev.next = node
else:
prev = node
有什么区别呢?为什么会结果不一样?
——很精彩,直接prev=node
的作用就相当于让prev = prev.next
,起到了指针向右移动的作用。
104. 二叉树的最大深度
解法1. 用Python特性直接return+递归
class Solution:
def maxDepth(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
return max(self.maxDepth(root.left), self.maxDepth(root.right)) + 1
解法2. 用层序遍历代码模板改一改,记录下遍历的层数就可以了。
111. 二叉树的最小深度
解法1. 用层序遍历代码模板改一改
class Solution:
def minDepth(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
queue, level = collections.deque([root]), 0
while queue:
length = len(queue)
level += 1
for _ in range(length):
node = queue.popleft()
print(node.val)
if not node.left and not node.right:
return level
if node.left: queue.append(node.left) # 为什么不能用elif?
if node.right: queue.append(node.right)
Python之if / elif 的区别: 如果是用 if 的话,他会一直遍历完所有的if,不管你想判断的条件有没有遍历到,他都会继续执行完所有的if;
而 elif 呢,则会比较快捷,主要还是看你的用处,如果你是想遍历到你的判断条件就不再执行其他判断条件分支语句,那么就用elif;elif 就是当走到符合查询条件的语句后,后面所有的elif和else就不会再被执行;
题解(多解法)
解法2. 递归法+深度优先搜索
解题思路:
节点不存在时,返回0
左右子节点均不存在时,说明该节点为叶子节点,返回1
左右子节点存在一个时,返回1+minDepth(子节点)
左右子节点均存在时,返回1+较小的minDepth(子节点)
作者:Nathan
我按上面思路字面意思写的代码:(AC)
class Solution:
def minDepth(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
if not root.left: return self.minDepth(root.right)+1
if not root.right: return self.minDepth(root.left)+1
if root.left and root.right: return 1+min(self.minDepth(root.right), self.minDepth(root.left))
Nanan的代码:(更简洁)
class Solution:
def minDepth(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
l = self.minDepth(root.left)
r = self.minDepth(root.right)
if root.left and root.right: return 1 + min(l, r)
else: return 1 + l + r
二叉树的属性
222.完全二叉树的节点个数
-
按普通二叉树来解
可以有递归+深度遍历和层次遍历两种解法。
先写递归+深度遍历的写法。class Solution: def __init__(self): self.count = 0 def countNodes(self, root: Optional[TreeNode]) -> int: def dfs(node): if not node: return self.count += 1 dfs(node.left) dfs(node.right) dfs(root) return self.count
-
利用完全二叉树的性质
完全二叉树只有两种情况,情况一:就是满二叉树,情况二:最后一层叶子节点没有满。- 对于情况一,可以直接用 2^树深度 - 1 来计算,注意这里根节点深度为1。
- 对于情况二,分别递归左孩子,和右孩子,递归到某一深度一定会有左孩子或者右孩子为满二叉树,然后依然可以按照情况1来计算。
这里关键在于如何去判断一个左子树或者右子树是不是满二叉树呢?
在完全二叉树中,如果递归向左遍历的深度等于递归向右遍历的深度,那说明就是满二叉树。
代码思路:
递归三部曲
1)确定递归参数和返回值:参数是树的根节点,返回值是满二叉树节点数量计算公式
2)确定终止条件:找到满二叉树
3)确定单层递归逻辑:后序遍历
class Solution:
def countNodes(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
level = 1
left, right = root.left, root.right
while left and right:
level += 1
left = left.left
right = right.right
if not left and not root:
return 2**level - 1
return 1 + self.countNodes(root.left) + self.countNodes(root.right)
这种解法好好消化一下。
110. 平衡二叉树
咋眼一看这道题目和104.二叉树的最大深度很像,其实有很大区别。
- 二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数。
- 二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数。
求深度可以从上到下去查,所以需要前序遍历(中左右),而高度只能从下到上去查,所以只能后序遍历(左右中)。
为什么104.二叉树的最大深度中求的是二叉树的最大深度,也用的是后序遍历?
那是因为代码的逻辑其实是求的根节点的高度,而根节点的高度就是这棵树的最大深度,所以才可以使用后序遍历。
递归三部曲:
- 参数和返回值
参数:当前传入节点。 返回值:以当前传入节点为根节点的树的高度。
如果已经不是二叉平衡树了,可以返回-1 来标记。 - 终止条件
遇到空节点了为终止,返回0。 - 单层递归的逻辑
如何判断以当前传入节点为根节点的二叉树是否是平衡二叉树呢?当然是其左子树高度和其右子树高度的差值。
分别求出其左右子树的高度,然后如果差值小于等于1,则返回当前二叉树的高度,否则返回-1,表示已经不是二叉平衡树了。
一开始根据上面的思路写出来的代码:
class Solution:
def isBalanced(self, root: Optional[TreeNode]) -> bool:
def getHight(node):
if not node:
return 0
if abs(getHight(node.left)-getHight(node.right)) > 1: return -1
else: return max(getHight(node.left), getHight(node.right)) + 1
result = getHight(root)
if result == -1:
return False
else:
return True
只通过了一部分测试用例,就像上面的测试用例通不过。
检查发现递归的代码里面没有检查左右子树,只比较了左右子树的高度。
添加两行代码:
class Solution:
def isBalanced(self, root: Optional[TreeNode]) -> bool:
def getHight(node):
if not node:
return 0
### 添加的代码 ###
if getHight(node.left) == -1: return -1
if getHight(node.right) == -1: return -1
################
if abs(getHight(node.left)-getHight(node.right)) > 1: return -1
else: return max(getHight(node.left), getHight(node.right)) + 1
result = getHight(root)
if result == -1:
return False
else:
return True
结果呢,超出了时间限制…
——把getHight(node.left)
和getHight(node.right)
分别用leftHeight
和rightHeight
保存下来,也就是说只调用一次:
class Solution:
def isBalanced(self, root: Optional[TreeNode]) -> bool:
def getHight(node):
if not node:
return 0
leftHeight, rightHeight = getHight(node.left), getHight(node.right)
if leftHeight == -1: return -1
if rightHeight == -1: return -1
if abs(leftHeight-rightHeight) > 1: return -1
else: return max(leftHeight, rightHeight) + 1
if getHight(root) == -1:
return False
else:
return True
AC!
257. 二叉树的所有路径
这道题目要求从根节点到叶子的路径,所以需要前序遍历,这样才方便让父节点指向孩子节点,找到对应的路径。
先使用递归的方式,来做前序遍历。要知道递归和回溯就是一家的,本题也需要回溯。
递归三部曲
- 递归函数参数及返回值
要传入根节点,记录每一条路径的path,和存放结果集的result,这里递归不需要返回值 - 递归终止条件
如果还是判断not root
的话会很麻烦,因为本题要找到叶子节点,就开始结束的处理逻辑了(把路径放进result里)。
那么什么时候算是找到了叶子节点? 是当cur
不为空,其左右孩子都为空的时候,就找到叶子节点。
所以本题的终止条件是:
为什么没有判断cur是否为空呢,因为下面的逻辑可以控制空节点不入循环。if not root.left and not root.right: 终止处理逻辑(注意要转化成字符串表示路径)
- 单层递归逻辑
因为是前序遍历,需要先处理中间节点,中间节点就是我们要记录路径上的节点,先放进path中:path.append(cur.val)
然后是递归和回溯的过程,上面说过没有判断cur是否为空,那么在这里递归的时候,如果为空就不进行下一层递归了:return
。
因此,单层递归逻辑是:
单层递归+回溯逻辑:if cur.left: traversal(cur.left, path, result) if cur.right: traversal(cur.right, path, result)
if cur.left: traversal(cur.left, path, result) path.pop() if cur.right: traversal(cur.right, path, result) path.pop()
按上面的分析写出来我理解的代码:
class Solution:
def binaryTreePaths(self, root: Optional[TreeNode]) -> List[str]:
def traverse(cur, path, result):
path.append(cur.val)
if not cur.left and not cur.right: #说明到达叶子结点了
result.append("->".join(map(str, path)))
#不用map的话,path里面是int,不可以用join
return
if cur.left:
traverse(cur.left, path, result)
path.pop()
if cur.right:
traverse(cur.right, path, result)
path.pop()
path = []
result = []
if not root: return result
traverse(root, path, result)
return result
if not root: return
既可以写在外函数binaryTreePaths
里面,也可以写在traverse
函数里面。写在binaryTreePaths
是if not root: return result
.
精简版的代码理解一下:
class Solution:
def binaryTreePaths(self, root: Optional[TreeNode]) -> List[str]:
def traverse(cur, path, result):
path.append(cur.val)
if not cur.left and not cur.right: #说明到达叶子结点了
result.append("->".join(map(str, path)))
#不用map的话,path里面是int,不可以用join
return
if cur.left:
traverse(cur.left, path[:], result)
# path.pop()
if cur.right:
traverse(cur.right, path[:], result)
# path.pop()
path = []
result = []
if not root: return result
traverse(root, path, result)
return result
变化:去掉path.pop()
,traverse
里面的path
改成path[:]
。
为什么可以这样?——好饿,第一遍先不想了。
404. 左叶子之和
有了思路,自己试一下:
class Solution:
result = 0
def sumOfLeftLeaves(self, root: Optional[TreeNode]) -> int:
def dfs(node):
if not node:
return
if node.left:
dfs(node.left)
if not node.left.left and not node.left.right:
self.result += 1
if node.right: dfs(node.right)
return self.result
运行出来的结果是错的。
和代码随想录视频讲的一对比,我的递归只是递归遍历,具体算result甚至有点像层序遍历,而人家的递归是后序遍历把左子树和右子树的结果加在一起,说白了就是更整体。
class Solution:
def sumOfLeftLeaves(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
leftNum = self.sumOfLeftLeaves(root.left)
if root.left and not root.left.left and not root.left.right:
leftNum = root.left.val
rightNum = self.sumOfLeftLeaves(root.right)
return leftNum + rightNum
112. 路径总和
按回溯法的思路写下来我的思路:
class Solution:
def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
def backtracking(node, path):
if not node:
if sum(path) == targetSum:
return True
return False
path.append(node.val)
if node.left: backtracking(node.left, path)
if node.right: backtracking(node.right, path)
path.pop()
path = []
return backtracking(root, path)
运行结果:都是错的…
【题解】
-
能掌握递归方式就够了。
-
没有中结点的处理逻辑,那么就前中后序都可以。
-
为什么需要判断
node.left
和node.right
是否同时存在,而不是判断node
是否存在?——因为只有左右孩子都不存在,才是叶子结点。
-
判断最大的根结点
root
是否存在还是需要做的,可以在外函数hasPathSum
里面做。
知道了这两点,修改代码为:
class Solution:
def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
def backtracking(node, path):
if not node.left and not node.right:
if sum(path) == targetSum:
return True
return False
path.append(node.val)
if node.left: backtracking(node.left, path)
if node.right: backtracking(node.right, path)
path.pop()
path = []
if not root:
return False
return backtracking(root, path)
运行结果:除了空结点,其他case都错误。
- 要判断
if backtracking(node.left, path)
是否为True的,否则整体运行结果的返回值是null - 加上
print("sum(path)", sum(path))
,发现path里面没有加node.left
和node.right
就直接return
了,每次计算只到叶子结点上面一层,所以计算的值永远都不对。
那么如果都只在if node.left
和if node.right
之后将node.left和node.right加入path,最大的根结点root.val什么时候加入path呢?在外函数hasPathSum
里面吗?
修改代码为:
class Solution:
def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
def backtracking(node, path):
if not node.left and not node.right:
print("sum(path)", sum(path))
if sum(path) == targetSum:
return True
return False
if node.left:
path.append(node.left.val)
if backtracking(node.left, path):
return True
path.pop()
if node.right:
path.append(node.right.val)
if backtracking(node.right, path):
# 不能直接改成return backtracking(node.right, path)
# 可能因为二叉树也可能没有右或者左叶子结点?
return True
path.pop()
return False # case2满足这一情况
if not root:
return False
path = [root.val]
return backtracking(root, path)
AC!
代码随想录的题解是用count递减代替了sum(path). 可以作为一种参考。
class Solution:
def traversal(self, cur: TreeNode, count: int) -> bool:
# 终止条件
if not cur.left and not cur.right and count == 0: # 遇到叶子节点,并且计数为0
return True
if not cur.left and not cur.right: # 遇到叶子节点直接返回
return False
# 单层处理的逻辑
if cur.left: # 左
count -= cur.left.val
if self.traversal(cur.left, count): # 递归,处理节点
return True
count += cur.left.val # 回溯,撤销处理结果
if cur.right: # 右
count -= cur.right.val
if self.traversal(cur.right, count): # 递归,处理节点
return True
count += cur.right.val # 回溯,撤销处理结果
return False
def hasPathSum(self, root: TreeNode, sum: int) -> bool:
if root is None:
return False
return self.traversal(root, sum - root.val) # 已经减去root.val
精简版代码:(留给二刷写)(代码随想录的视频有讲如何精简)
在这里插入代码片
【看了视频的理解】:
-
如何做到只要找到一条路径就返回,不继续遍历剩余结点的?靠的就是
return True
. 并且找到一条路径之后,要把Ture
一级一级return
上去。💡递归函数什么时候需要返回值?什么时候不需要返回值?这里总结如下三点:
如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就不要返回值。(这种情况就是本文下半部分介绍的113.路径总和ii)
如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就需要返回值。 (这种情况我们在236. 二叉树的最近公共祖先中介绍)
如果要搜索其中一条符合条件的路径,那么递归一定需要返回值,因为遇到符合条件的路径了就要及时返回。(本题的情况)
113. 路径总和 II
按照上题的理解写了我的代码:
class Solution:
def pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]:
def backtracking(node, path, result):
# 终止条件
if not node.left and not node.right:
print("path:", path)
if sum(path) == targetSum:
result.append(path)
print("result:", result)
return result
# 单层递归逻辑
if node.left:
path.append(node.left.val)
backtracking(node.left, path, result)
path.pop()
if node.right:
path.append(node.right.val)
backtracking(node.right, path, result)
path.pop()
if not root:
return []
path, result = [root.val], []
backtracking(root, path, result)
return result
运行结果:result里面只有[[5], [5]]
这是为什么呢?result过程中有了,但就是不能返回正确的结果。
因为
result.append(path[:])
而不是result.append(path)
。
看一下我去年写的方法:
更简洁一点,而且就是用的我之前想的判断root在不在,而不是左右孩子都要判断一下在不在。
应该就是leetcode的题解,二刷的时候看一下。
class Solution:
def pathSum(self, root: TreeNode, targetSum: int) -> List[List[int]]:
ret = list()
path = list()
def dfs(root: TreeNode, targetSum: int):
if not root:
return
path.append(root.val)
targetSum -= root.val
if not root.left and not root.right and targetSum == 0:
ret.append(path[:])
dfs(root.left, targetSum)
dfs(root.right, targetSum)
path.pop()
dfs(root, targetSum)
return ret
106. 从中序与后序遍历序列构造二叉树
- 中序为什么重要?因为中序切割了左右区间,这是前序和后序都做不到的。因此知道中序和前序后序的任何一个都可以构造一颗二叉树,但是只知道前序和后序,不知道中序是构造不了一颗二叉树的。
- 代码思路:
- 后序数组为0,表示空结点
- 后序数组最后一个元素为结点元素
- 寻找中序数组位置作切割点
- 切中序左数组和右数组(注意左闭右开还是左闭右闭,数组第一节)
- 切后序左数组和右数组
- 递归处理左区间、右区间
- 后序数组的切割点怎么找?
后序数组没有明确的切割元素来进行左右切割,不像中序数组有明确的切割点,切割点左右分开就可以了。
此时有一个很重的点,就是中序左右数组大小一定是和后序左右数组的大小相同的(这是必然)。 - 根据上面的思路写出来一版框架:
class Solution:
def buildTree(self, inorder: List[int], postorder: List[int]) -> Optional[TreeNode]:
# 第一步
if not postorder:
return []
# 第二步、第三步
node, index = TreeNode(postorder[-1]), 0
for i in range(len(inorder)):
if inorder[i] == node.val:
index = i
break # 我让i遍历,然后index=i说是局部变量——>要先把index初始化一下
# 第四步、第五步
inorderleft, inorderright = inorder[:index], inorder[index+1:]
postorderleft, postorderright = postorder[:index], postorder[index:]
# 第六步
if inorderleft: node.left = self.buildTree(inorderleft, postorderleft)
if inorderright: node.right = self.buildTree(inorderright, postorderright)
return node
运行结果:
加上日志,看看什么问题:
inorderleft: [9]
inorderright: [15, 20, 7]
postderleft: [9]
postorderright: [15, 7, 20, 3]
inorderleft: []
inorderright: []
postderleft: []
postorderright: [9]
inorderleft: [15, 20]
inorderright: []
postderleft: [15, 7]
postorderright: [20, 3]
inorderleft: [15]
inorderright: []
postderleft: [15]
postorderright: [7]
inorderleft: []
inorderright: []
postderleft: []
postorderright: [15]
发现每一步多打了很多空[],而且20消失了。
经验:
- 对于其他语言来说很复杂的找到index,python可以直接找:
idx = inorder.index(postorder[-1])
- 刚刚代码的错误在于,切割后序数组没有去掉最后一个元素,也就是中间结点:
postorderleft, postorderright = postorder[:idx], postorder[idx:-1]
可运行代码:
class Solution:
def buildTree(self, inorder: List[int], postorder: List[int]) -> Optional[TreeNode]:
# 第一步 —— 终止条件
if not postorder:
return
# 第二步、第三步
node = TreeNode(postorder[-1])
idx = inorder.index(postorder[-1])
# 第四步、第五步
inorderleft, inorderright = inorder[:idx], inorder[idx+1:]
# print("inorderleft:", inorderleft), print("inorderright:", inorderright)
postorderleft, postorderright = postorder[:idx], postorder[idx:-1]
# print("postderleft:", postorderleft), print("postorderright:", postorderright)
# 第六步
node.left = self.buildTree(inorderleft, postorderleft)
node.right = self.buildTree(inorderright, postorderright)
return node
AC!
去年看labuladong的代码,跟上面这版基本一样,更简洁一点:
(但是隐藏了一些细节,还是写明白,自己不容易搞糊涂)
class Solution:
def buildTree(self, inorder: List[int], postorder: List[int]) -> Optional[TreeNode]:
if not inorder or not postorder:
return
root = TreeNode(postorder[-1])
idx = inorder.index(postorder[-1])
root.left = self.buildTree(inorder[:idx], postorder[:idx])
root.right = self.buildTree(inorder[idx+1:], postorder[idx:-1])
return root
654. 最大二叉树
【题解思路】:
构造树一般采用的是前序遍历,因为先构造中间节点,然后递归构造左子树和右子树。
三部曲
- 确定递归函数的参数和返回值
参数传入的是存放元素的数组,返回该数组构造的二叉树的头结点,返回类型是指向节点的指针。 - 确定终止条件
题目中说了输入的数组大小一定是大于等于1的,所以我们不用考虑小于1的情况,那么当递归遍历的时候,如果传入的数组大小为1,说明遍历到了叶子节点了。
那么应该定义一个新的节点,并把这个数组的数值赋给新的节点,然后返回这个节点。 这表示一个数组大小是1的时候,构造了一个新的节点,并返回。
为什么106. 从中序与后序遍历序列构造二叉树就需要判断?
——因为106判断的不是整个数组,而是postorder or inorder。这个是可以当作递归的终止条件的。
用python的max(nums)的话,可以直接判断nums是否为0。
如果不用max(nums),而是像C++代码示例一样,遍历去替换maxValue的话,判断到了叶子结点就可以停止。(无非是早停止还是晚停止的问题?)
- 确定单层递归的逻辑
这里有"中左右"三步工作:- 先要找到数组中最大的值和对应的下标, 最大的值构造根节点,下标用来下一步分割数组。
- 最大值所在的下标左区间 构造左子树
- 最大值所在的下标右区间 构造右子树
以上代码比较冗余,效率也不高,但逻辑比较清晰。
class Solution:
def constructMaximumBinaryTree(self, nums: List[int]) -> Optional[TreeNode]:
# 递归终止条件
if not nums:
return
# 单层递归逻辑
maxValue = max(nums)
maxIndex = nums.index(maxValue)
root = TreeNode(maxValue)
leftnums, rightnums = nums[:maxIndex], nums[maxIndex+1:]
root.left = self.constructMaximumBinaryTree(leftnums)
root.right = self.constructMaximumBinaryTree(rightnums)
return root
改了递归终止条件就AC了。二刷可以看看其他复杂点的代码思路。主要是总结什么时候需要用什么。
看了看去年的解法,6.10的逻辑不清楚,6.8的跟代码随想录第二版有点像。
一般情况来说:如果让空节点(空指针)进入递归,就不加if,如果不让空节点进入递归,就加if限制一下, 终止条件也会相应的调整。
617.合并二叉树
【题解思路】:
- 如何同时遍历两个二叉树呢?
其实和遍历一个树逻辑是一样的,只不过传入两个树的节点,同时操作。 - 二叉树使用递归,就要想使用前中后哪种遍历方式?
本题使用哪种遍历都是可以的! - 递归三部曲:
- 确定递归函数的参数和返回值:
参数至少是要传入两个二叉树的根节点,返回值就是合并之后二叉树的根节点。 - 确定终止条件:
- 因为是传入了两个树,那么就有两个树遍历的节点
t1
和t2
,如果t1 == NULL
了,两个树合并就应该是t2
了(如果t2
也为NULL
也无所谓,合并之后就是NULL
)。 - 反过来如果
t2 == NULL
,那么两个数合并就是t1
(如果t1
也为NULL
也无所谓,合并之后就是NULL
)。
- 因为是传入了两个树,那么就有两个树遍历的节点
- 确定单层递归的逻辑:
- 这里我们重复利用一下t1这个树,t1就是合并之后树的根节点(就是修改了原来树的结构)。那么单层递归中,就要把两棵树的元素加到一起。
- 接下来t1 的左子树是:合并 t1左子树 t2左子树之后的左子树。
- t1 的右子树:是 合并 t1右子树 t2右子树之后的右子树。
最终t1就是合并之后的根节点。
按上面的思路写出来代码:
class Solution:
def mergeTrees(self, root1: Optional[TreeNode], root2: Optional[TreeNode]) -> Optional[TreeNode]:
# 终止条件
if not root1: return root2
if not root2: return root1
# 单层递归的逻辑
root1.val += root2.val
root1.left = self.mergeTrees(root1.left, root2.left)
root1.right = self.mergeTrees(root1.right, root2.right)
return root1
直接AC了!
【总结】
虽然中序遍历和后序遍历也可以的,但是前序遍历是最好理解的,我建议大家用前序遍历来做就OK。
如上的方法修改了t1的结构,当然也可以不修改t1和t2的结构,重新定义一个树。
这不是我们第一次操作两棵二叉树了,在二叉树:我对称么?中也一起操作了两棵二叉树。
700. 二叉搜索树中的搜索
本题考查的就是二叉搜索树的遍历方式。二叉搜索树,递归遍历和迭代遍历和普通二叉树都不一样。因为二叉搜索树的有序性,遍历的时候要比普通二叉树简单很多。针对二叉搜索树的题目,一样要利用其特性。
【递归】
- 三部曲:
- 确定递归函数的参数和返回值
递归函数的参数传入的就是根节点和要搜索的数值,返回的就是以这个搜索数值所在的节点。 - 确定终止条件
如果root为空,或者找到这个数值了,就返回root节点。 - 确定单层递归的逻辑
看看二叉搜索树的单层递归逻辑有何不同。
因为二叉搜索树的节点是有序的,所以可以有方向的去搜索。
如果root.val > val
,搜索左子树,如果root.val < val
,就搜索右子树,最后如果都没有搜索到,就返回NULL
。
-
根据上面的三部曲写出自己理解的代码:
class Solution: def searchBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]: if not root or root.val == val: return root if root.val > val: self.searchBST(root.right, val) elif root.val < val: self.searchBST(root.left, val)
返回的结果全是空值。
-
原因:
每一步self.searchBST没有向上返回吗?很多录友写递归函数的时候 习惯直接写
searchBST(root->left, val)
,却忘了 递归函数还有返回值。
递归函数的返回值是什么? 是 左子树如果搜索到了val,要将该节点返回。 如果不用一个变量将其接住,那么返回值不就没了。所以要 result = searchBST(root->left, val)。
-
按上面修改了:
class Solution: def searchBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]: if not root or root.val == val: return root if root.val > val: return self.searchBST(root.right, val) elif root.val < val: return self.searchBST(root.left, val)
结果还是全空值。
-
原因:if条件判断之后的语句,搜索方向写反了。
class Solution: def searchBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]: if not root or root.val == val: return root if root.val > val: return self.searchBST(root.left, val) elif root.val < val: return self.searchBST(root.right, val)
【迭代】
因为二叉搜索树的特殊性,也就是节点的有序性,可以不使用辅助栈或者队列就可以写出迭代法。
对于一般二叉树,递归过程中还有回溯的过程,例如走一个左方向的分支走到头了,那么要调头,在走右分支。
而对于二叉搜索树,不需要回溯的过程,因为节点的有序性就帮我们确定了搜索的方向。
根据上面的思路写出来:
class Solution:
def searchBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
while root:
if root.val > val:
root = root.left
elif root.val > val:
root = root.right
else:
return root
return []
结果:
case2返回不了空列表。
发现写错了,一直在判断root.val > val.
改成下面的:
class Solution:
def searchBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
while root:
if root.val > val:
root = root.left
elif root.val < val:
root = root.right
else:
return root
# return None
AC!
return None加或不加都不影响结果:试了下让这个函数直接return,case2也是通过的,因此,应该是leetcode运行的时候有一个初始的空列表。
class Solution:
def searchBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
return
98. 验证二叉搜索树
甚至18天前还刷过一次,刷完了现在丝毫没有印象,原因:1. 没有巩固 2. 不是理解了套路和整体思路自己写的代码,是单纯看懂了题解写出来的,不够模板化,没有形成自己的东西。
这题不管是递归还是迭代,代码随想录都给出了很多种解法。递归解法有两种:1. 使用一个最小值的方法 2. 使用一个指针pre直接取最小值。迭代解法和之前刷过的题解的迭代解法很像。二刷如果有捋不清这几种解法的地方,再去看一下视频讲解。
【递归思路】
要知道中序遍历下,输出的二叉搜索树节点的数值是有序序列。
有了这个特性,验证二叉搜索树,就相当于变成了判断一个序列是不是递增的了。
可以递归中序遍历将二叉搜索树转变成一个数组,然后只要比较一下,这个数组是否是有序的,注意二叉搜索树中不能有重复元素。这样写是最直观的,但这样不是最优的方法,需要额外定义一个数组。其实不用转变成数组,可以在递归遍历的过程中直接判断是否有序。
- 递归三部曲
-
确定递归函数,返回值以及参数
-
确定终止条件
如果是空二叉树,那它既是完全二叉树,也是满二叉树,也是二叉搜索树,也是平衡二叉搜索树,什么二叉树都是。return True
-
确定单层递归的逻辑
两种方法:- 最小值法:中序遍历,一直更新maxVal,一旦发现
maxVal >= root.val
,就返回false,注意元素相同时候也要返回false。 pre
指针迭代法:pre
指针始终指向当前结点的前一个结点。
这里代码随想录视频把为什么pre可以和当前结点root比较说清楚了,如果n刷有不清楚的地方再去看看视频。
- 最小值法:中序遍历,一直更新maxVal,一旦发现
按上面的思路写一版代码:
最小值法:
class Solution:
def isValidBST(self, root: Optional[TreeNode]) -> bool:
if not root:
return True
maxValue = - 2 ^ 31 - 1 # 注意不是2^^31,只有1个尖号
# 左
left = self.isValidBST(root.left)
# 中
if root.val >= maxValue:
maxValue = root.val
else:
return False
# 右
right = self.isValidBST(root.right)
# print(root.val, left, right) 加一行debug
return left and right
发现返回的全是True,就没有False的时候。
原因:
maxValue
要定在全局,而不是定义在isValidBST
函数里面,不然每次递归maxValue都会被重置。- 最小值要初始化成
float("-inf")
,防止有比- 2 ^ 31 - 1
更小的。
class Solution:
def __init__(self):
self.maxValue = float("-inf")
def isValidBST(self, root: Optional[TreeNode]) -> bool:
if not root:
return True
# 左
left = self.isValidBST(root.left)
# 中
if root.val > self.maxValue:
self.maxValue = root.val
else:
return False
# 右
right = self.isValidBST(root.right)
print(root.val, self.maxValue, left, right)
return left and right
AC!
pre
指针迭代法:
class Solution:
def __init__(self):
self.pre = None # pre指针
def isValidBST(self, root: Optional[TreeNode]) -> bool:
if not root:
return True
# 左
left = self.isValidBST(root.left)
# 中
if root.val > pre.val: # 有错误,应该用self
pre = root # 有错误,应该用self
else:
return False
# 右
right = self.isValidBST(root.right)
# print(root.val, self.maxValue, left, right)
return left and right
执行出错。
“# 中”
的逻辑也不是这样:
if self.pre and root.val < self.pre.val:
return False
else:
self.pre = root
改成正确的:
# 中
if self.pre and root.val < self.pre.val:
return False
self.pre = root
这样的方法在前面的二叉树题目也遇到过,不明白可以看看这题的代码随想录视频。
【迭代法】
层序遍历用stack。用两个指针cur和pre指向当前结点和上一个结点。
- 初始化stack, cur, pre
由于还没有开始遍历,stack为空,cur指向跟结点,pre为None(此时没有上一个结点) - 一层一层遍历,先找到二叉树最左边的结点,然后依次往右遍历去比较,题解代码,一刷只是理解了一下:
class Solution:
def isValidBST(self, root: Optional[TreeNode]) -> bool:
stack, cur, pre = [], root, None
while cur or stack:
if cur:
stack.append(cur)
cur = cur.left # 先找到最左边结点
else:
cur = stack.pop()
if pre and pre.val >= cur.val: # 记得有等号
return False
pre = cur
cur = cur.right
return True
530.二叉搜索树的最小绝对差
二叉搜索树采用中序遍历,其实就是一个有序数组。
在一个有序数组上求两个数最小差值,这是不是就是一道送分题了。
不知怎么想到了这种解法,写一下这道题:
class Solution:
def __init__(self):
self.result = 0
def getMinimumDifference(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
left = self.getMinimumDifference(root.left)
res = 0
if root.left: res = abs(root.val-root.left.val)
if root.right: res = min(res, abs(root.val-root.right.val))
right = self.getMinimumDifference(root.right)
self.result = min(left, res, right)
结果自然是不对了。
题解,用两个指针直接解这道题,不需要再开辟一个数组了。
递归三部曲:
- 递归函数参数和返回值
参数就是根节点root,由于用两个全局变量进行记录,因此递归函数不需要返回值 - 递归函数停止条件
if root: return
什么也不返回 - 单层递归逻辑
遵循左中右的顺序,pre初始化为None。那么cur初始化为什么呢?——其实traversal 里面的cur,完全就是root.
代码随想录里面用了一个嵌套的traversal函数,那么什么时候需要嵌套的函数,什么时候不需要呢?
——与主函数返回值不同,这可能就是最什么需要再定义一个递归函数,而不是直接用主函数递归的原因。
根据上面题解写了一版我自己的理解:
class Solution:
def __init__(self):
self.pre = None
self.result = float("inf")
def traversal(self, root):
if not root:
return
self.traversal(root.left)
if self.pre: self.result = min(self.result, abs(root.val-pre.val))
print(self.result)
pre = root
self.traversal(root.right)
def getMinimumDifference(self, root: Optional[TreeNode]) -> int:
self.traversal(root)
return self.result
结果:
为什么啥返回值都没有呢?——没有把self.pre和self.result调用对。
更改后:
class Solution:
def __init__(self):
self.pre = None
self.result = float("inf")
def traversal(self, root):
if not root:
return
self.traversal(root.left)
if self.pre: self.result = min(self.result, abs(root.val-self.pre.val))
print(self.result)
self.pre = root
self.traversal(root.right)
def getMinimumDifference(self, root: Optional[TreeNode]) -> int:
self.traversal(root)
return self.result
AC!
501.二叉搜索树中的众数
提到二叉搜索树,遍历顺序一定是中序遍历。
-
双指针思路:
遍历有序数组的元素出现频率,从头遍历,那么一定是相邻两个元素作比较,然后就把出现频率最高的元素输出就可以了。
关键是在有序数组上的话,好搞,在树上怎么搞呢?
这就考察对树的操作了。
在二叉树:搜索树的最小绝对差中我们就使用了pre指针和cur指针的技巧,这次又用上了。
弄一个指针指向前一个节点,这样每次cur
(当前节点)才能和pre
(前一个节点)作比较。
而且初始化的时候pre = NULL
,这样当pre为NULL时候,我们就知道这是比较的第一个元素。 -
可以只遍历一遍数组,找出最大频率(maxCount),如果 频率count 等于 maxCount(最大频率),当然要把这个元素加入到结果集中(以下代码为result数组),是不是感觉这里有问题,result怎么能轻易就把元素放进去了呢,万一,这个maxCount此时还不是真正最大频率呢。
所以下面要做如下操作:
频率count 大于 maxCount的时候,不仅要更新maxCount,而且要清空结果集(以下代码为result数组),因为结果集之前的元素都失效了。
先自己试着写一遍这个代码:(这样更能理解遇到的细节小坑如何解决)
class Solution:
def __init__(self):
self.pre = None
self.count = 1
self.maxCount = 1
self.res = []
def findMode(self, root: Optional[TreeNode]) -> List[int]:
def traversal(root): # root就是cur
if not root:
return
# 左
traversal(root.left)
# 中
if self.pre:
if self.pre.val == root.val:
self.count += 1
if self.count >= self.maxCount:
self.maxCount = self.count
self.res.append(self.pre.val)
else:
self.count = 1
self.res = []
self.pre = root
# 右
traversal(root.right)
traversal(root)
return self.res
结果:
应该把self.count >= self.maxCount
分成>和=两种情况分开讨论,Pre和count的更新分开在两段if 判断语句里。
修改如下:
class Solution:
def __init__(self):
self.pre = None
self.count = 1
self.maxCount = 1
self.res = []
def findMode(self, root: Optional[TreeNode]) -> List[int]:
def traversal(root): # root就是cur
if not root:
return
# 左
traversal(root.left)
# 中
if self.pre:
if self.pre.val == root.val:
self.count += 1
else:
self.count = 1
self.pre = root
if self.count == self.maxCount:
self.res.append(self.pre.val)
if self.count > self.maxCount:
self.maxCount = self.count
self.res = [self.pre.val]
# 右
traversal(root.right)
traversal(root)
return self.res
236. 二叉树的最近公共祖先
树的遍历不可以从下向上遍历,只能从根作为入口,由上向下进行遍历。但是,处理的顺序可以从下向上进行处理——回溯,将处理结果一层一层返回上去。遍历的顺序就是后序遍历——左右中了。中是处理逻辑,也就是回溯的过程。
两种情况:
- p和q分别在左子树和右子树:对于左右子树的处理——遇到了
p or q
就向上返回,如果没有就返回None
. 对于中间节点,左右子树都不为空就说明当前的中就是最近公共祖先。 - p在左子树or右子树,q在根节点,或者相反
代码处理这两种情况的逻辑相同。
试着分析代码逻辑,按递归三部曲来:
- 参数和返回值
二叉树根节点,p和q值。返回是否有p or q
。(与主函数lowestCommonAncestor
返回值不同,这可能就是最什么需要再定义一个递归函数,而不是直接用主函数递归的原因?) - 终止条件
当root
为空,终止遍历 - 单层处理逻辑
按左右中的顺序递归处理。
左:递归左结点
右:递归右结点
中:判断左右子树是否为空。
试着写出代码:
class Solution:
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
def traversal(root):
if not root:
return
left = traversal(root.left)
right = traversal(root.right)
if left and right:
return root
if left:
return True
if right:
return True
if root.val == p.val or root.val == q.val:
return True
else:
return False
写出一版,挺错的…输出全是False。
看了一遍代码随想录的代码讲解,再写一遍python代码:
class Solution:
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
if not root:
return # return和return root结果相同
if root.val == p.val or root.val == q.val:
return root # 这两个条件都是终止条件
left = self.lowestCommonAncestor(root.left, p, q)
right = self.lowestCommonAncestor(root.right, p, q)
if left and right: return root
if left and not right: return left
if right and not left: return right
总结:
- 加入定义一个traversal函数,其实和主函数
lowestCommonAncestor
递归返回结果都是一样的,因此不需要再定义一个traversal函数,直接用主函数递归。 - 巧妙利用回溯方法的一层一层返回处理结果
- 这个处理情况一的代码其实就包含了情况2,因为假如是一个在左or右孩子上,一个在中间节点上,上面的代码逻辑直接返回中间节点,就不去遍历中间节点下面的左右孩子了。
- 直接返回p or q的值就可以了,不需要返回True or False,反而掩盖了p or q的值,有p or q就代表不为空了。
- 终止条件可以直接写成:
if not root or root.val == p.val or root.val == q.val: return root
题解的笔记:
在递归函数有返回值的情况下:如果要搜索一条边,递归函数返回值不为空的时候,立刻返回,如果搜索整个树,直接用一个变量left、right接住返回值,这个left、right后序还有逻辑处理的需要,也就是后序遍历中处理中间节点的逻辑(也是回溯)。
这道题目刷过的同学未必真正了解这里面回溯的过程,以及结果是如何一层一层传上去的。
那么我给大家归纳如下三点:
- 求最小公共祖先,需要从底向上遍历,那么二叉树,只能通过后序遍历(即:回溯)实现从底向上的遍历方式。
- 在回溯的过程中,必然要遍历整棵二叉树,即使已经找到结果了,依然要把其他节点遍历完,因为要使用递归函数的返回值(也就是代码中的left和right)做逻辑判断。
- 要理解如果返回值left为空,right不为空为什么要返回right,为什么可以用返回right传给上一层结果。
可以说这里每一步,都是有难度的,都需要对二叉树,递归和回溯有一定的理解。
235. 二叉搜索树的最近公共祖先
【题解】
-
本题是二叉搜索树,二叉搜索树是有序的,那得好好利用一下这个特点。
在有序树里,如果判断一个节点的左子树里有p,右子树里有q呢?
因为是有序树,所以 如果 中间节点是 q 和 p 的公共祖先,那么 中节点的数组 一定是在 [p, q]区间的。即中节点 > p && 中节点 < q
或者中节点 > q && 中节点 < p
。
那么只要从上到下去遍历,遇到 cur节点是数值在[p, q]
区间中则一定可以说明该节点cur就是p 和 q的公共祖先。 -
那问题来了,一定是最近公共祖先吗?
此时节点5是不是最近公共祖先? 如果 从节点5继续向左遍历,那么将错过成为p的祖先, 如果从节点5继续向右遍历则错过成为q的祖先。
所以当我们从上向下去递归遍历,第一次遇到 cur节点是数值在[q, p]区间中,那么cur就是 q和p的最近公共祖先。 -
而递归遍历顺序,本题就不涉及到 前中后序了(这里没有中节点的处理逻辑,遍历顺序无所谓了)。
-
递归三部曲
- 确定递归函数返回值以及参数
参数就是当前节点,以及两个结点 p、q。
返回值是要返回最近公共祖先,所以是TreeNode * 。 - 确定终止条件
遇到空返回就可以了。
其实都不需要这个终止条件,因为题目中说了p、q 为不同节点且均存在于给定的二叉搜索树中。也就是说一定会找到公共祖先的,所以并不存在遇到空的情况。 - 确定单层递归的逻辑
在遍历二叉搜索树的时候就是寻找区间[p->val, q->val](注意这里是左闭又闭)- 那么如果 cur->val 大于 p->val,同时 cur->val 大于q->val,那么就应该向左遍历(说明目标区间在左子树上)。
需要注意的是此时不知道p和q谁大,所以两个都要判断。
在这里调用递归函数的地方,把递归函数的返回值left,直接return。
在二叉树:公共祖先问题中,如果递归函数有返回值,如何区分要搜索一条边,还是搜索整个树。本题就是标准的搜索一条边的写法,遇到递归函数的返回值,如果不为空,立刻返回。- 如果 cur->val 小于 p->val,同时 cur->val 小于 q->val,那么就应该向右遍历(目标区间在右子树)。
- 剩下的情况,就是cur节点在区间(p->val <= cur->val && cur->val <= q->val)或者 (q->val <= cur->val && cur->val <= p->val)中,那么cur就是最近公共祖先了,直接返回cur。
- 那么如果 cur->val 大于 p->val,同时 cur->val 大于q->val,那么就应该向左遍历(说明目标区间在左子树上)。
- 确定递归函数返回值以及参数
-
根据上面题解写出我的代码:
class Solution:
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
if not root:
return
if root.val > p.val and root.val > q.val:
return self.lowestCommonAncestor(root.left, p, q)
if root.val < p.val and root.val < q.val:
return self.lowestCommonAncestor(root.right, p, q)
else:
return root
AC!
本题的迭代法写法也很简单:
class Solution:
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
while root:
if root.val > p.val and root.val > q.val:
root = root.left
if root.val < p.val and root.val < q.val:
root = root.right
else:
return root
这样写有的测试用例通不过。
第二个if
改成elif
就通过了。
class Solution:
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
while root:
if root.val > p.val and root.val > q.val:
root = root.left
elif root.val < p.val and root.val < q.val:
root = root.right
else:
return root
为什么呢?
chatgpt的解答:
区别:
条件判断和分支结构:
第一段代码使用了elif,即如果root.val > p.val and root.val > q.val这个条件不满足,才会检查下一个条件root.val < p.val and root.val < q.val。这意味着一旦其中一个条件成立,就不会再检查另一个条件,直接进入else分支。
第二段代码使用了两个独立的if语句,分别检查root.val > p.val and root.val > q.val和root.val < p.val and root.val < q.val。这样即使第一个if条件成立,代码仍会继续检查第二个if条件。
可能的效果:
逻辑正确性:
第一段代码的逻辑是正确的,因为在一个节点不满足第一个条件时,才有必要检查第二个条件。这确保了在root.val同时大于p.val和q.val或者同时小于p.val和q.val时,树的遍历能在正确的子树继续。
第二段代码可能会引发逻辑错误,因为它会继续检查第二个条件,即使第一个条件已经改变了root节点。例如,如果root.val大于p.val和q.val,在执行了root = root.left之后,代码会继续检查root.val < p.val and root.val < q.val,即使root节点已经发生了变化,这可能导致跳过else部分的执行,进而可能导致在某些情况下返回错误的结果。
性能影响:
第一段代码在逻辑上是分支处理的,直接决定了下一个步骤,所以执行效率更高。
第二段代码由于每个循环都会检查两个条件,即使第一个条件成立后仍然会检查第二个条件,增加了不必要的检查次数,可能导致略微的性能损失。
所以递归法也应该用elif
,而不是if
.
701.二叉搜索树中的插入操作
【题解】
其实可以不考虑题目中提示所说的改变树的结构的插入方式。
只要按照二叉搜索树的规则去遍历,遇到空节点就插入节点就可以了。
其实在二叉搜索树中插入任何一个结点,都可以在叶子结点找到插入位置。
递归三部曲:
- 递归函数参数和返回值
参数是根节点和要插入的值。这里递归函数要不要有返回值呢?
可以有,也可以没有,但递归函数如果没有返回值的话,实现是比较麻烦的,下面也会给出其具体实现代码。
有返回值的话,可以利用返回值完成新加入的节点与其父节点的赋值操作。 - 递归终止条件
如果遇到空结点,说明遇到了可以插入的位置了。
这里把添加的节点返回给上一层,就完成了父子节点的赋值操作了,下面有解释。 - 确定单层递归逻辑
搜索树是有方向了,可以根据插入元素的数值,决定递归方向。
如何通过递归函数返回值完成了新加入节点的父子关系赋值操作:下一层将加入节点返回,本层用root.left
或者root.right
将其接住。
定义返回值的写法:
class Solution:
def insertIntoBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
if not root:
return TreeNode(val)
if val < root.val: root.left = self.insertIntoBST(root.left, val)
elif val > root.val: root.right = self.insertIntoBST(root.right, val)
return root
不定义返回值的写法:
找到插入的节点位置,直接让其父节点指向插入节点,结束递归,也是可以的,只是没有加上返回值便利。
在这里插入代码片
迭代法:
在迭代法遍历的过程中,需要记录一下当前遍历的节点的父节点,这样才能做插入节点的操作。
在二叉树:搜索树的最小绝对差 和二叉树:我的众数是多少?中,都是用了记录pre和cur两个指针的技巧,本题也是一样的。只不过改成用parent和cur记录。
在这里插入代码片
450. 删除二叉搜索树中的节点
【题解思路】
搜索树的节点删除要比节点增加复杂的多,有很多情况需要考虑。
【视频讲解】
- 5种情况:
- 没有找到要删除的结点
- 要删除的结点是叶子结点左空右空
- 要删除的结点左不空右为空——让父节点直接指向左孩子
- 要删除的结点左为空右不空——让父节点直接指向右孩子
- 要删除的结点左不空右不空:让左右孩子继位都可以,两种情况
假如让右孩子继位,左子树就需要放在原来右子树中最小也就是最左的结点下面左孩子的地方(这样才能比中间节点+比左子树稍微大一点)
- 递归三部曲:
-
确定递归函数参数和返回值
-
确定递归终止条件
遇到要删除的结点就终止了,也是删除结点的逻辑,这就用到了上面的5种情况。视频中讲了代码里面return的空返回到哪里去了——上一层递归的时候会让root.left等于下一层递归的返回值。
重点讲一下第5种情况的代码,分2步:
- 用一个指针cur去找到右子树最左边的叶子结点,让左子树成为它的左孩子
- 删除中间节点,并return右子树的中间结点
按上面的逻辑写出自己版本的代码:
class Solution:
def deleteNode(self, root: Optional[TreeNode], key: int) -> Optional[TreeNode]:
if not root: return # 1. 可能是树为空或者到达了叶子结点,也就是没找到要删除的节点
if root.val == key:
if not root.left and not root.right:
return # 2. 删除叶子结点
if root.left and not root.right:
return root.left # 3. 左不空右为空
if not root.left and root.right:
return root.right # 4. 左为空右不空
if root.left and root.right:
cur = root.right
while cur.left: cur = cur.left
cur.left = root.left
return root.right # 5. 左不空右不空
root.left = self.deleteNode(root.left, key)
root.right = self.deleteNode(root.right, key)
return root
AC!
- 二刷写一下普通二叉树如何删除结点
669. 修剪二叉搜索树
【解题思路】
递归三部曲:
- 确定递归函数的参数以及返回值
其实不需要返回值也可以,我们也可以完成修剪(其实就是从二叉树中移除节点)的操作。
但是有返回值,更方便,可以通过递归函数的返回值来移除节点。 - 确定终止条件
修剪的操作并不是在终止条件上进行的,所以就是遇到空节点返回就可以了。 - 确定单层递归的逻辑
- 如果root(当前节点)的元素小于low的数值,那么应该递归右子树,并返回右子树符合条件的头结点。
如果root(当前节点)的元素大于high的,那么应该递归左子树,并返回左子树符合条件的头结点。 - 接下来要将下一层处理完左子树的结果赋给root->left,处理完右子树的结果赋给root->right。
- 最后返回root节点。
相当于把节点0的右孩子(节点2)返回给上一层,然后用节点3的左孩子 把下一层返回的 节点0的右孩子(节点2) 接住。此时节点3的左孩子就变成了节点2,将节点0从二叉树中移除了。
- 如果root(当前节点)的元素小于low的数值,那么应该递归右子树,并返回右子树符合条件的头结点。
按上面的思路写出代码:
class Solution:
def trimBST(self, root: Optional[TreeNode], low: int, high: int) -> Optional[TreeNode]:
if not root: return
if root.val < low: return self.trimBST(root.right, low, high)
if root.val > high: return self.trimBST(root.left, low, high)
root.left = self.trimBST(root.left, low, high)
root.right = self.trimBST(root.right, low, high)
return root
AC!
108.将有序数组转换为二叉搜索树
刷递归法就行了。
之后刷二叉树的题也是什么方法方便刷什么方法,不需要非得递归/迭代都刷一遍。
【思路】
- 在二叉树:构造二叉树登场 和二叉树:构造一棵最大的二叉树 中其实已经讲过了,如何根据数组构造一棵二叉树。本质就是寻找分割点,分割点作为当前节点,然后递归左区间和右区间。
- 分割点就是数组中间位置的节点,这样构造的才是平衡二叉树。
如果要分割的数组长度为偶数的时候,中间元素为两个,是取左边元素 就是树1,取右边元素就是树2。
这也是题目中强调答案不是唯一的原因。 - 递归三部曲
-
确定递归函数返回值及其参数
- 删除二叉树节点,增加二叉树节点,都是用递归函数的返回值来完成,这样是比较方便的。
那么本题要构造二叉树,依然用递归函数的返回值来构造中节点的左右孩子。 - 再来看参数,首先是传入数组,然后就是左下标left和右下标right,我们在二叉树:构造二叉树登场 中提过,在构造二叉树的时候尽量不要重新定义左右区间数组,而是用下标来操作原数组。
这里定义的是左闭右闭区间,在不断分割的过程中,也会坚持左闭右闭的区间,这又涉及到我们讲过的循环不变量。
怎么看出来是左闭右闭区间的?
——可能因为数组下标都是具体指定好了的。 - 删除二叉树节点,增加二叉树节点,都是用递归函数的返回值来完成,这样是比较方便的。
-
确定递归终止条件
这里定义的是左闭右闭的区间,所以当区间left > right
的时候,就是空节点了。 -
确定单层递归的逻辑
- 首先取数组中间元素的位置:
mid = (left + right) / 2
这么写其实可能会有一个问题,就是数值越界,例如left和right都是最大int,这么操作就越界了,在二分法中尤其需要注意!(我感觉这样是c++代码需要考虑的?——做二分法的时候看一看)
所以可以这么写:mid = left + ((right - left) / 2)
chatgpt回复:
问题解析
方法 1:mid = (left + right) / 2
这种计算中间点的方式在数学上是正确的,但在计算机中可能导致整数溢出。原因如下:- 当left和right都是较大的整数时,它们的和left + right可能超出整数类型的最大表示范围,导致溢出。
- 在溢出发生时,left + right的结果可能是负数或者截断成一个错误的正数,这会导致mid的计算结果不正确,进而可能访问数组的非法索引或导致其他不可预期的行为。
例如,在使用32位整数的系统中,如果left和right都接近INT_MAX(2147483647),那么left + right可能会超过2147483647,引发溢出。
方法 2:mid = left + ((right - left) / 2)
这种计算方式则是更安全的,因为它避免了直接将两个大整数相加:- right - left是两个接近值的差,这个差在数值上总是比它们本身小得多。因此,right - left的计算不太可能导致溢出。
- 在计算(right - left) / 2时,差值已经较小,这个除法不会导致大数问题。
最终,left + (right - left) / 2也不会因为整数溢出而出现问题。
但使用python不会有溢出的问题:
在Python中,整数的处理与许多其他语言不同,因为Python中的整数是任意精度的。这意味着,Python的整数不会像C++或Java中的32位或64位整数那样固定大小,因此不会有溢出的问题。Python会动态调整整数的大小以适应需要的范围。
因此mid = (left + right) / 2和mid = left + ((right - left) / 2)在正常使用中不会出现错误。但在跨语言编程或保持代码良好习惯时,推荐使用后者。- 取了中间位置,就开始以中间位置的元素构造节点
- 接着划分区间,root的左孩子接住下一层左区间的构造节点,右孩子接住下一层右区间构造的节点。
mid = left + ((right - left) / 2)的写法相当于是如果数组长度为偶数,中间位置有两个元素,取靠左边的
- 最后返回root节点
- 首先取数组中间元素的位置:
根据上面的思路写出来一版代码:
由于上面题解的递归函数与主函数参数不同,因此还是要重新定义一个traversal
函数。
class Solution:
def sortedArrayToBST(self, nums: List[int]) -> Optional[TreeNode]:
def traversal(nums, left, right):
if left > right: return
mid = ( left + right ) // 2
root = TreeNode(nums[mid])
root.left = traversal(nums, left, mid-1)
root.right = traversal(nums, mid+1, right)
return root
return traversal(nums, 0, len(nums)-1)
之前的题解代码:(感觉像二分法)
由于主函数与需要递归的函数参数和返回值都相同,因此不需要额外定义一个递归函数了。
class Solution:
def sortedArrayToBST(self, nums: List[int]) -> Optional[TreeNode]:
if not nums:
return
mid = len(nums) // 2
mid_node = TreeNode(nums[mid])
mid_node.left = self.sortedArrayToBST(nums[:mid])
mid_node.right = self.sortedArrayToBST(nums[mid+1:])
return mid_node
538.把二叉搜索树转换为累加树
其实这就是一棵树,大家可能看起来有点别扭,换一个角度来看,这就是一个有序数组[2, 5, 13],求从后到前的累加数组,也就是[20, 18, 13],是不是感觉这就简单了。
那么知道如何遍历这个二叉树,也就迎刃而解了,从树中可以看出累加的顺序是右中左,所以我们需要反中序遍历这个二叉树,然后顺序累加就可以了。
递归法:
本题依然需要一个pre指针记录当前遍历节点cur的前一个节点,这样才方便做累加。
pre指针的使用技巧,我们在二叉树:搜索树的最小绝对差 (opens new window)和二叉树:我的众数是多少? (opens new window)都提到了,这是常用的操作手段。
- 递归函数参数以及返回值
不需要递归函数的返回值做什么操作了,要遍历整棵树。
同时需要定义一个全局变量pre,用来保存cur节点的前一个节点的数值,定义为int型就可以了(因为只需要记录结点的数值,定义为结点可能会遇到空指针的异常)。 - 确定终止条件
遇空就终止。 - 确定单层递归的逻辑
注意要右中左来遍历二叉树, 中节点的处理逻辑就是让cur的数值加上前一个节点的数值。
按上面的思路自己写了一遍代码:
class Solution:
def __init__(self):
self.pre = 0
self.cur = root.val
def convertBST(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
if not root:
return
self.convertBST(root.right)
self.cur += self.pre
self.pre = self.cur.val # 记得是self.cur.val
self.convertBST(root.left)
发现我不知道怎么用两个指针去遍历二叉搜索树。
看了看题解的写法,就是上面那样写的,自动就向下深度遍历了,同时全局变量cur和pre也会一直跟着变化。
但不同的是,traversal代码什么也不返回,而主函数是要返回累加树的根节点的。因此不能把主函数当作递归函数。
class Solution:
def __init__(self):
self.pre = 0
def convertBST(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
def traversal(cur):
if not cur:
return
traversal(cur.right)
cur.val += self.pre
self.pre = cur.val
traversal(cur.left)
traversal(root)
return root
迭代法:
迭代法其实就是中序模板题了,在二叉树:前中后序迭代法 和二叉树:前中后序统一方式迭代法 可以选一种自己习惯的写法。
按中序迭代遍历模板写了一版:
class Solution:
def convertBST(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
stack = []
res = []
cur = root
pre = 0
while cur or stack:
if cur:
stack.append(cur)
cur = cur.right
else:
cur = stack.pop()
pre += cur.val
res.append(pre)
cur = cur.left
return res
结果:
不是要返回这个res集合,而是要把这个二叉树改成累加树,然后返回累加树根节点。
改下代码:
class Solution:
def convertBST(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
stack = []
res = []
cur = root
pre = 0
while cur or stack:
if cur:
stack.append(cur)
cur = cur.right
else:
cur = stack.pop()
pre += cur.val
cur.val = pre # 修改cur的结点值
cur = cur.left
return root
AC!