可以,这题你目前掌握的两种写法分别是:
- 前序遍历 + vector 保存节点
- 右→左→根递归 + 头插法原地展开
解法一:前序遍历 + 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;
对应一句话:
右左根递归,当前节点做头插。
而你写递归时最重要的思想就是:
相信递归能处理好左右子树,我只负责当前这一层的逻辑。