【码道初阶】牛客TSINGK110:二叉树遍历(较难)如何根据“扩展先序遍历”构建二叉树?

【算法详解】如何根据"扩展先序遍历"构建二叉树?

在二叉树的算法题中,我们常遇到的问题是:给定二叉树求遍历序列。但反过来,给定一个遍历序列(字符串),如何还原出一棵二叉树?

通常情况下,单靠一个先序遍历是无法唯一确定一棵树的(需要先序+中序)。但是,如果输入序列中包含了空指针的信息 (比如用 # 表示空节点),那么仅凭先序遍历序列,就能唯一确定这棵二叉树。

今天我们通过一道经典题目(TSINGK110),来深入剖析这种"扩展先序遍历"的构建逻辑。

1. 题目核心分析

输入abc##de#g##f###
含义

  • 这是一个先序遍历(根 -> 左 -> 右)。
  • # 代表空节点(null)。
  • # 字符代表真实的节点值。

目标:构建出这棵树,并输出它的中序遍历。

2. 核心解题思路:递归 + 全局游标

代码采用了最直观也最有效的解法:递归构建

因为先序遍历的特性是:第一个字符肯定是根节点,紧接着是左子树的数据,再后面是右子树的数据。

但是,左子树占了多少个字符?右子树从哪里开始?我们不知道。

这就需要引入一个全局变量 i (或者叫全局游标)。它像一个指针,随着递归的进行,始终指向字符串中当前待处理 的那个字符。哪怕在递归深处,大家操作的都是同一个 i,这样就不会乱。

3. 深度拆解 createTree 方法(灵魂所在)

这是整个代码的核心,让我们逐行剖析逻辑:

java 复制代码
public static int i = 0; // 全局游标

public static TreeNode createTree(String str){
    // 1. 获取当前游标指向的字符
    char ch = str.charAt(i);
    TreeNode newroot = null;

    // 2. 判断是否是空节点标记
    if(ch != '#'){
        // === 情况 A:是真实节点 ===
        newroot = new TreeNode(ch); // 创建节点
        i++; // 游标后移,准备处理下一个字符
        
        // 【关键递归】
        // 既然我是根,那字符串里紧接着我的,肯定是我的左子树内容
        newroot.left = createTree(str); 
        
        // 当左子树全部构建完毕(递归返回)后,
        // 游标 i 已经跑到了右子树数据的开头
        newroot.right = createTree(str);
        
    } else {
        // === 情况 B:是空节点 ===
        // 不需要创建节点,newroot 保持为 null
        i++; // 游标后移,跳过这个 '#'
    }
    
    // 3. 返回构建好的节点(或者 null)
    return newroot;
}

图解执行流程(以 abc##... 为例)

让我们模拟一下计算机的堆栈,看 createTree 是如何"生长"出这棵树的:

  1. Layer 1 : 读入 a。创建节点 Ai 变为 1。
    • 调用 root.left = createTree()
  2. Layer 2 : 读入 b。创建节点 Bi 变为 2。
    • 调用 root.left = createTree()
  3. Layer 3 : 读入 c。创建节点 Ci 变为 3。
    • 调用 root.left = createTree()
  4. Layer 4 : 读入 #ch#
    • i 变为 4。返回 null
    • (回到 Layer 3,C 的左孩子设为 null)
    • Layer 3 继续执行,调用 root.right = createTree()
  5. Layer 4 : 读入 #ch#
    • i 变为 5。返回 null
    • (回到 Layer 3,C 的右孩子设为 null)
    • Layer 3 执行完毕,返回节点 C
    • (回到 Layer 2,B 的左孩子设为 C)
  6. Layer 2 : B 的左孩子搞定了,继续调用 root.right = createTree()...

总结 :只要遇到非 #,我就生孩子;只要遇到 #,我就告诉爸爸"这里没路了(null)",然后指针继续往后移,去处理树的下一部分。

4. ⚠️ 易错点修正(多组数据的坑)

这段代码逻辑在处理单组数据时是完美的。但是题目描述中提到:

"可能有多组测试数据"
while (in.hasNextLine()) { ... }

该代码存在一个严重的隐患: public static int i = 0; 是定义在类级别的静态变量。

场景模拟:

  1. 第一组数据 abc## 跑完,i 变成了 5。
  2. while 循环进入第二轮,读入新字符串。
  3. 再次调用 createTree此时 i 还是 5!
  4. 程序会直接从新字符串的第 5 个字符开始读,或者直接抛出 StringIndexOutOfBoundsException

修正方案:

必须在每组测试数据开始处理前,手动重置 i = 0

java 复制代码
public static void main(String[] args) {
    Scanner in = new Scanner(System.in);
    while (in.hasNextLine()) {
        String str = in.nextLine();
        
        i = 0; // 【重要】必须在这里重置游标!
        
        TreeNode Targetroot = createTree(str);
        inOrder(Targetroot);
        System.out.println(); // 建议每组输出后换行,方便阅读
    }
}

5. 完整代码优化

结合以上分析,最终完美的代码结构如下:

java 复制代码
import java.util.Scanner;

class TreeNode {
    public char val;
    public TreeNode left;
    public TreeNode right;
    public TreeNode(char val) {
        this.val = val;
    }
}

public class Main {
    // 全局游标,用于记录字符串处理到了哪个位置
    public static int i = 0;

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        while (in.hasNextLine()) {
            String str = in.nextLine();
            // 【修正】处理新的一行之前,必须重置游标
            i = 0; 
            TreeNode Targetroot = createTree(str);
            inOrder(Targetroot);
            System.out.println(); // 格式优化:换行
        }
    }

    // 核心构建逻辑
    public static TreeNode createTree(String str) {
        // 防止游标越界(虽然题目保证输入合法,但加上更安全)
        if (i >= str.length()) return null;

        char ch = str.charAt(i);
        i++; // 读取一个字符后,游标必须后移

        // 递归出口:遇到空节点标记
        if (ch == '#') {
            return null;
        }

        // 递归构建:根 -> 左 -> 右
        TreeNode newroot = new TreeNode(ch);
        newroot.left = createTree(str);
        newroot.right = createTree(str);
        
        return newroot;
    }

    // 中序遍历:左 -> 根 -> 右
    public static void inOrder(TreeNode root) {
        if (root == null) return;
        inOrder(root.left);
        System.out.print(root.val + " ");
        inOrder(root.right);
    }
}

6. 总结

这道题是理解二叉树序列化的基石。

  • 思路:利用先序遍历的顺序特性(根-左-右)。
  • 技巧 :使用全局变量 i 来在递归层级之间"传递"当前的进度。
  • 陷阱 :在多组输入的在线判题系统(OJ)中,永远不要忘记重置全局/静态变量

掌握了这个 createTree 的写法,以后遇到"序列化二叉树"或"反序列化二叉树"的题目,你就能信手拈来了!

相关推荐
秋饼几秒前
【手撕 @EnableAsync:揭秘 SpringBoot @Enable 注解的魔法开关】
java·spring boot·后端
m0_603888711 分钟前
Scaling Trends for Multi-Hop Contextual Reasoning in Mid-Scale Language Models
人工智能·算法·ai·语言模型·论文速览
Good_Starry4 分钟前
Java——正则表达式
java·开发语言·正则表达式
Xの哲學4 分钟前
Linux io_uring 深度剖析: 重新定义高性能I/O的架构革命
linux·服务器·网络·算法·边缘计算
萤丰信息7 分钟前
开启园区“生命体”时代——智慧园区系统,定义未来的办公与生活
java·大数据·运维·数据库·人工智能·生活·智慧园区
comli_cn11 分钟前
残差链接(Residual Connection)
人工智能·算法
欧洵.12 分钟前
Java.基于UDP协议的核心内容
java·开发语言·udp
xunyan623418 分钟前
第九章 JAVA常用类
java·开发语言
Aaron158818 分钟前
基于VU13P在人工智能高速接口传输上的应用浅析
人工智能·算法·fpga开发·硬件架构·信息与通信·信号处理·基带工程
予枫的编程笔记20 分钟前
【论文解读】DLF:以语言为核心的多模态情感分析新范式 (AAAI 2025)
人工智能·python·算法·机器学习