leetcode530.二叉搜索树的最小绝对差:递归中序遍历的差值追踪之道
一、题目深度解析与BST特性利用
题目描述
给定一棵二叉搜索树(BST),找到树中任意两个节点值之间的最小绝对差。题目要求:
- 树中至少存在两个节点
- 节点值均为整数
- 返回最小的绝对差值
BST的核心特性应用
二叉搜索树的中序遍历结果是一个严格递增的有序序列。这一特性带来两个关键优势:
- 最小绝对差必存在于相邻节点之间:无需比较所有节点对,将时间复杂度从O(n²)降至O(n)
- 递归遍历自然有序:利用中序遍历的递归实现,天然保证节点按升序访问
示例说明
输入BST:
4/ \2 6/ \
1 3
中序遍历结果: [1, 2, 3, 4, 6]
最小绝对差: |2-1|=1
二、递归解法的核心实现与逻辑框架
完整递归代码实现
/*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode() {}* TreeNode(int val) { this.val = val; }* TreeNode(int val, TreeNode left, TreeNode right) {* this.val = val;* this.left = left;* this.right = right;* }* }*/
class Solution {TreeNode pre; // 记录中序遍历的前一个节点int res = Integer.MAX_VALUE; // 记录最小绝对差,初始化为最大值public int getMinimumDifference(TreeNode root) {traversal(root);return res;}public void traversal(TreeNode root) {if (root == null) {return; // 递归终止条件}// 1. 递归遍历左子树traversal(root.left);// 2. 处理当前节点:计算与前一个节点的差值并更新最小值if (pre != null && Math.abs(pre.val - root.val) < res) {res = Math.abs(pre.val - root.val);}pre = root; // 更新前一个节点为当前节点// 3. 递归遍历右子树traversal(root.right);}
}
核心设计解析:
-
全局变量
pre
:- 作用:记录中序遍历过程中的前一个节点
- 更新时机:在处理当前节点后,递归右子树前更新
- 关键逻辑:确保在比较当前节点与前一个节点时,
pre
指向中序遍历序列中的前驱节点
-
全局变量
res
:- 作用:追踪并存储遍历过程中的最小绝对差
- 初始值:
Integer.MAX_VALUE
,确保任何有效差值都能更新该值 - 更新条件:当前节点与前一个节点的绝对差值小于
res
时更新
-
递归顺序:
- 左子树 → 当前节点 → 右子树(标准中序遍历顺序)
- 确保节点按升序访问,相邻节点的差值即为候选最小绝对差
三、核心问题解析:递归逻辑切入点与最小值更新
1. 递归逻辑的切入点:中序遍历的自然有序性
递归步骤拆解:
-
递归左子树:
traversal(root.left)
- 确保左子树的所有节点按升序处理完毕
- 此时
pre
指向左子树的最右节点(即当前节点的前驱)
-
处理当前节点:
- 计算当前节点与
pre
的绝对差值 - 更新
res
为较小值 - 更新
pre
为当前节点,作为后续右子树的前驱
- 计算当前节点与
-
递归右子树:
traversal(root.right)
- 处理右子树的所有节点,确保其值大于当前节点
中序遍历的数学映射:
递归顺序:左子树 → 当前节点 → 右子树
对应值序列:[..., pre.val] → root.val → [root.right.vals...]
- 关键不变式:每次处理当前节点时,
pre
始终指向中序序列中的前驱节点
2. 最小值更新的时机与条件
最小值更新的三个关键点:
- 时机:在递归左子树后、递归右子树前处理当前节点
- 条件:
pre != null
(首次访问根节点时pre
为null) - 比较:
Math.abs(pre.val - root.val) < res
代码逻辑展开:
if (pre != null && Math.abs(pre.val - root.val) < res) {res = Math.abs(pre.val - root.val);
}
pre = root; // 更新前一个节点
- 首次访问:处理最左叶子节点时,
pre
为null,跳过比较,直接更新pre
- 后续访问:
pre
始终指向当前节点的前驱,确保相邻节点比较
四、递归流程深度模拟:以示例BST为例
示例BST结构:
4/ \2 6/ \
1 3
递归调用过程:
- 初始调用:
traversal(4)
- 递归左子树:
traversal(2)
- 递归左子树:
traversal(1)
- 递归左子树:
null
,返回 - 处理节点1:
pre=null
,不更新res
,pre=1
- 递归右子树:
null
,返回
- 递归左子树:
- 处理节点2:
pre=1
,计算|2-1|=1,res=1
,pre=2
- 递归右子树:
traversal(3)
- 递归左子树:
null
,返回 - 处理节点3:
pre=2
,计算|3-2|=1,res=1
,pre=3
- 递归右子树:
null
,返回
- 递归左子树:
- 递归左子树:
- 处理节点4:
pre=3
,计算|4-3|=1,res=1
,pre=4
- 递归右子树:
traversal(6)
- 递归左子树:
null
,返回 - 处理节点6:
pre=4
,计算|6-4|=2,res=1
,pre=6
- 递归右子树:
null
,返回
- 递归左子树:
- 递归左子树:
- 最终结果:
res=1
五、算法复杂度分析
1. 时间复杂度
- O(n):每个节点仅被访问一次,n为树的节点数
- 递归过程中每个节点执行常数级操作,总时间线性于节点数
2. 空间复杂度
- O(h):h为树的高度(递归栈深度)
- 平衡BST:h=logn,空间复杂度O(logn)
- 最坏情况(退化为链表):h=n,空间复杂度O(n)
3. 与迭代法对比
方法 | 优势 | 劣势 |
---|---|---|
递归法 | 代码简洁,中序遍历自然实现 | 深树可能导致栈溢出 |
迭代法 | 避免栈溢出,空间更可控 | 栈操作逻辑较复杂 |
六、核心技术点总结:递归解法的三大关键
1. 中序遍历的递归映射
- 顺序保证:递归顺序天然符合中序遍历的左-中-右顺序
- 状态传递:通过全局变量
pre
隐式传递前驱节点,避免显式参数传递
2. 差值追踪的数学原理
- 有序序列性质:有序序列的最小绝对差必存在于相邻元素之间
- 增量计算:只需比较相邻节点,无需存储完整序列
- 动态更新:每次访问节点时更新最小值,确保结果正确性
3. 边界条件的处理
- 空树处理:递归自动处理,直接返回
Integer.MAX_VALUE
- 首次访问处理:通过
pre != null
判断,避免比较无效值 - 叶子节点处理:递归正确处理左右子树为空的情况
七、常见误区与优化建议
1. 错误的递归切入点选择
- 误区:尝试在前序或后序遍历中寻找最小差值
// 错误示例:前序遍历无法保证相邻节点比较 public void traversal(TreeNode root) {if (root == null) return;if (pre != null) updateRes(root);pre = root;traversal(root.left);traversal(root.right); }
- 正确逻辑:必须使用中序遍历保证节点有序访问
2. 全局变量使用不当
- 误区:将
pre
和res
作为局部变量传递 - 正确设计:全局变量避免复杂的参数传递,简化递归逻辑
3. 优化建议:尾递归优化(Java不支持)
// 伪代码:尾递归优化(Java实际不支持)
public int traversal(TreeNode root, TreeNode pre, int res) {if (root == null) return res;int leftRes = traversal(root.left, pre, res);TreeNode newPre = (pre != null) ? pre : root;int currentRes = Math.min(leftRes, Math.abs(newPre.val - root.val));return traversal(root.right, root, currentRes);
}
- 优势:理论上可优化栈空间至O(1)
- 限制:Java不支持尾递归优化,实际仍需O(h)栈空间
八、总结:递归中序遍历的本质是有序性的高效利用
本算法通过递归实现中序遍历,将BST的最小绝对差问题转化为有序序列的相邻差值计算,核心在于:
- 递归顺序的选择:左-中-右的递归顺序天然对应中序遍历,确保前驱节点的正确性
- 状态的隐式传递:通过全局变量
pre
记录前驱节点,避免复杂的参数传递 - 增量差值计算:仅跟踪前一个节点,动态更新最小差值,空间效率高
理解这种递归解法的关键是将BST的复杂定义转化为中序遍历的简单递增性判断。递归法的简洁性使其成为解决此类问题的首选,尤其适合树深度较小的场景。在实际工程中,需根据树的规模选择递归或迭代实现,平衡代码简洁性与稳定性。