# 从前序与中序遍历序列构造二叉树 LeetCode 105. 中等 ## 题目描述 给定两个整数数组 `preorder` 和 `inorder`,其中 `preorder` 是二叉树的先序遍历,`inorder` 是同一棵树的中序遍历,请构造二叉树并返回其根节点。 **示例 1:** ``` 输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7] 输出: [3,9,20,null,null,15,7] ``` **示例 2:** ``` 输入: preorder = [-1], inorder = [-1] 输出: [-1] ``` **限制**: - `1 <= preorder.length <= 3000` - `inorder.length == preorder.length` - `-3000 <= preorder[i], inorder[i] <= 3000` - `preorder` 和 `inorder` 均无重复元素 - `inorder` 保证是二叉树的中序遍历序列 - `preorder` 保证是同一棵二叉树的前序遍历序列 ## 思路推导 ### 理解前序和中序遍历 **前序遍历顺序**: 根 → 左 → 右 **中序遍历顺序**: 左 → 根 → 右 ``` 3 / \ 9 20 / \ 15 7 前序遍历: [3, 9, 20, 15, 7] ↑ 先访问根 中序遍历: [9, 3, 15, 20, 7] ↑ 根的左边是左子树 根的右边是右子树 ``` ### 暴力解法分析 **核心观察**: 1. 前序遍历的第一个元素是**根节点** 2. 在中序遍历中找到根节点的位置 3. 根节点左边是**左子树**,右边是**右子树** 4. 递归构造左右子树 **图解**: ``` preorder: [3, 9, 20, 15, 7] ↑ 根(3) inorder: [9, 3, 15, 20, 7] ↑ 根(3) 左子树 右子树 [9] [15,20,7] 递归构造: - 根节点: 3 - 左子树: preorder=[9], inorder=[9] - 右子树: preorder=[20,15,7], inorder=[15,20,7] ``` **算法步骤**: ```go func buildTree(preorder []int, inorder []int) *TreeNode { if len(preorder) == 0 { return nil } // 1. 前序第一个元素是根 rootVal := preorder[0] root := &TreeNode{Val: rootVal} // 2. 在中序中找到根的位置 rootIndex := findIndex(inorder, rootVal) // 3. 递归构造左右子树 root.Left = buildTree( preorder[1:1+rootIndex], // 左子树的前序 inorder[:rootIndex], // 左子树的中序 ) root.Right = buildTree( preorder[1+rootIndex:], // 右子树的前序 inorder[rootIndex+1:], // 右子树的中序 ) return root } func findIndex(arr []int, target int) int { for i, v := range arr { if v == target { return i } } return -1 } ``` **时间复杂度**: O(n²) - 每个节点需要查找: O(n) - n个节点: O(n²) **空间复杂度**: O(n) - 递归栈和切片 **问题**: 每次查找根节点位置需要O(n),可以优化 ### 优化思考 - 哈希表加速查找 **核心问题**: 如何快速在中序遍历中找到根节点位置? **优化思路**: 1. 预处理: 用哈希表存储 `value → index` 的映射 2. 查找: O(1) 时间找到根节点位置 **优化后的算法**: ```go func buildTree(preorder []int, inorder []int) *TreeNode { // 建立哈希表: value → index indexMap := make(map[int]int) for i, v := range inorder { indexMap[v] = i } return build(preorder, 0, len(preorder)-1, 0, indexMap) } func build(preorder []int, preStart, preEnd, inStart int, indexMap map[int]int) *TreeNode { if preStart > preEnd { return nil } // 前序第一个元素是根 rootVal := preorder[preStart] root := &TreeNode{Val: rootVal} // 在中序中找到根的位置(O(1)) rootIndex := indexMap[rootVal] // 左子树大小 leftSize := rootIndex - inStart // 递归构造 root.Left = build( preorder, preStart+1, // 左子树前序起点 preStart+leftSize, // 左子树前序终点 inStart, // 左子树中序起点 indexMap, ) root.Right = build( preorder, preStart+leftSize+1, // 右子树前序起点 preEnd, // 右子树前序终点 rootIndex+1, // 右子树中序起点 indexMap, ) return root } ``` **时间复杂度**: O(n) - 每个节点只处理一次 ### 为什么这样思考? **核心思想**: 1. **分治**: 大问题分解为小问题(构造整棵树 → 构造子树) 2. **前序确定根**: 前序第一个元素就是根 3. **中序分左右**: 根在中序的位置决定左右子树边界 4. **递归构造**: 对左右子树重复上述过程 **为什么前序和中序可以唯一确定二叉树?** - 前序告诉我们"谁是根" - 中序告诉我们"哪些是左子树,哪些是右子树" - 结合两者可以完全确定树的结构 ## 解题思路 ### 核心思想 利用前序遍历确定根节点,利用中序遍历划分左右子树,递归构造二叉树。 ### 详细算法流程 **步骤1: 确定根节点** ```go rootVal := preorder[0] // 前序遍历第一个元素是根 root := &TreeNode{Val: rootVal} ``` **关键点**: 前序遍历的特点是"根左右",第一个元素永远是根 **步骤2: 在中序遍历中找到根的位置** ```go rootIndex := findIndex(inorder, rootVal) // 或用哈希表 rootIndex := indexMap[rootVal] ``` **关键点**: 根节点在中序中的位置将数组分为两部分 - 左边: 左子树的所有节点 - 右边: 右子树的所有节点 **步骤3: 计算左右子树大小** ```go leftSize := rootIndex - inStart // 左子树节点数 rightSize := len(inorder) - rootIndex - 1 // 右子树节点数 ``` **步骤4: 递归构造左右子树** **图解**: ``` preorder: [3, 9, 20, 15, 7] ↑ ↑--------↑ 根 左子树 右子树 inorder: [9, 3, 15, 20, 7] ↑ ↑ ↑ 左子树 根 右子树 rootIndex = 2 (3在中序中的位置) leftSize = 2 (左子树有2个节点) 左子树: - preorder: preorder[1:3] = [9, 20] - inorder: inorder[0:2] = [9, 3] 右子树: - preorder: preorder[3:5] = [15, 7] - inorder: inorder[3:5] = [15, 20, 7] ``` ### 关键细节说明 **细节1: 为什么是 `preorder[1:1+rootIndex]` 而不是 `preorder[1:rootIndex]`?** ```go // preorder: [3, 9, 20, 15, 7] // ↑ ↑--------↑ // 根(索引0) 左子树(2个元素) // ❌ 错误写法 root.Left = buildTree(preorder[1:rootIndex], ...) // [9, 20]? 不对! // ✅ 正确写法 root.Left = buildTree(preorder[1:1+rootIndex], ...) // [9] ``` **原因**: - `preorder[0]` 是根 - `preorder[1:1+leftSize]` 是左子树(共leftSize个元素) - `preorder[1+leftSize:]` 是右子树 **细节2: 为什么 `inorder[:rootIndex]` 是左子树?** ```go // inorder: [9, 3, 15, 20, 7] // ↑ ↑ // 索引0 根在索引2 // 左子树: inorder[0:2] = [9] // 右子树: inorder[3:5] = [15, 20, 7] ``` **原因**: 中序遍历是"左根右",根节点左边全是左子树 **细节3: 如何计算左右子树的边界?** ```go // 左子树节点数 leftSize := rootIndex - inStart // 前序边界 preLeftStart := preStart + 1 preLeftEnd := preStart + leftSize // 中序边界 inLeftStart := inStart inLeftEnd := rootIndex - 1 ``` **图解**: ``` preorder: [根 | 左子树(leftSize个) | 右子树] ↑ ↑ ↑ preStart preStart+1 preStart+leftSize inorder: [左子树(leftSize个) | 根 | 右子树] ↑ ↑ ↑ inStart rootIndex rootIndex+1 ``` ### 边界条件分析 **边界1: 空树** ``` 输入: preorder = [], inorder = [] 输出: nil 处理: 直接返回nil ``` **边界2: 只有根节点** ``` 输入: preorder = [1], inorder = [1] 输出: TreeNode{Val: 1} 处理: - rootVal = 1 - rootIndex = 0 - leftSize = 0 - 左子树: preorder[1:1] = [], inorder[0:0] = [] - 右子树: preorder[1:] = [], inorder[1:] = [] ``` **边界3: 只有左子树** ``` 输入: preorder: [1, 2] inorder: [2, 1] 输出: 1 / 2 处理: - rootVal = 1, rootIndex = 1 - leftSize = 1 - 左子树: preorder[1:2] = [2], inorder[0:1] = [2] - 右子树: preorder[2:] = [], inorder[2:] = [] ``` **边界4: 只有右子树** ``` 输入: preorder: [1, 2] inorder: [1, 2] 输出: 1 \ 2 处理: - rootVal = 1, rootIndex = 0 - leftSize = 0 - 左子树: preorder[1:1] = [], inorder[0:0] = [] - 右子树: preorder[1:] = [2], inorder[1:] = [2] ``` ### 复杂度分析(详细版) #### 方法一: 暴力查找 **时间复杂度**: ``` - 构造每个节点: O(1) - 查找根位置: O(n) - 总计: n × O(n) = O(n²) 为什么是O(n²)? - 外层循环: 构造n个节点 - 内层循环: 每次查找O(n) - 总复杂度: O(n²) ``` **空间复杂度**: ``` - 递归栈: O(h) - h为树高 - 切片: O(n) - 每次创建新切片 - 总计: O(n) ``` #### 方法二: 哈希表优化 **时间复杂度**: ``` - 建立哈希表: O(n) - 构造每个节点: O(1) - 哈希查找O(1) - 总计: O(n) 为什么是O(n)? - 每个节点只处理一次 - 每次处理都是O(1)操作 - 总共n个节点 ``` **空间复杂度**: ``` - 哈希表: O(n) - 递归栈: O(h) - 总计: O(n) ``` ### 执行过程演示 **输入**: ``` preorder = [3, 9, 20, 15, 7] inorder = [9, 3, 15, 20, 7] ``` **执行过程**: ``` 第1层递归: ├─ preorder[0] = 3 是根 ├─ 在inorder中找到3,索引=2 ├─ 左子树: preorder=[9], inorder=[9] └─ 右子树: preorder=[20,15,7], inorder=[15,20,7] 第2层递归(左子树): ├─ preorder[0] = 9 是根 ├─ 在inorder中找到9,索引=0 ├─ 左子树: preorder=[], inorder=[] └─ 右子树: preorder=[], inorder=[] 第2层递归(右子树): ├─ preorder[0] = 20 是根 ├─ 在inorder中找到20,索引=1 ├─ 左子树: preorder=[15], inorder=[15] └─ 右子树: preorder=[7], inorder=[7] 第3层递归(20的左子树): ├─ preorder[0] = 15 是根 ├─ 左子树: [] └─ 右子树: [] 第3层递归(20的右子树): ├─ preorder[0] = 7 是根 ├─ 左子树: [] └─ 右子树: [] 最终构造的树: 3 / \ 9 20 / \ 15 7 ``` ## 代码实现 ### 方法一: 暴力查找(简单直观) ```go func buildTree(preorder []int, inorder []int) *TreeNode { if len(preorder) == 0 { return nil } root := &TreeNode{Val: preorder[0]} index := findIndex(inorder, preorder[0]) root.Left = buildTree( preorder[1:1+index], inorder[:index], ) root.Right = buildTree( preorder[1+index:], inorder[index+1:], ) return root } func findIndex(arr []int, target int) int { for i, v := range arr { if v == target { return i } } return -1 } ``` **复杂度**: O(n²) 时间,O(n) 空间 ### 方法二: 哈希表优化(推荐) ```go func buildTree(preorder []int, inorder []int) *TreeNode { // 建立哈希表: value → index indexMap := make(map[int]int) for i, v := range inorder { indexMap[v] = i } return build( preorder, 0, len(preorder)-1, 0, indexMap, ) } func build( preorder []int, preStart, preEnd int, inStart int, indexMap map[int]int, ) *TreeNode { if preStart > preEnd { return nil } // 前序第一个元素是根 rootVal := preorder[preStart] root := &TreeNode{Val: rootVal} // 在中序中找到根的位置 rootIndex := indexMap[rootVal] // 左子树大小 leftSize := rootIndex - inStart // 递归构造左右子树 root.Left = build( preorder, preStart+1, // 左子树前序起点 preStart+leftSize, // 左子树前序终点 inStart, // 左子树中序起点 indexMap, ) root.Right = build( preorder, preStart+leftSize+1, // 右子树前序起点 preEnd, // 右子树前序终点 rootIndex+1, // 右子树中序起点 indexMap, ) return root } ``` **复杂度**: O(n) 时间,O(n) 空间 ## 常见错误 ### 错误1: 切片边界错误 ❌ **错误写法**: ```go root.Left = buildTree(preorder[1:index], inorder[:index]) ``` ✅ **正确写法**: ```go root.Left = buildTree(preorder[1:1+index], inorder[:index]) ``` **原因**: 左子树有index个元素,应该从1到1+index ### 错误2: 忘记处理空数组 ❌ **错误写法**: ```go func buildTree(preorder []int, inorder []int) *TreeNode { rootVal := preorder[0] // 可能越界! // ... } ``` ✅ **正确写法**: ```go func buildTree(preorder []int, inorder []int) *TreeNode { if len(preorder) == 0 { return nil } rootVal := preorder[0] // ... } ``` **原因**: 空数组没有第0个元素 ### 错误3: 左右子树边界混淆 ❌ **错误写法**: ```go // 错误地认为右子树从rootIndex开始 root.Right = buildTree(preorder[1+index:], inorder[rootIndex:]) ``` ✅ **正确写法**: ```go // 右子树从rootIndex+1开始(跳过根节点) root.Right = buildTree(preorder[1+index:], inorder[rootIndex+1:]) ``` **原因**: `inorder[rootIndex]` 是根节点,右子树应该从 `rootIndex+1` 开始 ## 变体问题 ### 变体1: 从中序与后序遍历构造二叉树 **后序遍历**: 左 → 右 → 根(最后一个元素是根) ```go func buildTree(inorder []int, postorder []int) *TreeNode { indexMap := make(map[int]int) for i, v := range inorder { indexMap[v] = i } return build( postorder, len(postorder)-1, // 从最后一个元素开始 0, len(inorder)-1, indexMap, ) } func build( postorder []int, postStart, inStart, inEnd int, indexMap map[int]int, ) *TreeNode { if inStart > inEnd { return nil } rootVal := postorder[postStart] root := &TreeNode{Val: rootVal} rootIndex := indexMap[rootVal] rightSize := inEnd - rootIndex root.Right = build( postorder, postStart-1, rootIndex+1, inEnd, indexMap, ) root.Left = build( postorder, postStart-1-rightSize, inStart, rootIndex-1, indexMap, ) return root } ``` ### 变体2: 从前序与后序遍历构造二叉树 **注意**: 前序+后序无法唯一确定二叉树(除非是满二叉树) ### 变体3: 判断给定数组是否是某棵树的前序/中序遍历 需要验证数组是否满足遍历的性质(略) ## 总结 **核心要点**: 1. **前序定根**: preorder[0] 永远是当前子树的根 2. **中序分界**: 根在中序的位置决定左右子树边界 3. **递归构造**: 对左右子树重复相同过程 4. **哈希优化**: 用哈希表将查找优化到O(1) **易错点**: - 切片边界计算错误(`1:1+index` vs `1:index`) - 忘记处理空数组 - 左右子树边界混淆(`inorder[rootIndex:]` vs `inorder[rootIndex+1:]`) **关键规律**: ``` 前序: [根 | 左子树 | 右子树] 中序: [左子树 | 根 | 右子树] 通过前序找根,通过中序分左右 ``` **推荐写法**: 哈希表优化版(O(n)时间)