# 力扣解题-114. 二叉树展开为链表

0 阅读6分钟

力扣解题-114. 二叉树展开为链表

给你二叉树的根结点 root ,请你将它展开为一个单链表: 展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null 。 展开后的单链表应该与二叉树 先序遍历 顺序相同。

示例 1:

image.png

输入: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)。

核心逻辑拆解

二叉树展开为单链表的核心是“调整指针指向”,要求最终链表顺序与先序遍历(根→左→右)一致,核心步骤:

  1. 递归终止条件:若当前节点root == null,直接返回(空节点无需处理);
  2. 找前驱节点:若当前节点有左子树,找到左子树的最右节点(该节点是当前节点在“先序遍历”中的直接后继,称为前驱节点);
  3. 调整指针
    • 将当前节点的右子树(原右子树)挂载到前驱节点的右侧;
    • 将当前节点的左子树移到右侧(替代原右子树);
    • 将当前节点的左指针置为null(满足链表左指针为空的要求);
  4. 递归处理下一个节点:将当前节点指针移到新的右子节点(原左子树的根),递归处理该节点。
具体步骤(以示例1 root=[1,2,5,3,4,null,6]为例)
递归层级当前节点操作指针调整结果
11找左子树(2)最右节点44的right=5(原1的right)
111的right=2,1的left=null1→2→3→4→5→6
22找左子树(3)最右节点33的right=4(原2的right)
222的right=3,2的left=null1→2→3→4→5→6
33无左子树,直接递归右节点-
34无左子树,直接递归右节点-
25找左子树(null),递归右节点-
36无左子树,递归结束-
最终链表:1→2→3→4→5→6(左指针均为null),符合先序遍历顺序。
性能说明
  • 时间复杂度:O(n)(每个节点仅被访问两次:一次找前驱节点,一次递归处理,n为节点总数);
  • 空间复杂度:O(h)(递归栈深度等于树的高度,平衡树O(logn),斜树O(n));
  • 优势:
    1. 递归逻辑贴合二叉树的结构特性,易理解;
    2. 原地调整指针,无额外数据结构开销(除递归栈);
    3. 天然处理空树、单节点树等边界场景。
    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;
    }
}
核心逻辑说明
  1. 迭代终止条件curr == null(遍历到链表末尾);
  2. 循环处理每个节点
    • 若当前节点有左子树,找到左子树最右节点(前驱节点);
    • 调整指针:前驱节点的right指向当前节点的右子树,当前节点的right指向左子树,左指针置null;
    • 移动当前节点到新的右子节点,继续循环;
  3. 核心优势:完全原地操作,无递归栈/额外数据结构开销,满足进阶的O(1)空间要求。
性能说明
  • 时间复杂度:O(n)(每个节点仅被访问两次:一次找前驱节点,一次迭代处理);
  • 空间复杂度:O(1)(仅使用指针变量,无额外空间);
  • 优势:
    1. 满足进阶的原地算法要求,空间效率最优;
    2. 迭代逻辑无栈溢出风险,适合树高度较大的场景;
    3. 代码简洁,指针调整逻辑与递归版完全一致。
解法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;
    }
}
核心逻辑说明
  1. 栈模拟先序遍历:利用栈“后进先出”的特性,先压右子树、后压左子树,保证弹出顺序为“根→左→右”;
  2. 记录节点顺序:将先序遍历的节点存入列表,保证顺序正确;
  3. 调整指针:遍历节点列表,将每个节点的左指针置null,右指针指向下一个节点。
性能说明
  • 时间复杂度:O(n)(先序遍历O(n) + 调整指针O(n));
  • 空间复杂度:O(n)(栈最多存储n个节点 + 列表存储n个节点);
  • 优势:逻辑直观,新手易理解,无需处理复杂的前驱节点查找;
  • 劣势:额外空间开销大,不满足进阶的O(1)空间要求。

总结

  1. 迭代版莫里斯遍历(最优解):O(n)时间+O(1)空间,满足进阶要求,原地调整指针,工程中首选;
  2. 递归版前驱节点法:O(n)时间+O(h)空间,逻辑易理解,适合新手入门;
  3. 栈模拟先序遍历:O(n)时间+O(n)空间,逻辑最直观,但空间效率低;
  4. 关键技巧
    • 核心思想:展开链表的本质是“先序遍历顺序+左指针置null+右指针串联”;
    • 原地算法关键:找到左子树最右节点作为前驱,将原右子树挂载到前驱右侧;
    • 方法选择:优先选迭代版莫里斯遍历(空间最优),新手可先掌握栈模拟法理解核心逻辑。