114. 二叉树展开为链表 (Medium)

专题归类: 05-二叉树 LeetCode 链接: https://leetcode.cn/problems/flatten-binary-tree-to-linked-list/


在线做题: 写 Python 代码并运行 · Java 跳转 LeetCode

题目描述

给你二叉树的根节点 root,请你将它展开为一个单链表:

  • 展开后的单链表应该与二叉树 前序遍历 顺序相同。
  • 使用节点的 right 指针作为链表的 next 指针,left 指针置为 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) 额外空间)展开吗?


题目详细分析

  • 数据范围含义: 最多 2000 节点,递归深度最多 2000,Python 默认递归深度不够(约 1000),可能需要迭代法或提高递归限制。但一般二叉树不会是链状,实际深度远小于 2000。
  • 输入输出特征: 输入根节点,原地修改树,不需要返回(或返回根节点)。修改后的树只有 right 指针有效,left 全部为 None。
  • 边界条件: 空树直接返回 None;单节点无需处理。
  • 核心约束: 必须按照前序遍历的顺序串联;原地修改(不能新建节点);left 指针必须置空。
  • 隐藏条件: 进阶要求 O(1) 空间——即不能用递归(隐式栈)或额外队列存储遍历结果。

小白版直白理解

就像把一棵圣诞树拆解后平铺在地上——按照「先看树顶→再看左边树枝→最后看右边树枝」的顺序,把每个节点用一根绳子(right 指针)串起来。串好后每个节点的左边(left 指针)不再指向任何东西,只有右边(right 指针)指向下一个节点。


解题思路

思路一:后序遍历 + prev 指针(推荐)

核心思想: 按照 右→左→根 的顺序(后序遍历变体)处理节点。用一个 prev 指针记录上一个处理完的节点。每处理一个节点,就把它的 right 指向 prevleft 置空。

为什么用这个遍历顺序? 前序遍历的顺序是「根→左→右」。如果按这个顺序正着处理,根节点的右指针需要指向左子树的根,但此时左子树还没处理完。如果我们反过来想——从最后一个节点开始往前串,前序遍历序列的最后一个节点是右子树的最右节点。按「右→左→根」的顺序处理,相当于从后往前构建链表。每处理一个节点,把它链接到已经处理好的「子链表」前面,这样当前节点就成了新的链表头。

def flatten(root):
    prev = None  # 记录上一个处理完的节点
 
    def dfs(node):
        nonlocal prev
        if not node:
            return
 
        # 后序遍历的变体:右→左→根
        dfs(node.right)  # 先处理右子树
        dfs(node.left)   # 再处理左子树
 
        # 当前节点:right 指向 prev,left 置空
        node.right = prev
        node.left = None
        prev = node      # 更新 prev 为当前节点
 
    dfs(root)

思路二:迭代法(寻找前驱,O(1) 空间)

核心思想: 从根节点开始,如果当前节点有左子树,找到左子树中最右边的节点(前驱节点),将当前节点的右子树接到这个前驱的 right 上,然后把左子树移到右边,左子树置空。然后移动到下一个右节点继续。

为什么这样可行? 前序遍历的顺序是「根→左→右」。对于当前节点,它的下一个节点应该是左子树的根节点。所以把左子树移到右边,然后把原来的右子树接到左子树的最右节点后面——这样「左」和「右」在链表上就衔接上了。

def flatten(root):
    cur = root
    while cur:
        if cur.left:
            # 找到左子树的最右节点(前驱)
            pre = cur.left
            while pre.right:
                pre = pre.right
            # 将原右子树接到前驱的右边
            pre.right = cur.right
            # 将左子树移到右边
            cur.right = cur.left
            cur.left = None
        # 继续处理下一个节点
        cur = cur.right

易错点

  • 后序法的遍历顺序不是标准后序: 标准后序是「左→右→根」,这里用的是「右→左→根」。目的是先处理序列末尾的节点,从后往前串联。
  • 迭代法找到前驱后,要记得把左子树置空: cur.left = None 很容易忘记。不置空的话树结构没有被完全展开。
  • 迭代法循环条件: 循环条件是 cur(不断走 right),而不是 cur.right。因为展开过程中 cur.right 会变化。
  • 后序法的 prev 指针初始为 None: 这样最后一个处理的节点(前序遍历的第一个节点,即根节点)的 right 会被置为 None,符合要求。

框架提炼

后序变体串联链表模板: 当需要按某种遍历顺序将树串联为链表时,从尾到头反向构建。

prev = None
def dfs(node):
    nonlocal prev
    if not node: return
    dfs(node.right)   # 先处理「在序列中更靠后的」子树
    dfs(node.left)    # 再处理「在序列中更靠前的」子树
    node.right = prev # 当前节点的 next 指向已串联好的部分
    node.left = None
    prev = node       # 当前节点成为新的链表头

迭代式原地修改树结构模板: 当需要原地将树转换为另一种结构时,使用 while 循环 + 寻找前驱/后继的方式:

cur = root
while cur:
    if cur.left:
        pre = 左子树的最右节点
        pre.right = cur.right   # 嫁接右子树
        cur.right = cur.left    # 左子树移到右边
        cur.left = None
    cur = cur.right

关联题目