二叉树遍历不会优化?5道经典题带你拿下字节、阿里offer

第一章:二叉树遍历的核心思想与面试定位

二叉树遍历是数据结构中的基础但关键的内容,广泛应用于算法设计与系统开发中。掌握其核心思想不仅有助于理解树形结构的访问逻辑,更是应对技术面试中高频考题的必备技能。遍历的本质是按照某种顺序访问所有节点,确保不重不漏,常见的有三种深度优先方式:前序、中序和后序,以及一种广度优先方式:层序遍历。

遍历方式的本质区别

  • 前序遍历:先访问根节点,再遍历左子树,最后右子树(根→左→右)
  • 中序遍历:先遍历左子树,再访问根节点,最后右子树(左→根→右)
  • 后序遍历:先遍历左右子树,最后访问根节点(左→右→根)
  • 层序遍历:按层级从上到下、每层从左到右逐个访问

递归实现示例(Go语言)


// 定义二叉树节点
type TreeNode struct {
    Val   int
    Left  *TreeNode
    Right *TreeNode
}

// 前序遍历:根 -> 左 -> 右
func preorderTraversal(root *TreeNode) []int {
    var result []int
    if root == nil {
        return result
    }
    result = append(result, root.Val)           // 访问根
    result = append(result, preorderTraversal(root.Left)...)  // 遍历左子树
    result = append(result, preorderTraversal(root.Right)...) // 遍历右子树
    return result
}

常见应用场景对比

遍历类型典型用途
前序遍历复制树、序列化树结构
中序遍历二叉搜索树的有序输出
后序遍历释放树节点、计算表达式树
层序遍历求树高、判断完全二叉树
graph TD A[根节点] --> B[左子树] A --> C[右子树] B --> D[左节点] B --> E[右节点] C --> F[左节点] C --> G[右节点]

第二章:递归与迭代基础题精讲

2.1 前序遍历的递归与迭代统一解法

递归实现:直观清晰的逻辑结构
前序遍历遵循“根-左-右”的访问顺序,递归写法自然贴合这一逻辑。

void preorder(TreeNode* root) {
    if (!root) return;
    cout << root->val << " ";  // 访问根节点
    preorder(root->left);       // 遍历左子树
    preorder(root->right);      // 遍历右子树
}
该实现简洁明了,函数调用栈隐式保存了回溯路径。
迭代实现:显式栈模拟调用过程
使用栈显式维护待处理节点,可避免递归带来的深层调用开销。
  • 初始化栈并压入根节点
  • 循环弹出栈顶并访问
  • 先压右子再压左子,保证左子优先处理
两种方法时间复杂度均为 O(n),空间复杂度在最坏情况下为 O(h),h 为树高。

2.2 中序遍历的栈模拟实现技巧

在二叉树遍历中,中序遍历要求访问顺序为“左-根-右”。当无法使用递归时,利用栈模拟递归调用过程是一种高效替代方案。
核心思路
通过显式栈模拟函数调用栈,先深入遍历左子树,再处理节点,最后转向右子树。
代码实现

public List<Integer> inorderTraversal(TreeNode root) {
    Stack<TreeNode> stack = new Stack<>();
    List<Integer> result = new ArrayList<>();
    TreeNode curr = root;
    
    while (curr != null || !stack.isEmpty()) {
        while (curr != null) {
            stack.push(curr);
            curr = curr.left;  // 深入左子树
        }
        curr = stack.pop();      // 取出栈顶节点
        result.add(curr.val);    // 访问该节点
        curr = curr.right;       // 转向右子树
    }
    return result;
}
上述代码中,curr 用于追踪当前访问节点,stack 存储待处理的父节点。内层 while 实现左路下降,pop 操作回溯并访问,随后转向右子树,完整复现中序逻辑。

2.3 后序遍历的双栈法与逆序构造思路

双栈法的核心思想
后序遍历要求访问顺序为“左-右-根”,而利用栈结构天然的LIFO特性,可通过两个栈协同工作实现该逻辑。第一个栈用于模拟前序遍历(根-右-左),第二个栈用于反转访问顺序。
  • 将根节点压入栈1
  • 从栈1弹出节点并压入栈2,然后将其左、右子节点依次压入栈1
  • 最终从栈2依次弹出即得后序序列
public List<Integer> postorderTraversal(TreeNode root) {
    if (root == null) return new ArrayList<>();
    Stack<TreeNode> stack1 = new Stack<>();
    Stack<TreeNode> stack2 = new Stack<>();
    stack1.push(root);
    
    while (!stack1.isEmpty()) {
        TreeNode node = stack1.pop();
        stack2.push(node); // 存入栈2
        if (node.left != null) stack1.push(node.left);
        if (node.right != null) stack1.push(node.right);
    }
    
    List<Integer> result = new ArrayList<>();
    while (!stack2.isEmpty()) {
        result.add(stack2.pop().val);
    }
    return result;
}
上述代码中,栈1按“根-右-左”入栈,栈2接收逆序输出,最终实现“左-右-根”的后序效果。

