【力扣100题】36.二叉树展开为链表

题目描述

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

  • 展开后的单链表应该同样使用 TreeNode,其中 right 子指针指向链表中下一个结点
  • left 子指针始终为 null
  • 展开后的单链表应该与二叉树 先序遍历 顺序相同

示例 1:

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

展开过程:
      1                      1
     / \                      \
    2   5          →           2
   / \   \                      \
  3   4   6                      4
                                  \
                                   5
                                    \
                                     6
                                      \
                                       6

最终链表:1 → 2 → 3 → 4 → 5 → 6

示例 2:

复制代码
输入:root = []
输出:[]

示例 3:

复制代码
输入:root = [0]
输出:[0]

提示:

  • 树中结点数在范围 [0, 2000] 内
  • -100 <= Node.val <= 100

进阶: 你可以使用原地算法(O(1) 额外空间)展开这棵树吗?


解题思路总览

方法 思路 时间复杂度 空间复杂度 适用场景
方法一:后序遍历 + 倒置链表 逆后序遍历(右-左-根),倒着构建链表 O(n) O(h) 面试首选,代码简洁
方法二:前序遍历 + 栈 先序遍历,使用栈存储右子树 O(n) O(n) 容易理解
方法三:莫里斯遍历 线索二叉树,O(1) 空间原地算法 O(n) O(1) 进阶挑战

核心原理: 利用后序遍历的逆序恰好是先序遍历顺序的特性,倒着构建链表


方法一:后序遍历 + 倒置链表(推荐)

思路

  • 先递归遍历右子树
  • 再递归遍历左子树
  • 最后处理根节点

处理根节点时,将 root->right 指向当前链表的头节点(倒着连),然后更新链表头为 root。这样每次都把当前节点接到链表最前面,最后就得到正向后序遍历(实际上是先序遍历)顺序的链表。

完整代码

cpp 复制代码
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    TreeNode* head = NULL;  // 链表头指针

    void flatten(TreeNode* root) {
        if (root == NULL) return;

        // 逆后序遍历:右 → 左 → 根
        flatten(root->right);   // 先递归右子树
        flatten(root->left);     // 再递归左子树

        // 处理根节点,倒着构建链表
        root->left = NULL;       // 左子指针始终为 null
        root->right = head;      // 右子指针指向当前链表头
        head = root;             // 更新链表头为当前节点
    }
};

算法流程图

root = [1,2,5,3,4,null,6] 为例:

复制代码
原始二叉树:
      1
     / \
    2   5
   / \   \
  3   4   6

逆后序遍历顺序:右 → 左 → 根

Step 1: flatten(6)
  root->left = NULL, root->right = NULL
  head = 6
  链表:6

Step 2: flatten(4)
  root->left = NULL, root->right = 6
  head = 4
  链表:4 → 6

Step 3: flatten(3)
  root->left = NULL, root->right = 4
  head = 3
  链表:3 → 4 → 6

Step 4: flatten(5)
  root->left = NULL, root->right = 3
  head = 5
  链表:5 → 3 → 4 → 6

Step 5: flatten(2)
  root->left = NULL, root->right = 5
  head = 2
  链表:2 → 5 → 3 → 4 → 6

Step 6: flatten(1)
  root->left = NULL, root->right = 2
  head = 1
  链表:1 → 2 → 5 → 3 → 4 → 6

最终结果:[1,null,2,null,5,null,3,null,4,null,6]

逐行解析

cpp 复制代码
TreeNode* head = NULL;  // 链表头指针
  • 定义一个成员变量 head,指向当前构建好的链表头。
  • 初始为 NULL,表示空链表。
