代码随想录算法训练营|Day 17
Day 17
第六章 二叉树 part05
详细布置
654.最大二叉树
又是构造二叉树,昨天大家刚刚做完 中序后序确定二叉树,今天做这个 应该会容易一些, 先看视频,好好体会一下 为什么构造二叉树都是 前序遍历
题目链接/文章讲解:https://programmercarl.com/0654.最大二叉树.html
视频讲解:https://www.bilibili.com/video/BV1MG411G7ox
写出来了,很简单
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def constructMaximumBinaryTree(self, nums: List[int]) -> Optional[TreeNode]:
if not nums:
return None
root_val = max(nums)
root = TreeNode(root_val)
separator_idx = nums.index(root_val)
nums_left = nums[:separator_idx]
nums_right = nums[separator_idx+1:]
root.left = self.constructMaximumBinaryTree(nums_left)
root.right = self.constructMaximumBinaryTree(nums_right)
return root
构造二叉树题目一定要用前序,先构造出根结点,再递归地去构造左子树和右子树
617.合并二叉树
这次是一起操作两个二叉树了, 估计大家也没一起操作过两个二叉树,也不知道该如何一起操作,可以看视频先理解一下。 优先掌握递归。
题目链接/文章讲解:https://programmercarl.com/0617.合并二叉树.html
视频讲解:https://www.bilibili.com/video/BV1m14y1Y7JK
按照前序来。中左右
递归法
1.确定递归函数参数和返回值
返回合并树的根结点,参数是tree1和tree2
2.处理终止条件
tree1遇到空节点,返回tree2对应位置的节点
-> if (tree1 == Null) return tree2;
-> if (tree2 == Null) return tree1;
这两句暗含了tree1和tree2同时为空的情况
3.确定单层递归的逻辑
不去定义新的二叉树,而去修改tree1的结构
新的tree1的左子树和右子树应该分别是新的合并两个tree的过程
class Solution:
def mergeTrees(self, root1: TreeNode, root2: TreeNode) -> TreeNode:
# 递归终止条件:
# 但凡有一个节点为空, 就立刻返回另外一个. 如果另外一个也为None就直接返回None.
if not root1:
return root2
if not root2:
return root1
# 上面的递归终止条件保证了代码执行到这里root1, root2都非空.
root1.val += root2.val # 中
root1.left = self.mergeTrees(root1.left, root2.left) #左
root1.right = self.mergeTrees(root1.right, root2.right) # 右
return root1 # ⚠️ 注意: 本题我们重复使用了题目给出的节点而不是创建新节点. 节省时间, 空间.
同样是递归,但是创建新树
class Solution:
def mergeTrees(self, root1: TreeNode, root2: TreeNode) -> TreeNode:
# 递归终止条件:
# 但凡有一个节点为空, 就立刻返回另外一个. 如果另外一个也为None就直接返回None.
if not root1:
return root2
if not root2:
return root1
# 上面的递归终止条件保证了代码执行到这里root1, root2都非空.
root = TreeNode() # 创建新节点
root.val += root1.val + root2.val# 中
root.left = self.mergeTrees(root1.left, root2.left) #左
root.right = self.mergeTrees(root1.right, root2.right) # 右
return root # ⚠️ 注意: 本题我们创建了新节点.
迭代法
class Solution:
def mergeTrees(self, root1: TreeNode, root2: TreeNode) -> TreeNode:
if not root1:
return root2
if not root2:
return root1
queue = deque()
queue.append(root1)
queue.append(root2)
while queue:
node1 = queue.popleft()
node2 = queue.popleft()
# 更新queue
# 只有两个节点都有左节点时, 再往queue里面放.
if node1.left and node2.left:
queue.append(node1.left)
queue.append(node2.left)
# 只有两个节点都有右节点时, 再往queue里面放.
if node1.right and node2.right:
queue.append(node1.right)
queue.append(node2.right)
# 更新当前节点. 同时改变当前节点的左右孩子.
node1.val += node2.val
if not node1.left and node2.left:
node1.left = node2.left
if not node1.right and node2.right:
node1.right = node2.right
return root1
这段代码用的是迭代的宽度优先搜索(BFS)来合并两棵二叉树,思路如下:
-
处理空树
- 如果
root1是空,就直接返回root2; - 如果
root2是空,就直接返回root1。
- 如果
-
初始化队列
用一个双端队列queue,每次存放一对要合并的节点(node1, node2):queue = deque() queue.append(root1) queue.append(root2) -
BFS 合并
当队列非空时,每次从队头取出一对节点node1, node2:node1 = queue.popleft() node2 = queue.popleft()-
合并节点值:
node1.val += node2.val -
处理左子节点:
- 如果两棵树在这一位置都存在左子节点,就把这一对子节点继续入队,留待后面合并;
- 如果只有
node2.left存在,就把它直接挂到node1.left(不用再进一步合并);
-
处理右子节点 同理。
-
-
返回结果
队列遍历结束后,root1上的每个节点都已经累加了root2对应节点的值,子节点也已正确挂接,最后返回root1。
关键点
-
队列里“成对”存节点,保证每次处理的都是同一位置上要合并的两节点。
-
先合并值,再决定怎么处理子节点:
- 对于“同在”的子节点,入队合并;
- 对于“只有一边”的子节点,直接接过去。
-
时空复杂度 都是 O(N),N 为两棵树中节点总数上限。
迭代法 代码优化
from collections import deque
class Solution:
def mergeTrees(self, root1: TreeNode, root2: TreeNode) -> TreeNode:
if not root1:
return root2
if not root2:
return root1
queue = deque()
queue.append((root1, root2))
while queue:
node1, node2 = queue.popleft()
node1.val += node2.val
if node1.left and node2.left:
queue.append((node1.left, node2.left))
elif not node1.left:
node1.left = node2.left
if node1.right and node2.right:
queue.append((node1.right, node2.right))
elif not node1.right:
node1.right = node2.right
return root1
700.二叉搜索树中的搜索
递归和迭代 都可以掌握以下,因为本题比较简单, 了解一下 二叉搜索树的特性
题目链接/文章讲解: https://programmercarl.com/0700.二叉搜索树中的搜索.html
视频讲解:https://www.bilibili.com/video/BV1wG411g7sF
递归法
class Solution:
def searchBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
if not root:
return None
#也可以和return root合并
#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)
else:
return root
迭代法
class Solution:
def searchBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
cur = root
while cur:
if cur.val > val:
cur = cur.left
elif cur.val < val:
cur = cur.right
else:
return cur
return None
98.验证二叉搜索树
遇到 搜索树,一定想着中序遍历,这样才能利用上特性。
但本题是有陷阱的,可以自己先做一做,然后在看题解,看看自己是不是掉陷阱里了。这样理解的更深刻。
题目链接/文章讲解:https://programmercarl.com/0098.验证二叉搜索树.html
视频讲解:https://www.bilibili.com/video/BV18P411n7Q4
由于二叉搜索树的特性,如果是中序遍历,可以得到有序数组
陷阱:不止要判断一个小三角。BST要求根结点大于左子树每个节点值,小于右子树每个节点值
即:中节点的值,大于左子树最大值,小于右子树最小值

