判断两个二叉树是否相似(结构相同)
代码实现
| C |
|---|
| // 函数 func:判断两个二叉树是否相似(结构相同)
// 参数:T1 - 第一个二叉树的根节点,T2 - 第二个二叉树的根节点
// 返回值:1表示相似,0表示不相似
int func(BiTree T1, BiTree T2) {
// 情况1:两个节点都为空,结构相同
if (T1 == NULL && T2 == NULL) {
return 1;
}
// 情况2:其中一个节点为空,另一个不为空,结构不同
if (T1 == NULL || T2 == NULL) {
return 0;
}
// 递归比较左子树是否相似
int left = func(T1->lchild, T2->lchild);
// 递归比较右子树是否相似
int right = func(T1->rchild, T2->rchild);
// 只有当左右子树都相似时,整棵树才相似
return left && right;
}
|
代码逻辑解析
1. 基本概念
- 树相似:两个二叉树具有相同的结构形态,即对应节点的子树分布相同
- 树相等:两个二叉树不仅结构相同,而且对应节点的数据也相同
2. 算法思路
采用递归分治的思想:
- 比较当前节点的结构关系
- 递归比较左右子树
- 综合左右子树的结果得出最终结论
3. 递归终止条件
| C |
|---|
| // 基础情况判断
if (T1 == NULL && T2 == NULL) // 两个空节点 → 相似
return 1;
if (T1 == NULL || T2 == NULL) // 一个空一个非空 → 不相似
return 0;
|
4. 递归分解
- 分解:将问题分解为比较左子树和右子树
- 解决:递归调用函数比较子树
- 合并:通过逻辑与操作合并子问题结果
时间复杂度分析
1. 节点访问次数
- 在最坏情况下,需要访问两个树中的所有节点
- 设第一个树有 m 个节点,第二个树有 n 个节点
- 总访问次数为 min(m, n)(因为一旦发现不匹配就提前终止)
2. 每次操作复杂度
3. 总体时间复杂度
O(min(m, n)),在最坏情况下为 O(n)(假设两树节点数相近)
空间复杂度分析
1. 额外空间使用
- 只使用了常量级别的局部变量(left, right)
- 没有使用额外的数据结构
2. 递归调用栈空间
- 递归深度取决于树的高度
- 最好情况(平衡二叉树):O(log n)
- 最坏情况(退化为链表):O(n)
3. 总体空间复杂度
O(h),其中 h 是树的高度,在最坏情况下为 O(n)
较难理解部分的说明
1. 相似与相等的区别
| C |
|---|
| // 树相似的例子:
// T1: T2:
// A X
// / \ / \
// B C Y Z
// / /
// D W
// 结构相同,但节点数据可以不同 → 相似
// 树相等的例子:
// T1: T2:
// A A
// / \ / \
// B C B C
// / /
// D D
// 结构和数据都相同 → 相等
|
2. 逻辑与操作的短路特性
利用逻辑与的短路特性:
- 如果 left 为 0(假),不会计算 right,直接返回 0
- 这提供了早期终止优化,提高效率
3. 图解说明
假设两棵树:
| Text Only |
|---|
| T1: T2:
A X
/ \ / \
B C Y Z
/ / \ / / \
D E F W U V
|
递归比较过程:
| Text Only |
|---|
| func(A,X) → 比较数据(可选)→ func(B,Y) && func(C,Z)
↓
func(B,Y) → 比较数据(可选)→ func(D,W) && func(NULL,NULL)
↓
func(D,W) → 比较数据(可选)→ func(NULL,NULL) && func(NULL,NULL) → 1&&1=1
↓
func(NULL,NULL) → 1
↓
func(C,Z) → 比较数据(可选)→ func(E,U) && func(F,V)
↓
... 继续递归比较
|
延伸知识点
1. 只判断结构相似的版本
| C |
|---|
| // 仅判断结构相似,忽略节点数据
int isSimilarStructure(BiTree T1, BiTree T2) {
if (T1 == NULL && T2 == NULL) {
return 1; // 两个空树结构相同
}
if (T1 == NULL || T2 == NULL) {
return 0; // 一个空一个非空,结构不同
}
// 只比较结构,不比较数据
return isSimilarStructure(T1->lchild, T2->lchild) &&
isSimilarStructure(T1->rchild, T2->rchild);
}
|
2. 非递归版本 - 使用栈
| C |
|---|
| int isSimilarIterative(BiTree T1, BiTree T2) {
Stack* stack1 = createStack();
Stack* stack2 = createStack();
push(stack1, T1);
push(stack2, T2);
while (!isEmpty(stack1) && !isEmpty(stack2)) {
BiTree node1 = pop(stack1);
BiTree node2 = pop(stack2);
// 检查结构一致性
if ((node1 == NULL) != (node2 == NULL)) {
return 0; // 一个空一个非空
}
if (node1 != NULL && node2 != NULL) {
// 将子节点入栈
push(stack1, node1->lchild);
push(stack1, node1->rchild);
push(stack2, node2->lchild);
push(stack2, node2->rchild);
}
}
return isEmpty(stack1) && isEmpty(stack2);
}
|
3. 扩展应用 - 判断同构
| C |
|---|
| // 判断两个树是否同构(可以通过交换左右子树变得相同)
int isIsomorphic(BiTree T1, BiTree T2) {
if (T1 == NULL && T2 == NULL) {
return 1;
}
if (T1 == NULL || T2 == NULL) {
return 0;
}
if (T1->data != T2->data) {
return 0;
}
// 两种情况:不交换子树 或 交换子树
return (isIsomorphic(T1->lchild, T2->lchild) &&
isIsomorphic(T1->rchild, T2->rchild)) ||
(isIsomorphic(T1->lchild, T2->rchild) &&
isIsomorphic(T1->rchild, T2->lchild));
}
|
4. 相关算法问题
子树判断:
| C |
|---|
| // 判断 T2 是否是 T1 的子树
int isSubtree(BiTree T1, BiTree T2) {
if (T2 == NULL) return 1; // 空树是任何树的子树
if (T1 == NULL) return 0; // 空树不能包含非空树
// 在当前节点匹配 或 在左子树中匹配 或 在右子树中匹配
return isSameTree(T1, T2) ||
isSubtree(T1->lchild, T2) ||
isSubtree(T1->rchild, T2);
}
|
树的镜像判断:
| C |
|---|
| // 判断两个树是否互为镜像
int isMirror(BiTree T1, BiTree T2) {
if (T1 == NULL && T2 == NULL) {
return 1;
}
if (T1 == NULL || T2 == NULL) {
return 0;
}
return (T1->data == T2->data) &&
isMirror(T1->lchild, T2->rchild) &&
isMirror(T1->rchild, T2->lchild);
}
|
5. 实际应用场景
- 编译器优化:判断表达式树的结构相似性
- 模式识别:识别具有相同结构的决策树
- 数据库索引:比较 B+ 树的结构
- 生物信息学:比较进化树的拓扑结构
- 图形用户界面:比较 UI 组件树的布局结构
6. 性能优化技巧
| C |
|---|
| // 带剪枝优化的版本
int isSimilarOptimized(BiTree T1, BiTree T2) {
// 提前计算节点数进行快速判断(如果可以获取)
// if (getNodeCount(T1) != getNodeCount(T2)) return 0;
if (T1 == NULL && T2 == NULL) {
return 1;
}
if (T1 == NULL || T2 == NULL) {
return 0;
}
// 可以添加节点数据的快速比较
// if (T1->data != T2->data) return 0;
// 先比较一个子树,如果失败则不必比较另一个
if (!isSimilarOptimized(T1->lchild, T2->lchild)) {
return 0;
}
return isSimilarOptimized(T1->rchild, T2->rchild);
}
|
这个算法体现了递归分治的经典思想,通过将复杂问题分解为更小的子问题来解决,是树算法中的基础而重要的问题。