LeetCode 114. 二叉树展开为链表:详细解题思路与 TS 实现

LeetCode中等难度题目「114. 二叉树展开为链表」,这道题核心考察二叉树的遍历(前序遍历为主)和节点指针的原地修改,是面试中高频出现的二叉树应用题。本文会用TypeScript实现三种不同思路,从直观到优化,逐步拆解逻辑,新手也能轻松看懂。

一、题目解析(题干+核心要求)

题干回顾

给你二叉树的根结点 root ,请你将它展开为一个单链表:

  • 展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null 。

  • 展开后的单链表应该与二叉树 先序遍历 顺序相同。

核心要点

  1. 原地修改:不能创建新的TreeNode链表,必须直接修改原二叉树的left和right指针;

  2. 顺序要求:单链表顺序 = 原二叉树前序遍历顺序(根 → 左 → 右);

  3. 指针规范:所有节点的left指针必须置为null,right指针串联整个链表。

示例辅助理解(简化版)

原二叉树(前序遍历:1 → 2 → 3 → 4 → 5 → 6):

plain 复制代码
      1
     / \
    2   5
   / \   \
  3   4   6

展开后单链表:

plain 复制代码
1 → 2 → 3 → 4 → 5 → 6
(所有left为null,right指向后续节点)

二、前置准备(TreeNode类型定义)

题目已给出TreeNode类的定义,TypeScript版本如下(直接复用,无需修改):

typescript 复制代码
class TreeNode {
  val: number
  left: TreeNode | null
  right: TreeNode | null
  constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
    this.val = (val === undefined ? 0 : val)
    this.left = (left === undefined ? null : left)
    this.right = (right === undefined ? null : right)
  }
}

三、三种实现方案(TS版,含详细注释)

核心思路:围绕「前序遍历」展开------因为单链表顺序和前序遍历一致,所以先获取前序遍历的节点序列,再调整指针;或边遍历边调整指针,优化空间复杂度。

方案一:先序遍历(递归)+ 后续指针调整(直观易懂)

思路拆解
  1. 用一个数组(stack)存储前序遍历的所有节点(递归实现前序遍历);

  2. 遍历存储节点的数组,依次调整每个节点的指针:

  • 前一个节点的left置为null;

  • 前一个节点的right指向当前节点;

  1. 优点:逻辑简单,容易上手,适合新手;缺点:需要额外数组存储节点,空间复杂度O(n)(n为节点数)。
完整代码(含注释)
typescript 复制代码
/**
 Do not return anything, modify root in-place instead.
 */
function flatten_1(root: TreeNode | null): void {
  // 边界处理:空树直接返回
  if (root === null) {
    return;
  }
  // 用于存储前序遍历的节点序列
  const stack: TreeNode[] = [];
  // 执行递归版前序遍历,将节点存入stack
  preOrder_1(root, stack);
  
  // 遍历节点序列,调整指针(从第二个节点开始)
  for (let i = 1; i < stack.length; i++) {
    const prev = stack[i - 1]; // 前一个节点
    const curr = stack[i];     // 当前节点
    prev.left = null;          // 前节点left置空
    prev.right = curr;         // 前节点right指向当前节点
  }
}

// 辅助函数:前序遍历(递归实现)
const preOrder_1 = (root: TreeNode | null, list: TreeNode[]): void => {
  if (root != null) {
    list.push(root);          // 先访问根节点,存入数组
    preOrder_1(root.left, list); // 递归遍历左子树
    preOrder_1(root.right, list); // 递归遍历右子树
  }
}

方案二:先序遍历(栈)+ 后续指针调整(避免递归栈溢出)

思路拆解

和方案一逻辑一致,唯一区别是「前序遍历用栈实现」,替代递归实现。

为什么用栈?递归遍历的本质是利用系统调用栈,当二叉树深度极深时,会出现栈溢出问题;用手动维护的栈实现前序遍历,可避免此问题,更适合生产环境。

空间复杂度依然是O(n)(数组存储节点+栈存储遍历节点)。

完整代码(含注释)
typescript 复制代码
// 复用方案一的flatten_1函数,仅修改前序遍历的实现
function flatten_1(root: TreeNode | null): void {
  if (root === null) {
    return;
  }
  const stack: TreeNode[] = [];
  // 替换为栈实现的前序遍历
  preOrder_2(root, stack);
  for (let i = 1; i < stack.length; i++) {
    const prev = stack[i - 1];
    const curr = stack[i];
    prev.left = null;
    prev.right = curr;
  }
}

// 辅助函数:前序遍历(栈实现,核心!)
const preOrder_2 = (root: TreeNode | null, list: TreeNode[]): void => {
  if (!root) {
    return;
  }
  const stack = [root]; // 栈初始化,存入根节点
  while (stack.length) { // 栈不为空则继续遍历
    const node: TreeNode | undefined = stack.pop(); // 弹出栈顶节点(先处理根)
    if (!node) continue; // 兜底:避免节点为undefined(实际不会触发)
    list.push(node); // 访问节点,存入数组
    
    // 关键:栈是后进先出,所以先压右子树,再压左子树(保证左子树先被访问)
    if (node.right) {
      stack.push(node.right);
    }
    if (node.left) {
      stack.push(node.left);
    }
  }
}

