【LeetCode】100. 相同的树

程序员成长:技术、职场与思维模式实战指南 10w+人浏览 1.3k人参与

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 TD
    A[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 -->|否| E
    F -->|是| 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:]
        
        // 检查nil
        if 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) - 1
        pNode := pStack[n1]
        qNode := qStack[n1]
        pStack = pStack[:n1]
        qStack = qStack[:n1]
        
        // 检查nil
        if 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 main

import (
	"fmt"
)

type TreeNode struct {
	Val   int
	Left  *TreeNode
	Right *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:]

		// 检查nil
		if 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) - 1
		pNode := pStack[n1]
		qNode := qStack[n1]
		pStack = pStack[:n1]
		qStack = qStack[:n1]

		// 检查nil
		if 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 := 1

	for 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 = left
			queue = append(queue, left)
		}
		i++

		// 右子节点
		if i < len(arr) && arr[i] != nil {
			right := &TreeNode{Val: arr[i].(int)}
			node.Right = right
			queue = append(queue, right)
		}
		i++
	}

	return root
}

// =========================== 测试 ===========================
func main() {
	fmt.Println("=== LeetCode 100: 相同的树 ===\n")

	testCases := []struct {
		name     string
		p        *TreeNode
		q        *TreeNode
		expected 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 := 0
		for i, tc := range testCases {
			got := methodFunc(tc.p, tc.q)
			ok := got == tc.expected
			status := "✅"
			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))
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值