当前位置: 首页 > news >正文

【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

解题思路

问题深度分析

这是经典的树比较问题,也是树遍历的基础应用。核心在于同时遍历两棵树,比较它们的结构和节点值

问题本质

给定两棵二叉树,判断它们是否完全相同。需要同时满足两个条件:

  1. 结构相同:两棵树的节点位置和连接关系完全一致
  2. 值相同:对应位置的节点值完全相同

这是一个树遍历 + 同步比较问题,需要同时访问两棵树的对应节点并进行比较。

核心思想

同步遍历两棵树

  1. 递归比较:同时递归访问两棵树的对应节点
  2. 节点比较
    • 如果两个节点都为nil,返回true
    • 如果只有一个为nil,返回false
    • 如果两个节点值不同,返回false
    • 如果两个节点值相同,继续比较左右子树
  3. 迭代比较:使用队列或栈同时遍历两棵树
  4. 序列化比较:将两棵树序列化为字符串,然后比较字符串

关键技巧

  • 同时访问两棵树的对应节点
  • 先检查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
递归比较详细流程
比较节点p和q
两个都为nil?
返回true
只有一个为nil?
返回false
值相同?
比较p.Left和q.Left
比较p.Right和q.Right
左右子树都相同?
BFS迭代流程
初始化队列p_queue和q_queue
队列不空?
返回true
同时出队p_node和q_node
两个都为nil?
只有一个为nil?
返回false
值相同?
同时入队左右子节点

复杂度分析

时间复杂度详解

递归比较算法: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. 完全相同[1,2,3] vs [1,2,3]true
  2. 结构不同[1,2] vs [1,null,2]false
  3. 值不同[1,2,1] vs [1,1,2]false
  4. 都为空[] vs []true
进阶测试用例
  1. 单节点相同[1] vs [1]true
  2. 单节点不同[1] vs [2]false
  3. 一棵为空[1] vs []false
  4. 完全二叉树相同[4,2,6,1,3,5,7] vs [4,2,6,1,3,5,7]true
  5. 链状树相同[1,null,2,null,3] vs [1,null,2,null,3]true
  6. 深度不同[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)

原因:栈是后进先出,需要先压右后压左才能先访问左

实用技巧

  1. 优先使用递归方法:代码简洁,逻辑清晰,易于理解和调试
  2. 提前终止:发现不同立即返回false,不需要继续比较
  3. nil检查放在前面:先处理nil情况,避免空指针异常
  4. BFS适合层次比较:如果需要逐层比较,使用BFS
  5. DFS适合深度比较:如果需要深度优先比较,使用DFS
  6. 迭代方法避免栈溢出:对于深度很大的树,使用迭代方法

进阶扩展

扩展1:判断子树是否相同
  • 判断一棵树是否是另一棵树的子树(需要先找到对应节点)
扩展2:判断树是否对称
  • 判断一棵树是否对称(镜像比较)
扩展3:判断树的结构是否相同
  • 只比较结构,不比较值
扩展4:找出不同的节点
  • 不仅返回true/false,还返回具体哪些节点不同

应用场景

  1. 数据结构验证:验证两个树结构是否相同
  2. 算法测试:测试树操作算法的正确性
  3. 代码审查:检查树构建代码的一致性
  4. 数据同步:检查两个数据源中的树结构是否一致
  5. 版本控制:比较不同版本的树结构

总结

判断两棵树是否相同是一个经典的树遍历 + 同步比较问题,核心在于:

  1. 同步遍历两棵树:同时访问对应位置的节点
  2. 先检查nil:避免空指针异常
  3. 递归比较最直观:代码简洁,易于理解
  4. 提前终止优化:发现不同立即返回,提高效率

完整题解代码

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))}
}
http://www.dtcms.com/a/573337.html

相关文章:

  • 51单片机数码管显示函数分享(8051汇编)
  • 国外互联网科技网站微信引流用什么软件好用
  • 静态交叉编译rust程序
  • 1.2.STM32简介——全程手敲板书
  • 2.2.6【2020统考真题】
  • Doris 并入CMP7(类Cloudera CDP 7.3.1 404华为鲲鹏ARM版)的方案和实施源代码
  • Vue3项目实战:从0到1开发企业级中后台系统(3):架构核心!手把手封装Axios、Pinia、Router
  • Spark on Yarn安装部署
  • 建设系统网站怎么做自然优化
  • 国产化数据库选型深度剖析:金仓KES与达梦DM全生命周期成本对比
  • Java 读取 Word 文本框中的文本和图片:Spire.Doc for Java 实践指南
  • 网站建设开发定制微信网站如何做
  • 商城项目业务总结
  • 安卓16提前发布能否改写移动生态格局
  • JVM :内存、性能调优与 JIT
  • JVM问题排查流程
  • 仲恺做网站wordpress屏蔽功能org
  • AI视频创作工具汇总:MoneyPrinterTurbo、KrillinAI、NarratoAI、ViMax
  • 部署我的世界-java版服务器-frp内网穿透
  • Eureka 注册中心原理与服务注册发现机制
  • Unity使用RVM实现实时人物视频抠像(无绿幕)
  • 物联网传感器环境自适应校准与精度补偿技术
  • 【低空安全】低空安防威胁与挑战
  • 微网站建设包括哪些iis5.1怎么新建网站
  • 45_AI智能体核心业务之Agent决策流程管理器:构建智能对话系统的工作流引擎
  • wordpress api定制济南seo公司案例
  • vscode运行ipynb文件:使用docker中的虚拟环境
  • 网站布局有哪些企业网站源码怎么获取
  • 如何科学地对单片机进行AI性能测试:指标、方法与实战
  • 软件设计师-树-叶子结点个数