方案三:边遍历边展开(最优解,空间复杂度O(1))

思路拆解

这是最优方案------无需额外数组存储节点,利用栈遍历的同时,直接调整节点指针,实现原地展开,空间复杂度优化至O(1)(仅用栈存储待遍历节点,栈的最大深度为二叉树高度,最坏情况O(n),平均情况O(logn))。

核心逻辑:

  1. 用栈存储待遍历的节点,初始化存入根节点;

  2. 用prev指针记录上一个访问的节点(初始为null);

  3. 弹出栈顶节点(curr),调整prev和curr的指针:

  • 若prev不为null,prev.left置为null,prev.right指向curr;

  • 更新prev为curr;

  1. 栈中压入节点时,依然遵循「先右后左」(保证前序遍历顺序);

  2. 循环直至栈为空,此时二叉树已展开为单链表。

完整代码(含注释)
typescript 复制代码
/**
 Do not return anything, modify root in-place instead.
 */
function flatten_2(root: TreeNode | null): void {
  // 边界处理:空树直接返回
  if (root === null) {
    return;
  }
  const stack: TreeNode[] = [root]; // 栈初始化,存入根节点
  let prev: TreeNode | null = null; // 记录上一个访问的节点
  
  while (stack.length) { // 栈不为空,继续遍历
    const curr: TreeNode | undefined = stack.pop(); // 弹出栈顶节点(当前节点)
    if (!curr) continue; // 兜底处理
    
    // 调整上一个节点的指针(核心:原地修改)
    if (prev !== null) {
      prev.left = null;    // 上一个节点left置空
      prev.right = curr;   // 上一个节点right指向当前节点
    }
    
    prev = curr; // 更新prev为当前节点,为下一次调整做准备
    
    // 关键:先压右子树,再压左子树(栈后进先出,保证左子树先被访问,符合前序)
    if (curr.right) {
      stack.push(curr.right);
    }
    if (curr.left) {
      stack.push(curr.left);
    }
  }
}

四、三种方案对比(选型建议)

方案 前序实现方式 空间复杂度 时间复杂度 优点 缺点
方案一 递归 O(n) O(n) 逻辑简单,新手易上手 递归深度过深会栈溢出,额外占用数组空间
方案二 O(n) O(n) 避免递归栈溢出,稳定性更好 仍需额外数组存储节点,空间利用率不高
方案三 栈(边遍历边调整) O(h)(h为树高,最优) O(n) 原地修改,空间利用率最高,面试首选 逻辑稍复杂,需注意指针更新顺序

五、关键注意事项(避坑指南)

  • 前序遍历的顺序陷阱:栈实现时,必须「先压右子树,再压左子树」------因为栈是后进先出,这样弹出时才能先访问左子树,符合「根→左→右」的前序顺序;若顺序颠倒,会导致单链表顺序错误。

  • 原地修改要求:严禁创建新的TreeNode实例,所有操作必须针对原节点的left和right指针进行修改,否则会不符合题目要求。

  • 边界处理:必须先判断root是否为null,否则会触发空指针异常(比如stack.pop()后获取node.val时)。

  • 指针更新顺序:方案三中,必须先调整prev的指针,再更新prev为curr,否则会出现指针混乱(比如prev和curr指向同一个节点)。

六、总结

本题的核心是「前序遍历」和「原地指针修改」,三种方案各有侧重:

  1. 新手入门:优先方案一,理解「前序遍历→指针调整」的核心逻辑;

  2. 稳定性优化:方案二,用栈替代递归,避免栈溢出;

  3. 面试最优:方案三,边遍历边调整,空间复杂度最优,体现对二叉树遍历和指针操作的熟练程度。

其实这道题还有更进阶的解法(比如Morris遍历,空间复杂度O(1)),但上述三种方案已经能覆盖大部分面试场景,新手先掌握这三种,再进阶学习Morris遍历即可。

相关推荐
像素猎人2 小时前
范围for语法(除for循环/while循环/do...while循环的第四种循环)
数据结构·算法
2 小时前
2.20进制转化,表达式求值,删除字符
开发语言·c++·算法
追随者永远是胜利者2 小时前
(LeetCode-Hot100)461. 汉明距离
java·算法·leetcode·职场和发展·go
努力学算法的蒟蒻2 小时前
day90(2.19)——leetcode面试经典150
算法·leetcode·面试
啊阿狸不会拉杆2 小时前
《计算机视觉:模型、学习和推理》第 5 章-正态分布
人工智能·python·学习·算法·机器学习·计算机视觉·正态分布
踩坑记录2 小时前
leetcode hot100 22. 括号生成 medium 递归回溯
leetcode
样例过了就是过了2 小时前
LeetCode热题100 缺失的第一个正数
数据结构·算法·leetcode
L-李俊漩2 小时前
手机端的google chrome 浏览器 怎么看响应的日志和请求报文
前端·chrome·智能手机
样例过了就是过了2 小时前
LeetCode热题100 除了自身以外数组的乘积
数据结构·算法·leetcode