2.4 层序遍历的队列实现与层级分割

层序遍历,又称广度优先遍历,依赖队列的先进先出特性实现节点的逐层访问。通过将每层节点入队,并在处理时记录当前层的节点数量,可实现层级分割。
队列驱动的遍历流程
使用标准队列结构,初始将根节点入队。每次外层循环开始前,获取队列当前长度,即为当前层的节点数。通过固定长度的内层循环,确保仅处理该层所有节点。
func levelOrder(root *TreeNode) [][]int {
    if root == nil { return nil }
    var result [][]int
    queue := []*TreeNode{root}
    
    for len(queue) > 0 {
        levelSize := len(queue)
        var currentLevel []int
        
        for i := 0; i < levelSize; i++ {
            node := queue[0]
            queue = queue[1:]
            currentLevel = append(currentLevel, node.Val)
            
            if node.Left != nil { queue = append(queue, node.Left) }
            if node.Right != nil { queue = append(queue, node.Right) }
        }
        result = append(result, currentLevel)
    }
    return result
}
上述代码中,levelSize 记录每层节点数,内层循环仅处理这些节点,从而实现层级分割。子节点在处理过程中被加入队列尾部,保证下一层按序遍历。

2.5 二叉树最大深度的DFS与BFS对比分析

深度优先搜索(DFS)实现

DFS通过递归方式遍历子树,逐层深入计算最大深度。

def maxDepthDFS(root):
    if not root:
        return 0
    left = maxDepthDFS(root.left)
    right = maxDepthDFS(root.right)
    return max(left, right) + 1

该方法时间复杂度为O(n),每个节点访问一次;空间复杂度为O(h),h为树高,取决于递归栈深度。

广度优先搜索(BFS)实现

BFS利用队列逐层扩展,记录层级数即为最大深度。

from collections import deque
def maxDepthBFS(root):
    if not root:
        return 0
    queue = deque([root])
    depth = 0
    while queue:
        depth += 1
        for _ in range(len(queue)):
            node = queue.popleft()
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
    return depth
性能对比分析
算法时间复杂度空间复杂度适用场景
DFSO(n)O(h)树较深,结构偏斜
BFSO(n)O(w)树较宽,需早期终止

其中h为树的高度,w为最大宽度。DFS在递归深度受限时可能栈溢出,而BFS在宽树中占用更多内存。

第三章:路径与求和类高频题剖析

3.1 路径总和 I:递归终止条件设计

在二叉树路径总和问题中,递归终止条件的设计是算法正确性的核心。合理的终止判断能有效避免无效遍历。
基础终止场景分析
当节点为空时,说明当前路径已走到尽头,无法构成有效路径,应立即返回:

if root == nil {
    return false
}
该条件作为递归的最基本出口,确保空节点不参与后续计算。
目标值匹配判断
若当前节点为叶子节点(左右子树均为空),且剩余目标值等于当前节点值,则找到一条有效路径:

if root.Left == nil && root.Right == nil && targetSum == root.Val {
    return true
}
此条件必须在空节点判断之后执行,保证逻辑顺序正确。
  • 递归函数需同步更新目标值:targetSum - root.Val
  • 最终结果为左子树或右子树任一满足路径条件

3.2 路径总和 II:回溯路径记录技巧

在二叉树中寻找所有从根到叶子节点的路径,使其路径和等于目标值,是回溯算法的经典应用场景。与路径总和 I 不同,本题要求记录完整路径,因此需维护当前路径状态并适时回退。
回溯的核心机制
通过递归遍历左右子树,在进入节点时将值加入路径,满足条件时复制路径快照。返回前必须移除当前节点,保证路径正确性。
func pathSum(root *TreeNode, targetSum int) [][]int {
    var result [][]int
    var path []int
    dfs(root, targetSum, &path, &result)
    return result
}

func dfs(node *TreeNode, sum int, path *[]int, result *[][]int) {
    if node == nil {
        return
    }
    *path = append(*path, node.Val) // 记录当前节点
    if node.Left == nil && node.Right == nil && sum == node.Val {
        temp := make([]int, len(*path))
        copy(temp, *path)         // 拷贝当前路径
        *result = append(*result, temp)
    }
    dfs(node.Left, sum-node.Val, path, result)  // 递归左子树
    dfs(node.Right, sum-node.Val, path, result) // 递归右子树
    *path = (*path)[:len(*path)-1] // 回溯:移除当前节点
}
上述代码中,path 是共享切片,每次递归后必须回退最后一个元素,避免路径污染。使用 copy 创建独立副本,防止后续修改影响已收集结果。

