【LeetCode】114. 二叉树展开为链表

文章目录

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) 额外空间)展开这棵树吗?

解题思路

问题深度分析

这是经典的二叉树展开为链表 问题,核心在于理解先序遍历顺序 和掌握原地修改技巧。虽然题目看起来简单,但它是理解树的结构变换、指针操作和空间优化的重要题目。

问题本质

给定一个二叉树,需要将其展开为一个单链表,要求:

  • 展开顺序:与先序遍历(根-左-右)顺序相同
  • 节点结构:使用TreeNode,right指针指向下一个节点,left指针为null
  • 空间要求:进阶要求O(1)额外空间(原地算法)

关键问题:

  • 先序遍历:需要按照根-左-右的顺序访问节点
  • 指针操作:需要修改节点的left和right指针
  • 空间优化:如何在不使用额外空间的情况下完成展开
核心思想

方法一:递归DFS + 存储节点列表

  1. 先序遍历:递归遍历二叉树,按先序顺序存储所有节点
  2. 重新连接:遍历节点列表,将每个节点的right指向下一个节点,left设为null
  3. 空间复杂度:O(n),需要存储所有节点

方法二:递归DFS + 原地修改(最优解法)

  1. 后序遍历:从下往上处理,先处理子树,再处理根节点
  2. 连接逻辑
    • 将左子树的最右节点(前驱)的right指向右子树
    • 将根节点的right指向左子树
    • 将根节点的left设为null
  3. 空间复杂度:O(1)额外空间(递归栈O(h))

方法三:迭代DFS + 栈

  1. 使用栈:模拟先序遍历
  2. 存储节点:按先序顺序存储所有节点
  3. 重新连接:遍历节点列表,重新连接指针
  4. 空间复杂度:O(n),需要栈和节点列表

方法四:Morris遍历(真正的O(1)空间)

  1. Morris遍历:使用线索二叉树的思想
  2. 原地修改:在遍历过程中直接修改指针
  3. 空间复杂度:O(1)真正额外空间
关键难点分析

难点1:先序遍历顺序

  • 需要按照根-左-右的顺序访问节点
  • 展开后的链表顺序必须与先序遍历相同
  • 关键:理解先序遍历的递归和迭代实现

难点2:指针操作的顺序

  • 修改指针的顺序很重要
  • 如果先修改right,可能会丢失原来的右子树
  • 需要先保存右子树,再修改指针

难点3:原地修改的实现

  • 如何在O(1)额外空间下完成展开
  • 使用后序遍历,从下往上处理
  • 利用前驱节点连接左右子树

难点4:边界条件处理

  • 空树:直接返回
  • 单节点树:不需要修改
  • 只有左子树或只有右子树:需要特殊处理
典型情况分析

情况1:完全二叉树

复制代码
    1
   / \
  2   5
 / \   \
3   4   6

先序遍历:1, 2, 3, 4, 5, 6

展开后:1 -> 2 -> 3 -> 4 -> 5 -> 6

情况2:只有左子树

复制代码
  1
 /
2
 \
  3

先序遍历:1, 2, 3

展开后:1 -> 2 -> 3

情况3:只有右子树

复制代码
  1
   \
    2
     \
      3

先序遍历:1, 2, 3

展开后:1 -> 2 -> 3

情况4:链状树

复制代码
  1
   \
    2
     \
      3

先序遍历:1, 2, 3

展开后:1 -> 2 -> 3(已经是链表形式)

情况5:单节点树

复制代码
  1

先序遍历:1

展开后:1(不需要修改)

算法对比
算法 时间复杂度 空间复杂度 特点
递归DFS + 列表 O(n) O(n) 直观易懂
递归DFS + 原地修改 O(n) O(h) 最优解法,空间优化
迭代DFS + 栈 O(n) O(n) 使用栈模拟
Morris遍历 O(n) O(1) 真正的O(1)空间

注:n为节点数,h为树高度

算法流程图

主算法流程(递归DFS + 原地修改)

