力扣实训 _ [98].验证二叉搜索树 _ 将二叉树展开成链表

验证二叉搜索树(中序遍历非递归版)

1. 题目回顾

问题描述: 给定一个二叉树的根节点 root,判断其是否是一个有效的二叉搜索树(BST)。

核心性质: 有效二叉搜索树的中序遍历结果必然是一个严格递增的序列。因此,我们只需在迭代遍历的过程中,检查当前访问到的节点值是否严格大于前一个被访问的节点值即可。

2. 核心思路:分治与模块化(非递归版)

我们将中序遍历的过程看作是一个 "压栈 →→ 弹栈 →→ 校验" 的流水线:

  • 分解过程(寻找极左): 利用栈的特性,不断将当前节点及其所有左孩子压入栈中,直到没有左孩子为止。这保证了弹出的顺序天然符合 BST 的升序特性。
  • 解决过程(单调性校验): 弹出栈顶元素(即当前未处理部分的最小值),将其与前驱节点的值进行比较。如果当前值小于等于前驱值,说明序列不再严格递增,直接返回 false
  • 合并过程(转向右侧): 校验通过后,将指针指向当前节点的右孩子,重复上述过程。一旦整个遍历过程中都没有发现逆序对,最终返回 true

3. 算法详细步骤

3.1 递归终止条件(边界处理)

在迭代法中,循环的终止条件替代了递归的 Base Case。只要还有节点没被压入栈中(root != null)或者栈里还有存货(!stack.isEmpty()),就说明遍历尚未结束。同时需要初始化一个全局的前驱变量 prev,通常设为极小值(如 Long.MIN_VALUE),以确保第一次比较时不会出错。

3.2 分解过程:寻找与断链(内层循环)

这是中序遍历的核心动作------"一路向左" 。通过一个内层 while 循环,将沿途经过的所有节点存入栈中。当 rootnull 时,说明到达了当前分支的最左端。

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) 。
相关推荐
8Qi81 小时前
LeetCode 377:组合总和 Ⅳ(Combination Sum IV)—— 题解 ✅
算法·leetcode·动态规划·完全背包
凯瑟琳.奥古斯特1 小时前
力扣1002题C++解法详解
开发语言·c++·算法·leetcode·职场和发展
小小工匠1 小时前
Redis - 从数据结构到高可用的九个关键问题
数据结构·redis
CHHH_HHH1 小时前
【C++】红黑树:比AVL树更实用的平衡二叉搜索树
开发语言·数据结构·c++·算法·stl
Lazionr1 小时前
基础算法 | 模拟算法练习
c++·算法
_日拱一卒2 小时前
LeetCode:17电话号码的字母组合
java·数据结构·算法·leetcode·职场和发展
醉颜凉2 小时前
Scala自定义Monad实战:从理论到应用的完整指南
大数据·算法·scala
STY_fish_20122 小时前
KMP-前缀函数
算法
zzz_23682 小时前
【Redis】Redis 数据结构与 Spring Boot 集成
数据结构·spring boot·redis