3.3 求根到叶子节点的数字总和问题

在二叉树中,从根节点到每个叶子节点的路径可以表示一个数字。例如,路径 `1->2->3` 表示数字 123。目标是计算所有根到叶子路径所形成数字的总和。
递归思路解析
采用深度优先搜索(DFS),在遍历过程中累积当前路径的数值。每当到达叶子节点时,将当前值加入总和。

def sumNumbers(root):
    def dfs(node, current_sum):
        if not node:
            return 0
        current_sum = current_sum * 10 + node.val
        if not node.left and not node.right:  # 叶子节点
            return current_sum
        return dfs(node.left, current_sum) + dfs(node.right, current_sum)
    return dfs(root, 0)
上述代码中,current_sum 记录从根到当前节点的数值。每下一层,原值乘以 10 并加上当前节点值,模拟数位左移。
时间与空间复杂度
  • 时间复杂度:O(N),N 为节点总数,每个节点访问一次
  • 空间复杂度:O(H),H 为树的高度,由递归栈深度决定

第四章:构造与重建类难题突破

4.1 前序与中序构造二叉树的索引映射优化

在重建二叉树的过程中,前序遍历确定根节点,中序遍历划分左右子树。传统方法需频繁查找根节点在中序序列中的位置,时间复杂度为 O(n²)。
索引映射优化策略
通过哈希表预存储中序遍历中各节点值与其索引的映射关系,将查找操作降至 O(1)。
func buildTree(preorder []int, inorder []int) *TreeNode {
    indexMap := make(map[int]int)
    for i, val := range inorder {
        indexMap[val] = i
    }
    var dfs func(int, int, int, int) *TreeNode
    dfs = func(preLeft, preRight, inLeft, inRight int) *TreeNode {
        if preLeft > preRight { return nil }
        rootVal := preorder[preLeft]
        root := &TreeNode{Val: rootVal}
        pivot := indexMap[rootVal]
        leftSize := pivot - inLeft
        root.Left = dfs(preLeft+1, preLeft+leftSize, inLeft, pivot-1)
        root.Right = dfs(preLeft+leftSize+1, preRight, pivot+1, inRight)
        return root
    }
    return dfs(0, len(preorder)-1, 0, len(inorder)-1)
}
代码中,indexMap 存储中序索引,递归函数通过计算子树大小直接划分前序区间,避免重复搜索。

4.2 中序与后序构造二叉树的分治策略

在二叉树重建问题中,中序和后序遍历序列可用于唯一还原树结构。核心思想是利用分治策略:后序遍历的最后一个节点为当前子树的根节点,通过该节点在中序遍历中划分左右子树。
算法步骤
  • 从后序数组末尾获取根节点值
  • 在中序数组中定位该值,分割左右子树区间
  • 递归构建右子树与左子树(注意顺序)
代码实现
func buildTree(inorder []int, postorder []int) *TreeNode {
    if len(postorder) == 0 { return nil }
    rootVal := postorder[len(postorder)-1]
    root := &TreeNode{Val: rootVal}
    
    i := 0
    for ; i < len(inorder); i++ {
        if inorder[i] == rootVal { break }
    }
    
    root.Left = buildTree(inorder[:i], postorder[:i])
    root.Right = buildTree(inorder[i+1:], postorder[i:len(postorder)-1])
    return root
}
上述代码通过切片划分子问题,每次递归处理一个子树。参数说明:inorder 表示中序序列,postorder 为后序序列;关键在于确定根节点位置后,正确划分左右子树区间。

4.3 根据前序遍历序列构建BST的单调栈解法

在已知二叉搜索树(BST)前序遍历序列的前提下,可利用单调栈在线性时间内重构原始树结构。核心思想是维护一个单调递减的节点值栈,依据BST性质判断当前节点应作为左子树还是回溯后作为右子树插入。
算法流程
  • 初始化空栈,按前序顺序遍历每个节点
  • 若当前值小于栈顶,则为左子节点,直接入栈
  • 否则不断出栈,直到栈为空或栈顶小于当前值,最后将当前节点作为最后一个出栈节点的右子节点
代码实现
type TreeNode struct {
    Val   int
    Left  *TreeNode
    Right *TreeNode
}