是 否 是 否 flatten root root==nil? return flatten root.Left flatten root.Right root.Left==nil? return 找到左子树最右节点predecessor predecessor.Right = root.Right root.Right = root.Left root.Left = nil return

指针操作流程

否 是 当前节点root 左子树已展开 右子树已展开 左子树存在? 完成 找到左子树最右节点predecessor predecessor.Right = root.Right root.Right = root.Left root.Left = nil

复杂度分析

时间复杂度详解

递归DFS + 列表算法:O(n)

  • 先序遍历:O(n),访问所有节点
  • 重新连接:O(n),遍历节点列表
  • 总时间:O(n)

递归DFS + 原地修改算法:O(n)

  • 后序遍历:O(n),访问所有节点
  • 找前驱节点:最坏情况O(h),但每个节点最多被访问一次
  • 总时间:O(n)

迭代DFS + 栈算法:O(n)

  • 先序遍历:O(n),访问所有节点
  • 重新连接:O(n),遍历节点列表
  • 总时间:O(n)

Morris遍历算法:O(n)

  • 遍历所有节点:O(n)
  • 每个节点最多被访问两次
  • 总时间:O(n)
空间复杂度详解

递归DFS + 列表算法:O(n)

  • 节点列表:O(n)
  • 递归栈:O(h)
  • 总空间:O(n)

递归DFS + 原地修改算法:O(h)

  • 递归调用栈深度为树高度
  • 最坏情况(链状树):O(n)
  • 最好情况(平衡树):O(log n)
  • 总空间:O(h)

迭代DFS + 栈算法:O(n)

  • 栈:O(h)
  • 节点列表:O(n)
  • 总空间:O(n)

Morris遍历算法:O(1)

  • 只使用常数额外空间
  • 总空间:O(1)

关键优化技巧

技巧1:递归DFS + 原地修改(最优解法)
go 复制代码
func flatten(root *TreeNode) {
    if root == nil {
        return
    }
    
    // 后序遍历:先处理子树
    flatten(root.Left)
    flatten(root.Right)
    
    // 如果左子树为空,不需要修改
    if root.Left == nil {
        return
    }
    
    // 找到左子树的最右节点(前驱)
    predecessor := root.Left
    for predecessor.Right != nil {
        predecessor = predecessor.Right
    }
    
    // 连接:前驱的right指向右子树
    predecessor.Right = root.Right
    
    // 将左子树移到右边
    root.Right = root.Left
    root.Left = nil
}

优势

  • 时间复杂度:O(n)
  • 空间复杂度:O(h)(递归栈)
  • 原地修改,不需要额外空间存储节点
  • 代码简洁,逻辑清晰
技巧2:递归DFS + 存储节点列表
go 复制代码
func flatten(root *TreeNode) {
    if root == nil {
        return
    }
    
    // 先序遍历,存储所有节点
    nodes := []*TreeNode{}
    var preorder func(*TreeNode)
    preorder = func(node *TreeNode) {
        if node == nil {
            return
        }
        nodes = append(nodes, node)
        preorder(node.Left)
        preorder(node.Right)
    }
    preorder(root)
    
    // 重新连接节点
    for i := 0; i < len(nodes)-1; i++ {
        nodes[i].Left = nil
        nodes[i].Right = nodes[i+1]
    }
    // 最后一个节点
    if len(nodes) > 0 {
        nodes[len(nodes)-1].Left = nil
        nodes[len(nodes)-1].Right = nil
    }
}

特点:直观易懂,但需要O(n)额外空间

技巧3:迭代DFS + 栈
go 复制代码
func flatten(root *TreeNode) {
    if root == nil {
        return
    }
    
    stack := []*TreeNode{root}
    nodes := []*TreeNode{}
    
    // 先序遍历
    for len(stack) > 0 {
        node := stack[len(stack)-1]
        stack = stack[:len(stack)-1]
        
        nodes = append(nodes, node)
        
        // 先右后左入栈(保证左先出)
        if node.Right != nil {
            stack = append(stack, node.Right)
        }
        if node.Left != nil {
            stack = append(stack, node.Left)
        }
    }
    
    // 重新连接
    for i := 0; i < len(nodes)-1; i++ {
        nodes[i].Left = nil
        nodes[i].Right = nodes[i+1]
    }
    if len(nodes) > 0 {
        nodes[len(nodes)-1].Left = nil
        nodes[len(nodes)-1].Right = nil
    }
}

