【每日算法】LeetCode 114. 二叉树展开为链表:从树结构到线性结构的优雅转换

对前端开发者而言,学习算法绝非为了"炫技"。它是你从"页面构建者"迈向"复杂系统设计者"的关键阶梯。它将你的编码能力从"实现功能"提升到"设计优雅、高效解决方案"的层面。从现在开始,每天投入一小段时间,结合前端场景去理解和练习,你将会感受到自身技术视野和问题解决能力的质的飞跃。------ 算法:资深前端开发者的进阶引擎

LeetCode 114. 二叉树展开为链表:从树结构到线性结构的优雅转换

1. 题目描述

给定一个二叉树的根节点 root,请将该二叉树展开为一个单链表:

  • 展开后的单链表应该同样使用 TreeNode,其中 right 子指针指向链表中下一个节点,而 left 子指针始终为 null
  • 展开后的单链表应该与二叉树的前序遍历顺序相同
  • 需要原地 展开,即 O(1) 额外空间(递归栈空间不算在内)

示例:

复制代码
输入:root = [1,2,5,3,4,null,6]
输出:[1,null,2,null,3,null,4,null,5,null,6]

二叉树结构:

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

展开后的链表结构:

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

2. 问题分析

这是一个典型的树形结构转线性结构的问题,具有以下特点:

  1. 顺序要求:展开后的链表必须与前序遍历顺序一致
  2. 原地操作:不能创建新节点,只能修改现有节点的指针
  3. 空间限制:理想情况下应该使用常数级额外空间

前端视角理解:这类似于将DOM树中的节点按照特定顺序(如前序遍历)重新连接成链表。想象一下,你需要将一个嵌套的UI组件树(如React/Vue组件树)按照某种顺序扁平化,同时保持原有父子关系。

3. 解题思路

3.1 直观思路对比

思路 时间复杂度 空间复杂度 是否原地 实现难度
前序遍历+存储节点 O(n) O(n) 简单
递归后序遍历 O(n) O(h) 中等
迭代前驱节点 O(n) O(1) 中等
栈辅助迭代 O(n) O(h) 中等

最优解:迭代前驱节点法,时间复杂度 O(n),空间复杂度 O(1),完全符合题目要求。

3.2 核心算法思路详解

3.2.1 前序遍历+存储节点(基础解法)
  1. 对二叉树进行前序遍历,将节点按顺序存入数组
  2. 遍历数组,将每个节点的左指针设为null,右指针指向下一个节点
3.2.2 递归后序遍历(分治思想)
  1. 递归处理左子树,将其展开为链表
  2. 递归处理右子树,将其展开为链表
  3. 将左子树链表插入到根节点和右子树链表之间
3.2.3 迭代前驱节点(最优解)

核心思想:对于当前节点,找到其左子树中最右侧的节点(前驱节点),将右子树连接到该节点后,然后将左子树移到右侧。

算法步骤:

  1. 从根节点开始遍历
  2. 如果当前节点有左子树:
    • 找到左子树中最右侧的节点(前驱节点)
    • 将当前节点的右子树接到前驱节点的右侧
    • 将左子树移到右侧,左子树置为null
  3. 移动到下一个节点(原左子树的根节点)

4. 各思路代码实现

4.1 前序遍历+存储节点法

javascript 复制代码
/**
 * 方法1:前序遍历+存储节点
 * 时间复杂度:O(n),空间复杂度:O(n)
 */
var flatten = function(root) {
    if (!root) return;
    
    const nodes = [];
    
    // 前序遍历收集节点
    function preorder(node) {
        if (!node) return;
        nodes.push(node);
        preorder(node.left);
        preorder(node.right);
    }
    
    preorder(root);
    
    // 重新连接节点
    for (let i = 0; i < nodes.length - 1; i++) {
        nodes[i].left = null;
        nodes[i].right = nodes[i + 1];
    }
    
    // 处理最后一个节点
    if (nodes.length > 0) {
        nodes[nodes.length - 1].left = null;
        nodes[nodes.length - 1].right = null;
    }
};

4.2 递归后序遍历法

javascript 复制代码
/**
 * 方法2:递归后序遍历
 * 时间复杂度:O(n),空间复杂度:O(h) 递归栈深度
 */
var flatten = function(root) {
    // 辅助函数:展开树并返回最后一个节点
    const flattenTree = function(node) {
        if (!node) return null;
        
        // 如果是叶子节点,直接返回
        if (!node.left && !node.right) {
            return node;
        }
        
        // 递归处理左右子树
        const leftTail = flattenTree(node.left);
        const rightTail = flattenTree(node.right);
        
        // 如果存在左子树,需要将其插入到根节点和右子树之间
        if (leftTail) {
            leftTail.right = node.right;
            node.right = node.left;
            node.left = null;
        }
        
        // 返回最后一个节点
        return rightTail || leftTail;
    };
    
    flattenTree(root);
};