func bstFromPreorder(preorder []int) *TreeNode {
    root := &TreeNode{Val: preorder[0]}
    stack := []*TreeNode{root}
    
    for i := 1; i < len(preorder); i++ {
        node := &TreeNode{Val: preorder[i]}
        if preorder[i] < stack[len(stack)-1].Val {
            stack[len(stack)-1].Left = node
        } else {
            for len(stack) > 0 && stack[len(stack)-1].Val < preorder[i] {
                pop := stack[len(stack)-1]
                stack = stack[:len(stack)-1]
                node = pop
            }
            node.Right = &TreeNode{Val: preorder[i]}
            stack = append(stack, node.Right)
        }
        stack = append(stack, &TreeNode{Val: preorder[i]})
    }
    return root
}
上述代码通过维护单调栈实现O(n)时间复杂度的构建过程,适用于大规模BST重建场景。

4.4 序列化与反序列化二叉树的DFS编码方案

在分布式系统或持久化场景中,将二叉树结构转换为字符串表示(序列化)以及从字符串重建原始结构(反序列化)是常见需求。深度优先搜索(DFS)提供了一种简洁高效的编码策略。
递归前序遍历编码
采用前序遍历结合分隔符记录节点值,空节点用特殊符号(如`#`)表示,可唯一还原树结构。
func serialize(root *TreeNode) string {
    if root == nil {
        return "#"
    }
    left := serialize(root.Left)
    right := serialize(root.Right)
    return strconv.Itoa(root.Val) + "," + left + "," + right
}
该函数递归拼接当前节点值与左右子树编码结果,逗号分隔,`#`标记空节点,保证结构信息完整。
反序列化重建
利用前序遍历顺序,逐个读取值并构建节点,递归恢复左右子树。
  • 每次消费一个值,若为`#`则返回nil
  • 否则创建新节点,并递归构建其左右子树
此方案时间复杂度为O(n),适用于任意二叉树的紧凑编码与无损还原。

第五章:从刷题到offer——二叉树面试全复盘

高频考点与思维模式
二叉树在面试中频繁出现,核心考察点包括递归、DFS/BFS遍历、路径求和、对称性判断及BST性质应用。掌握“分解问题+递归处理”的思维模式至关重要。
经典题目实战:路径总和 II
给定二叉树和目标和,返回所有从根到叶路径总和等于目标值的路径。关键在于回溯法维护当前路径:

func pathSum(root *TreeNode, targetSum int) [][]int {
    var result [][]int
    var path []int
    dfs(root, targetSum, path, &result)
    return result
}

func dfs(node *TreeNode, remaining int, path []int, result *[][]int) {
    if node == nil {
        return
    }
    path = append(path, node.Val)
    if node.Left == nil && node.Right == nil && remaining == node.Val {
        temp := make([]int, len(path))
        copy(temp, path)
        *result = append(*result, temp)
    }
    dfs(node.Left, remaining-node.Val, path, result)
    dfs(node.Right, remaining-node.Val, path, result)
    path = path[:len(path)-1] // 回溯
}
常见陷阱与优化策略
  • 忽略空树边界条件,导致空指针异常
  • 未及时回溯路径,引发结果污染
  • DFS使用全局变量时未重置状态
  • 对BST未利用有序性,盲目遍历整棵树
真实面试案例对比
公司题目变体考察重点
Google路径和等于目标的非根节点起始路径双层DFS + 前缀和优化
Amazon镜像对称路径统计递归结构比对 + 路径哈希记录
进阶建议
熟练掌握 Morris 遍历可在 O(1) 空间下完成中序遍历,适用于严格空间限制场景。同时,将树形 DP 思想融入递归设计,可高效解决直径、最大路径和等问题。
【无人机】基于改进粒子群算法的无人机路径规划研究[和遗传算法、粒子群算法进行比较](Matlab代码实现)内容概要:本文围绕基于改进粒子群算法的无人机路径规划展开研究,重点探讨了在复杂环境中利用改进粒子群算法(PSO)实现无人机三维路径规划的方法,并将其与遗传算法(GA)、标准粒子群算法等传统优化算法进行对比分析。研究内容涵盖路径规划的多目标优化、避障策略、航路点约束以及算法收敛性和寻优能力的评估,所有实验均通过Matlab代码实现,提供了完整的仿真验证流程。文章还提到了多种智能优化算法在无人机路径规划中的应用比较,突出了改进PSO在收敛速度和全局寻优方面的优势。; 适合人群:具备一定Matlab编程基础和优化算法知识的研究生、科研人员及从事无人机路径规划、智能优化算法研究的相关技术人员。; 使用场景及目标:①用于无人机在复杂地形或动态环境下的三维路径规划仿真研究;②比较不同智能优化算法(如PSO、GA、蚁群算法、RRT等)在路径规划中的性能差异;③为多目标优化提供算法选型和改进思路。; 阅读建议:建议读者结合文中提供的Matlab代码进行实践操作,重点关注算法的参数设置、适应度函数设计及路径约束处理方式,同时可参考文中提到的多种算法对比思路,拓展到其他智能优化算法的研究与改进中。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值