递归法 转换成数组
class Solution:
def __init__(self):
self.vec = []
def traversal(self, root):
if root is None:
return
self.traversal(root.left)
self.vec.append(root.val) # 将二叉搜索树转换为有序数组
self.traversal(root.right)
def isValidBST(self, root):
self.vec = [] # 清空数组
self.traversal(root)
for i in range(1, len(self.vec)):
# 注意要小于等于,搜索树里不能有相同元素
if self.vec[i] <= self.vec[i - 1]:
return False
return True
递归法 设定极小值,进行比较
class Solution:
def __init__(self):
self.maxVal = float('-inf') # 因为后台测试数据中有int最小值
def isValidBST(self, root):
if root is None:
return True
left = self.isValidBST(root.left)
# 中序遍历,验证遍历的元素是不是从小到大
if self.maxVal < root.val:
self.maxVal = root.val
else:
return False
right = self.isValidBST(root.right)
return left and right
这个解法利用了二叉搜索树(BST)的一个重要性质:中序遍历(in‑order traversal)会按从小到大的顺序访问节点。只要在中序遍历的过程中,发现当前访问的节点值不大于上一个访问的节点值,就能断定这棵树不是合法的 BST。
1. 核心思路
-
中序遍历顺序
- 对一棵 BST 做“先遍历左子树 → 访问根节点 → 再遍历右子树”的中序遍历,会得到一个严格递增的序列。
-
跟踪上一个访问的值
- 用
self.maxVal记录“上一次访问(上一个节点)的值”。 - 初始设置为
-∞(float('-inf')),这样任意整数第一次比较时都能通过。
- 用
-
比较与更新
-
每访问到一个节点,就检查
node.val是否严格大于self.maxVal:- 如果是,就更新
self.maxVal = node.val,继续遍历; - 如果不是,立刻返回
False,表示不满足 BST 的有序性。
- 如果是,就更新
-
2. 代码分步解析
class Solution:
def __init__(self):
# 记录中序遍历时“上一个”节点的值,初始设为负无穷
self.maxVal = float('-inf')
def isValidBST(self, root):
# 空树也算合法 BST
if root is None:
return True
# 1) 递归验证左子树
left = self.isValidBST(root.left)
# 如果左子树已经不是 BST,直接返回 False
if not left:
return False
# 2) “访问”当前节点,检查顺序是否严格递增
if root.val > self.maxVal:
# 合法则更新 maxVal 为当前节点值
self.maxVal = root.val
else:
# 遇到不满足“严格递增”时,立刻判定失败
return False
# 3) 递归验证右子树,并且只有左子树和当前节点都合法时,才继续检查右子树
return self.isValidBST(root.right)
-
__init__:
用来初始化状态 ——self.maxVal保存了“上一次访问节点的值”。 -
if root is None: return True:
空节点看作合法,递归到叶子孩子时就返回True。 -
递归左子树
self.isValidBST(root.left):
先保证左子树本身也是合法的 BST,如果左子树不合法,直接向上层返回False,无需再看当前节点或右子树。 -
检查当前节点:
- 比较
root.val和self.maxVal。 - 如果
root.val <= self.maxVal,说明中序遍历序列没有保持严格递增,整个树肯定不是 BST。
- 比较
-
更新
self.maxVal:
当root.val合法时,把它记作新的“上一次访问值”,然后去遍历右子树。 -
递归右子树:
右子树的所有节点都必须比当前节点大,同时右子树本身也要满足 BST 条件,最终结果是左子树合法 且 当前节点合法 且 右子树合法。
3. 为什么可行?
- 全局唯一的
self.maxVal:
因为中序遍历是深度优先的「左→根→右」顺序,在遍历完一个节点的左子树后,self.maxVal恰好是左子树中最大的节点值;接着比较根节点值,就能保证根节点大于左子树所有值。 - 递归保证:
每一步都先验证“左子树合法”再验证“当前节点顺序正确”再验证“右子树合法”,逻辑严密,不会漏掉任何子树或分支。
总结:这是一种「中序遍历 + 追踪上一个节点值」的巧妙做法,用 O(N) 时间、O(H) 递归栈空间(H 是树高)就能完成 BST 验证。
递归法 直接取该树最小值
class Solution:
def __init__(self):
self.pre = None # 用来记录前一个节点
def isValidBST(self, root):
if root is None:
return True
left = self.isValidBST(root.left)
if self.pre is not None and self.pre.val >= root.val:
return False
self.pre = root # 记录前一个节点
right = self.isValidBST(root.right)
return left and right
这一版依然是基于 中序遍历(in‑order traversal)的验证,只不过不再用一个“全局最大值”数值做比较,而是用一个 指向「上一次访问节点」的指针 self.pre。
核心流程
def isValidBST(self, root):
if root is None:
return True
# —— 1)先遍历左子树
if not self.isValidBST(root.left):
return False
# —— 2)访问当前节点:用 self.pre 保存「前一个」节点
# 对比前一个节点的值和当前节点的值,必须严格递增
if self.pre is not None and self.pre.val >= root.val:
return False
# —— 3)更新 self.pre,表示「上一次访问」已经移动到当前节点
self.pre = root
# —— 4)再遍历右子树
return self.isValidBST(root.right)
-
初始化
构造函数里把self.pre = None,表示“还没访问过任何节点”。 -
左 → 中 → 右
按照 BST 的中序顺序,先递归走到最左,然后一路“回升”到父节点,最后遍历右子树。
** -
严格递增判断
- 中序遍历时,前一次访问的节点一定是「比当前节点值小的最大那个值」。
- 用
self.pre.val < root.val来验证这一点。 - 一旦发现
self.pre.val >= root.val,就能立即断定不是合法 BST。
-
状态维护
- 每次“访问”完一个节点(也就是中序遍历的“中”),都要把
self.pre指向它,才能给下一次比较做准备。
- 每次“访问”完一个节点(也就是中序遍历的“中”),都要把
-
递归短路
- 左子树如果已经不合法,直接
return False,不会再去比较当前或右子树。 - 同样,比较失败也立即返回,不用继续往下。
- 左子树如果已经不合法,直接
为什么这样可行?
-
中序遍历 BST → 节点值严格递增:这是验证二叉搜索树最常用也最直观的性质。
-
self.pre追踪上一个节点:相比用float('-inf')这种“哨兵值”,用指针更语义化,也不用担心节点值可能等于最小整数的问题。 -
时间/空间复杂度:
- 时间 O(N),每个节点访问一次;
- 空间 O(H),递归栈深度最多为树的高度 H。
小贴士:如果在同一个
Solution实例上多次调用isValidBST,记得 每次调用前 都把self.pre = None重置一遍,否则上一次的状态会影响下一次的判断。
迭代法
class Solution:
def isValidBST(self, root):
stack = []
cur = root
pre = None # 记录前一个节点
while cur is not None or len(stack) > 0:
if cur is not None:
stack.append(cur)
cur = cur.left # 左
else:
cur = stack.pop() # 中
if pre is not None and cur.val <= pre.val:
return False
pre = cur # 保存前一个访问的结点
cur = cur.right # 右
return True
这一版用的还是 中序遍历 验证 BST,但把递归改成了显式的栈来实现——即所谓的「迭代中序遍历」。流程如下:
-
初始化
stack = []:用来模拟系统调用栈,保存「待访问」的节点。cur = root:当前指针从根开始。pre = None:记录上一次“访问”(中序中的“中”)的节点。
-
左→中→右 的迭代实现
while cur is not None or stack: if cur is not None: # 一直往左走,把沿途节点都 push 进 stack stack.append(cur) cur = cur.left else: # 左子树走完了,pop 出栈顶节点当“中” cur = stack.pop() # —— 访问节点:检查和上一次访问值的大小关系 if pre is not None and cur.val <= pre.val: return False pre = cur # 更新“前一个节点” # 然后转向右子树,继续下一轮 cur = cur.right- 压左:只要
cur不为空,就把它压栈并继续cur = cur.left,这样能先把所有左孩子都入栈。 - 弹中:一旦
cur为None,说明左子树走到底了,就pop()最近那个节点,把它当作“中序的中间节点”来“访问”。 - 访问逻辑:中序访问时,保证前一次访问的节点
pre的值一定小于当前节点值,若不满足就立刻return False。 - 转右:访问完一个节点,就转向它的右子树——即
cur = cur.right,下次循环再开始把右子树的左链一路压栈。
- 压左:只要
-
结束条件
- 当
cur是None且stack空了,说明所有节点都访问完,没有发现逆序情况,就返回True。
- 当
为什么可行?
- 中序遍历 BST → 严格递增序列:这条性质同递归版一致。
- 显式栈:你把「递归框架」拆成两部分——往左走(压栈)和回溯访问(弹栈)——手动维护了调用顺序。
pre指针:记录上一个中序访问的节点,用来和当前节点比较。
复杂度
- 时间 O(N):每个节点最多压入和弹出各一次。
- 空间 O(H):
stack最多保存从根到最深左链上的节点数,H 是树高。
这样就实现了和递归版本完全等价的验证逻辑,但避免了函数调用开销,也能更灵活地控制遍历过程。

浙公网安备 33010602011771号