【LeetCode】100. 相同的树
文章目录
- 100. 相同的树
- 题目描述
- 示例 1:
- 示例 2:
- 示例 3:
- 提示:
- 解题思路
- 问题深度分析
- 问题本质
- 核心思想
- 关键难点分析
- 典型情况分析
- 算法对比
- 算法流程图
- 主算法流程(递归比较)
- 递归比较详细流程
- BFS迭代流程
- 复杂度分析
- 时间复杂度详解
- 空间复杂度详解
- 关键优化技巧
- 技巧1:递归比较(最优解法)
- 技巧2:BFS迭代比较
- 技巧3:DFS迭代比较
- 技巧4:序列化比较
- 边界条件处理
- 边界情况1:两棵树都为空
- 边界情况2:一棵树为空
- 边界情况3:单节点树
- 边界情况4:完全相同的树
- 边界情况5:结构相同但值不同
- 边界情况6:结构不同
- 测试用例设计
- 基础测试用例
- 进阶测试用例
- 常见错误和陷阱
- 错误1:忘记检查nil
- 错误2:使用||而不是&&
- 错误3:BFS时队列不同步
- 错误4:DFS时栈顺序错误
- 实用技巧
- 进阶扩展
- 扩展1:判断子树是否相同
- 扩展2:判断树是否对称
- 扩展3:判断树的结构是否相同
- 扩展4:找出不同的节点
- 应用场景
- 总结
- 完整题解代码
100. 相同的树
题目描述
给你两棵二叉树的根节点 p 和 q ,编写一个函数来检验这两棵树是否相同。
如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
示例 1:

输入:p = [1,2,3], q = [1,2,3]
输出:true
示例 2:

输入:p = [1,2], q = [1,null,2]
输出:false
示例 3:

输入:p = [1,2,1], q = [1,1,2]
输出:false
提示:
- 两棵树上的节点数目都在范围 [0, 100] 内
- -10^4 <= Node.val <= 10^4
解题思路
问题深度分析
这是经典的树比较问题,也是树遍历的基础应用。核心在于同时遍历两棵树,比较它们的结构和节点值。
问题本质
给定两棵二叉树,判断它们是否完全相同。需要同时满足两个条件:
- 结构相同:两棵树的节点位置和连接关系完全一致
- 值相同:对应位置的节点值完全相同
这是一个树遍历 + 同步比较问题,需要同时访问两棵树的对应节点并进行比较。
核心思想
同步遍历两棵树:
- 递归比较:同时递归访问两棵树的对应节点
- 节点比较:
- 如果两个节点都为nil,返回true
- 如果只有一个为nil,返回false
- 如果两个节点值不同,返回false
- 如果两个节点值相同,继续比较左右子树
- 迭代比较:使用队列或栈同时遍历两棵树
- 序列化比较:将两棵树序列化为字符串,然后比较字符串
关键技巧:
- 同时访问两棵树的对应节点
- 先检查nil情况,避免空指针异常
- 使用递归或迭代实现同步遍历
- 利用队列实现BFS,利用栈实现DFS
关键难点分析
难点1:同步遍历
- 需要同时访问两棵树的对应位置
- 递归时两个树的递归深度必须同步
- 迭代时需要同时维护两个队列或栈
难点2:边界条件处理
- 两棵树都为空:返回true
- 一棵树为空,另一棵不为空:返回false
- 节点值不同:立即返回false
难点3:迭代实现
- BFS需要同时维护两个队列
- DFS需要同时维护两个栈
- 需要确保两个队列/栈的操作同步
典型情况分析
情况1:完全相同的树
树p: 树q:1 1/ \ / \2 3 2 3比较过程:
1. 根节点:1 == 1 ✓
2. 左子树:2 == 2 ✓
3. 右子树:3 == 3 ✓
结果:true
情况2:结构不同
树p: 树q:1 1/ \2 2比较过程:
1. 根节点:1 == 1 ✓
2. 左子树:p.Left=2, q.Left=nil ✗
结果:false
情况3:值不同
树p: 树q:1 1/ \ / \2 3 2 4比较过程:
1. 根节点:1 == 1 ✓
2. 左子树:2 == 2 ✓
3. 右子树:3 != 4 ✗
结果:false
情况4:一棵树为空
树p: 树q:1 (空)比较过程:
1. p != nil, q == nil ✗
结果:false
算法对比
| 算法 | 时间复杂度 | 空间复杂度 | 特点 |
|---|---|---|---|
| 递归比较 | O(n) | O(h) | 最优解法,代码简洁 |
| BFS迭代 | O(n) | O(n) | 层次遍历,易于理解 |
| DFS迭代 | O(n) | O(h) | 深度优先,空间优化 |
| 序列化比较 | O(n) | O(n) | 思路新颖,但效率较低 |
注:n为节点数,h为树高度
算法流程图
主算法流程(递归比较)
graph TDA[isSameTree(p, q)] --> B{p == nil && q == nil?}B -->|是| C[return true]B -->|否| D{p == nil || q == nil?}D -->|是| E[return false]D -->|否| F{p.Val == q.Val?}F -->|否| EF -->|是| G[递归比较左子树]G --> H[递归比较右子树]H --> I{左右子树都相同?}I -->|是| J[return true]I -->|否| E
递归比较详细流程
BFS迭代流程
复杂度分析
时间复杂度详解
递归比较算法:O(n)
- 需要访问两棵树的所有节点
- 每个节点访问一次,进行常数时间比较
- 最坏情况:需要比较所有节点
- 总时间:O(n)
BFS迭代算法:O(n)
- 需要遍历两棵树的所有节点
- 每个节点入队和出队一次
- 总时间:O(n)
DFS迭代算法:O(n)
- 需要遍历两棵树的所有节点
- 每个节点入栈和出栈一次
- 总时间:O(n)
空间复杂度详解
递归比较算法:O(h)
- 递归调用栈深度为树高度
- 最坏情况(链状树):O(n)
- 最好情况(平衡树):O(log n)
BFS迭代算法:O(n)
- 需要队列存储节点
- 最坏情况(完全二叉树):队列大小为叶子节点数,约为n/2
- 总空间:O(n)
DFS迭代算法:O(h)
- 需要栈存储节点
- 最坏情况(链状树):O(n)
- 最好情况(平衡树):O(log n)
关键优化技巧
技巧1:递归比较(最优解法)
func isSameTree(p *TreeNode, q *TreeNode) bool {// 两棵树都为空if p == nil && q == nil {return true}// 一棵树为空,另一棵不为空if p == nil || q == nil {return false}// 节点值不同if p.Val != q.Val {return false}// 递归比较左右子树return isSameTree(p.Left, q.Left) && isSameTree(p.Right, q.Right)
}
优势:
- 时间复杂度:O(n)
- 空间复杂度:O(h)
- 代码简洁,逻辑清晰
- 易于理解和实现
技巧2:BFS迭代比较
func isSameTree(p *TreeNode, q *TreeNode) bool {pQueue := []*TreeNode{p}qQueue := []*TreeNode{q}for len(pQueue) > 0 {// 同时出队pNode := pQueue[0]qNode := qQueue[0]pQueue = pQueue[1:]qQueue = qQueue[1:]// 检查nilif pNode == nil && qNode == nil {continue}if pNode == nil || qNode == nil {return false}if pNode.Val != qNode.Val {return false}// 同时入队左右子节点pQueue = append(pQueue, pNode.Left, pNode.Right)qQueue = append(qQueue, qNode.Left, qNode.Right)}return true
}
特点:层次遍历,适合需要逐层比较的场景
技巧3:DFS迭代比较
func isSameTree(p *TreeNode, q *TreeNode) bool {pStack := []*TreeNode{p}qStack := []*TreeNode{q}for len(pStack) > 0 {// 同时出栈n1 := len(pStack) - 1pNode := pStack[n1]qNode := qStack[n1]pStack = pStack[:n1]qStack = qStack[:n1]// 检查nilif pNode == nil && qNode == nil {continue}if pNode == nil || qNode == nil {return false}if pNode.Val != qNode.Val {return false}// 同时入栈左右子节点(注意顺序:先右后左)pStack = append(pStack, pNode.Right, pNode.Left)qStack = append(qStack, qNode.Right, qNode.Left)}return true
}
特点:深度优先遍历,空间复杂度O(h)
技巧4:序列化比较
func isSameTree(p *TreeNode, q *TreeNode) bool {return serialize(p) == serialize(q)
}func serialize(root *TreeNode) string {if root == nil {return "#"}return fmt.Sprintf("%d,%s,%s", root.Val, serialize(root.Left), serialize(root.Right))
}
特点:思路新颖,但效率较低(字符串拼接开销大)
边界条件处理
边界情况1:两棵树都为空
- 处理:返回true
- 验证:空树和空树相同
边界情况2:一棵树为空
- 处理:返回false
- 验证:空树和非空树不同
边界情况3:单节点树
- 处理:比较根节点值
- 验证:值相同返回true,值不同返回false
边界情况4:完全相同的树
- 处理:递归比较所有节点
- 验证:所有节点值相同,结构相同,返回true
边界情况5:结构相同但值不同
- 处理:比较到第一个不同值时立即返回false
- 验证:不需要比较所有节点,提前终止
边界情况6:结构不同
- 处理:比较到第一个结构不一致时立即返回false
- 验证:一个节点有子节点,另一个没有,立即返回false
测试用例设计
基础测试用例
- 完全相同:
[1,2,3]vs[1,2,3]→true - 结构不同:
[1,2]vs[1,null,2]→false - 值不同:
[1,2,1]vs[1,1,2]→false - 都为空:
[]vs[]→true
进阶测试用例
- 单节点相同:
[1]vs[1]→true - 单节点不同:
[1]vs[2]→false - 一棵为空:
[1]vs[]→false - 完全二叉树相同:
[4,2,6,1,3,5,7]vs[4,2,6,1,3,5,7]→true - 链状树相同:
[1,null,2,null,3]vs[1,null,2,null,3]→true - 深度不同:
[1,2]vs[1,2,null]→false
常见错误和陷阱
错误1:忘记检查nil
// 错误写法
if p.Val != q.Val {return false
}// 正确写法
if p == nil && q == nil {return true
}
if p == nil || q == nil {return false
}
if p.Val != q.Val {return false
}
原因:直接访问p.Val或q.Val可能导致空指针异常
错误2:使用||而不是&&
// 错误写法
return isSameTree(p.Left, q.Left) || isSameTree(p.Right, q.Right)// 正确写法
return isSameTree(p.Left, q.Left) && isSameTree(p.Right, q.Right)
原因:左右子树都必须相同,树才相同
错误3:BFS时队列不同步
// 错误写法
pQueue = append(pQueue, pNode.Left)
qQueue = append(qQueue, qNode.Left)
// 忘记入队右子节点// 正确写法
pQueue = append(pQueue, pNode.Left, pNode.Right)
qQueue = append(qQueue, qNode.Left, qNode.Right)
原因:必须同时入队左右子节点,保持队列同步
错误4:DFS时栈顺序错误
// 错误写法:先左后右
pStack = append(pStack, pNode.Left, pNode.Right)// 正确写法:先右后左(因为栈是后进先出)
pStack = append(pStack, pNode.Right, pNode.Left)
原因:栈是后进先出,需要先压右后压左才能先访问左
实用技巧
- 优先使用递归方法:代码简洁,逻辑清晰,易于理解和调试
- 提前终止:发现不同立即返回false,不需要继续比较
- nil检查放在前面:先处理nil情况,避免空指针异常
- BFS适合层次比较:如果需要逐层比较,使用BFS
- DFS适合深度比较:如果需要深度优先比较,使用DFS
- 迭代方法避免栈溢出:对于深度很大的树,使用迭代方法
进阶扩展
扩展1:判断子树是否相同
- 判断一棵树是否是另一棵树的子树(需要先找到对应节点)
扩展2:判断树是否对称
- 判断一棵树是否对称(镜像比较)
扩展3:判断树的结构是否相同
- 只比较结构,不比较值
扩展4:找出不同的节点
- 不仅返回true/false,还返回具体哪些节点不同
应用场景
- 数据结构验证:验证两个树结构是否相同
- 算法测试:测试树操作算法的正确性
- 代码审查:检查树构建代码的一致性
- 数据同步:检查两个数据源中的树结构是否一致
- 版本控制:比较不同版本的树结构
总结
判断两棵树是否相同是一个经典的树遍历 + 同步比较问题,核心在于:
- 同步遍历两棵树:同时访问对应位置的节点
- 先检查nil:避免空指针异常
- 递归比较最直观:代码简洁,易于理解
- 提前终止优化:发现不同立即返回,提高效率
完整题解代码
package mainimport ("fmt"
)type TreeNode struct {Val intLeft *TreeNodeRight *TreeNode
}// =========================== 方法一:递归比较(最优解法) ===========================
func isSameTree1(p *TreeNode, q *TreeNode) bool {// 两棵树都为空if p == nil && q == nil {return true}// 一棵树为空,另一棵不为空if p == nil || q == nil {return false}// 节点值不同if p.Val != q.Val {return false}// 递归比较左右子树return isSameTree1(p.Left, q.Left) && isSameTree1(p.Right, q.Right)
}// =========================== 方法二:BFS迭代比较 ===========================
func isSameTree2(p *TreeNode, q *TreeNode) bool {pQueue := []*TreeNode{p}qQueue := []*TreeNode{q}for len(pQueue) > 0 {// 同时出队pNode := pQueue[0]qNode := qQueue[0]pQueue = pQueue[1:]qQueue = qQueue[1:]// 检查nilif pNode == nil && qNode == nil {continue}if pNode == nil || qNode == nil {return false}if pNode.Val != qNode.Val {return false}// 同时入队左右子节点pQueue = append(pQueue, pNode.Left, pNode.Right)qQueue = append(qQueue, qNode.Left, qNode.Right)}return true
}// =========================== 方法三:DFS迭代比较 ===========================
func isSameTree3(p *TreeNode, q *TreeNode) bool {pStack := []*TreeNode{p}qStack := []*TreeNode{q}for len(pStack) > 0 {// 同时出栈n1 := len(pStack) - 1pNode := pStack[n1]qNode := qStack[n1]pStack = pStack[:n1]qStack = qStack[:n1]// 检查nilif pNode == nil && qNode == nil {continue}if pNode == nil || qNode == nil {return false}if pNode.Val != qNode.Val {return false}// 同时入栈左右子节点(注意顺序:先右后左,因为栈是后进先出)pStack = append(pStack, pNode.Right, pNode.Left)qStack = append(qStack, qNode.Right, qNode.Left)}return true
}// =========================== 方法四:序列化比较 ===========================
func isSameTree4(p *TreeNode, q *TreeNode) bool {return serialize4(p) == serialize4(q)
}func serialize4(root *TreeNode) string {if root == nil {return "#"}return fmt.Sprintf("%d,%s,%s",root.Val,serialize4(root.Left),serialize4(root.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 := 1for i < len(arr) && len(queue) > 0 {node := queue[0]queue = queue[1:]// 左子节点if i < len(arr) && arr[i] != nil {left := &TreeNode{Val: arr[i].(int)}node.Left = leftqueue = append(queue, left)}i++// 右子节点if i < len(arr) && arr[i] != nil {right := &TreeNode{Val: arr[i].(int)}node.Right = rightqueue = append(queue, right)}i++}return root
}// =========================== 测试 ===========================
func main() {fmt.Println("=== LeetCode 100: 相同的树 ===\n")testCases := []struct {name stringp *TreeNodeq *TreeNodeexpected bool}{{name: "例1: [1,2,3] vs [1,2,3] - 完全相同",p: arrayToTreeLevelOrder([]interface{}{1, 2, 3}),q: arrayToTreeLevelOrder([]interface{}{1, 2, 3}),expected: true,},{name: "例2: [1,2] vs [1,null,2] - 结构不同",p: arrayToTreeLevelOrder([]interface{}{1, 2}),q: arrayToTreeLevelOrder([]interface{}{1, nil, 2}),expected: false,},{name: "例3: [1,2,1] vs [1,1,2] - 值不同",p: arrayToTreeLevelOrder([]interface{}{1, 2, 1}),q: arrayToTreeLevelOrder([]interface{}{1, 1, 2}),expected: false,},{name: "都为空 - 相同",p: arrayToTreeLevelOrder([]interface{}{}),q: arrayToTreeLevelOrder([]interface{}{}),expected: true,},{name: "单节点相同: [1] vs [1]",p: arrayToTreeLevelOrder([]interface{}{1}),q: arrayToTreeLevelOrder([]interface{}{1}),expected: true,},{name: "单节点不同: [1] vs [2]",p: arrayToTreeLevelOrder([]interface{}{1}),q: arrayToTreeLevelOrder([]interface{}{2}),expected: false,},{name: "一棵为空: [1] vs []",p: arrayToTreeLevelOrder([]interface{}{1}),q: arrayToTreeLevelOrder([]interface{}{}),expected: false,},{name: "完全二叉树相同: [4,2,6,1,3,5,7] vs [4,2,6,1,3,5,7]",p: arrayToTreeLevelOrder([]interface{}{4, 2, 6, 1, 3, 5, 7}),q: arrayToTreeLevelOrder([]interface{}{4, 2, 6, 1, 3, 5, 7}),expected: true,},{name: "链状树相同: [1,null,2,null,3] vs [1,null,2,null,3]",p: arrayToTreeLevelOrder([]interface{}{1, nil, 2, nil, nil, nil, 3}),q: arrayToTreeLevelOrder([]interface{}{1, nil, 2, nil, nil, nil, 3}),expected: true,},{name: "结构细微不同: [1,2,3] vs [1,2,3,null]",p: arrayToTreeLevelOrder([]interface{}{1, 2, 3}),q: arrayToTreeLevelOrder([]interface{}{1, 2, 3, nil}),expected: true, // 层序遍历中,末尾的nil不影响树结构},}methods := map[string]func(*TreeNode, *TreeNode) bool{"递归比较": isSameTree1,"BFS迭代": isSameTree2,"DFS迭代": isSameTree3,"序列化比较": isSameTree4,}for methodName, methodFunc := range methods {fmt.Printf("方法:%s\n", methodName)pass := 0for i, tc := range testCases {got := methodFunc(tc.p, tc.q)ok := got == tc.expectedstatus := "✅"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))}
}
