验证二叉搜索树(中序遍历非递归版)
1. 题目回顾
问题描述: 给定一个二叉树的根节点 root,判断其是否是一个有效的二叉搜索树(BST)。
核心性质: 有效二叉搜索树的中序遍历结果必然是一个严格递增的序列。因此,我们只需在迭代遍历的过程中,检查当前访问到的节点值是否严格大于前一个被访问的节点值即可。

2. 核心思路:分治与模块化(非递归版)
我们将中序遍历的过程看作是一个 "压栈 →→ 弹栈 →→ 校验" 的流水线:
- 分解过程(寻找极左): 利用栈的特性,不断将当前节点及其所有左孩子压入栈中,直到没有左孩子为止。这保证了弹出的顺序天然符合 BST 的升序特性。
- 解决过程(单调性校验): 弹出栈顶元素(即当前未处理部分的最小值),将其与前驱节点的值进行比较。如果当前值小于等于前驱值,说明序列不再严格递增,直接返回
false。 - 合并过程(转向右侧): 校验通过后,将指针指向当前节点的右孩子,重复上述过程。一旦整个遍历过程中都没有发现逆序对,最终返回
true。
3. 算法详细步骤
3.1 递归终止条件(边界处理)
在迭代法中,循环的终止条件替代了递归的 Base Case。只要还有节点没被压入栈中(root != null)或者栈里还有存货(!stack.isEmpty()),就说明遍历尚未结束。同时需要初始化一个全局的前驱变量 prev,通常设为极小值(如 Long.MIN_VALUE),以确保第一次比较时不会出错。
3.2 分解过程:寻找与断链(内层循环)
这是中序遍历的核心动作------"一路向左" 。通过一个内层 while 循环,将沿途经过的所有节点存入栈中。当 root 为 null 时,说明到达了当前分支的最左端。
3.3 解决过程:调用通用反转/遍历逻辑
当内层循环结束,栈顶元素即为当前子树中的最小值。我们需要执行"访问节点"的操作:
root = stack.pop(); // 取出当前最小节点
if (root.val <= prev) return false; // 【关键点】校验单调性
prev = root.val; // 更新前驱节点
3.4 合并过程:重连与复位(转向右子树)
校验通过后,说明当前节点合法。此时将指针移向右孩子(root = root.right)。在下一次外层循环开始时,这个右孩子会被当作新的根节点,再次进入内层循环去"寻找它的极左节点"。这就完成了从左子树到根,再到右子树的无缝衔接。
4. 代码实现
以下是基于 ArrayDeque 实现的高效解法:
class Solution {
public boolean isValidBST(TreeNode root) {
Deque<TreeNode> stack = new ArrayDeque<>();
long prev = Long.MIN_VALUE; // 记录前驱节点的值,使用long避免边界溢出
while (root != null || !stack.isEmpty()) {
// 3.2 分解过程:一路向左,把所有左孩子压栈
while (root != null) {
stack.push(root);
root = root.left;
}
// 3.3 解决过程:弹出栈顶(当前最小值)并校验
root = stack.pop();
if (root.val <= prev) {
return false; // 发现不满足严格递增,提前终止
}
prev = root.val; // 更新前驱节点
// 3.4 合并过程:转向右子树,准备下一轮"一路向左"
root = root.right;
}
// 完整遍历完毕且未触发return false,说明是合法的BST
return true;
}
}
5. 复杂度分析
- 时间复杂度: O(n)。其中 n 是二叉树的节点数。每个节点都会被精确地压入和弹出栈各一次,总操作次数与节点数成正比。
- 空间复杂度: O(h) 。主要消耗在于显式栈的空间,其中 hh 是树的高度。在最坏情况下(树退化为链表),空间复杂度为 O(n) ;对于平衡二叉树,空间复杂度为O(logn) 。
二叉树展开为链表