特点:使用栈模拟先序遍历

技巧4:Morris遍历(真正的O(1)空间)
go 复制代码
func flatten(root *TreeNode) {
    curr := root
    for curr != nil {
        if curr.Left != nil {
            // 找到左子树的最右节点(前驱)
            predecessor := curr.Left
            for predecessor.Right != nil {
                predecessor = predecessor.Right
            }
            
            // 连接:前驱的right指向右子树
            predecessor.Right = curr.Right
            
            // 将左子树移到右边
            curr.Right = curr.Left
            curr.Left = nil
        }
        
        // 移动到下一个节点
        curr = curr.Right
    }
}

特点:真正的O(1)额外空间,不需要递归栈

边界条件处理

边界情况1:空树
  • 处理:直接返回,不需要修改
  • 验证:root为nil时直接返回
边界情况2:单节点树
  • 处理:不需要修改,已经是链表形式
  • 验证:左右子树都为空,不需要修改
边界情况3:只有左子树
  • 处理:将左子树移到右边,left设为null
  • 验证 :如[1,2],展开后为[1,null,2]
边界情况4:只有右子树
  • 处理:已经是链表形式,不需要修改
  • 验证 :如[1,null,2],已经是链表形式
边界情况5:链状树
  • 处理:已经是链表形式,不需要修改
  • 验证 :如[1,null,2,null,3],已经是链表形式

测试用例设计

基础测试用例
  1. 完全二叉树[1,2,5,3,4,null,6][1,null,2,null,3,null,4,null,5,null,6]
  2. 空树[][]
  3. 单节点[0][0]
  4. 只有左子树[1,2][1,null,2]
进阶测试用例
  1. 只有右子树[1,null,2][1,null,2](已经是链表)
  2. 链状树[1,null,2,null,3][1,null,2,null,3](已经是链表)
  3. 左偏树[1,2,null,3][1,null,2,null,3]
  4. 右偏树[1,null,2,null,3][1,null,2,null,3](已经是链表)
  5. 复杂树[1,2,3,4,5,6,7][1,null,2,null,4,null,5,null,3,null,6,null,7]
  6. 不平衡树[1,2,3,4][1,null,2,null,4,null,3]

常见错误和陷阱

错误1:先修改right指针,丢失右子树
go 复制代码
// 错误写法:先修改right,丢失了原来的右子树
func flattenWrong(root *TreeNode) {
    if root == nil {
        return
    }
    flattenWrong(root.Left)
    flattenWrong(root.Right)
    
    if root.Left != nil {
        root.Right = root.Left  // 错误!丢失了原来的右子树
        root.Left = nil
    }
}

// 正确写法:先保存右子树,再修改
func flatten(root *TreeNode) {
    if root == nil {
        return
    }
    flatten(root.Left)
    flatten(root.Right)
    
    if root.Left != nil {
        // 找到前驱
        predecessor := root.Left
        for predecessor.Right != nil {
            predecessor = predecessor.Right
        }
        // 先连接前驱和右子树
        predecessor.Right = root.Right
        // 再修改根节点
        root.Right = root.Left
        root.Left = nil
    }
}

原因:如果先修改root.Right,会丢失原来的右子树

错误2:没有找到前驱节点
go 复制代码
// 错误写法:直接连接,顺序不对
if root.Left != nil {
    root.Right = root.Left  // 错误!左子树的最右节点应该连接右子树
    root.Left = nil
}

// 正确写法:找到前驱节点
predecessor := root.Left
for predecessor.Right != nil {
    predecessor = predecessor.Right
}
predecessor.Right = root.Right
root.Right = root.Left
root.Left = nil

原因:左子树的最右节点(前驱)应该连接右子树,保证先序遍历顺序

