LeetCode 第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]
提示
- 树中结点数在范围
[0, 2000]内 -100 <= Node.val <= 100
解题思路
方法一:前序遍历 + 重建
最直观的方法是先对二叉树进行前序遍历,将所有节点保存在一个列表中,然后重建二叉树,使每个节点的左子节点为null,右子节点指向下一个节点。
关键点:
- 使用前序遍历获取节点顺序
- 重建二叉树,将每个节点的左子节点设为null,右子节点指向下一个节点
具体步骤:
- 如果根节点为空,直接返回
- 使用前序遍历将所有节点保存在列表中
- 遍历节点列表,将每个节点的左子节点设为null,右子节点指向下一个节点
- 最后一个节点的右子节点设为null
时间复杂度:O(n),其中n是树中节点的数量,需要遍历所有节点两次 空间复杂度:O(n),需要额外的列表存储所有节点
方法二:原地展开(迭代)
我们可以不使用额外空间,直接在原二叉树上进行操作。基本思路是对于当前节点,将其左子树插入到右子树的位置,然后将原来的右子树接到左子树的最右节点后面。
关键点:
- 对于当前节点,找到其左子树的最右节点
- 将当前节点的右子树接到左子树的最右节点后面
- 将当前节点的左子树移到右子树的位置
- 将当前节点的左子节点设为null
- 移动到下一个节点继续操作
具体步骤:
- 如果根节点为空,直接返回
- 从根节点开始,对于当前节点: a. 如果当前节点的左子节点不为空: i. 找到左子树的最右节点 ii. 将当前节点的右子树接到左子树的最右节点的右子节点 iii. 将当前节点的左子树移到右子树的位置 iv. 将当前节点的左子节点设为null b. 移动到当前节点的右子节点继续操作
时间复杂度:O(n),每个节点最多被访问两次 空间复杂度:O(1),只使用常数额外空间
方法三:后序遍历(递归)
我们可以使用后序遍历的方式,从右到左、从下到上地处理节点,将已经展开的右子树和左子树连接起来。
关键点:
- 后序遍历的顺序是左子树、右子树、根节点
- 对于每个节点,先将右子树展开,再将左子树展开
- 然后将展开后的左子树插入到根节点和展开后的右子树之间
具体步骤:
- 如果根节点为空,直接返回
- 递归展开右子树
- 递归展开左子树
- 如果左子树不为空: a. 找到展开后左子树的最右节点 b. 将展开后的右子树接到左子树的最右节点的右子节点 c. 将展开后的左子树移到右子树的位置 d. 将根节点的左子节点设为null
时间复杂度:O(n),每个节点最多被访问两次 空间复杂度:O(n),递归调用栈的深度
图解思路
方法一:前序遍历 + 重建过程
| 步骤 | 操作 | 结果 |
|---|---|---|
| 初始状态 | 原始二叉树 | |
| 前序遍历 | 获取节点顺序 | [1, 2, 3, 4, 5, 6] |
| 重建 | 将节点按前序顺序连接 | 1 -> 2 -> 3 -> 4 -> 5 -> 6 |
方法二:原地展开过程分析表
以示例1为例:
| 当前节点 | 操作 | 树的状态 |
|---|---|---|
| 1 | 找到左子树(2,3,4)的最右节点4,将右子树(5,6)接到4的右子节点,将左子树移到右子树位置,左子节点设为null | 1 -> 2 -> 3 -> 4 -> 5 -> 6 |
| 2 | 找到左子树(3)的最右节点3,将右子树(4,5,6)接到3的右子节点,将左子树移到右子树位置,左子节点设为null | 1 -> 2 -> 3 -> 4 -> 5 -> 6 |
| 3 | 左子节点为空,移动到右子节点 | 不变 |
| 4 | 左子节点为空,移动到右子节点 | 不变 |
| 5 | 左子节点为空,移动到右子节点 | 不变 |
| 6 | 左子节点为空,右子节点也为空,结束 | 不变 |
代码实现
C# 实现
/**
* Definition for a binary tree node.
* public class TreeNode {
* public int val;
* public TreeNode left;
* public TreeNode right;
* public TreeNode(int val=0, TreeNode left=null, TreeNode right=null) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
public class Solution {
// 方法一:前序遍历 + 重建
public void Flatten(TreeNode root) {
// 如果根节点为空,直接返回
if (root == null) {
return;
}
// 使用前序遍历获取节点顺序
List<TreeNode> nodes = new List<TreeNode>();
PreorderTraversal(root, nodes);
// 重建二叉树
for (int i = 0; i < nodes.Count - 1; i++) {
nodes[i].left = null;
nodes[i].right = nodes[i + 1];
}
// 设置最后一个节点
nodes[nodes.Count - 1].left = null;
nodes[nodes.Count - 1].right = null;
}
private void PreorderTraversal(TreeNode root, List<TreeNode> nodes) {
if (root == null) {
return;
}
nodes.Add(root);
PreorderTraversal(root.left, nodes);
PreorderTraversal(root.right, nodes);
}
// 方法二:原地展开(迭代)
public void FlattenIterative(TreeNode root) {
// 如果根节点为空,直接返回
if (root == null) {
return;
}
TreeNode current = root;
while (current != null) {
// 如果当前节点有左子树
if (current.left != null) {
// 找到左子树的最右节点
TreeNode rightmost = current.left;
while (rightmost.right != null) {
rightmost = rightmost.right;
}
// 将当前节点的右子树接到左子树的最右节点后面
rightmost.right = current.right;
// 将左子树移到右子树的位置
current.right = current.left;
// 将左子节点设为null
current.left = null;
}
// 移动到下一个节点
current = current.right;
}
}
// 方法三:后序遍历(递归)
public void FlattenPostorder(TreeNode root) {
// 如果根节点为空,直接返回
if (root == null) {
return;
}
// 递归展开右子树
FlattenPostorder(root.right);
// 递归展开左子树
FlattenPostorder(root.left);
// 如果左子树不为空
if (root.left != null) {
// 找到展开后左子树的最右节点
TreeNode rightmost = root.left;
while (rightmost.right != null) {
rightmost = rightmost.right;
}
// 将展开后的右子树接到左子树的最右节点后面
rightmost.right = root.right;
// 将展开后的左子树移到右子树的位置
root.right = root.left;
// 将左子节点设为null
root.left = null;
}
}
}
Python 实现
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
# 方法一:前序遍历 + 重建
def flatten(self, root: Optional[TreeNode]) -> None:
"""
Do not return anything, modify root in-place instead.
"""
# 如果根节点为空,直接返回
if not root:
return
# 使用前序遍历获取节点顺序
nodes = []
def preorder_traversal(node):
if not node:
return
nodes.append(node)
preorder_traversal(node.left)
preorder_traversal(node.right)
preorder_traversal(root)
# 重建二叉树
for i in range(len(nodes) - 1):
nodes[i].left = None
nodes[i].right = nodes[i + 1]
# 设置最后一个节点
nodes[-1].left = None
nodes[-1].right = None
# 方法二:原地展开(迭代)
def flattenIterative(self, root: Optional[TreeNode]) -> None:
# 如果根节点为空,直接返回
if not root:
return
current = root
while current:
# 如果当前节点有左子树
if current.left:
# 找到左子树的最右节点
rightmost = current.left
while rightmost.right:
rightmost = rightmost.right
# 将当前节点的右子树接到左子树的最右节点后面
rightmost.right = current.right
# 将左子树移到右子树的位置
current.right = current.left
# 将左子节点设为None
current.left = None
# 移动到下一个节点
current = current.right
# 方法三:后序遍历(递归)
def flattenPostorder(self, root: Optional[TreeNode]) -> None:
# 如果根节点为空,直接返回
if not root:
return
# 递归展开右子树
self.flattenPostorder(root.right)
# 递归展开左子树
self.flattenPostorder(root.left)
# 如果左子树不为空
if root.left:
# 找到展开后左子树的最右节点
rightmost = root.left
while rightmost.right:
rightmost = rightmost.right
# 将展开后的右子树接到左子树的最右节点后面
rightmost.right = root.right
# 将展开后的左子树移到右子树的位置
root.right = root.left
# 将左子节点设为None
root.left = None
C++ 实现
/**
* 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) {
// 如果根节点为空,直接返回
if (root == nullptr) {
return;
}
// 使用前序遍历获取节点顺序
vector<TreeNode*> nodes;
preorderTraversal(root, nodes);
// 重建二叉树
for (int i = 0; i < nodes.size() - 1; i++) {
nodes[i]->left = nullptr;
nodes[i]->right = nodes[i + 1];
}
// 设置最后一个节点
nodes.back()->left = nullptr;
nodes.back()->right = nullptr;
}
private:
void preorderTraversal(TreeNode* root, vector<TreeNode*>& nodes) {
if (root == nullptr) {
return;
}
nodes.push_back(root);
preorderTraversal(root->left, nodes);
preorderTraversal(root->right, nodes);
}
public:
// 方法二:原地展开(迭代)
void flattenIterative(TreeNode* root) {
// 如果根节点为空,直接返回
if (root == nullptr) {
return;
}
TreeNode* current = root;
while (current != nullptr) {
// 如果当前节点有左子树
if (current->left != nullptr) {
// 找到左子树的最右节点
TreeNode* rightmost = current->left;
while (rightmost->right != nullptr) {
rightmost = rightmost->right;
}
// 将当前节点的右子树接到左子树的最右节点后面
rightmost->right = current->right;
// 将左子树移到右子树的位置
current->right = current->left;
// 将左子节点设为nullptr
current->left = nullptr;
}
// 移动到下一个节点
current = current->right;
}
}
// 方法三:后序遍历(递归)
void flattenPostorder(TreeNode* root) {
// 如果根节点为空,直接返回
if (root == nullptr) {
return;
}
// 递归展开右子树
flattenPostorder(root->right);
// 递归展开左子树
flattenPostorder(root->left);
// 如果左子树不为空
if (root->left != nullptr) {
// 找到展开后左子树的最右节点
TreeNode* rightmost = root->left;
while (rightmost->right != nullptr) {
rightmost = rightmost->right;
}
// 将展开后的右子树接到左子树的最右节点后面
rightmost->right = root->right;
// 将展开后的左子树移到右子树的位置
root->right = root->left;
// 将左子节点设为nullptr
root->left = nullptr;
}
}
};
执行结果
C# 实现
- 执行用时:88 ms
- 内存消耗:38.2 MB
Python 实现
- 执行用时:36 ms
- 内存消耗:16.4 MB
C++ 实现
- 执行用时:4 ms
- 内存消耗:12.6 MB
性能对比
| 语言 | 执行用时 | 内存消耗 | 特点 |
|---|---|---|---|
| C# | 88 ms | 38.2 MB | 代码结构清晰,但性能较慢 |
| Python | 36 ms | 16.4 MB | 代码简洁,性能适中 |
| C++ | 4 ms | 12.6 MB | 执行速度最快,内存占用最少 |
代码亮点
- 🎯 原地展开方法避免了额外空间的使用,直接在原二叉树上操作
- 💡 后序遍历方法利用了递归的特性,从下到上地处理节点,思路巧妙
- 🔍 正确处理了各种边界情况,如空树和只有一个节点的树
- 🎨 三种不同的实现方法各有优势,适用于不同的场景
常见错误分析
- 🚫 忘记处理左子树为空的情况,导致无限循环
- 🚫 在重建过程中没有正确设置最后一个节点的右子节点为null
- 🚫 在查找最右节点时没有正确遍历到最后一个节点
- 🚫 没有正确理解前序遍历的顺序,导致展开后的链表顺序错误
解法对比
| 解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 前序遍历 + 重建 | O(n) | O(n) | 实现简单,思路清晰 | 需要额外空间存储节点 |
| 原地展开(迭代) | O(n) | O(1) | 不需要额外空间,效率高 | 实现稍复杂 |
| 后序遍历(递归) | O(n) | O(n) | 代码简洁,思路巧妙 | 递归调用栈可能导致栈溢出 |