【LeetCode Hot100 刷题日记 (46/100)】114. 二叉树展开为链表 —— 原地展开与前序遍历的深度联动🌳

4 阅读4分钟

🔗 题目链接114. 二叉树展开为链表 - 力扣(LeetCode)

🔍 难度:中等 | 🏷️ 标签:树、深度优先搜索、链表、递归、原地算法

⏱️ 目标时间复杂度:O(n)

💾 空间复杂度:O(1)(进阶要求)


📌 题目分析

本题要求将一棵二叉树 原地展开为单链表,且满足以下两个条件:

  1. 链表使用原 TreeNode 节点left 指针始终为 nullptrright 指针指向下一个节点;
  2. 链表顺序 = 二叉树的先序遍历顺序(根 → 左 → 右)。

这道题看似简单,但考察了对树结构操作遍历顺序理解以及原地修改技巧的综合掌握。尤其在面试中,常作为“能否写出 O(1) 空间解法”的筛选题。


🧠 核心算法及代码讲解

✅ 寻找前驱节点(Morris 风格,O(1) 空间)

这是本题最优雅、最符合“进阶”要求的解法,灵感来源于 Morris 遍历——一种无需栈或递归即可实现树遍历的技巧。

🎯 核心思想:

  • 对于当前节点 curr

    • 如果它有左子树:

      1. 找到其左子树中最右边的节点(即前驱节点 predecessor);
      2. curr 的右子树接到 predecessor->right
      3. curr->left 移到 curr->right,并置 curr->left = nullptr
    • 然后移动到 curr->right,继续处理。

这样做的本质是:把右子树“挂”到左子树的末尾,从而在后续遍历时自然接上,完全保留了先序遍历的顺序。

📜 C++ 代码(带详细行注释):

void flatten(TreeNode* root) {
    TreeNode* curr = root;
    while (curr != nullptr) {
        if (curr->left != nullptr) {
            // Step 1: 获取左子树的根
            TreeNode* next = curr->left;
            // Step 2: 找到左子树的最右节点(前驱)
            TreeNode* predecessor = next;
            while (predecessor->right != nullptr) {
                predecessor = predecessor->right;
            }
            // Step 3: 将原右子树接到前驱的 right
            predecessor->right = curr->right;
            // Step 4: 将左子树移到右边,并清空 left
            curr->left = nullptr;
            curr->right = next;
        }
        // Step 5: 继续处理下一个节点(现在在 right 上)
        curr = curr->right;
    }
}

💡 为什么这是 O(1) 空间?
因为我们没有使用任何额外容器(如 vector、stack),也没有递归调用栈,仅用几个指针完成操作。

💡 为什么不会漏掉节点?
每次都将右子树“延迟”挂到左子树末尾,确保先序遍历顺序不变:根 → 整个左子树 → 原右子树。


🧩 解题思路(分步拆解)

  1. 明确目标:输出链表 = 先序遍历序列,且原地修改。

  2. 观察结构:先序遍历中,左子树全部访问完后才访问右子树。

  3. 关键洞察:左子树的最后一个节点(最右节点)应连接原右子树。

  4. 操作策略

    • 遍历每个节点;
    • 若有左子树,找到其最右节点;
    • 把当前右子树“嫁接”过去;
    • 左子树整体移到右边;
    • 继续向右走(此时右子树已包含原左+原右)。
  5. 终止条件curr == nullptr,即走到链表末尾。


📊 算法分析

方法时间复杂度空间复杂度是否原地面试推荐度
前序遍历 + 重建O(n)O(n)⭐⭐
迭代同步展开O(n)O(n)⭐⭐⭐
前驱节点法O(n)O(1)⭐⭐⭐⭐⭐
  • 时间复杂度 O(n) :每个节点最多被访问两次(一次主循环,一次找前驱),仍是线性。
  • 空间复杂度 O(1) :仅用常数个指针,满足进阶要求。
  • 面试价值高:展示你对树结构、遍历顺序、指针操作的深刻理解。

🎯 面试高频追问

  • “如果要求后序遍历展开,怎么做?”
  • “能否用递归实现 O(1) 空间?”(答案:不能,递归隐式使用栈)
  • “这种方法会破坏原树结构吗?”(会,但题目允许)

💻 代码

✅ C++

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

// 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) {
        TreeNode* curr = root;
        while (curr != nullptr) {
            if (curr->left != nullptr) {
                TreeNode* next = curr->left;
                TreeNode* predecessor = next;
                while (predecessor->right != nullptr) {
                    predecessor = predecessor->right;
                }
                predecessor->right = curr->right;
                curr->left = nullptr;
                curr->right = next;
            }
            curr = curr->right;
        }
    }
};

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    // 构建示例1: [1,2,5,3,4,null,6]
    TreeNode* root = new TreeNode(1);
    root->left = new TreeNode(2);
    root->right = new TreeNode(5);
    root->left->left = new TreeNode(3);
    root->left->right = new TreeNode(4);
    root->right->right = new TreeNode(6);

    Solution sol;
    sol.flatten(root);

    // 输出链表(只打印 right 路径)
    TreeNode* p = root;
    while (p) {
        cout << p->val << " ";
        p = p->right;
    }
    // 预期输出: 1 2 3 4 5 6

    return 0;
}

✅ JavaScript

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */

/**
 * @param {TreeNode} root
 * @return {void} Do not return anything, modify root in-place instead.
 */
var flatten = function(root) {
    let curr = root;
    while (curr !== null) {
        if (curr.left !== null) {
            // 找到左子树的最右节点(前驱)
            let next = curr.left;
            let predecessor = next;
            while (predecessor.right !== null) {
                predecessor = predecessor.right;
            }
            // 将原右子树接到前驱的 right
            predecessor.right = curr.right;
            // 左子树整体移到右边
            curr.left = null;
            curr.right = next;
        }
        // 继续向右处理
        curr = curr.right;
    }
};

🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!