【LeetCode热题100道笔记】二叉树展开为链表

题目描述

给你二叉树的根结点 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) 额外空间)展开这棵树吗?

思考:前序遍历+左子树链表化

核心是递归处理左右子树:先将左、右子树分别展开为链表,再将左子树链表接到根节点的右指针,最后将原右子树链表接到左子树链表的尾部,实现“根→左子树链表→右子树链表”的前序顺序。

算法过程

  1. 递归终止条件:若当前节点为null(空树/叶子节点的子树),返回null(无需处理)。
  2. 递归展开子树
    • 递归展开左子树,得到左子树链表的头节点left
    • 递归展开右子树,得到右子树链表的头节点right
  3. 重组当前节点的指针
    • 将当前节点的右指针指向左子树链表(root.right = left),左指针置为nullroot.left = null);
    • 遍历左子树链表至尾部(找到最后一个节点),将其右指针指向右子树链表(current.right = right)。
  4. 返回当前节点:作为上层节点的左/右子树链表头,完成递归回溯。

时空复杂度

  • 时间复杂度:O(n),n为二叉树节点总数。
    原因:每个节点仅被访问1次(递归处理),遍历左子树链表尾部的操作总次数为O(n)(每个节点最多被遍历1次),总时间与节点数线性相关。
  • 空间复杂度:O(h),h为二叉树高度。
    原因:递归调用栈深度取决于树高,平衡树h=O(log n),链状树h=O(n);额外空间仅为递归栈,无其他存储开销。

代码(前序遍历版)

/**
 * 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) {
    preOrder(root);
};

// 辅助函数:将以root为根的树展开为链表,返回展开后的头节点
function preOrder(root) {
    if (!root) return null; // 空节点直接返回

    // 递归展开左、右子树
    let left = preOrder(root.left);
    let right = preOrder(root.right);

    // 1. 将左子树链表接到根节点的右指针,左指针置空
    root.right = left;
    root.left = null;

    // 2. 找到左子树链表的尾部,将右子树链表接在其后
    let current = root;
    while (current.right) { // 遍历至左子树链表的最后一个节点
        current = current.right;
    }
    current.right = right;

    return root; // 返回当前节点作为上层的子树链表头
}

关键逻辑解析

  1. 为何用前序遍历?
    题目要求展开后的链表顺序与前序遍历一致(根→左→右),前序遍历的递归顺序天然匹配这一需求:先处理根节点,再处理左子树,最后处理右子树,确保重组后的指针顺序正确。

  2. 左指针为何必须置空?
    展开后的链表要求所有节点的左指针为null,仅保留右指针指向后续节点。若不置空左指针,会导致链表中残留左子树引用,破坏“链表”的结构定义。

  3. 如何高效找到左子树链表的尾部?
    代码中通过while (current.right)循环遍历左子树链表至尾部,这是最直观的实现。对于极端情况(左子树为链状),该操作时间复杂度为O(k)(k为左子树节点数),但整棵树的总遍历次数仍为O(n)(每个节点最多被遍历1次),不影响整体时间复杂度。

  4. 是否可以优化空间复杂度?
    上述递归实现的空间复杂度为O(h),若需进一步优化至O(1),可采用Morris遍历(线索化遍历),通过修改空指针临时指向前驱/后继节点,避免递归栈开销,但代码逻辑会更复杂。

示例解析(以 root = [1,2,5,3,4,null,6] 为例)

  1. 递归展开左子树(2→3→4)和右子树(5→6);
  2. 根节点1的右指针指向左子树2,左指针置空;
  3. 遍历左子树链表至尾部(4),将其右指针指向右子树5;
  4. 最终链表为:1→2→3→4→5→6,符合前序遍历顺序。

该过程通过递归自然实现了“根→左→右”的顺序重组,确保链表结构正确且原地修改(不创建新节点)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值