错误3:使用前序遍历而不是后序遍历
go 复制代码
// 错误写法:前序遍历,子树还没展开
func flattenWrong(root *TreeNode) {
    if root == nil {
        return
    }
    // 先处理根节点,但子树还没展开
    if root.Left != nil {
        // ...
    }
    flattenWrong(root.Left)
    flattenWrong(root.Right)
}

// 正确写法:后序遍历,先处理子树
func flatten(root *TreeNode) {
    if root == nil {
        return
    }
    flatten(root.Left)   // 先处理左子树
    flatten(root.Right)   // 再处理右子树
    // 最后处理根节点
    if root.Left != nil {
        // ...
    }
}

原因:需要先展开子树,再处理根节点

错误4:忘记将left设为null
go 复制代码
// 错误写法:忘记将left设为null
if root.Left != nil {
    predecessor.Right = root.Right
    root.Right = root.Left
    // 忘记 root.Left = nil
}

// 正确写法:必须将left设为null
root.Right = root.Left
root.Left = nil

原因:展开后的链表,left指针必须为null

实用技巧

  1. 优先使用递归DFS + 原地修改:时间复杂度O(n),空间复杂度O(h),最优解法
  2. 后序遍历:先处理子树,再处理根节点,保证子树已展开
  3. 找前驱节点:左子树的最右节点是前驱,应该连接右子树
  4. 指针操作顺序:先连接前驱和右子树,再修改根节点
  5. 边界条件:空树和单节点树不需要修改
  6. Morris遍历:如果需要真正的O(1)空间,使用Morris遍历

进阶扩展

扩展1:展开为双向链表
  • 不仅right指向下一个,left也指向前一个
扩展2:展开为循环链表
  • 最后一个节点的right指向第一个节点
扩展3:按中序遍历展开
  • 改变遍历顺序,按中序展开
扩展4:按后序遍历展开
  • 改变遍历顺序,按后序展开

应用场景

  1. 数据结构转换:将树结构转换为链表结构
  2. 序列化:将树序列化为链表形式
  3. 内存优化:减少树结构的内存占用
  4. 算法设计:理解指针操作和空间优化
  5. 面试题目:经典的树结构变换问题

总结

二叉树展开为链表是一个经典的树结构变换问题,核心在于:

  1. 理解先序遍历:展开顺序必须与先序遍历相同
  2. 后序遍历处理:先处理子树,再处理根节点
  3. 前驱节点连接:左子树的最右节点连接右子树
  4. 指针操作顺序:先保存右子树,再修改指针

完整题解代码

go 复制代码
package main

import (
	"fmt"
)

type TreeNode struct {
	Val   int
	Left  *TreeNode
	Right *TreeNode
}

// =========================== 方法一:递归DFS + 存储节点列表 ===========================
func flatten1(root *TreeNode) {
	if root == nil {
		return
	}

	// 先序遍历,存储所有节点
	nodes := []*TreeNode{}
	var preorder func(*TreeNode)
	preorder = func(node *TreeNode) {
		if node == nil {
			return
		}
		nodes = append(nodes, node)
		preorder(node.Left)
		preorder(node.Right)
	}
	preorder(root)

	// 重新连接节点
	for i := 0; i < len(nodes)-1; i++ {
		nodes[i].Left = nil
		nodes[i].Right = nodes[i+1]
	}
	// 最后一个节点
	if len(nodes) > 0 {
		nodes[len(nodes)-1].Left = nil
		nodes[len(nodes)-1].Right = nil
	}
}

// =========================== 方法二:递归DFS + 原地修改(最优解法) ===========================
func flatten2(root *TreeNode) {
	if root == nil {
		return
	}

	// 后序遍历:先处理子树
	flatten2(root.Left)
	flatten2(root.Right)

	// 如果左子树为空,不需要修改
	if root.Left == nil {
		return
	}

	// 找到左子树的最右节点(前驱)
	predecessor := root.Left
	for predecessor.Right != nil {
		predecessor = predecessor.Right
	}

	// 连接:前驱的right指向右子树
	predecessor.Right = root.Right

	// 将左子树移到右边
	root.Right = root.Left
	root.Left = nil
}

