力扣解题-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
进阶:你可以使用原地算法(O(1) 额外空间)展开这棵树吗?
Related Topics
栈、树、深度优先搜索、链表、二叉树
第一次解答
解题思路
核心方法:递归版莫里斯遍历思想(前驱节点法),利用“寻找左子树最右节点作为前驱”的核心逻辑,递归地将左子树挂载到根节点右侧、原右子树挂载到左子树最右节点右侧,最终实现先序遍历的单链表展开,空间复杂度为O(h)(h为树的高度,递归栈开销),时间复杂度O(n)。
核心逻辑拆解
二叉树展开为单链表的核心是“调整指针指向”,要求最终链表顺序与先序遍历(根→左→右)一致,核心步骤:
- 递归终止条件:若当前节点
root == null,直接返回(空节点无需处理); - 找前驱节点:若当前节点有左子树,找到左子树的最右节点(该节点是当前节点在“先序遍历”中的直接后继,称为前驱节点);
- 调整指针:
- 将当前节点的右子树(原右子树)挂载到前驱节点的右侧;
- 将当前节点的左子树移到右侧(替代原右子树);
- 将当前节点的左指针置为null(满足链表左指针为空的要求);
- 递归处理下一个节点:将当前节点指针移到新的右子节点(原左子树的根),递归处理该节点。
具体步骤(以示例1 root=[1,2,5,3,4,null,6]为例)
| 递归层级 | 当前节点 | 操作 | 指针调整结果 |
|---|---|---|---|
| 1 | 1 | 找左子树(2)最右节点4 | 4的right=5(原1的right) |
| 1 | 1 | 1的right=2,1的left=null | 1→2→3→4→5→6 |
| 2 | 2 | 找左子树(3)最右节点3 | 3的right=4(原2的right) |
| 2 | 2 | 2的right=3,2的left=null | 1→2→3→4→5→6 |
| 3 | 3 | 无左子树,直接递归右节点 | - |
| 3 | 4 | 无左子树,直接递归右节点 | - |
| 2 | 5 | 找左子树(null),递归右节点 | - |
| 3 | 6 | 无左子树,递归结束 | - |
| 最终链表:1→2→3→4→5→6(左指针均为null),符合先序遍历顺序。 |
性能说明
- 时间复杂度:O(n)(每个节点仅被访问两次:一次找前驱节点,一次递归处理,n为节点总数);
- 空间复杂度:O(h)(递归栈深度等于树的高度,平衡树O(logn),斜树O(n));
- 优势:
- 递归逻辑贴合二叉树的结构特性,易理解;
- 原地调整指针,无额外数据结构开销(除递归栈);
- 天然处理空树、单节点树等边界场景。
public void flatten(TreeNode root) {
flattenTress(root);
}
public void flattenTress(TreeNode root){
if(root==null){
return;
}
TreeNode curr=root;
if(curr.left!=null){
//找到最右节点
TreeNode predecessor=curr.left;
while(predecessor.right!=null){
predecessor=predecessor.right;
}
predecessor.right=curr.right;
curr.right=curr.left;
curr.left=null;
}
curr=curr.right;
flattenTress(curr);
}
示例解答
解题思路
解法1:迭代版莫里斯遍历(原地算法,O(1)空间)
核心方法:迭代版前驱节点法,完全复刻递归版的核心逻辑,但通过循环替代递归,消除递归栈开销,实现进阶要求的O(1)额外空间复杂度,是本题的最优解法。
代码实现
public void flatten(TreeNode root) {
if(root==null){
return;
}
TreeNode curr = root;
while(curr!=null){
if(curr.left!=null){
//找到左边子树的最右节点
TreeNode predecessor=curr.left;
while(predecessor.right!=null){
predecessor=predecessor.right;
}
//把右子树接到最右节点上
predecessor.right=curr.right;
//把左子树移到右边
curr.right=curr.left;
curr.left=null;
}
//向右移动,这时候的right已经包含原来的左子树
curr=curr.right;
}
}
核心逻辑说明
- 迭代终止条件:
curr == null(遍历到链表末尾); - 循环处理每个节点:
- 若当前节点有左子树,找到左子树最右节点(前驱节点);
- 调整指针:前驱节点的right指向当前节点的右子树,当前节点的right指向左子树,左指针置null;
- 移动当前节点到新的右子节点,继续循环;
- 核心优势:完全原地操作,无递归栈/额外数据结构开销,满足进阶的O(1)空间要求。
性能说明
- 时间复杂度:O(n)(每个节点仅被访问两次:一次找前驱节点,一次迭代处理);
- 空间复杂度:O(1)(仅使用指针变量,无额外空间);
- 优势:
- 满足进阶的原地算法要求,空间效率最优;
- 迭代逻辑无栈溢出风险,适合树高度较大的场景;
- 代码简洁,指针调整逻辑与递归版完全一致。
解法2:栈模拟先序遍历(易理解,O(n)空间)
核心方法:栈辅助先序遍历,先通过栈实现二叉树的先序遍历,记录所有节点的顺序,再遍历节点列表调整指针指向,逻辑直观但需要额外的O(n)空间存储节点,不满足进阶要求。
代码实现
import java.util.Stack;
import java.util.ArrayList;
import java.util.List;
public void flatten(TreeNode root) {
if (root == null) {
return;
}
// 步骤1:栈模拟先序遍历,记录节点顺序
Stack<TreeNode> stack = new Stack<>();
List<TreeNode> nodeList = new ArrayList<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode curr = stack.pop();
nodeList.add(curr);
// 先压右子树,后压左子树(栈后进先出,保证先序顺序)
if (curr.right != null) {
stack.push(curr.right);
}
if (curr.left != null) {
stack.push(curr.left);
}
}
// 步骤2:调整指针,构建单链表
for (int i = 0; i < nodeList.size() - 1; i++) {
TreeNode curr = nodeList.get(i);
curr.left = null;
curr.right = nodeList.get(i + 1);
}
// 最后一个节点的左右指针置null(冗余但更健壮)
if (!nodeList.isEmpty()) {
TreeNode last = nodeList.get(nodeList.size() - 1);
last.left = null;
last.right = null;
}
}
核心逻辑说明
- 栈模拟先序遍历:利用栈“后进先出”的特性,先压右子树、后压左子树,保证弹出顺序为“根→左→右”;
- 记录节点顺序:将先序遍历的节点存入列表,保证顺序正确;
- 调整指针:遍历节点列表,将每个节点的左指针置null,右指针指向下一个节点。
性能说明
- 时间复杂度:O(n)(先序遍历O(n) + 调整指针O(n));
- 空间复杂度:O(n)(栈最多存储n个节点 + 列表存储n个节点);
- 优势:逻辑直观,新手易理解,无需处理复杂的前驱节点查找;
- 劣势:额外空间开销大,不满足进阶的O(1)空间要求。
总结
- 迭代版莫里斯遍历(最优解):O(n)时间+O(1)空间,满足进阶要求,原地调整指针,工程中首选;
- 递归版前驱节点法:O(n)时间+O(h)空间,逻辑易理解,适合新手入门;
- 栈模拟先序遍历:O(n)时间+O(n)空间,逻辑最直观,但空间效率低;
- 关键技巧:
- 核心思想:展开链表的本质是“先序遍历顺序+左指针置null+右指针串联”;
- 原地算法关键:找到左子树最右节点作为前驱,将原右子树挂载到前驱右侧;
- 方法选择:优先选迭代版莫里斯遍历(空间最优),新手可先掌握栈模拟法理解核心逻辑。