本系列文章为浙江大学陈越、何钦铭数据结构学习笔记,前面的系列文章链接如下:
数据结构基础:P1-基本概念
数据结构基础:P2.1-线性结构—>线性表
数据结构基础:P2.2-线性结构—>堆栈
数据结构基础:P2.3-线性结构—>队列
数据结构基础:P2.4-应用实例—>多项式加法运算
数据结构基础:P2.5-应用实例—>多项式乘法与加法运算-C实现
数据结构基础:P3.1-树(一)—>树与树的表示
数据结构基础:P3.2-树(一)—>二叉树及存储结构
数据结构基础:P3.3-树(一)—>二叉树的遍历
题目描述
给定两棵树T1和T2。如果T1可以通过若干次左右孩子互换就变成T2,则我们称两棵树是“同构”的。例如图1给出的两棵树就是同构的,因为我们把其中一棵树的结点A、B、G的左右孩子互换后,就得到另外一棵树。而图2就不是同构的。
现给定两棵树,请你判断它们是否是同构的。
输入格式:
输入给出2棵二叉树树的信息。对于每棵树,首先在一行中给出一个非负整数N (≤10),即该树的结点数(此时假设结点从0到N−1编号);随后N行,第i行对应编号第i个结点,给出该结点中存储的1个英文大写字母、其左孩子结点的编号、右孩子结点的编号。如果孩子结点为空,则在相应位置上给出“-”。给出的数据间用一个空格分隔。注意:题目保证每个结点中存储的字母是不同的。
输出格式:
如果两棵树是同构的,输出“Yes”,否则输出“No”。
输入样例1(对应图1):
8
A 1 2
B 3 4
C 5 -
D - -
E 6 -
G 7 -
F - -
H - -
8
G - 4
B 7 6
F - -
A 5 1
H - -
C 0 -
D - -
E 2 -
输出样例1:
Yes
输入样例2(对应图2):
8
B 5 7
F - -
A 0 3
C 6 -
H - -
D - -
G 4 -
E 1 -
8
D 6 -
B 5 -
E - -
H - -
C 0 2
G - 3
F - -
A 1 4
输出样例2:
No
一、题意理解及二叉树表示
1.1 同构二叉树
给定两棵树T1和T2。如果T1可以通过若干次左右孩子互换变成T2,则我们称两棵树是“同构”的。现给定两棵树,请你判断它们是否是同构的。
思路:乍一看的话,这两棵树好像样子不一样,实际上他是同构的。根节点A下面分别都有B跟C,只要判别B的指数跟另外一个B的指数是不是一样的,C的跟另外一个C的是不是一样的。比方说C,两棵树的C下面都有左儿子是G ,G下面都有一个儿子是H,一个是左边,一个在右边。但是我们说了可以左右交换,所以他还是一样的,所以说这两棵树是同构的。
那我们再看这棵树是不是同构的
思路:这棵树他就不是同构的。我们看节点C,这个C在左边这棵树里边只有一个儿子是G,在右边这棵树里边有两个儿子是D跟E。所以很明显的,在C这个组数上就不一样了,所以整个树就不同构了。
1.2 题意理解
我们现在理解了两棵树同构是什么意思,现在回到题目本身。
输入格式: 输入2棵二叉树的信息:
• 先在一行中给出该树的结点数,随后N行
• 第i行对应编号第i个结点,给出该结点中存储的字母、其左孩子结点的编号、右孩子结点的编号。
• 如果孩子结点为空,则在相应位置上给出“-”。
输入的两棵树:
输入样例:
8
A 1 2
B 3 4
C 5 -
D - -
E 6 -
G 7 -
F - -
H - -
8
G - 4
B 7 6
F - -
A 5 1
H - -
C 0 -
D - -
E 2 -
注:关于输入的第一个数据
在我们这样的一种输入表示方法里面,不要求根结点作为第一个数据来输入,可以按任意的顺序来输入每个结点的信息。比方说我们第一个是G,他不是根结点。第二个是B,第三个是F,它的顺序没有任何规律,你可以按任意的顺序来输入。但是每一行对应的一个结点中,第一个数据代表这个结点本身的信息,后面两个整数是代表他左右儿子所在的编号。
1.3 思路
在这里面很关键的一个东西就是根结点在哪里。如果仅仅告诉你左边这一系列数据,你能很快的看出根结点在哪里吗,或者说弄一个程序来判别根结点到底是谁。因为根结点不是一定要摆在第一个数据里面,所以找到根结点在哪里就是这道题要解决的三个问题的前提:
1、二叉树表示
2、建二叉树
3、同构判别
首先我们来看第一个问题:二叉树表示
我们最常见的二叉树的表示方法就是两个指针 left 跟 right 这样的一种链表结构的一种表示方法。
除了链表表示二叉树之外,实际上我们知道也可以用数组表示。数组表示二叉树最基本的一种形式就是把这个二叉树看成是一个完全二叉树,缺少的结点在数组里面把它空出来。
我们在这里要采用的是一种结构数组的表示方法,就是我们基本的存储是用数组把我们所需要的几点信息存储在数组里面。但是左右儿子用类似链表的这种方法来表示,有一个数据来指示左儿子在哪里 右儿子在哪里。物理上的存储是数组,但是他的思想是一种链表的思想,所以这种链表我们称为静态链表。
比方说我们这样的一个二叉树,有四个节点,怎么样用结构数组这样的方法来表示呢?
我们一种表示方法是表示成这样:
这是一个数组,每一列就是数组的一个分量,每个分量是一个结构。这个结构包含了三个信息:一个ABCD,代表了这个结点本身的信息,用来标识节点。另外 left 跟 right 不是指向左儿子右儿子指针,而是指向左儿子右儿子位置的下标这个整数。数组正常的下标是从0开始的,我们用 -1 表示空的结点。所以从这里面大家可以看到,A没有左儿子,所以它的 left 是-1。右儿子在下标为1的地方,所以他的 right 是1。B的左儿子是C,在下标为2的地方,所以B的 left 是2。B的右儿子是D,D在下标为3的地方,所以的话B的right是3。C跟D左右儿子都没有,所以他们的 Left 和 Right 都是-1。
根据这样的一种结构数组的表示方法,我们就可以定义相应的数据结构:
#define MaxTree 10
#define ElementType char
#define Tree int
#define Null -1 //注意,不能用NULL,NULL在C语言里面是0
struct TreeNode
{
ElementType Element;
Tree Left;
Tree Right;
} T1[MaxTree], T2[MaxTree];
不唯一的表示方法:
二叉树用这个结构数组来进行表示,他的表示不是唯一的,我们也可以表示成这样。
这里 ABCD的顺序可以没有任何关系,在数组里面可以随便换。对应的树仍然为:
所以,同样的一棵树在结构数组里的静态链表的表示方法里面可以有不同的方法,这就是他的灵活性。他有链表的灵活性,但是他的存储又是在数组上面,所以这个叫静态链表。
如何找到根结点:
因为他的顺序是ABCD可以随便换的,所以你可能一下子就不知道根在哪里。实际上我们仔细分析,其实不难找出根在哪里。我们ABCD这四个结点点放的位置在0134这四个下标里面,然后我们来看0134这四个下标里面有哪几个在我们的结构数组里面的left right 里面出现了,有哪个没出现。大家可以看得到在0134里面,B的左右是4跟3,A右边是0,也就说034用掉了。所以在0134里面,这1没用掉,那么1所对应的那个结点A就是根。所以这是我们判别根的一个很有效的方法:看看有没有谁没指向他,那个节点就是根。
二、程序框架、建树及同构判别
2.1 程序框架
这个程序的框架比较简单,因为我们是要判别两个二叉树是不是同构,那么一开始输入的信息是两个二叉树有关的信息,接下来判别这两个二叉组是不是同构。
int main()
{
建二叉树1
建二叉树2
判别是否同构并输出
return 0;
}
在这里我们只需要设计两个函数,一个怎么从输入数据里面建立对应的二叉树,另外一个怎么判别这两个二叉树是不是同构的。
#define MaxTree 10
#define ElementType char
#define Tree int
#define Null -1 //注意,不能用NULL,NULL在C语言里面是0
struct TreeNode
{
ElementType Element;
Tree Left;
Tree Right;
} T1[MaxTree], T2[MaxTree];
int main()
{
Tree R1, R2;
R1 = BuildTree(T1); //建立二叉树
R2 = BuildTree(T2);
if (Isomorphic(R1, R2)) printf("Yes\n"); //判断是否同构
else printf("No\n");
return 0;
}
代码解释为:
首先我们通过BuildTree来建第一棵树R1,然后再调用BuildTree来建第二棵树 R2。这里T1 T2就是我们前面提到的结构数组,是个全局变量,他作为参数传进去。接下来我们调用Isomorphic这个函数来判别R1 R2这两棵二叉树是不是同构的。
2.2 建树
读入数据
我们先讨论一下怎么建这个二叉树,也就说BulidTree这个函数该怎么写。我们知道BulidTree的输入是我们的数据,首先要输入的是这个树有多少个节点,然后是每个节点的基本信息。
Tree BuildTree( struct TreeNode T[] )
{
scanf("%d\n", &N);
if (N) {
for (i=0; i<N; i++) {
scanf("%c %c %c\n", &T[i].Element, &cl, &cr);
}
Root = ??? //根结点在哪里???我们后面你需要写代码去寻找
}
return Root;
}
代码对应解释为:
①我们整个程序里面,首先使用scanf把这个结点数(就是我们这个例子里看到的8)读进来。
②读入N之后,判别一下N是不是不等于0。
③N不等于0的时候,就把这个N个结点的信息一个个的读进来。
④所以我们就设置了一个for循环,从0开始一直到N-1。每轮循环读一行数据,这一行数据就包括了三个信息:一个是Element的信息,一个是 left,一个是 right。
⑤为了处理方便,这三个信息读进来的时候都处理成字符的方式读进来。我们再把字符转化为整数,再放到left跟right里面去,所以就是用了这样的一个scanf三个%c把这三个数据读进来。
⑥这样的话就把二叉树这N个结点信息都读好了,这里我们这个函数有个返回值,就是树根。所以接下来我们很关键的一个问题就是如何去找这个树根Root。
找到根结点
前面我们也提到了相应的想法,可以把这个结构数组从头到尾扫描一遍,然后看看有没有哪个结点不存在其他结点指向他。如果没人指向他,他就是根结点了,非根结点肯定有人指向他了。所以我们的策略就是把T[i]这个数组遍历一遍,看看有没有谁指向他。
Tree BuildTree( struct TreeNode T[] )
{
scanf("%d\n", &N);
if (N) {
for (i=0; i<N; i++) check[i] = 0;
for (i=0; i<N; i++) {
scanf("%c %c %c\n", &T[i].Element, &cl, &cr);
if (cl != '-') {
T[i].Left = cl-'0';
check[T[i].Left] = 1;
}
else T[i].Left = Null;
/*对cr的对应处理 */
if (cr != '-') {
T[i].Right = cr-'0';
check[T[i].Right] = 1;
}
else T[i].Right = Null;
}
for (i=0; i<N; i++)
if (!check[i]) break;
Root = i;
}
return Root;
}
代码对应解释为:
①定义一个数组check,它对应的是结构数组的那N个结点,check的值一开始都等于0
②在读信息的过程当中,将Element、left和right读进来之后,同时对他的left跟right进行处理。如果我一个结点有一个left指向了某个位置,那么就把那个位置的check设为1。如果读进来这个right指向着另外一个结点,那么我就把right指向那个位置的check设为1。
③整个循环做完了之后,没有被别人check 被改为1的结点(也说仍然保留0的结点)就是根结点了。所以这个for循环从0到N 退出来之后,再用一个for循环从0到N-1判别一下哪一个结点的check值是0的,所以就break出来。这个break的位置 i 就是Root的位置,把这个i赋给Root,然后return Root。
2.3 同构判别
这个函数一进来首先要对一些基本情况做个判别:
①如果这两棵树的根结点都是空的,也就是说R1 R2的值都是 -1,这个时候两个空的树我们认为是同构的,就return 1。
②反过来根结点一个空一个不空,那就肯定不同构,就return 0。
③如果两棵树的根结点的值不一样,那肯定就不同构,就return 0。
④如果这两棵树根结点的左子树都是空的,那这个树是否同构就看右边子树是否同构。
⑤除此之外,左边不是同时空的这样的一种情况,那么看看左边是不是同时不空。左边如果同时不空,那么我们接下来看看左边的Element是不是一样的。如果左边Element是一样的,那么这个时候就变成是看看左边相等不相等 右边相等不相等。所以接下来就变成是return左边左边的判别 & 右边右边的判别。
⑥反过来,如果这个Element值是不一样的,那么这时候有可能是左边跟右边同构,右边跟左边同构。所以就变成了是左边右边的判别。
⑦当然还有一种情况,左边的这个指向的是空树,右边指向的不空树。那么这个时候同样也变得是左边跟右边的判别,左边跟右边的判别。
对应代码为:
int Isomorphic ( Tree R1, Tree R2 )
{
if ( (R1==Null )&& (R2==Null) ) /* both empty */
return 1;
if ( ((R1==Null)&&(R2!=Null)) || ((R1!=Null)&&(R2==Null)) )
return 0; /* one of them is empty */
if ( T1[R1].Element != T2[R2].Element )
return 0; /* roots are different */
if ( ( T1[R1].Left == Null )&&( T2[R2].Left == Null ) )
/* both have no left subtree */
return Isomorphic( T1[R1].Right, T2[R2].Right );
if ( ((T1[R1].Left!=Null)&&(T2[R2].Left!=Null))&&
((T1[T1[R1].Left].Element)==(T2[T2[R2].Left].Element)) )
/* no need to swap the left and the right */
return ( Isomorphic( T1[R1].Left, T2[R2].Left ) &&
Isomorphic( T1[R1].Right, T2[R2].Right ) );
else /* need to swap the left and the right */
return ( Isomorphic( T1[R1].Left, T2[R2].Right) &&
Isomorphic( T1[R1].Right, T2[R2].Left ) );
}
三、整体代码
整体代码如下
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#define MaxTree 10
#define Null -1
#define ElementType char
#define Tree int
struct TreeNode {
ElementType data; // 存值
Tree left; // 左子树的下标
Tree right; // 右子树的下标
}T1[MaxTree], T2[MaxTree];
// 建二叉树,返回根结点 (根节点编号未出现在其他结点编号的后面,创建一个check数组来确定)
Tree BuildTree(struct TreeNode T[])
{
int i;
int n;
int check[MaxTree]; //创建一个check数组来确定根节点,若在静态链表中未出现的下标则为根节点
char left, right;
Tree root = Null; //若n为0,返回Null
scanf("%d\n", &n);
if (n)
{
for (i = 0; i < n; i++)
{
check[i] = 0;
}
for (i = 0; i < n; i++)
{
getchar();
scanf("%c %c %c", &T[i].data, &left, &right);
if (left != '-')
{
T[i].left = left - '0'; //若输入不为'-',那字符减去字符0转换为整型数值
check[T[i].left] = 1; //把在静态链表中出现过的数值标记为1
}
else if (left == '-')
T[i].left = Null;
if (right != '-')
{
T[i].right = right - '0';
check[T[i].right] = 1;
}
else if (right == '-')
T[i].right = Null;
}
for (i = 0; i < n; i++)
{
if (!check[i])
break;
}
root = i;
}
return root;
}
// 判断是否同构
bool Isomorphic(int R1, int R2)
{
if (R1 == Null && R2 == Null) // 都为空
return true;
if (R1 == Null && R2 != Null || R1 != Null && R2 == Null) // 一个为空,一个不为空
return false;
if (T1[R1].data != T2[R2].data) // 值不同
return false;
if ((T1[R1].left == Null) && (T2[R2].left == Null)) //左儿子均为空
{
return Isomorphic(T1[R1].right, T2[R2].right);
}
if ((T1[R1].left != Null && T2[R2].left != Null) && (T1[T1[R1].left].data == T2[T2[R2].left].data)) // 左儿子不为空且值相等
return Isomorphic(T1[R1].left, T2[R2].left) && Isomorphic(T1[R1].right, T2[R2].right);
else // 左儿子不为空且值不等 或者 某一个左儿子为空(有可能左边和右边同构,右边和左边同构)
return Isomorphic(T1[R1].right, T2[R2].left) && Isomorphic(T1[R1].left, T2[R2].right);
}
int main()
{
Tree R1, R2;
R1 = BuildTree(T1);
R2 = BuildTree(T2);
if (Isomorphic(R1, R2))
printf("Yes\n");
else
printf("No\n");
return 0;
}
运行,输入两个测试案例,结果正确