4.3 迭代前驱节点法(最优解)

javascript 复制代码
/**
 * 方法3:迭代前驱节点法(最优解)
 * 时间复杂度:O(n),空间复杂度:O(1)
 */
var flatten = function(root) {
    let curr = root;
    
    while (curr) {
        // 如果当前节点有左子树
        if (curr.left) {
            // 找到左子树中最右侧的节点(前驱节点)
            let predecessor = curr.left;
            while (predecessor.right) {
                predecessor = predecessor.right;
            }
            
            // 将当前节点的右子树接到前驱节点的右侧
            predecessor.right = curr.right;
            
            // 将左子树移到右侧
            curr.right = curr.left;
            curr.left = null;
        }
        
        // 处理下一个节点
        curr = curr.right;
    }
};

4.4 栈辅助迭代法

javascript 复制代码
/**
 * 方法4:栈辅助迭代
 * 时间复杂度:O(n),空间复杂度:O(h)
 */
var flatten = function(root) {
    if (!root) return;
    
    const stack = [root];
    let prev = null;
    
    while (stack.length) {
        const curr = stack.pop();
        
        if (prev) {
            prev.left = null;
            prev.right = curr;
        }
        
        // 注意:栈是后进先出,所以先压入右子树
        if (curr.right) {
            stack.push(curr.right);
        }
        if (curr.left) {
            stack.push(curr.left);
        }
        
        prev = curr;
    }
};

5. 各实现思路的复杂度、优缺点对比表格

方法 时间复杂度 空间复杂度 优点 缺点 适用场景
前序遍历+存储节点 O(n) O(n) 思路简单,代码直观 不符合原地修改要求,空间占用大 学习理解、允许额外空间时
递归后序遍历 O(n) O(h) 原地修改,代码简洁 递归栈空间可能较大(最坏O(n)) 树深度不大时,代码简洁性优先
迭代前驱节点 O(n) O(1) 完全原地,空间最优 逻辑稍复杂,需要理解前驱节点概念 严格空间限制,最优解要求
栈辅助迭代 O(n) O(h) 避免递归,直观的前序遍历 需要栈空间,不是常数空间 避免递归,中等空间限制

6. 总结

6.1 核心要点总结

  1. 前序遍历是关键:所有解法都围绕前序遍历的顺序展开
  2. 指针操作是核心:二叉树展开本质上是重新连接节点的左右指针
  3. 空间复杂度是区分点:不同解法的主要区别在于空间使用

6.2 实际应用场景

6.2.1 前端开发中的应用
  1. DOM树扁平化处理:将嵌套的DOM结构按特定顺序转换为线性结构,便于批量操作
  2. 组件树遍历优化:在React/Vue等框架中,需要遍历组件树执行特定操作时
  3. 文件目录结构展示:将树形目录结构展开为扁平列表,支持面包屑导航
  4. 无限滚动列表:将树形评论结构展开为线性时间线显示
相关推荐
毕设源码-钟学长2 小时前
【开题答辩全过程】以 基于Spark机器学习算法的体育新闻智能分类系统设计与实现为例,包含答辩的问题和答案
算法·机器学习·spark
天勤量化大唯粉2 小时前
基于距离的配对交易策略:捕捉价差异常偏离的均值回归机会(天勤量化代码实现)
android·开发语言·python·算法·kotlin·开源软件·策略模式
智航GIS2 小时前
ArcGIS大师之路500技---036通俗易懂讲解克里金法
人工智能·算法·arcgis
Q741_1472 小时前
Linux 进程核心解析 fork()详解 多进程的创建与回收 C++
linux·c++·面试·笔试·进程
拼好饭和她皆失2 小时前
逆元,除法同余原理
算法·逆元·除法同余原理
leiming62 小时前
c++ 利用模板创建一个可以储存任意类型数据的数组类
开发语言·c++·算法
TL滕2 小时前
从0开始学算法——第二十天(简易搜索引擎)
笔记·学习·算法
cpp_25012 小时前
P8723 [蓝桥杯 2020 省 AB3] 乘法表
数据结构·c++·算法·蓝桥杯·题解·洛谷
你好~每一天2 小时前
数据分析专员:当传统汽车销售融入AI智能,如何驱动业绩新增长
大数据·数据结构·人工智能·学习·数据分析·汽车·高性价比