【二叉树-10】114.二叉树展开为链表

题目描述:

给你二叉树的根结点 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]

解题思路:

方法一:递归(后序遍历)

核心思路:

对于任意节点 root:

  1. 递归展开左子树,得到左子树链表

  2. 递归展开右子树,得到右子树链表

  3. 拼接:

    • 把左子树链表接到 root->right

    • 把右子树链表接到左子树链表的末尾

    • root->left = nullptr

具体过程示例:

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

第1步: 递归展开左子树(2)
       2
        \
         3
          \
           4

第2步: 递归展开右子树(5)
       5
        \
         6

第3步: 拼接
       1
        \
         2
          \
           3
            \
             4
              \
               5
                \
                 6 ✅

代码实现:

写法1:后序遍历(推荐)
cpp 复制代码
class Solution {
public:
    void flatten(TreeNode* root) {
        if (root == nullptr) return;
        
        // 先递归展开左右子树
        flatten(root->left);
        flatten(root->right);
        
        // 保存右子树
        TreeNode* right = root->right;
        
        // 把左子树接到右边
        root->right = root->left;
        root->left = nullptr;
        
        // 找到当前右子树的末尾,接上原来的右子树
        TreeNode* curr = root;
        while (curr->right != nullptr) {
            curr = curr->right;
        }
        curr->right = right;
    }
};
写法2:前序遍历(用栈)
cpp 复制代码
class Solution {
public:
    void flatten(TreeNode* root) {
        if (root == nullptr) return;
        
        stack<TreeNode*> stk;
        stk.push(root);
        TreeNode* prev = nullptr;
        
        while (!stk.empty()) {
            TreeNode* curr = stk.top();
            stk.pop();
            
            if (prev != nullptr) {
                prev->right = curr;
                prev->left = nullptr;
            }
            
            // 先压右,再压左(保证左先出栈)
            if (curr->right) stk.push(curr->right);
            if (curr->left) stk.push(curr->left);
            
            prev = curr;
        }
    }
};

复杂度分析:

写法1(后序递归)
维度 复杂度 说明
时间复杂度 O(n²) 每次找右子树末尾需要 O(n)
空间复杂度 O(h) 递归栈深度

问题 :找右子树末尾的 while 循环导致 O(n²)。

写法2(前序栈)
维度 复杂度 说明
时间复杂度 O(n) 每个节点入栈出栈各一次
空间复杂度 O(n) 栈最多存储 n 个节点

关键细节:

1. 为什么后序递归(全局 prev)能 O(n)?
  • 遍历顺序:右 → 左 → 根

  • 每次处理当前节点时,prev 已经指向了"前序遍历中的下一个节点"

  • 直接把 root->right = prev 即可,不需要找末尾

2. 图解后序递归(全局 prev)
复制代码
    1
   / \
  2   5
 / \   \
3   4   6

遍历顺序: 6 → 5 → 4 → 3 → 2 → 1

处理6: prev=null, 6->right=null, prev=6
处理5: prev=6, 5->right=6, prev=5
处理4: prev=5, 4->right=5, prev=4
处理3: prev=4, 3->right=4, prev=3
处理2: prev=3, 2->right=3, prev=2
处理1: prev=2, 1->right=2, prev=1

结果: 1 → 2 → 3 → 4 → 5 → 6 ✅
3. 为什么前序栈要"先压右,再压左"?

因为栈是后进先出:

  • 先压右,右在栈底

  • 再压左,左在栈顶

  • 弹出时先弹出左,符合前序顺序

方法二:找左子树的最右节点(原地算法)

核心思路:

对于每个节点 root:

  1. 如果 root->left == nullptr,直接跳到 root->right

  2. 如果 root->left != nullptr:

    • 找到左子树的最右节点(前序前驱)

    • 把 root->right 接到这个最右节点的右边

    • 把 root->left 移到 root->right

    • root->left = nullptr

    • 继续处理新的 root->right

具体过程示例:

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

处理节点1:
  左子树是2,找左子树的最右节点 → 4
  把 1->right (5) 接到 4->right
  把 1->left (2) 移到 1->right
  1->left = nullptr

  1
   \
    2
   / \
  3   4
       \
        5
         \
          6