1. 题目回顾
力扣 114. 二叉树展开为链表 (Flatten Binary Tree to Linked List)
题目描述
给你二叉树的根结点 root,请你将它展开为一个单链表:
- 展开后的单链表应该同样使用
TreeNode,其中right子指针指向链表中下一个结点,而左子指针始终为null。 - 展开后的单链表应该与二叉树先序遍历顺序相同。
示例
-
示例 1:
- 输入:
root = [1,2,5,3,4,null,6] - 输出:
[1,null,2,null,3,null,4,null,5,null,6]
- 输入:
-
示例 2:
- 输入:
root = [] - 输出:
[]
- 输入:
-
示例 3:
- 输入:
root = [0] - 输出:
[0]
- 输入:
约束条件
- 树中结点数在范围
[0, 2000]内。 -100 <= Node.val <= 100。- 进阶:你可以使用原地算法( O(1)O(1) 额外空间)展开这棵树吗?
2. 核心思路:先序遍历 + 列表收集(直观易懂)
这道题的核心要求是"展开后的单链表顺序必须与先序遍历(根-左-右)顺序相同"。最直观的解题思路是将"遍历"和"重构"两个步骤解耦:
- 先序遍历收集节点 :通过递归的方式,严格按照"根-左-右"的顺序遍历整棵二叉树,并将遍历到的每一个节点依次存入一个线性列表(如
ArrayList)中。此时,列表中的节点顺序就是最终链表应有的顺序。 - 重构链表指针 :遍历列表,依次取出相邻的两个节点(前一个节点
pre和当前节点cur)。将pre的左指针置为null,右指针指向cur。 - 优势与局限:这种方法的逻辑极其清晰,代码可读性高,非常适合初学者理解题意。但它的缺点是使用了额外的列表空间,空间复杂度为 O(N)O(N) ,未能满足题目进阶要求的 O(1)O(1) 原地算法。
3. 算法详细步骤
3.1 初始化与边界处理
- 创建一个
ArrayList<TreeNode>用于按顺序存储节点。 - 调用先序遍历辅助函数,将根节点及所有子节点按顺序加入列表。
- 获取列表的大小
size,如果size <= 1,则无需重构,直接返回。
3.2 分解过程:先序遍历收集
- 递归函数
preOrder接收当前节点root和列表list。 - 终止条件 :如果
root == null,直接返回。 - 处理当前节点 :将
root加入列表。 - 递归左子树 :调用
preOrder(root.left, list)。 - 递归右子树 :调用
preOrder(root.right, list)。
3.3 解决过程:重构链表指针
- 使用
for循环从索引1遍历到size - 1。 - 在每次迭代中,获取前一个节点
pre = list.get(i - 1)和当前节点cur = list.get(i)。 - 修改指针:
pre.left = null; pre.right = cur;。
3.4 合并过程:结果输出
- 循环结束后,原二叉树的节点已经被重新串联成单链表。由于题目要求原地修改(
void返回值),无需额外返回操作,直接结束方法即可。
4. 代码实现(Java 版本 - 代码分析)
/**
* 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 void flatten(TreeNode root) {
// 1. 边界处理:如果根节点为空,直接返回
if (root == null) return;
// 2. 创建列表,按先序遍历顺序收集所有节点
List<TreeNode> list = new ArrayList<>();
preOrder(root, list);
// 3. 遍历列表,重新连接节点的左右指针
int size = list.size();
for (int i = 1; i < size; i++) {
TreeNode pre = list.get(i - 1);
TreeNode cur = list.get(i);
pre.left = null; // 左指针始终为 null
pre.right = cur; // 右指针指向链表中的下一个节点
}
}
// 辅助函数:先序遍历(根-左-右)
void preOrder(TreeNode root, List<TreeNode> list) {
if (root == null) return;
list.add(root); // 先处理根节点
preOrder(root.left, list); // 递归遍历左子树
preOrder(root.right, list); // 递归遍历右子树
}
}
代码关键点分析
- 解耦思想:将复杂的树形结构指针操作,转化为简单的线性列表遍历操作,极大地降低了代码出错的概率。
- 先序遍历的严谨性 :
list.add(root)必须放在两次递归调用之前,严格保证"根-左-右"的访问顺序。 - 指针修改的顺序 :在
for循环中,必须先修改left = null,再修改right = cur。虽然在这个特定场景下顺序颠倒也不会报错,但养成"先断后连"的习惯在处理复杂链表/树操作时能有效避免死循环或节点丢失。
5. 复杂度分析
- 时间复杂度 : O(N),其中 N 是二叉树的节点数。先序遍历需要访问每个节点一次,耗时 O(N) ;随后的
for循环遍历列表也需要 O(N) 。两者相加仍为 O(N) 。 - 空间复杂度 : O(N) 。主要空间开销来自两个方面:一是用于存储节点的
ArrayList,最多存储 N 个节点;二是递归调用栈的深度,在最坏情况下(树退化为链表),递归深度为 N 。因此总体空间复杂度为 O(N) 。