剑指Offer07:重建二叉树算法详解
前言
在数据结构与算法领域,二叉树是一种非常重要的非线性数据结构。本文将详细解析如何根据二叉树的前序遍历和中序遍历结果重建原始二叉树,这是算法学习中的一个经典问题。
问题描述
给定一个二叉树的前序遍历和中序遍历结果,要求重建该二叉树。假设输入的前序遍历和中序遍历结果中都不包含重复的数字。
示例
给定:
- 前序遍历 preorder = [3,9,20,15,7]
- 中序遍历 inorder = [9,3,15,20,7]
重建后的二叉树应为:
3
/ \
9 20
/ \
15 7
核心思路
二叉树遍历特性
理解这个问题需要先掌握二叉树前序遍历和中序遍历的特性:
- 前序遍历:根节点 → 左子树 → 右子树
- 中序遍历:左子树 → 根节点 → 右子树
关键观察
从前序遍历的第一个元素我们可以确定根节点,然后在中序遍历中找到这个根节点的位置,这样就能将中序遍历分为左子树和右子树两部分。
详细解法
递归解法
递归是解决这类分治问题的自然方式。基本思路如下:
- 前序遍历的第一个元素就是当前子树的根节点
- 在中序遍历中找到这个根节点的位置,左边就是左子树的中序遍历结果,右边是右子树的中序遍历结果
- 根据左子树的长度,可以确定前序遍历中左子树和右子树的分界点
- 递归构建左子树和右子树
Go语言实现
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
func buildTree(pre []int, in []int) *TreeNode {
if len(pre) == 0 || len(in) == 0 {
return nil
}
mid := search(in, pre[0])
return &TreeNode{
Val: pre[0],
Left: buildTree(pre[1:mid+1], in[:mid]),
Right: buildTree(pre[mid+1:], in[mid+1:]),
}
}
func search(nodes []int, val int) int {
for p, v := range nodes {
if v == val {
return p
}
}
return -1
}
复杂度分析
- 时间复杂度:O(n),每个节点都会被访问一次
- 空间复杂度:O(n),递归调用栈的深度在最坏情况下为O(n)
优化思路
上述解法每次都需要在中序遍历中线性搜索根节点的位置,这会导致时间复杂度增加。可以使用哈希表预先存储中序遍历的值和索引的映射关系,将搜索时间降低到O(1)。
优化后的Go实现
func buildTree(preorder []int, inorder []int) *TreeNode {
indexMap := make(map[int]int)
for i, v := range inorder {
indexMap[v] = i
}
var build func(int, int) *TreeNode
preIndex := 0
build = func(left, right int) *TreeNode {
if left > right {
return nil
}
rootVal := preorder[preIndex]
preIndex++
root := &TreeNode{Val: rootVal}
root.Left = build(left, indexMap[rootVal]-1)
root.Right = build(indexMap[rootVal]+1, right)
return root
}
return build(0, len(inorder)-1)
}
边界情况处理
在实际编码中,需要考虑以下边界情况:
- 输入的前序和中序遍历结果为空
- 输入的前序和中序遍历结果长度不一致
- 输入的前序和中序遍历结果不匹配(无法构建有效二叉树)
总结
重建二叉树问题很好地展示了如何利用不同遍历方式的特性来解决问题。掌握这种分治思想对于解决其他树相关问题非常有帮助。通过这个问题,我们也可以加深对二叉树遍历特性的理解。
对于算法学习者来说,建议在理解递归解法的基础上,尝试使用迭代方法来解决这个问题,这能进一步加深对二叉树遍历过程的理解。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考