🔗 题目链接:114. 二叉树展开为链表 - 力扣(LeetCode)
🔍 难度:中等 | 🏷️ 标签:树、深度优先搜索、链表、递归、原地算法
⏱️ 目标时间复杂度:O(n)
💾 空间复杂度:O(1)(进阶要求)
📌 题目分析
本题要求将一棵二叉树 原地展开为单链表,且满足以下两个条件:
- 链表使用原 TreeNode 节点,
left指针始终为nullptr,right指针指向下一个节点; - 链表顺序 = 二叉树的先序遍历顺序(根 → 左 → 右)。
这道题看似简单,但考察了对树结构操作、遍历顺序理解以及原地修改技巧的综合掌握。尤其在面试中,常作为“能否写出 O(1) 空间解法”的筛选题。
🧠 核心算法及代码讲解
✅ 寻找前驱节点(Morris 风格,O(1) 空间)
这是本题最优雅、最符合“进阶”要求的解法,灵感来源于 Morris 遍历——一种无需栈或递归即可实现树遍历的技巧。
🎯 核心思想:
-
对于当前节点
curr:-
如果它有左子树:
- 找到其左子树中最右边的节点(即前驱节点
predecessor); - 将
curr的右子树接到predecessor->right; - 将
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),也没有递归调用栈,仅用几个指针完成操作。
💡 为什么不会漏掉节点?
每次都将右子树“延迟”挂到左子树末尾,确保先序遍历顺序不变:根 → 整个左子树 → 原右子树。
🧩 解题思路(分步拆解)
-
明确目标:输出链表 = 先序遍历序列,且原地修改。
-
观察结构:先序遍历中,左子树全部访问完后才访问右子树。
-
关键洞察:左子树的最后一个节点(最右节点)应连接原右子树。
-
操作策略:
- 遍历每个节点;
- 若有左子树,找到其最右节点;
- 把当前右子树“嫁接”过去;
- 左子树整体移到右边;
- 继续向右走(此时右子树已包含原左+原右)。
-
终止条件:
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!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!