处理节点2:
  左子树是3,找左子树的最右节点 → 3
  把 2->right (4) 接到 3->right
  把 2->left (3) 移到 2->right
  2->left = nullptr

  1
   \
    2
     \
      3
       \
        4
         \
          5
           \
            6

继续处理3、4、5、6,最终得到:
1 → 2 → 3 → 4 → 5 → 6 ✅

代码实现:

cpp 复制代码
class Solution {
public:
    void flatten(TreeNode* root) {
        TreeNode* curr = root;
        
        while (curr != nullptr) {
            if (curr->left != nullptr) {
                // 找到左子树的最右节点(前序前驱)
                TreeNode* prev = curr->left;
                while (prev->right != nullptr) {
                    prev = prev->right;
                }
                
                // 把当前节点的右子树接到前驱的右边
                prev->right = curr->right;
                
                // 把左子树移到右边
                curr->right = curr->left;
                curr->left = nullptr;
            }
            
            // 继续处理下一个节点
            curr = curr->right;
        }
    }
};

复杂度分析:

维度 复杂度 说明
时间复杂度 O(n) 每个节点最多被访问两次
空间复杂度 O(1) 只用了几个指针

为什么是 O(n)?

  • 外层 while 遍历每个节点一次

  • 内层 while 找最右节点,但每条边最多被走两次

  • 总操作次数 O(n)

关键细节:

1. 为什么找"左子树的最右节点"?

因为前序遍历的顺序是:根 → 左子树 → 右子树

  • 左子树的最后一个节点(最右节点)就是右子树的前驱

  • 把右子树接到它后面,正好符合前序顺序

2. 为什么 curr = curr->right 不会死循环?

每次处理完当前节点后:

  • curr->right 指向了左子树的根

  • curr->left 被置空

  • 所以 curr = curr->right 会走到左子树的根,继续处理

  • 最终会走到 nullptr,循环结束

3. 为什么时间复杂度是 O(n) 而不是 O(n²)?

虽然内层 while 看起来可能很耗时,但:

  • 每条边最多被走两次(一次找最右节点,一次遍历)

  • 总操作次数与节点数成正比

  • 所以是 O(n)

三种方法对比:

方法 时间复杂度 空间复杂度 是否原地 推荐度
后序递归(全局 prev) O(n) O(h) ❌ 递归栈 ⭐⭐⭐⭐⭐
前序栈 O(n) O(n) ❌ 栈 ⭐⭐⭐⭐
原地算法 O(n) O(1) ✅ 原地 ⭐⭐⭐⭐⭐

总结:

要点 说明
核心思想 找左子树最右节点,把右子树接过去
关键操作 prev->right = curr->right; curr->right = curr->left; curr->left = nullptr
时间复杂度 O(n)
空间复杂度 O(1)
相关推荐
叠层归一研究院1 小时前
区分预算与通道诱导:Ω–L叠层体系的观测赋值与定量验证
人工智能·算法
多弗朗皮卡丘1 小时前
C++string类
c++
BYSJMG1 小时前
计算机毕业设计选题推荐:基于大数据的快递物流运营数据分析与可视化,Spark与K-Means
大数据·算法·数据分析·spark·课程设计
阳明山水2 小时前
因果一致性正则化实战解析
人工智能·深度学习·算法·机器学习·架构
Yunovian2 小时前
AI时代,针对模型与应用,浅谈一下各编程语言
开发语言·c++·人工智能·python·ai·rust·ai编程
黄金龙PLUS3 小时前
5个1024比特大状态置换算法的设计与分析
算法·网络安全·密码学·哈希算法·同态加密
心中有你02143 小时前
C语言埃氏筛法(素数筛选)
c语言·数据结构·算法
Omics Pro4 小时前
Cell封面|衰老生物学开源AI工具包
数据库·人工智能·算法·机器学习·自然语言处理
释厄6236 小时前
多黑洞具现论·物理/生物/金融三域显化·虚拟宇宙黑洞·奇点=0 圆圈
开发语言·数据结构·人工智能·程序人生·安全威胁分析