cpp 复制代码
void flatten(TreeNode* root) {
    if (root == NULL) return;

    // 逆后序遍历:右 → 左 → 根
    flatten(root->right);   // 先递归右子树
    flatten(root->left);     // 再递归左子树
  • 逆后序遍历:先右后左最后根,这与标准后序遍历相反。
  • 选择逆后序是因为:倒着构建链表时,先处理右子树再处理左子树,最后根节点时正好把根接到最前面。
  • 递归顺序:6 → 4 → 3 → 5 → 2 → 1
cpp 复制代码
    root->left = NULL;       // 左子指针始终为 null
  • 题目要求左子指针始终为 null,直接置空。
cpp 复制代码
    root->right = head;      // 右子指针指向当前链表头
    head = root;             // 更新链表头为当前节点
  • 倒着构建链表的核心
    • root->right = head:将当前节点的右指针指向已构建好的链表头
    • head = root:将当前节点更新为新的链表头
  • 这样每次都把当前节点接到链表最前面,最终得到的就是正序链表

复杂度分析

复杂度 说明
时间 O(n) 每个节点访问一次
空间 O(h) 递归栈深度,h 为树高

优点: 代码极其简洁,空间效率高
缺点: 需要理解逆后序遍历和倒置链表的关联


方法二:前序遍历 + 栈

思路

标准的先序遍历(根-左-右)顺序正好是目标链表的顺序。使用栈保存右子树,先遍历左子树,再处理保存的右子树。

完整代码

cpp 复制代码
class Solution {
public:
    void flatten(TreeNode* root) {
        if (!root) return;

        stack<TreeNode*> st;
        TreeNode* cur = root;

        while (cur) {
            // 如果有左子树,先处理左子树
            if (cur->left) {
                st.push(cur->right);  // 右子树入栈暂存

                // 将左子树移到右子树位置
                cur->right = cur->left;
                cur->left = NULL;
                cur = cur->right;
            } else {
                // 没有左子树,直接向右走或从栈中取右子树
                if (!st.empty()) {
                    cur->right = st.top();
                    st.pop();
                    cur->left = NULL;
                    cur = cur->right;
                } else {
                    break;  // 栈空且无左子树,遍历完成
                }
            }
        }
    }
};

算法流程图

复制代码
原始二叉树:
      1
     / \
    2   5
   / \   \
  3   4   6

Step 1: cur = 1,有左子树
  st.push(5)
  1.right = 2, 1.left = NULL
  cur = 2

Step 2: cur = 2,有左子树
  st.push(4)
  2.right = 3, 2.left = NULL
  cur = 3

Step 3: cur = 3,无左子树
  st.empty()? 否 → 3.right = 4(出栈), 3.left = NULL
  cur = 4

Step 4: cur = 4,无左子树
  st.empty()? 否 → 4.right = 6(出栈), 4.left = NULL
  cur = 6

Step 5: cur = 6,无左右子树
  st.empty()? 是 → break

最终链表:1 → 2 → 3 → 4 → 5 → 6

复杂度分析

复杂度 说明
时间 O(n) 每个节点最多入栈出栈各一次
空间 O(n) 栈最坏存储所有右子树节点

方法三:莫里斯遍历(进阶 O(1) 空间)

思路

利用二叉树中闲置的右子指针,将其改为指向前驱节点,形成线索二叉树,从而在遍历过程中不用栈也不需要递归。

完整代码

cpp 复制代码
class Solution {
public:
    void flatten(TreeNode* root) {
        TreeNode* cur = root;
        while (cur) {
            if (cur->left) {
                // 找到左子树中的最右节点(当前节点的前驱)
                TreeNode* pre = cur->left;
                while (pre->right) pre = pre->right;

                // 将前驱的右指针指向当前节点的右子树
                pre->right = cur->right;

                // 将左子树移到右子树位置
                cur->right = cur->left;
                cur->left = NULL;
            }
            cur = cur->right;
        }
    }
};

复杂度分析

复杂度 说明
时间 O(n) 每个节点最多访问常数次
空间 O(1) 原地算法,无额外空间

三种方法对比

维度 方法一逆后序 方法二前序+栈 方法三莫里斯遍历
代码复杂度 极简 中等 复杂
时间复杂度 O(n) O(n) O(n)
空间复杂度 O(h) O(n) O(1)
面试推荐度 首选 容易想到 进阶加分
进阶要求 一般 一般 进阶挑战

面试追问 FAQ

问题 解答
Q1:为什么逆后序遍历(右-左-根)能得到正确的链表? 逆后序遍历的顺序是 6→4→3→5→2→1,这是原始二叉树节点值的倒序 。当我们每次把当前节点接到链表头(root->right = head; head = root)时,相当于在前面插入节点。倒序插入的结果就是正序:1→2→5→3→4→6,正好是先序遍历的顺序。
Q2:head 为什么要用成员变量而不是局部变量? 需要在递归的每一层共享和更新同一个链表头。如果用局部变量并在返回值中传递,每次递归返回后无法正确更新上层节点的 right 指针。使用成员变量确保所有递归层级访问同一个 head
Q3:如何理解倒着构建链表的过程? 想象链表初始为空 [6](处理节点6后),处理节点4时执行 4->right = 6,链表变为 [4→6],再处理节点3时执行 3->right = 4,链表变为 [3→4→6]。每次都在头部插入,所以最后得到正序。
Q4:进阶的 O(1) 空间原地算法怎么实现? 使用莫里斯遍历,利用左子树最右节点的空闲 right 指针指向当前节点的下一个节点,形成临时线索。这样在遍历完左子树后,可以通过这个临时线索回到右子树继续遍历,避免使用栈或递归。
Q5:莫里斯遍历的核心思想是什么? 对于每个有左子树的节点,找到左子树中的最右节点(前驱),将其 right 指针指向当前节点。这样在遍历完左子树后,可以通过这个临时线索回到右子树继续遍历,避免使用栈或递归。
Q6:如果要按标准前序遍历顺序展开,能否不用递归? 可以用栈模拟,参考方法二。核心思想是:先处理左子树,将左子树移到右子树位置,同时把原右子树入栈暂存。遍历完左子树后再从栈中取出原右子树继续处理。

相关题目

题目 难度 关键点
114. 二叉树展开为链表 中等 后序遍历,链表倒置
116. 填充每个节点的下一个右侧节点指针 中等 层序遍历,链表构建
144. 二叉树的前序遍历 简单 前序遍历模板
145. 二叉树的后序遍历 简单 后序遍历模板
剑指 Offer 36. 二叉搜索树与双向链表 中等 中序遍历,链表构建

总结

要点 说明
核心原理 逆后序遍历(6→4→3→5→2→1)倒着构建链表,得到先序遍历顺序(1→2→3→4→5→6)
关键技巧 root->right = head; head = root 在链表头部插入节点
时间复杂度 O(n),每个节点访问一次
空间复杂度 O(h),递归栈深度
进阶优化 莫里斯遍历实现 O(1) 空间原地算法

相关推荐
lwf0061641 小时前
PNN (Product-based Neural Network) 学习日记
算法·机器学习
ZPC82101 小时前
YOLO-3D + 双目相机 (RGB + 深度 + 点云) → 3D 位置 + 抓取姿态
人工智能·算法·计算机视觉·机器人
ZPC82101 小时前
YOLOv8-3D(3D 目标检测 + 6D 抓取姿态)
算法·机器人
bubiyoushang8881 小时前
基于 TGLVM 算法的迁移学习分类系统
算法·分类·迁移学习
Rabitebla1 小时前
深入理解 C++ STL:stack 和 queue 的底层原理与实现
c语言·开发语言·数据结构·c++·算法
通信仿真爱好者1 小时前
【无标题】
人工智能·算法·机器学习
落羽的落羽2 小时前
【算法札记】练习 | Week3
linux·服务器·数据结构·c++·人工智能·算法·动态规划
艾iYYY2 小时前
类和对象(详解初始化列表, static成员变量, 友元,内部类)
c语言·数据结构·c++·算法
AbandonForce2 小时前
C++11:列表初始化||右值和移动语义||引用折叠和完美转发||可变参数模板||lambda表达式||包装器(function bind)
开发语言·数据结构·c++·算法