leetcode98.验证二叉搜索树:迭代法中序遍历与栈操作的深度剖析
一、题目深度解析与BST核心定义
题目描述
验证二叉搜索树(BST)是算法中的经典问题,要求我们判断给定的二叉树是否为有效的二叉搜索树。根据定义,二叉搜索树需满足以下条件:
- 左子树上所有节点的值均严格小于根节点的值
- 右子树上所有节点的值均严格大于根节点的值
- 左右子树也必须为二叉搜索树
BST的本质特性
- 中序遍历性质:二叉搜索树的中序遍历结果是一个严格递增的序列。例如:
2/ \1 3 中序遍历结果:[1, 2, 3](严格递增)
- 递归定义:每个节点需同时满足左右子树的约束,传统递归解法需传递上下界,但迭代法通过中序遍历可更简洁地验证。
二、迭代解法的核心实现与栈结构设计
完整迭代代码实现
/*** 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 {public boolean isValidBST(TreeNode root) {Stack<TreeNode> stack = new Stack<>();TreeNode pre = null; // 记录中序遍历的前一个节点if (root == null) {return true; // 空树视为有效BST}TreeNode current = root;stack.push(current);while (!stack.isEmpty()) {current = stack.pop();if (current != null) {// 压栈顺序:右子树 → 当前节点 → 标记null → 左子树if (current.right != null) {stack.push(current.right); // 先压右子树(后续处理)}stack.push(current); // 压入当前节点(待处理)stack.push(null); // 压入null作为标记if (current.left != null) {stack.push(current.left); // 最后压左子树(优先处理)}} else {// 遇到null标记,处理当前节点(中序遍历的访问时机)current = stack.pop(); // 取出当前节点if (pre != null && pre.val >= current.val) {return false; // 违反递增顺序,非BST}pre = current; // 更新前一个节点}}return true; // 所有节点满足递增顺序,是BST}
}
核心数据结构设计:
-
栈(Stack):
- 作用:模拟中序遍历的非递归实现,通过压栈顺序控制遍历顺序
- 存储内容:节点与null标记(null作为访问节点的触发条件)
- 压栈策略:右子树→当前节点→null→左子树,确保左子树优先处理
-
pre节点:
- 作用:记录中序遍历的前一个节点值
- 判断逻辑:当前节点值必须大于pre节点值,否则非BST
三、核心问题解析:中序遍历顺序与栈操作逻辑
1. 中序遍历的栈操作本质
传统中序遍历步骤:
- 遍历左子树
- 访问当前节点
- 遍历右子树
栈操作的创新压栈顺序:
if (current.right != null) {stack.push(current.right); // 步骤3:右子树后处理,先压栈
}
stack.push(current); // 步骤2:当前节点待访问(通过null标记触发)
stack.push(null); // 标记:触发访问当前节点的信号
if (current.left != null) {stack.push(current.left); // 步骤1:左子树先处理,最后压栈(先弹出)
}
- 压栈顺序逆向:左子树最后压栈→最先弹出(符合栈的LIFO特性)
- null标记作用:作为访问当前节点的分隔符,区分节点的“待处理”和“已处理”状态
2. 栈操作流程解析
压栈阶段(处理非null节点):
- 压右子树:右子树后处理,先压栈(后续弹出顺序靠后)
- 压当前节点:作为中间节点,等待左子树处理完毕后访问
- 压null标记:作为触发访问当前节点的信号
- 压左子树:左子树先处理,最后压栈(最先弹出,优先处理)
弹栈阶段(处理null标记):
- 遇到null标记,弹出当前节点(栈顶为null,下一个是当前节点)
- 比较当前节点值与pre节点值,验证递增性
- 更新pre节点为当前节点,继续处理右子树
四、栈操作深度模拟:以有效BST为例
示例BST结构:
5/ \3 7/ \ / \
2 4 6 8
栈操作流程:
- 初始压栈:压入root(5),栈:[5]
- 第一次弹栈:弹出5(非null),压右子树7、当前节点5、null、左子树3,栈:[7,5,null,3]
- 处理左子树3:弹出3(非null),压右子树4、当前节点3、null、左子树2,栈:[7,5,null,4,3,null,2]
- 处理左子树2:弹出2(非null),无右子树,压当前节点2、null,无左子树,栈:[7,5,null,4,3,null,null,2]
- 遇到null标记:弹出null,再弹出2,pre=null,pre.val不比较,pre=2,栈:[7,5,null,4,3,null]
- 处理3的右子树4:弹出4(非null),无右子树,压当前节点4、null,无左子树,栈:[7,5,null,3,null,null,4]
- 遇到null标记:弹出null,弹出4,4>2,pre=4,栈:[7,5,null,3,null]
- 处理3的null标记:弹出null,弹出3,3<4,pre=3,栈:[7,5,null]
- 处理5的右子树7:弹出7(非null),压右子树8、当前节点7、null、左子树6,栈:[8,7,null,6,5,null]
- 处理左子树6:弹出6(非null),无右子树,压当前节点6、null,无左子树,栈:[8,7,null,null,6,5,null]
- 遇到null标记:弹出null,弹出6,6>3,pre=6,栈:[8,7,null,5,null]
- 处理7的右子树8:弹出8(非null),无右子树,压当前节点8、null,无左子树,栈:[7,null,5,null,null,8]
- 遇到null标记:弹出null,弹出8,8>6,pre=8,栈:[7,null,5,null]
- 处理7的null标记:弹出null,弹出7,7<8,pre=7,栈:[5,null]
- 处理5的null标记:弹出null,弹出5,5<7,pre=5,栈为空
- 遍历结束:所有节点满足递增,返回true
五、算法复杂度分析
1. 时间复杂度
- O(n):每个节点入栈和出栈各一次,共2n次操作,线性时间复杂度
2. 空间复杂度
- O(h):h为树的高度,栈的最大深度为树的高度
- 平衡BST:h=logn,空间复杂度O(logn)
- 最坏情况(退化为链表):h=n,空间复杂度O(n)
3. 与递归解法对比
方法 | 优势 | 劣势 |
---|---|---|
迭代法 | 避免递归栈溢出,空间更可控 | 栈操作逻辑较复杂 |
递归法 | 代码简洁,符合BST递归定义 | 深树可能导致栈溢出 |
六、核心技术点总结:栈操作的三大设计原则
1. 中序遍历的逆向压栈策略
- 左子树优先:通过“右-中-左”的压栈顺序,利用栈的LIFO特性实现“左-中-右”的访问顺序
- null标记的分隔作用:区分节点的“待处理”和“已访问”状态,避免复杂的状态标记
2. 递增性的核心判断逻辑
- pre节点的作用:中序遍历的前一个节点值,确保当前节点值>pre节点值
- 严格递增约束:BST要求严格大于(非大于等于),代码中使用
>=
判断违反条件
3. 边界条件的处理
- 空树处理:直接返回true,符合BST定义
- 叶子节点处理:左右子树为空时,仅需判断自身与pre节点的关系
七、常见误区与优化建议
1. 错误理解BST定义
- 误区:认为每个节点的左右子节点值直接与当前节点比较即可
- 正确逻辑:需保证左子树所有节点<当前节点,右子树所有节点>当前节点(中序遍历递增)
2. 栈操作顺序错误
- 错误压栈:先压左子树再压右子树,导致遍历顺序错误
- 正确顺序:右-中-左的压栈顺序,确保左子树优先处理
3. 优化建议:更简洁的中序遍历实现
public boolean isValidBST(TreeNode root) {Stack<TreeNode> stack = new Stack<>();TreeNode pre = null;while (root != null || !stack.isEmpty()) {while (root != null) { // 先压左子树到栈底stack.push(root);root = root.left;}root = stack.pop();if (pre != null && root.val <= pre.val) {return false;}pre = root;root = root.right; // 处理右子树}return true;
}
- 优势:传统中序遍历实现,逻辑更清晰,无需null标记
- 原理:通过循环压左子树,弹出时访问当前节点,再处理右子树
八、总结:迭代法验证BST的本质是中序遍历的巧妙实现
本算法通过栈模拟中序遍历,将BST的验证转化为递增序列的判断,核心在于:
- 中序遍历的性质:BST的中序遍历必为严格递增序列,抓住这一本质可简化验证逻辑
- 栈的逆向压栈:通过“右-中-左”的压栈顺序,利用栈的LIFO特性实现左子树优先访问
- null标记的创新:作为访问节点的触发信号,避免复杂的状态管理
理解这种迭代法的关键是将树的结构特性(BST的有序性)转化为线性序列的递增性判断。栈的巧妙操作使得非递归实现既保持了O(n)的效率,又避免了递归的栈溢出风险。在实际工程中,这种基于栈的迭代法常用于处理树结构的遍历问题,尤其是需要严格控制空间复杂度的场景。