leetcode 114二叉树展开为链表

可以,这题你目前掌握的两种写法分别是:

  1. 前序遍历 + vector 保存节点
  2. 右→左→根递归 + 头插法原地展开

解法一:前序遍历 + vector

代码

复制代码
/**
 * 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:
    void flatten(TreeNode* root) {

        // 保存前序遍历得到的节点
        vector<TreeNode*> res;

        // 前序遍历:根 -> 左 -> 右
        dfs(root, res);

        // 按照前序遍历的顺序重新连接节点
        for (int i = 1; i < res.size(); i++) {

            // 前一个节点
            TreeNode* pre = res[i - 1];

            // 当前节点
            TreeNode* cur = res[i];

            // 展开后左指针全部为空
            pre->left = nullptr;

            // 前一个节点的 right 指向当前节点
            pre->right = cur;
        }
    }

    // 前序遍历
    void dfs(TreeNode* root, vector<TreeNode*>& res) {

        // 递归结束条件
        if (root == nullptr) {
            return;
        }

        // 根
        res.push_back(root);

        // 左
        dfs(root->left, res);

        // 右
        dfs(root->right, res);
    }
};

思路总结

先做一次:

复制代码
根 -> 左 -> 右

也就是前序遍历。

例如:

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

保存以后:

复制代码
res = [1, 2, 3, 4, 5, 6]

然后再按照 res 中的顺序连接:

复制代码
1 -> 2 -> 3 -> 4 -> 5 -> 6

每次:

复制代码
pre->left = nullptr;
pre->right = cur;

易错点

第一个易错点是,vector 里存的不是节点值:

复制代码
vector<int> res;       // ❌

而是节点指针:

复制代码
vector<TreeNode*> res; // ✅

因为后面要修改原来的树。

第二个易错点是前序遍历顺序一定是:

复制代码
res.push_back(root);
dfs(root->left, res);
dfs(root->right, res);

也就是:

复制代码
根 -> 左 -> 右

不能写乱。

第三个易错点是连接时从:

复制代码
i = 1

开始,因为要取:

复制代码
res[i - 1]

如果从 0 开始:

复制代码
res[i - 1]

就越界了。

第四个易错点是别忘了:

复制代码
pre->left = nullptr;

因为题目要求展开之后所有 left 都必须为空。

复杂度

时间复杂度:

复制代码
O(n)

空间复杂度:

复制代码
O(n)

因为额外用了 vector 保存全部节点。


解法二:右 → 左 → 根 + 头插法

这个方法更巧。

代码

复制代码
/**
 * 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:

    // pre 指向当前已经展开好的链表头
    TreeNode* pre = nullptr;

    void flatten(TreeNode* root) {

        // 递归结束条件
        if (root == nullptr) {
            return;
        }

        // 先相信递归能把右子树处理好
        flatten(root->right);

        // 再相信递归能把左子树处理好
        flatten(root->left);

        // -----------------------
        // 下面处理当前 root 节点
        // -----------------------

        // 当前节点的右边接上已经处理好的链表
        root->right = pre;

        // 展开以后不能有左孩子
        root->left = nullptr;

        // 当前 root 成为新的链表头
        pre = root;
    }
};

思路总结

题目最终想得到的是:

复制代码
根 -> 左 -> 右

也就是前序遍历顺序。

但是这个方法不是从前往后连接,而是使用头插法,从后往前构造

所以处理顺序要反过来:

复制代码
右 -> 左 -> 根

例如:

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

实际处理节点的顺序是:

复制代码
6 -> 5 -> 4 -> 3 -> 2 -> 1

假设现在已经处理好了:

复制代码
pre
 ↓
 5 -> 6

当前:

复制代码
root = 4

执行:

复制代码
root->right = pre;

得到:

复制代码
4 -> 5 -> 6

然后:

复制代码
pre = root;

变成:

复制代码
pre
 ↓
 4 -> 5 -> 6

所以本质就是不断头插。


这三句一定要理解

复制代码
root->right = pre;
root->left = nullptr;
pre = root;

可以翻译成:

复制代码
当前 root 接到已有链表前面
        ↓
清空左指针
        ↓
当前 root 成为新的链表头

所以你可以记:

右左根 + 头插法。


为什么先右再左?

因为最终需要:

复制代码
根 -> 左 -> 右

而你是在反着构造

所以反过来就是:

复制代码
右 -> 左 -> 根

因此:

复制代码
flatten(root->right);
flatten(root->left);

顺序不能随便调换。


易错点

第一个最重要:

1. pre 必须初始化

应该:

复制代码
TreeNode* pre = nullptr;

不要写:

复制代码
TreeNode* pre;   // ❌ 未初始化

否则里面可能是随机地址。


2. 递归顺序不能反

正确:

复制代码
flatten(root->right);
flatten(root->left);

不能写成:

复制代码
flatten(root->left);
flatten(root->right); // ❌

否则最终链表顺序会错误。


3. 修改顺序最好保持这样

推荐:

复制代码
root->right = pre;
root->left = nullptr;
pre = root;

最核心的是:

复制代码
pre = root;

一定放在当前节点连接完成之后。

因为 pre 表示:

已经处理好的链表头。


4. 不能写 root->next

这题的节点结构只有:

复制代码
root->left
root->right

没有:

复制代码
root->next

虽然最终展开后看起来像链表:

复制代码
1 -> 2 -> 3

但实际上使用的是:

复制代码
1.right = 2
2.right = 3

所以:

复制代码
root->right

相当于链表里的 next


两种方法对比

方法 核心思路 额外空间 难度
前序遍历 + vector 先保存顺序,再连接 O(n) 简单
右左根 + 头插法 从后往前直接连接 O(h) 递归栈 稍难

第一种非常直观:

复制代码
先记下来
再连起来

第二种更适合练递归:

复制代码
相信右子树
相信左子树
处理当前 root

你现在可以把第二种牢牢记成:

复制代码
flatten(root->right);
flatten(root->left);

root->right = pre;
root->left = nullptr;
pre = root;

对应一句话:

右左根递归,当前节点做头插。

而你写递归时最重要的思想就是:

相信递归能处理好左右子树,我只负责当前这一层的逻辑。

相关推荐
cz07101 小时前
hot100_搜索二维矩阵 II
算法·leetcode
圣保罗的大教堂3 小时前
leetcode 3718. 缺失的最小倍数 简单
leetcode
青 春 记 忆3 小时前
LeetCode 234. 回文链表|Python 解法详解
python·leetcode·链表
啊嘞嘞?5 小时前
力扣(LRU缓存)
算法·leetcode
珂朵莉MM5 小时前
2026国信杯具身智能创新大赛-编程技能赛--本科组国赛解题报告 | 珂学家
算法·深度优先·图论
啊嘞嘞?5 小时前
力扣(下一个排列)
算法·leetcode
Forever Nore6 小时前
LeetCode 16 最接近的三数之和 - 双指针逼近
算法·leetcode·职场和发展
smj2302_796826526 小时前
解决leetcode第4033题有效K个不同元素子数组I
数据结构·python·算法·leetcode
rannn_1117 小时前
【力扣hot100】回溯专题|全排列、子集、字母组合、组合总和、括号生成、单词搜索、分割回文串、N皇后
java·算法·leetcode·回溯