【LeetCode】94. 二叉树的中序遍历

文章目录

94. 二叉树的中序遍历

题目描述

给定一个二叉树的根节点 root ,返回 它的 中序 遍历 。

示例 1:

输入:root = [1,null,2,3]

输出:[1,3,2]

示例 2:

输入:root = []

输出:[]

示例 3:

输入:root = [1]

输出:[1]

提示:

  • 树中节点数目在范围 [0, 100] 内
  • -100 <= Node.val <= 100

进阶: 递归算法很简单,你可以通过迭代算法完成吗?

解题思路

问题深度分析

本题要求输出二叉树的中序遍历序列(Left -> Root -> Right)。属于二叉树遍历的基础题,但可扩展为多种实现:递归、迭代(显式栈)、统一迭代(颜色标记)、以及 Morris 遍历(O(1) 额外空间)。

  • 中序本质:访问顺序为左子树 -> 根节点 -> 右子树。
  • 难点:在不使用递归的情况下如何"回溯"(用栈或临时线索)。

核心方法对比

  • 方法一(递归):最直观,代码最短,空间为递归栈O(h)。
  • 方法二(显式栈迭代):用栈模拟系统递归栈,通用稳定。
  • 方法三(颜色标记统一迭代):将"访问/展开"统一成一个模板,适合多种遍历统一写法。
  • 方法四(Morris 遍历):利用线索二叉树思想,空间O(1),不需要栈与递归,但会暂时修改树指针(遍历过程中会复原)。

算法流程图

递归(中序)

graph TD A[Inorder(root)] --> B{root==nil?} B -->|是| C[return] B -->|否| D[Inorder(root.Left)] D --> E[输出 root.Val] E --> F[Inorder(root.Right)]

显式栈迭代
是 是 否 否 stack空, cur=root cur!=nil or stack不空? cur!=nil? 压栈cur, cur=cur.Left 出栈node 输出node.Val cur=node.Right 结束

Morris 遍历
否 是 是 否 cur=root cur是否有左子树? 输出cur.Val, cur=cur.Right pre=cur.Left的最右节点 pre.Right==nil? pre.Right=cur, cur=cur.Left pre.Right=nil, 输出cur.Val, cur=cur.Right

复杂度分析

  • 时间复杂度:四种方法均为 O(n),n 为节点数。
  • 空间复杂度:
    • 递归:O(h) 递归栈
    • 显式栈/颜色标记:O(h) 栈空间
    • Morris:O(1) 额外空间(会临时建立和拆除线索)

关键边界与陷阱

  • 空树:返回空切片。
  • 只有一个节点:直接返回该值。
  • 极度不平衡树(链状):递归可能接近最坏栈深;Morris优势明显。
  • Morris一定要"复原"pre.Right,否则破坏原树结构。

方法与要点

  • 递归:模板清晰,先左后根再右。
  • 栈迭代:while(cur!=nil)先一路向左入栈;否则出栈访问,再转向右。
  • 颜色标记:节点入栈两次,第一次展开(入右、入自身白、入左),第二次(黑)访问。
  • Morris:寻找左子树最右节点pre;首遇建立线索,二遇拆线索并访问根。

测试用例设计

  • 1,null,2,3\] -\> \[1,3,2

  • \] -\> \[

  • 1\] -\> \[1

  • 完全二叉树如 [4,2,6,1,3,5,7] -> [1,2,3,4,5,6,7]
  • 退化链(全左/全右)

实战技巧

  • Morris 空间O(1)是亮点,注意指针复原顺序。
  • 颜色标记可统一前/中/后序实现,便于模板化。
  • 递归实现最易读,作为基线版本很有用。

完整题解代码

go 复制代码
package main

import (
	"fmt"
)

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

// =========================== 方法一:递归 ===========================
func inorderTraversal1(root *TreeNode) []int {
	var res []int
	var dfs func(*TreeNode)
	dfs = func(node *TreeNode) {
		if node == nil {
			return
		}
		dfs(node.Left)
		res = append(res, node.Val)
		dfs(node.Right)
	}
	dfs(root)
	return res
}

// =========================== 方法二:显式栈迭代 ===========================
func inorderTraversal2(root *TreeNode) []int {
	var res []int
	stack := []*TreeNode{}
	cur := root
	for cur != nil || len(stack) > 0 {
		for cur != nil {
			stack = append(stack, cur)
			cur = cur.Left
		}
		n := len(stack) - 1
		node := stack[n]
		stack = stack[:n]
		res = append(res, node.Val)
		cur = node.Right
	}
	return res
}

// =========================== 方法三:颜色标记统一迭代 ===========================
// 白色:0 表示展开;黑色:1 表示访问
func inorderTraversal3(root *TreeNode) []int {
	if root == nil {
		return nil
	}
	type item struct {
		node *TreeNode
		clr  int
	}
	const white, black = 0, 1
	var res []int
	stack := []item{{root, white}}
	for len(stack) > 0 {
		n := len(stack) - 1
		i := stack[n]
		stack = stack[:n]
		if i.node == nil {
			continue
		}
		if i.clr == white {
			// 中序:右 黑 自 黑 左 白
			stack = append(stack, item{i.node.Right, white})
			stack = append(stack, item{i.node, black})
			stack = append(stack, item{i.node.Left, white})
		} else {
			res = append(res, i.node.Val)
		}
	}
	return res
}

// =========================== 方法四:Morris 遍历 ===========================
func inorderTraversal4(root *TreeNode) []int {
	var res []int
	cur := root
	for cur != nil {
		if cur.Left == nil {
			res = append(res, cur.Val)
			cur = cur.Right
			continue
		}
		// 寻找前驱(左子树最右)
		pre := cur.Left
		for pre.Right != nil && pre.Right != cur {
			pre = pre.Right
		}
		if pre.Right == nil {
			pre.Right = cur
			cur = cur.Left
		} else {
			pre.Right = nil
			res = append(res, cur.Val)
			cur = cur.Right
		}
	}
	return res
}

// =========================== 构建/工具 ===========================
func arrayToTreeLevelOrder(arr []any) *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 {
		n := queue[0]
		queue = queue[1:]
		// 左孩子
		if i < len(arr) {
			if arr[i] != nil {
				left := &TreeNode{Val: arr[i].(int)}
				n.Left = left
				queue = append(queue, left)
			}
			i++
		}
		// 右孩子
		if i < len(arr) {
			if arr[i] != nil {
				right := &TreeNode{Val: arr[i].(int)}
				n.Right = right
				queue = append(queue, right)
			}
			i++
		}
	}
	return root
}

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
}