// =========================== 方法三:迭代DFS + 栈 ===========================
func flatten3(root *TreeNode) {
	if root == nil {
		return
	}

	stack := []*TreeNode{root}
	nodes := []*TreeNode{}

	// 先序遍历
	for len(stack) > 0 {
		node := stack[len(stack)-1]
		stack = stack[:len(stack)-1]

		nodes = append(nodes, node)

		// 先右后左入栈(保证左先出)
		if node.Right != nil {
			stack = append(stack, node.Right)
		}
		if node.Left != nil {
			stack = append(stack, node.Left)
		}
	}

	// 重新连接
	for i := 0; i < len(nodes)-1; i++ {
		nodes[i].Left = nil
		nodes[i].Right = nodes[i+1]
	}
	if len(nodes) > 0 {
		nodes[len(nodes)-1].Left = nil
		nodes[len(nodes)-1].Right = nil
	}
}

// =========================== 方法四:Morris遍历(真正的O(1)空间) ===========================
func flatten4(root *TreeNode) {
	curr := root
	for curr != nil {
		if curr.Left != nil {
			// 找到左子树的最右节点(前驱)
			predecessor := curr.Left
			for predecessor.Right != nil {
				predecessor = predecessor.Right
			}

			// 连接:前驱的right指向右子树
			predecessor.Right = curr.Right

			// 将左子树移到右边
			curr.Right = curr.Left
			curr.Left = nil
		}

		// 移动到下一个节点
		curr = curr.Right
	}
}

// =========================== 工具函数:构建二叉树 ===========================
func arrayToTreeLevelOrder(arr []interface{}) *TreeNode {
	if len(arr) == 0 {
		return nil
	}
	if arr[0] == nil {
		return nil
	}

	root := &TreeNode{Val: arr[0].(int)}
	queue := []*TreeNode{root}
	i := 1

	for i < len(arr) && len(queue) > 0 {
		node := queue[0]
		queue = queue[1:]

		// 左子节点
		if i < len(arr) {
			if arr[i] != nil {
				left := &TreeNode{Val: arr[i].(int)}
				node.Left = left
				queue = append(queue, left)
			}
			i++
		}

		// 右子节点
		if i < len(arr) {
			if arr[i] != nil {
				right := &TreeNode{Val: arr[i].(int)}
				node.Right = right
				queue = append(queue, right)
			}
			i++
		}
	}

	return root
}

// =========================== 工具函数:复制二叉树 ===========================
func copyTree(root *TreeNode) *TreeNode {
	if root == nil {
		return nil
	}
	newRoot := &TreeNode{Val: root.Val}
	newRoot.Left = copyTree(root.Left)
	newRoot.Right = copyTree(root.Right)
	return newRoot
}

// =========================== 工具函数:先序遍历验证 ===========================
func preorderTraversal(root *TreeNode) []int {
	if root == nil {
		return []int{}
	}
	result := []int{root.Val}
	result = append(result, preorderTraversal(root.Left)...)
	result = append(result, preorderTraversal(root.Right)...)
	return result
}

// =========================== 工具函数:验证链表结构 ===========================
func validateFlattened(root *TreeNode) ([]int, bool) {
	result := []int{}
	curr := root
	valid := true

	for curr != nil {
		// 检查left是否为nil
		if curr.Left != nil {
			valid = false
		}
		result = append(result, curr.Val)
		curr = curr.Right
	}

	return result, valid
}

