引言 参照的是左程云的课程:https://space.bilibili.com/8888480/lists/3509640?type=series
本笔记是class036→037的内容,总结了二叉树相关的9+7=16道高频算法题目。class36涵盖了二叉树的遍历、序列化、构造、验证等核心操作,但不包含树型动态规划的内容。class037算是036的补充,讲了二叉树相关的另外7道高频算法题目,包含了最近公共祖先(LCA)问题、路径搜索、平衡性验证、搜索二叉树相关操作。
前置知识 在学习二叉树高频题目之前,需要掌握以下基础知识:
队列用数组实现(讲解013)
二叉树入门内容(讲解017~018)
重要说明
本期和下期视频会讲解二叉树高频题目,但不含树型dp的题目
树型dp问题会放在【必备】课程的动态规划大章节部分讲述
树型dp中的换根dp问题会放在【扩展】课程的动态规划大章节部分讲述
AVL树的实现、树的左旋右旋等内容也会在【扩展】课程里讲述
问题1又叫LCA问题,非常重要!Tarjan算法解决LCA的批量查询、树链剖分算法解决LCA的在线查询会在【扩展】课程讲述
数组的打家劫舍问题变形很多,会在【必备】课程的动态规划大章节部分讲述
再次强调树型dp的整体讲解,会在【必备】课程的动态规划大章节部分讲述
036【必备】二叉树高频题目-上-不含树型dp 初始化的二叉树类 1 2 3 4 5 class TreeNode : def __init__ (self, val=0 , left=None , right=None ): self .val = val self .left = left self .right = right
题目一:二叉树的层序遍历 问题描述 给你二叉树的根节点 root,返回其节点值的层序遍历。(即逐层地,从左到右访问所有节点)
测试链接 :https://leetcode.cn/problems/binary-tree-level-order-traversal/
核心思想 使用广度优先搜索(BFS)进行层序遍历,有两种实现方式:
普通BFS :使用队列存储节点,用哈希表记录每个节点的层级
优化BFS :按层处理,每次处理完整一层的所有节点
算法实现 方法一:普通BFS 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 from collections import dequefrom typing import List , Optional def levelOrder1 (self, root: Optional [TreeNode] ) -> List [List [int ]]: ans = [] if root: queue = deque([root]) levels = {root: 0 } while queue: cur = queue.popleft() level = levels[cur] if len (ans) == level: ans.append([]) ans[level].append(cur.val) if cur.left: queue.append(cur.left) levels[cur.left] = level + 1 if cur.right: queue.append(cur.right) levels[cur.right] = level + 1 return ans
方法二:优化BFS(推荐) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def levelOrder2 (self, root: Optional [TreeNode] ) -> List [List [int ]]: ans = [] if root: queue = deque([root]) while queue: size = len (queue) level_list = [] for _ in range (size): cur = queue.popleft() level_list.append(cur.val) if cur.left: queue.append(cur.left) if cur.right: queue.append(cur.right) ans.append(level_list) return ans
执行过程示例 以树结构 1->2,3; 2->4,5; 3->6 为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 初始状态:queue = [1], ans = [] 处理第0层: size = 1, level_list = [] 取出节点1,level_list = [1] 将节点2,3入队,queue = [2, 3] ans = [[1]] 处理第1层: size = 2, level_list = [] 取出节点2,level_list = [2],将4,5入队 取出节点3,level_list = [2, 3],将6入队 queue = [4, 5, 6] ans = [[1], [2, 3]] 处理第2层: size = 3, level_list = [] 依次取出4,5,6,level_list = [4, 5, 6] ans = [[1], [2, 3], [4, 5, 6]]
算法分析
时间复杂度 :O(N),N为节点数
空间复杂度 :O(N)
核心技巧 :按层BFS遍历
题目二:二叉树的锯齿形层序遍历 问题描述 给定一个二叉树,返回其节点值的锯齿形层序遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)
测试链接 :https://leetcode.cn/problems/binary-tree-zigzag-level-order-traversal/
核心思想 在按层BFS的基础上,增加一个布尔标记 reverse。每一层遍历结束后,根据 reverse 的值决定是否要将当前层收集到的节点值列表进行反转。
算法实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 def zigzagLevelOrder (self, root: Optional [TreeNode] ) -> List [List [int ]]: ans = [] if root: queue = deque([root]) reverse = False while queue: size = len (queue) level_list = [] for _ in range (size): cur = queue.popleft() level_list.append(cur.val) if cur.left: queue.append(cur.left) if cur.right: queue.append(cur.right) if reverse: level_list.reverse() ans.append(level_list) reverse = not reverse return ans
算法分析
时间复杂度 :O(N)
空间复杂度 :O(N)
核心技巧 :层序遍历 + 交替反转
题目三:二叉树的最大特殊宽度 问题描述 给定一个二叉树,编写一个函数来获取这个树的最大宽度。树的宽度是所有层中的最大宽度。这个二叉树的宽度与满二叉树相同,但不一定是满的。
测试链接 :https://leetcode.cn/problems/maximum-width-of-binary-tree/
核心思想 给每个节点进行编号,就像在一个完全二叉树中一样。根节点编号为1,其左子节点为 2i,右子节点为 2 i + 1。每一层的宽度就等于该层最右边节点的编号减去最左边节点的编号,再加1。
算法实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 def widthOfBinaryTree (self, root: Optional [TreeNode] ) -> int : if not root: return 0 ans = 1 queue = deque([(root, 1 )]) while queue: size = len (queue) start_id = queue[0 ][1 ] for i in range (size): node, node_id = queue.popleft() if i == size - 1 : ans = max (ans, node_id - start_id + 1 ) if node.left: queue.append((node.left, node_id * 2 )) if node.right: queue.append((node.right, node_id * 2 + 1 )) return ans
算法分析
时间复杂度 :O(N)
空间复杂度 :O(N)
核心技巧 :完全二叉树编号规则
题目四:求二叉树的最大深度、最小深度 问题描述
求二叉树的最大深度
求二叉树的最小深度
测试链接 :
核心思想 最大深度 核心思想:递归。一棵树的最大深度等于其左、右子树最大深度中的较大者,再加1(根节点本身)。空树的深度为0,这是递归的基准情况,一定要到叶节点底部。
最小深度 递归,但需要特殊处理。最小深度是从根节点到最近的”叶子节点”的路径长度。如果一个节点只有一个子树,那么我们必须沿着这个非空的子树继续寻找叶子节点。
算法实现 最大深度 1 2 3 4 def maxDepth (self, root: Optional [TreeNode] ) -> int : if not root: return 0 return max (self .maxDepth(root.left), self .maxDepth(root.right)) + 1
最小深度 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def minDepth (self, root: Optional [TreeNode] ) -> int : if not root: return 0 left_depth = self .minDepth(root.left) right_depth = self .minDepth(root.right) if left_depth == 0 or right_depth == 0 : return left_depth + right_depth + 1 return min (left_depth, right_depth) + 1
算法分析
时间复杂度 :O(N)
空间复杂度 :O(H),H为树的高度
核心技巧 :递归分治
题目五:二叉树先序序列化和反序列化 问题描述 设计一个算法来序列化和反序列化二叉树。将树转换为字符串(序列化),再将字符串转换为树(反序列化)。
测试链接 :https://leetcode.cn/problems/serialize-and-deserialize-binary-tree/
核心思想 序列化 使用先序遍历(根-左-右)将树递归地转换成字符串。空节点用特殊字符’#’表示,节点之间用’,’分隔。
反序列化 利用先序遍历的顺序,递归地重建树。字符串按’,’分割成列表,然后用一个迭代器顺序消费这些值来构建节点。
重要说明 二叉树可以通过先序、后序或者按层遍历的方式序列化和反序列化,但是无法通过中序遍历 的方式实现序列化和反序列化,因为不同的两棵树可能得到同样的中序序列。
算法实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 class Codec : def serialize (self, root ): res = [] self ._f(root, res) return "," .join(res) def _f (self, root, res ): if not root: res.append("#" ) return res.append(str (root.val)) self ._f(root.left, res) self ._f(root.right, res) def deserialize (self, data ): if not data: return None vals = iter (data.split(',' )) return self ._g(vals) def _g (self, vals ): val = next (vals) if val == "#" : return None head = Code05_PreorderSerializeAndDeserialize.TreeNode(int (val)) head.left = self ._g(vals) head.right = self ._g(vals) return head
算法分析
时间复杂度 :O(N)
空间复杂度 :O(N)
核心技巧 :先序遍历 + 迭代器
题目六:二叉树按层序列化和反序列化 问题描述 使用层序遍历的方式实现二叉树的序列化和反序列化。
测试链接 :https://leetcode.cn/problems/serialize-and-deserialize-binary-tree/
核心思想 序列化 使用广度优先搜索(BFS)进行层序遍历。队列中的每个节点,都将其左右子节点(即使是None)的信息加入结果字符串。
反序列化 同样使用BFS和队列来重建树。先创建根节点并入队,然后依次出队父节点,并从字符串值列表中读出左右子节点的信息进行构建和连接。
算法实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 class Codec : def serialize (self, root ): if not root: return "" res = [] queue = deque([root]) while queue: cur = queue.popleft() if cur: res.append(str (cur.val)) queue.append(cur.left) queue.append(cur.right) else : res.append("#" ) return "," .join(res) def deserialize (self, data ): if not data: return None nodes = data.split(',' ) root = self .generate(nodes[0 ]) queue = deque([root]) index = 1 while queue: parent = queue.popleft() if index < len (nodes): parent.left = self .generate(nodes[index]) index += 1 if index < len (nodes): parent.right = self .generate(nodes[index]) index += 1 if parent.left: queue.append(parent.left) if parent.right: queue.append(parent.right) return root def generate (self, val ): if val == "#" : return None return Code06_LevelorderSerializeAndDeserialize.TreeNode(int (val))
算法分析
时间复杂度 :O(N)
空间复杂度 :O(N)
核心技巧 :层序遍历 + 补点思想
题目七:利用先序与中序遍历序列构造二叉树 问题描述 根据一棵树的前序遍历与中序遍历构造二叉树。要求没有重复元素。
测试链接 :https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/
核心思想 递归分治。先序遍历的第一个元素是当前子树的根。在中序遍历中找到这个根,其左边的所有元素构成左子树,右边的所有元素构成右子树。根据左子树的元素数量,可以确定先序遍历中左右子树的范围,从而递归构建。
算法实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 def buildTree (self, pre: List [int ], tin: List [int ] ) -> Optional [TreeNode]: if not pre or not tin or len (pre) != len (tin): return None in_map = {val: i for i, val in enumerate (tin)} return self ._f(pre, 0 , len (pre) - 1 , tin, 0 , len (tin) - 1 , in_map) def _f (self, pre, l1, r1, tin, l2, r2, in_map ): if l1 > r1: return None head = self .TreeNode(pre[l1]) if l1 == r1: return head k = in_map[pre[l1]] left_size = k - l2 head.left = self ._f(pre, l1 + 1 , l1 + left_size, tin, l2, k - 1 , in_map) head.right = self ._f(pre, l1 + left_size + 1 , r1, tin, k + 1 , r2, in_map) return head
算法分析
时间复杂度 :O(N)
空间复杂度 :O(N)
核心技巧 :递归分治 + 哈希表优化
题目八:验证完全二叉树 问题描述 给定一个二叉树,确定它是否是一个完全二叉树。在一棵完全二叉树中,除了最后一层外,所有层都被完全填满,并且最后一层中的所有节点都要靠左。
测试链接 :https://leetcode.cn/problems/check-completeness-of-a-binary-tree/
核心思想 使用BFS进行层序遍历。一棵完全二叉树有两个特点:
任何节点不能只有右孩子没有左孩子
在层序遍历中,一旦遇到第一个孩子不双全的节点,之后遇到的所有节点都必须是叶子节点
算法实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 def isCompleteTree (self, h: Optional [TreeNode] ) -> bool : if not h: return True queue = deque([h]) leaf_stage = False while queue: node = queue.popleft() if (not node.left and node.right) or \ (leaf_stage and (node.left or node.right)): return False if node.left: queue.append(node.left) if node.right: queue.append(node.right) if not node.left or not node.right: leaf_stage = True return True
算法分析
时间复杂度 :O(N)
空间复杂度 :O(N)
核心技巧 :BFS + 状态标记
题目九:求完全二叉树的节点个数 问题描述 给出一个完全二叉树,求出该树的节点个数。要求时间复杂度低于O(N)。
测试链接 :https://leetcode.cn/problems/count-complete-tree-nodes/
核心思想 利用完全二叉树的性质进行优化。对于任意节点,其左子树和右子树中,至少有一个是满二叉树。通过比较左右子树的高度,可以判断出哪个是满二叉树,从而用公式 (2^h - 1) 快速计算其节点数,然后只需递归计算另一半子树。每次递归都会下降一层,所以递归深度最多是 O(logN),每次调用 _mostLeft 的时间复杂度是 O(logN),这使得时间复杂度从O(N)降低到O((logN)^2)。
算法实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 def countNodes (self, head: Optional [TreeNode] ) -> int : if not head: return 0 h = self ._mostLeft(head, 1 ) return self ._f(head, 1 , h) def _f (self, cur, level, h ): """ cur: 当前来到的节点 level: 当前cur来到的节点在第几层 h: 整棵树的高度 返回: cur这棵子树上有多少节点 """ if level == h: return 1 if self ._mostLeft(cur.right, level + 1 ) == h: return (1 << (h - level)) + self ._f(cur.right, level + 1 , h) else : return (1 << (h - level - 1 )) + self ._f(cur.left, level + 1 , h) def _mostLeft (self, cur, level ): """ 当前节点是cur,并且它在level层 返回从cur开始不停往左,能扎到几层 """ while cur: level += 1 cur = cur.left return level - 1
算法分析
时间复杂度 :O((logN)²)
空间复杂度 :O(logN)
核心技巧 :完全二叉树性质 + 满二叉树公式
核心技巧总结 1. BFS层序遍历 1 2 3 4 5 6 7 8 9 10 11 queue = deque([root]) while queue: size = len (queue) for _ in range (size): node = queue.popleft() if node.left: queue.append(node.left) if node.right: queue.append(node.right)
2. 树的递归 1 2 3 4 5 6 7 def traverse (root ): if not root: return traverse(root.left) traverse(root.right)
3. 完全二叉树编号
4. 序列化技巧
复杂度分析总结
题目
时间复杂度
空间复杂度
核心算法
层序遍历
O(N)
O(N)
BFS
锯齿形遍历
O(N)
O(N)
BFS + 反转
最大宽度
O(N)
O(N)
BFS + 编号
最大/最小深度
O(N)
O(H)
递归
先序序列化
O(N)
O(N)
递归 + 迭代器
层序序列化
O(N)
O(N)
BFS + 补点
构造二叉树
O(N)
O(N)
递归 + 哈希表
验证完全二叉树
O(N)
O(N)
BFS + 状态
完全二叉树节点数
O((logN)²)
O(logN)
递归 + 性质
学习建议
掌握BFS和DFS :这是处理树问题的两大基本方法
理解递归本质 :树的递归结构使得很多问题都可以用递归解决
灵活运用数据结构 :
队列(BFS)
哈希表(快速查找)
迭代器(序列化处理)
注意边界条件 :
理解树的性质 :
完全二叉树的特点
满二叉树的节点公式
不同遍历方式的特点
练习组合技巧 :很多树的问题需要组合多种基础算法
通过掌握这些经典的二叉树算法,可以为后续学习更复杂的树型动态规划和高级树结构打下坚实的基础。
037【必备】二叉树高频题目-下-不含树型dp 初始化的二叉树类 1 2 3 4 5 class TreeNode : def __init__ (self, val=0 , left=None , right=None ): self .val = val self .left = left self .right = right
题目一:普通二叉树上寻找两个节点的最近公共祖先 问题描述 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
测试链接 :https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/
核心思想 采用递归的方式深度优先搜索:
如果当前节点为空,或者等于p或q中的一个,那么它本身就是其子树中p或q的LCA
否则,递归地在左子树和右子树中寻找p和q
如果左右子树都返回了非空节点,说明p和q分别位于当前节点的两侧,当前节点就是LCA
如果只有一个子树返回了非空节点,说明p和q都在那个子树中,返回那个非空节点即可
如果左右子树都返回空,说明p和q都不在此子树中。
算法实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class TreeNode : def __init__ (self, val=0 , left=None , right=None ): self .val = val self .left = left self .right = right class Solution : def lowestCommonAncestor (self, root: 'TreeNode' , p: 'TreeNode' , q: 'TreeNode' ) -> 'TreeNode' : """ 寻找最近公共祖先的核心函数 采用递归的方式深度优先搜索 """ if not root or root == p or root == q: return root l = self .lowestCommonAncestor(root.left, p, q) r = self .lowestCommonAncestor(root.right, p, q) if l and r: return root if not l and not r: return None return l if l else r
算法分析
时间复杂度 :O(N),最坏情况下需要遍历所有节点
空间复杂度 :O(H),H为树的高度,递归栈的深度
核心技巧 :递归 + 分情况讨论
题目二:搜索二叉树上寻找两个节点的最近公共祖先 问题描述 给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
测试链接 :https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-search-tree/
核心思想 利用BST的特性,可以高效地进行迭代查找:
从根节点开始遍历
如果p和q的值都小于当前节点的值,说明LCA必定在左子树,往左走
如果p和q的值都大于当前节点的值,说明LCA必定在右子树,往右走
如果当前节点的值在p和q的值之间(或者等于其中一个),那么当前节点就是第一个”分叉点”,即为LCA
算法实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution : def lowestCommonAncestor (self, root: 'TreeNode' , p: 'TreeNode' , q: 'TreeNode' ) -> 'TreeNode' : """ 在二叉搜索树(BST)中寻找最近公共祖先 利用BST的特性,可以高效地进行迭代查找 """ min_val = min (p.val, q.val) max_val = max (p.val, q.val) while root: if root.val > max_val: root = root.left elif root.val < min_val: root = root.right else : return root return None
算法分析
时间复杂度 :O(H),H为树的高度
空间复杂度 :O(1)
核心技巧 :利用BST性质 + 迭代
题目三:收集累加和等于aim的所有路径 问题描述 给你二叉树的根节点 root 和一个表示目标和的整数 targetSum ,返回所有从根节点到叶子节点路径总和等于给定目标和的路径。
测试链接 :https://leetcode.cn/problems/path-sum-ii/
核心思想 使用DFS + 回溯的方法:
维护一个当前路径 path 和当前路径和 current_sum
深入遍历树,每经过一个节点,就将其加入 path,并更新 current_sum
当到达一个叶子节点时,检查 current_sum + cur.val是否等于目标值,如果是,将当前路径(包括叶子节点)的一个副本添加到最终结果 ans 中
遍历完一个节点的所有子树后,需要回溯,即将该节点从 path 中移除,以便返回到父节点继续搜索其他分支
算法实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 from typing import List , Optional class Solution : def pathSum (self, root: Optional [TreeNode], targetSum: int ) -> List [List [int ]]: """ 主函数,初始化结果列表和路径列表,并启动递归搜索 """ ans = [] if root: path = [] self .f(root, targetSum, 0 , path, ans) return ans def f (self, cur: TreeNode, aim: int , current_sum: int , path: List [int ], ans: List [List [int ]] ): """ 递归辅助函数,用于深度优先搜索所有路径 """ path.append(cur.val) is_leaf = cur.left is None and cur.right is None if is_leaf: if current_sum + cur.val == aim: ans.append(path[:]) else : if cur.left: self .f(cur.left, aim, current_sum + cur.val, path, ans) if cur.right: self .f(cur.right, aim, current_sum + cur.val, path, ans) path.pop()
回溯不会死循环的原因
控制流是”递归调用”不是”循环依赖” :每次调用只会递归左子树、右子树各最多一次
for/if 的次数是固定的 :子树递归只发生在固定的分支中,不会因为pop重复触发
pop 只是在撤销路径,不改变遍历指针 :pop 修改的是 path 内容,用于恢复现场;并不改变 cur、cur.left、cur.right 的结构或递归栈帧。可以把它看作:DFS 到底(或到叶/路尽)后,撤销上一步选择,然后走兄弟分支;兄弟分支走完,再撤销并上返……直到根。整个过程单调“回退”栈帧,不可能形成死循环。这个调用栈帧的所有工作(检查/递归左右)都完成了,撤销现场后,自然结束返回到父调用。
算法分析
时间复杂度 :O(N²),最坏情况下每条路径都需要复制
空间复杂度 :O(H),H为树的高度
核心技巧 :DFS + 回溯 + 路径复制
题目四:验证平衡二叉树 问题描述 给定一个二叉树,判断它是否是高度平衡的二叉树。一个高度平衡二叉树定义为:一个二叉树每个节点的左右两个子树的高度差的绝对值不超过1。
测试链接 :https://leetcode.cn/problems/balanced-binary-tree/
核心思想 使用后序遍历的思想:
要判断当前节点是否平衡,需要先知道其左右子树的高度
这天然地符合后序遍历的顺序(先左、再右、后根)
递归地计算左子树和右子树的高度
在计算完左右子树高度后,检查它们的高度差
使用一个实例变量来记录是否已发现不平衡,一旦发现不平衡,就将此标志设为False,后续的递归调用可以提前终止。
算法实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 from typing import Optional class Solution : def __init__ (self ): self .balance = True def isBalanced (self, root: Optional [TreeNode] ) -> bool : """ 主函数,用于启动平衡性检查 """ self .balance = True self .height(root) return self .balance def height (self, cur: Optional [TreeNode] ) -> int : """ 递归计算节点高度,并在此过程中检查平衡性 """ if not self .balance or cur is None : return 0 lh = self .height(cur.left) rh = self .height(cur.right) if abs (lh - rh) > 1 : self .balance = False return max (lh, rh) + 1
算法分析
时间复杂度 :O(N),每个节点访问一次
空间复杂度 :O(H),H为树的高度
核心技巧 :后序遍历 + 全局状态
题目五:验证搜索二叉树 问题描述 给定一个二叉树,判断其是否是一个有效的二叉搜索树。
测试链接 :https://leetcode.cn/problems/validate-binary-search-tree/
核心思想 有两种实现方法:
方法一:中序遍历 一个有效的BST,其中序遍历的结果必然是一个严格递增的序列。
方法二:递归验证 对任意一个节点,它必须满足:
它的左子树是BST,且左子树所有节点的值都小于它自身的值
它的右子树是BST,且右子树所有节点的值都大于它自身的值
算法实现 方法一:迭代实现的中序遍历 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 def isValidBST1 (self, head: Optional [TreeNode] ) -> bool : """ 通过迭代方式进行中序遍历来验证BST。 核心思想: 一个有效的BST,其中序遍历的结果必然是一个严格递增的序列。 因此,我们可以在遍历过程中,持续比较当前节点的值和前一个节点的值。 如果发现当前节点值小于或等于前一个节点值,那么它就不是一个BST。 """ if not head: return True stack = [] pre_node = None cur = head while stack or cur: if cur: stack.append(cur) cur = cur.left else : cur = stack.pop() if pre_node is not None and pre_node.val >= cur.val: return False pre_node = cur cur = cur.right return True
方法二:递归实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 def __init__ (self ): self .min_val = float ('inf' ) self .max_val = float ('-inf' ) def isValidBST2 (self, head: Optional [TreeNode] ) -> bool : """ 通过递归方式验证BST。 核心思想: 对任意一个节点,它必须满足: 1. 它的左子树是BST,且左子树所有节点的值都小于它自身的值。 2. 它的右子树是BST,且右子树所有节点的值都大于它自身的值。 这个过程可以通过后序遍历,在返回时收集子树的信息(是否为BST,最大值,最小值)来完成。 """ if head is None : self .min_val = float ('inf' ) self .max_val = float ('-inf' ) return True is_left_ok = self .isValidBST2(head.left) l_min = self .min_val l_max = self .max_val is_right_ok = self .isValidBST2(head.right) r_min = self .min_val r_max = self .max_val self .min_val = min (l_min, r_min, head.val) self .max_val = max (l_max, r_max, head.val) return is_left_ok and is_right_ok and l_max < head.val and head.val < r_min
算法分析
时间复杂度 :O(N),每个节点访问一次
空间复杂度 :O(H),H为树的高度
核心技巧 :中序遍历有序性 / 递归验证BST性质
题目六:修剪搜索二叉树 问题描述 给你二叉搜索树的根节点 root ,同时给定最小边界 low 和最大边界 high。通过修剪二叉搜索树,使得所有节点的值在 [low, high] 中。
测试链接 :https://leetcode.cn/problems/trim-a-binary-search-tree/
核心思想 利用BST的性质进行递归:
如果 cur.val < low,那么 cur 和它的整个左子树都应该被删除,修剪后的树必定在右子树中
如果 cur.val > high,那么 cur 和它的整个右子树都应该被删除,修剪后的树必定在左子树中
如果 low <= cur.val <= high,那么当前节点应该被保留,继续递归地修剪左右子树
算法实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 from typing import Optional class Solution : def trimBST (self, cur: Optional [TreeNode], low: int , high: int ) -> Optional [TreeNode]: """ 递归地修剪二叉搜索树。 核心思想: 利用BST的性质进行递归。对于当前节点 `cur`: 1. 如果 `cur.val < low`,那么 `cur` 和它的整个左子树都应该被删除。 修剪后的树必定在 `cur` 的右子树中,因此我们返回对右子树的修剪结果。 2. 如果 `cur.val > high`,那么 `cur` 和它的整个右子树都应该被删除。 修剪后的树必定在 `cur` 的左子树中,因此我们返回对左子树的修剪结果。 3. 如果 `low <= cur.val <= high`,那么 `cur` 节点应该被保留。 我们继续递归地修剪它的左子树和右子树,并将返回的结果作为 `cur` 新的左、右孩子。 最后返回 `cur` 本身。 """ if cur is None : return None if cur.val < low: return self .trimBST(cur.right, low, high) if cur.val > high: return self .trimBST(cur.left, low, high) cur.left = self .trimBST(cur.left, low, high) cur.right = self .trimBST(cur.right, low, high) return cur
算法分析
时间复杂度 :O(N),最坏情况下访问所有节点
空间复杂度 :O(H),H为树的高度
核心技巧 :利用BST性质 + 递归修剪
题目七:二叉树打家劫舍问题 问题描述 在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为”根”。除了”根”之外,每栋房子有且只有一个”父”房子与之相连。一番侦察之后,聪明的小偷意识到”这个地方的所有房屋的排列类似于一棵二叉树”。如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。计算在不触动警报的前提下,小偷一晚能够盗取的最高金额。
测试链接 :https://leetcode.cn/problems/house-robber-iii/
核心思想 树形DP问题。对于任意一个节点,我们考虑两种情况:
偷当前节点:那么它的左右孩子节点都不能偷
不偷当前节点:那么它的左右孩子可以偷也可以不偷,取两者中的较大值
通过后序遍历,我们可以先计算出左右子树的结果,再用来推导当前节点的结果。
算法实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 from typing import Optional class Solution : def __init__ (self ): self .yes = 0 self .no = 0 def rob (self, root: Optional [TreeNode] ) -> int : """ 主函数,启动递归计算 """ self .f(root) return max (self .yes, self .no) def f (self, root: Optional [TreeNode] ): """ 递归函数,采用后序遍历计算以root为根的子树的打劫收益 核心思想 (树形DP): 对于任意一个节点 `root`,我们考虑两种情况: 1. 偷 `root` 节点:那么它的左右孩子节点都不能偷。 最大收益 = `root.val` + 左子树不偷的最大收益 + 右子树不偷的最大收益。 2. 不偷 `root` 节点:那么它的左右孩子可以偷也可以不偷,取两者中的较大值。 最大收益 = max(偷左孩子,不偷左孩子) + max(偷右孩子,不偷右孩子)。 通过后序遍历,我们可以先计算出左右子树的结果,再用来推导当前节点的结果。 """ if root is None : self .yes = 0 self .no = 0 return current_yes = root.val current_no = 0 self .f(root.left) current_yes += self .no current_no += max (self .yes, self .no) self .f(root.right) current_yes += self .no current_no += max (self .yes, self .no) self .yes = current_yes self .no = current_no
算法分析
时间复杂度 :O(N),每个节点访问一次
空间复杂度 :O(H),H为树的高度
核心技巧 :树形DP + 后序遍历
核心技巧总结 1. LCA问题模板 1 2 3 4 5 6 7 8 9 10 def lowestCommonAncestor (self, root, p, q ): if not root or root == p or root == q: return root left = self .lowestCommonAncestor(root.left, p, q) right = self .lowestCommonAncestor(root.right, p, q) if left and right: return root return left if left else right
2. 路径搜索 + 回溯模板 1 2 3 4 5 6 7 8 9 10 11 def dfs (node, path, target ): path.append(node.val) if is_leaf(node): if meets_condition(): result.append(path[:]) else : dfs(node.left, path, target) dfs(node.right, path, target) path.pop()
3. BST性质利用
4. 树形DP模板 1 2 3 4 5 6 7 8 9 10 11 12 def tree_dp (node ): if not node: return base_case left_result = tree_dp(node.left) right_result = tree_dp(node.right) current_result = combine(node.val, left_result, right_result) return current_result
复杂度分析总结
题目
时间复杂度
空间复杂度
核心算法
普通二叉树LCA
O(N)
O(H)
递归DFS
BST的LCA
O(H)
O(1)
利用BST性质
路径和问题
O(N²)
O(H)
DFS + 回溯
验证平衡二叉树
O(N)
O(H)
后序遍历
验证BST
O(N)
O(H)
中序遍历/递归
修剪BST
O(N)
O(H)
递归修剪
打家劫舍III
O(N)
O(H)
树形DP
学习建议
掌握LCA问题 :这是树算法中的经典问题,有多种解法和应用
理解BST性质 :
中序遍历有序
可以利用大小关系进行搜索优化
左子树 < 根 < 右子树
掌握回溯模板 :
做选择 → 递归 → 撤销选择
注意保存结果时要使用副本
理解树形DP :
后序遍历获取子树信息
根据子树状态计算当前状态
状态定义要考虑所有可能情况
注意边界条件 :
练习状态管理 :
通过掌握这些经典的二叉树问题,可以深入理解树的递归性质,为学习更高级的树算法和动态规划打下基础。这些问题模式在实际编程中经常出现,是算法面试的重点内容。