func buildRightChain(vals []int) *TreeNode {
	if len(vals) == 0 {
		return nil
	}
	root := &TreeNode{Val: vals[0]}
	cur := root
	for i := 1; i < len(vals); i++ {
		cur.Right = &TreeNode{Val: vals[i]}
		cur = cur.Right
	}
	return root
}

// =========================== 测试 ===========================
func main() {
	fmt.Println("=== LeetCode 94: 二叉树的中序遍历 ===\n")

	testCases := []struct {
		name     string
		root     *TreeNode
		expected []int
	}{
		{
			name:     "例1: [1,null,2,3]",
			root:     arrayToTreeLevelOrder([]any{1, nil, 2, 3}),
			expected: []int{1, 3, 2},
		},
		{
			name:     "空树",
			root:     arrayToTreeLevelOrder([]any{}),
			expected: []int{},
		},
		{
			name:     "单节点",
			root:     arrayToTreeLevelOrder([]any{1}),
			expected: []int{1},
		},
		{
			name:     "完全二叉树",
			root:     arrayToTreeLevelOrder([]any{4, 2, 6, 1, 3, 5, 7}),
			expected: []int{1, 2, 3, 4, 5, 6, 7},
		},
		{
			name:     "全左链",
			root:     arrayToTreeLevelOrder([]any{3, 2, nil, 1}),
			expected: []int{1, 2, 3},
		},
		{
			name:     "全右链",
			root:     buildRightChain([]int{1, 2, 3}),
			expected: []int{1, 2, 3},
		},
	}

	methods := map[string]func(*TreeNode) []int{
		"递归":         inorderTraversal1,
		"栈迭代":       inorderTraversal2,
		"颜色标记":      inorderTraversal3,
		"Morris O(1)": inorderTraversal4,
	}

	for name, f := range methods {
		fmt.Printf("方法:%s\n", name)
		pass := 0
		for i, tc := range testCases {
			got := f(tc.root)
			ok := equalSlice(got, tc.expected)
			status := "✅"
			if !ok {
				status = "❌"
			}
			fmt.Printf("  测试%d(%s): %s\n", i+1, tc.name, status)
			if !ok {
				fmt.Printf("    输出: %v\n    期望: %v\n", got, tc.expected)
			} else {
				pass++
			}
		}
		fmt.Printf("  通过: %d/%d\n\n", pass, len(testCases))
	}
}
相关推荐
放羊郎6 小时前
SLAM各类算法特点对比
人工智能·算法·slam·视觉slam·建图·激光slam
熬了夜的程序员6 小时前
【LeetCode】92. 反转链表 II
数据结构·算法·leetcode·链表·职场和发展·排序算法
Aurorar0rua6 小时前
C Primer Plus Notes 10
c语言·开发语言·算法
知花实央l6 小时前
【数字逻辑】24小时数字钟实战!74HC161搭24/60进制计数器+Multisim仿真
算法·测试用例·1024程序员节
兮山与6 小时前
算法20.0
算法
头发还没掉光光6 小时前
Linux多线程之生产消费模型,日志版线程池
linux·运维·开发语言·数据结构·c++
hans汉斯6 小时前
基于机器学习的商业银行信贷风险评估系统构建与实证研究
大数据·人工智能·爬虫·算法·yolo·机器学习·支持向量机
laocooon5238578867 小时前
一个蛇形填充n×n矩阵的算法
数据结构·算法
岑梓铭7 小时前
《考研408数据结构》第六章(5.4树和森林)复习笔记
数据结构·笔记·考研·算法·408·ds