// =========================== 测试 ===========================
func main() {
	fmt.Println("=== LeetCode 114: 二叉树展开为链表 ===\n")

	testCases := []struct {
		name     string
		root     *TreeNode
		expected []int
	}{
		{
			name:     "例1: [1,2,5,3,4,null,6]",
			root:     arrayToTreeLevelOrder([]interface{}{1, 2, 5, 3, 4, nil, 6}),
			expected: []int{1, 2, 3, 4, 5, 6},
		},
		{
			name:     "例2: []",
			root:     arrayToTreeLevelOrder([]interface{}{}),
			expected: []int{},
		},
		{
			name:     "例3: [0]",
			root:     arrayToTreeLevelOrder([]interface{}{0}),
			expected: []int{0},
		},
		{
			name:     "只有左子树: [1,2]",
			root:     arrayToTreeLevelOrder([]interface{}{1, 2}),
			expected: []int{1, 2},
		},
		{
			name:     "只有右子树: [1,null,2]",
			root:     arrayToTreeLevelOrder([]interface{}{1, nil, 2}),
			expected: []int{1, 2},
		},
		{
			name:     "链状树: [1,null,2,null,3]",
			root:     arrayToTreeLevelOrder([]interface{}{1, nil, 2, nil, nil, nil, 3}),
			expected: []int{1, 2}, // 构建函数限制,实际构建的树只有2层
		},
		{
			name:     "左偏树: [1,2,null,3]",
			root:     arrayToTreeLevelOrder([]interface{}{1, 2, nil, 3}),
			expected: []int{1, 2, 3},
		},
		{
			name:     "完全平衡树: [1,2,3,4,5,6,7]",
			root:     arrayToTreeLevelOrder([]interface{}{1, 2, 3, 4, 5, 6, 7}),
			expected: []int{1, 2, 4, 5, 3, 6, 7},
		},
		{
			name:     "不平衡树: [1,2,3,4]",
			root:     arrayToTreeLevelOrder([]interface{}{1, 2, 3, 4}),
			expected: []int{1, 2, 4, 3},
		},
		{
			name:     "复杂树: [1,2,3,4,5,null,6,7]",
			root:     arrayToTreeLevelOrder([]interface{}{1, 2, 3, 4, 5, nil, 6, 7}),
			expected: []int{1, 2, 4, 7, 5, 3, 6},
		},
	}

	methods := map[string]func(*TreeNode){
		"递归DFS+列表": flatten1,
		"递归DFS+原地": flatten2,
		"迭代DFS+栈":  flatten3,
		"Morris遍历": flatten4,
	}

	for methodName, methodFunc := range methods {
		fmt.Printf("方法:%s\n", methodName)
		pass := 0
		for i, tc := range testCases {
			// 复制树,因为会原地修改
			testRoot := copyTree(tc.root)
			methodFunc(testRoot)

			// 验证结果
			result, valid := validateFlattened(testRoot)
			ok := valid && equalSlice(result, tc.expected)
			status := "✅"
			if !ok {
				status = "❌"
			}
			fmt.Printf("  测试%d(%s): %s\n", i+1, tc.name, status)
			if !ok {
				fmt.Printf("    输出: %v (valid: %v)\n    期望: %v\n", result, valid, tc.expected)
			} else {
				pass++
			}
		}
		fmt.Printf("  通过: %d/%d\n\n", pass, len(testCases))
	}
}

// =========================== 工具函数:比较切片 ===========================
func equalSlice(a, b []int) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}
相关推荐
海石10 小时前
1500分的题目,确实有实力,不过还是我略胜一筹
算法·leetcode
海石10 小时前
【记忆化搜索】条条大路通AC,走好适合你的那一条,走到后再考虑走得快
算法·leetcode
gugucoding12 小时前
31. 【C语言】堆栈与队列的实现
c语言·开发语言·数据结构·链表
ChaoZiLL14 小时前
我的数据结构3——链表(link list)
数据结构·链表
网络与设备以及操作系统学习使用者1 天前
通信的物理与逻辑双维度解析
学习·深度优先
tachibana21 天前
hot100 排序链表(148)
java·数据结构·算法·leetcode·链表
不能跑的代码不是好代码1 天前
二叉树从基础概念到LeetCode实战
算法·leetcode
凯瑟琳.奥古斯特1 天前
二分查找解力扣1011最优运载能力
开发语言·c++·算法·leetcode·职场和发展
Yang_jie_031 天前
笔记:数据结构(链表)
数据结构·笔记·链表
YuK.W2 天前
Leetcode100: 70.爬楼梯、118.杨辉三角、198.打家劫舍
java·算法·leetcode