DS博客作业03--树
0.展示PTA总分
1.本周学习总结
1.1 总结树及串内容
串的BF\KMP算法
BF算法
- BF算法,亦称简单匹配算法,此算法的主要思路是通过穷举的方法来实现
如图,算法的思路是从s的每一个字符开始依次与t的字符匹配。
2. 对应的BF算法如下:
int index(SqString s,SqString t)
{ int i=0, j=0;
while (i<s.length && j<t.length)
{ if (s.data[i]==t.data[j]) //继续匹配下一个字符
{ i++; //主串和子串依次匹配下一个字符
j++;
}
else //主串、子串指针回溯重新开始下一次匹配
{ i=i-j+1; //主串从下一个位置开始匹配
j=0; //子串从头开始匹配
}
}
if (j>=t.length)
return(i-t.length); //返回匹配的第一个字符的下标
else
return(-1); //模式匹配不成功
}
- BF算法的分析
1.BF算法实现起来简单,但是在遇到字符比较不相等(匹配失败)的时候,主串指针需就要进行回溯操作(即i=i-j+1):即退到s中的下一个字符开始进行继续匹配。这样的操作会浪费大量的时间,主串越长,其实现功能的时间代价越大,因此,BF算法并不是模式匹配的最佳算法。
2.最好情况下,即在主串的开头就配对成功,时间复杂度为O(m)。
3.最坏情况下,即在主串的末尾才配对成功,时间复杂度为O(n×m)。
KMP算法
-
KMP算法算是BF算法的改进算法,对于BF算法中主串指针的回溯问题实现了消除,从而使算法效率有了一定程度的提高。
-
如下,图为KMP算法用next数组保存部分匹配信息的演示
-
next[j]
是指t[j]
字符前有多少个字符与t开头的字符相同。
-
KMP算法如下:
int KMPIndex(SqString s,SqString t)
{ int next[MaxSize], i=0, j=0;
GetNext(t,next);
while (i<s.length && j<t.length)
{
if (j==-1 || s.data[i]==t.data[j])
{ i++;
j++; //i、j各增1
}
else j=next[j]; //i不变,j后退
}
if (j>=t.length)
return(i-t.length); //返回匹配模式串的首字符下标
else
return(-1); //返回不匹配标志
}
- KMP算法的改进:将
next
改为nextval
在KMP算法的匹配中,虽然已经避免了BF算法的回溯问题,但是当前采用next[j]
数组的方法任然存在缺陷,即当匹配过程中遇到相同的字符时会出现重复匹配的问题。因此,我们需要将KMP算法进一步改进。
改进后的KMP算法,较原来的算法来说,省略了一些不必要的操作,进一步提高模式匹配的效率。
二叉树存储结构、建法、遍历及应用
二叉树的定义
二叉树是有限的结点的集合。这个集合或者是空,或者是由一个根节点和两颗互不相交的称为左子树和右子树的二叉树组成。
两种特殊的二叉树
- 满二叉树
- 特点:
- 所有分支结点都有双份结点
- 叶结点都集中在二叉树的最下一层
- 高度为h的满二叉树恰好有2^h-1个结点
- 完全二叉树
- 特点:
- 最多只要下面两层的结点的度数小于2
- 最下面一层的叶结点都依次排列在该层的最左边的位置上
- 完全二叉树实际上可以认为是对应的满二叉树删除叶结点层最右边若干个结点得到的
二叉树的存储结构
- 顺序存储结构
-
完全二叉树的顺序存储结构
-
非完全二叉树的顺序存储结构
typedef ElemType SqBTree[MaxSize];
SqBTree bt="#ABD#C#E######F";
- 顺序存储结构的特点
- 在顺序存储结构中,我们无论是找一个结点的双亲还是孩子都很容易。
- 对于完全二叉树来说,使用顺序存储是十分合适的。
- 但对于一般的二叉树,尤其是单分支结点较多的二叉树,那么使用顺序存储结构会造成大量的空间浪费,因此对于此类的二叉树,顺序存储结构并不适用。
- 链式存储结构
- 结点的定义:
typedef struct node
{ ElemType data;
struct node *lchild, *rchild;
} BTNode;
二叉树的创建
- 括号法建二叉树
void CreateBTNode(BTNode * &b,char *str)
{ //由str 二叉链b
BTNode *St[MaxSize], *p;
int top=-1, k , j=0;
char ch;
b=NULL; //建立的二叉链初始时为空
ch=str[j];
while (ch!='\0') //str未扫描完时循环
{ switch(ch)
{
case '(': top++; St[top]=p; k=1; break; //可能有左孩子结点,进栈
case ')': top--; break;
case ',': k=2; break; //后面为右孩子结点
default: //遇到结点值
p=(BTNode *)malloc(sizeof(BTNode));
p->data=ch; p->lchild=p->rchild=NULL;
if (b==NULL) //p为二叉树的根结点
b=p;
else //已建立二叉树根结点
{ switch(k)
{
case 1: St[top]->lchild=p; break;
case 2: St[top]->rchild=p; break;
}
}
}
j++; ch=str[j]; //继续扫描str
}
}
- 层次法建二叉树
/*这里需要注意Root与T的区别,Root是整棵树的根节点,T是新建节点的根节点,用队列与输入下标确定新建节点的位置*/
void CreatTree(Tree* &Root,int n)//建树,Root是返回的树根
{
int i=0;
queue<Tree*>Q;
Tree *T;//建树过程中的每个结点的根
while(i<n)
{
char elem;
cin>>elem;
if(i==0)
{
Root=new Tree();
Root->data= elem;
i++;
Q.push (Root);
continue;
}
T=Q.front ();//每次循环都将队头赋值给,将要建立的结点的祖先结点
if(elem=='#')
{
if(i%2==1)//按输入顺序,奇数为左结点
T->L =NULL;
if(i%2==0)//按输入顺序,偶数为右结点
{
T->R =NULL;
Q.pop ();//每一次建完右结点,其祖先结点就没用了,出队列
}
i++;
}
else
{
Tree *TMP;
TMP=new Tree();
TMP->data =elem;
if(i%2==1)//左
{
T->L =TMP;
}
if(i%2==0)//右
{
T->R =TMP;
Q.pop ();//每一次建完右结点,其祖先结点就没用了,出队列
}
Q.push (TMP);//将建立的结点入队列,elem=‘#’的空结点不用入
i++;
}
}
}
- 遍历递归建二叉树
BTree CreateTree(string str,int &i)
{
BTree T = new BTnode;
T->lchild = NULL;
T->rchild = NULL;
if (i > str.size() - 1 || i < 0 || str.at(i) == '#')
{
return NULL;
}
T->data = str.at(i);
T->lchild = CreateTree(str, ++i);
T->rchild = CreateTree(str, ++i);
return T;
}
二叉树的遍历
- 先序遍历
-
过程
- 先对根节点进行访问
- 之后先序遍历左子树
- 最后先序遍历右子树
-
先序序列的第一个结点是根结点
-
代码实现:
void PreOrder(BiTree T)//先序递归遍历
{
if(T!=NULL)
{
cout<<T->data<<" ";
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
- 中序遍历
-
过程
- 先中序遍历左子树
- 再对根结点进行访问
- 最后中序遍历右子树
-
中序序列的根结点位于中间位置,根结点的左边是左子树的结点,右边是右子树的结点
-
代码实现:
void InOrder(BiTree T)//中序递归遍历
{
if(T!=NULL)
{
InOrder(T->lchild);
cout<<T->data<<" ";
InOrder(T->rchild);
}
}
- 后序遍历
-
过程
- 先后序遍历左子树
- 再后序遍历右子树
- 最后对根结点进行访问
-
后序序列的最后一个结点时根结点
-
代码实现:
void PostOrder(BiTree T)//后序递归遍历
{
if(T!=NULL)
{
PostOrder(T->lchild);
PostOrder(T->rchild);
cout<<T->data<<" ";
}
}
- 层次遍历
- 代码实现:
void LevelOrder(BTNode *b)
{ BTNode *p;
SqQueue *qu; //定义环形队列指针
InitQueue(qu); //初始化队列
enQueue(qu,b); //根结点指针进入队列
while (!QueueEmpty(qu)) //队不为空循环
{ deQueue(qu,p); //出队结点p
printf("%c ",p->data); //访问结点p
if (p->lchild!=NULL) //有左孩子时将其进队
enQueue(qu,p->lchild);
if (p->rchild!=NULL) //有右孩子时将其进队
enQueue(qu,p->rchild);
}
}
二叉树的应用
例如:利用层次遍历,采用类似用队列求解迷宫问题的方法。这里设计的队列为非环形队列,队列的类型声明如下:
typedef struct snode
{ BTNode *pt; //存放当前结点指针
int parent; //存放双亲结点在队列中的位置
} NodeType; //非环形队列元素类型
typedef struct
{ NodeType data[MaxSize]; //存放队列元素
int front,rear; //队头指针和队尾指针
} QuType;
当找到一个叶子结点时,我们在队列中可以通过双亲结点的位置输出根结点到该叶子结点的逆路径。这条逆路径就是我们所要求的迷宫的出口路径。
具体算法如下:
void AllPath2(BTNode *b)
{ int k;
BTNode *p;
NodeType qelem;
QuType *qu; //定义非非环形队列指针
InitQueue(qu); //初始化队列
qelem.pt=b; qelem.parent=-1; //创建根结点对应的队列元素
enQueue(qu,qelem);
while (!QueueEmpty(qu)) //队不空循环
{ deQueue(qu,qelem); //出队元素在队中下标为qu->front
p=qelem.pt; //取元素qelem对应的结点p
if (p->lchild==NULL && p->rchild==NULL)
{ k=qu->front; //输出结点p到根结点的路径逆序列
while (qu->data[k].parent!=-1)
{ printf("%c->",qu->data[k].pt->data);
k=qu->data[k].parent;
}
printf("%c\n",qu->data[k].pt->data);
}
if (p->lchild!=NULL) //结点p有左孩子
{ qelem.pt=p->lchild; //创建左孩子对应的队列元素
qelem.parent=qu->front; //其双亲位置为qu->front
enQueue(qu,qelem); //结点p的左孩子进队
}
if (p->rchild!=NULL) //结点p有右孩子
{ qelem.pt=p->rchild; //创建右孩子对应的队列元素
qelem.parent=qu->front; //其双亲位置为qu->front
enQueue(qu,qelem); //结点p的右孩子进队
}
}
}
树的结构、操作、遍历及应用
树的定义
树是由n(n≥0)个结点组成的有限集合(记为T)。其中:
- 如果n=0,它是一棵空树,空树并不是错误的树,而是树的一种特例;
- 其余结点可分为m (m≥0)个互不相交的有限子集T1、T2、…、Tm,而每个子集本身又是一棵树,称为根结点root的子树。
- 所以,在一棵树中,所有的结点构成一种层次关系!
树的存储结构
- 双亲存储结构
- 结构体的定义:
typedef struct
{ ElemType data; //结点的值
int parent; //指向双亲的位置
} PTree[MaxSize];
- 双亲存储结构的分析:该结构寻找一个结点的双亲结点时会比较方便,但是对于寻找一个结点的孩子节点操作实现却不太方便。
- 孩子链存储结构
- 结构体的定义:
typedef struct node
{ ElemType data; //结点的值
struct node *sons[MaxSons]; //指向孩子结点
} TSonNode;
- 孩子链存储结构的分析:该结构在寻找一个节点的孩子节点操作比较方便,但是寻找一个结点的双亲结点就比较麻烦了。
- 孩子兄弟链存储结构
- 结构体的定义:
typedef struct tnode
{ ElemType data; //结点的值
struct tnode *hp; //指向兄弟
struct tnode *vp; //指向孩子结点
} TSBNode;
- 孩子兄弟链存储结构的分析:该结构的每个结点固定只有两个指针域,若寻找双亲结点,会比较麻烦。
树的运算操作
- 树的运算主要分为三大类:
- 查找满足某种特定关系的结点,例如查找当前结点的双亲结点等;
- 插入或删除某个结点,例如在树的某个结点之上上插入一个新结点或删除该结点的某一个孩子结点等;
- 遍历树中每个结点。
树的遍历
- 先根遍历
- 若树不空,则先访问根结点,然后依次先根遍历各棵子树
- 后根遍历
- 若树不空,则先依次后根遍历各棵子树,然后访问根结点
- 层次遍历
- 若树不空,则自上而下、自左至右访问树中每个结点
树的应用
树的应用十分的广泛
- 树与森林的互相转化
- 森林:n(n>0)个互不相交的树的集合称为森林。
- 因此,删去树的根结点,即可把树变为森林;同理,给n颗独立的树加上一个结点,同时把这n棵树作为该结点的子树,那么森林就转化为了一棵树。
- 在生活中,很多具有层次关系的数据都可以抽象为树,例如:
- 汽车产品库:车-品牌-车系-配置
- 公司组织架构:董事长-CXO-总监-经理-主管-员工
因此,公司组织架构可以用下图来描述:
PS:此例子来源于网络
所以,如果我们要对整个公司的人员进行梳理,那么就涉及上述内容所讲到的树的遍历的应用。而若要进行遍历,就涉及采用何种遍历的方式,如何对结点操作等等树的基本运算操作,这些这就利用到了上述的几种不同的树的运算操作;同时,在树的遍历过程中,比如分别输出公司的每一个等级的职员等等,又可以与之前的栈与队列结合(例如PTA-树-7-4 jmu-ds-输出二叉树每层节点),因此,树在应用方面可以说并不是单一的,而是覆盖了各个层面并且与我们之前所学的所有内容结合起来。
线索二叉树
- 概念
我们在采用某种方法遍历二叉树,其结果是一个结点的序列。这时,我们修改空链域让他变成存放结点的前驱和后继结点的地址,这样的指针,我们称作线索。线索化的二叉树我们把他称作线索二叉树。这样对二叉树进行线索化,可以提高遍历过程的效率。
线索二叉树可按照采取的遍历方式的不同而分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。
现将二叉树的结点结构重新定义如下:
- 线索化
- 我们要对二叉树进行线索化,首先需要明确我们要以哪一种遍历方法对二叉树进行遍历。
- 当我们确定了遍历次序的时候,当我们在遍历过程前,我们首先需要对二叉树进行概念中提到的修改空链域。
- 开始遍历二叉树,我们需要在遍历的过程中检查当前访问结点的左右指针域是否为空:
- 当左指针域为空时,我们就要将它改成指向前驱结点的线索
- 当右指针域为空时,我们就要将它改成指向后继结点的线索
- 以这种方式遍历完整棵树后,我们就将该二叉树线索化完成。
哈夫曼树、并查集
哈夫曼树
-
定义
设二叉树具有n个带权值的叶结点,那么从根结点到各个叶结点的路径长度与相应结点权值的乘积的和,叫做二叉树的带权路径长度(WPL)。 WPL值的计算方法如下
PS:WPL的另一种计算方法:WPL = 二叉树中除根结点之外所有结点的权值之和。
对于相同的叶结点来说,可以构造出很多种不同的二叉树,而这些二叉树种,具有最小带权路径长度的二叉树就称为哈夫曼树。因此哈夫曼树也被称为最优树。
-
构造哈夫曼树
- 构造哈夫曼树的原则:
- 权值越大的叶子结点越靠近根结点。
- 权值越小的叶子结点越远离根结点。
- 过程:
(1)在二叉树集合F中选取根结点权值最小和次小的两棵二叉树作为左、右子树构造一棵新的二叉树,这棵新的二叉树根结点的权值为其左、右子树根结点权值之和。
(2)在集合F中删除第(1)步中作为左、右子树的两棵二叉树,并将新建立的二叉树加入到集合F中。
(3)重复第(1)、第(2)两步,直到当集合F中只剩下一棵二叉树时,这棵二叉树便是我们所要建立的最优树(哈夫曼树)。
- 结构体的定义
typedef s truct
{
char data;//节点值
float weight;// 权重
int parent;//双亲节点
int lchild;//左孩子节点
int rchild;//右孩子节点
}HTNode;
- 算法实现
void Select(HuffmanTree &HT,int index, int &s1, int &s2)//选择全是最小的两个结点或树
{
int min1=MAX;
int min2=MAX;
for(int i=1;i<=index;++i)
{
if(HT[i].parent==0&&HT[i].tag)
{
if(HT[i].weight<min1)
{
s1=i;
min1=HT[i].weight;
}
}
}
HT[s1].tag=0;
for(int i=1;i<=index;++i)
{
if(HT[i].parent==0&&HT[i].tag)
{
if(HT[i].weight<min2)
{
s2=i;
min2=HT[i].weight;
}
}
}
HT[s2].tag=0;
}
void CreatHuffmanTree(HuffmanTree &HT,int n) //构造哈夫曼树
{
if(n<=1)return;
int m=2*n-1;
HT=new Huffnode[m+1];
for(int i=1;i<=m;++i)
{
HT[i].data=0;
HT[i].weight=0;
HT[i].lchild=0;
HT[i].rchild=0;
HT[i].parent=0;
HT[i].tag=1;
HT[i].deep=0;
}
for(int i=1;i<=n;++i)
{
HT[i].data=i;
scanf("%d",&HT[i].weight);
}
int s1,s2;
for(int i=n+1;i<=m;++i)
{
Select(HT,i-1,s1,s2);
HT[s1].parent=i;
HT[s2].parent=i;
HT[i].lchild=s1;
HT[i].rchild=s2;
HT[i].weight=HT[s1].weight + HT[s2].weight;
HT[i].data=i;
}
}
并查集
- 概念
并查集。是一种树型的数据结构,通常被我们用于处理一些不相交集合的合并及查询问题。例如亲属问题,家谱问题,朋友圈问题等等。比如判断两个人是否有亲属关系,而这两个人的共同祖先又与他们相隔数代,这种时候,若是有完整的家谱,但家谱十分庞大,无疑会对我们的判断造成一些困难。在这种情况下,我们就需要运用并查集 - 结构体的定义
typedef struct node
{
int data; //结点对应人的编号
int rank; //结点秩,大致为树的高度
int parent; //结点对应双亲下标
} UFSTree; //并查集树的结点类型
- 并查集树的初始化
void MAKE_SET(UFSTree t[],int n) //初始化并查集树
{ int i;
for (i=1;i<=n;i++)
{ t[i].data=i; //数据为该人的编号
t[i].rank=0; //秩初始化为0
t[i].parent=i; //双亲初始化指向自已
}
}
- 查找一个元素所属的集合
int FIND_SET(UFSTree t[],int x) //在x所在子树中查找集合编号
{ if (x!=t[x].parent) //双亲不是自已
return(FIND_SET(t,t[x].parent)); //递归在双亲中找x
else
return(x); //双亲是自已,返回x
}
- 两个元素各自所属的集合的合并
void UNION(UFSTree t[],int x,int y) //将x和y所在的子树合并
{ x=FIND_SET(t,x); //查找x所在分离集合树的编号
y=FIND_SET(t,y); //查找y所在分离集合树的编号
if (t[x].rank>t[y].rank) //y结点的秩小于x结点的秩
t[y].parent=x; //将y连到x结点上,x作为y的双亲结点
else //y结点的秩大于等于x结点的秩
{ t[x].parent=y; //将x连到y结点上,y作为x的双亲结点
if (t[x].rank==t[y].rank) //x和y结点的秩相同
t[y].rank++; //y结点的秩增1
}
}
1.2.谈谈你对树的认识及学习体会。
经过两周的学习,我对于树有了一些自己的想法。树结构主要是多分支结构,因此,在建树的过程中需要频繁的使用递归的写法,这也导致了我在刚刚开始学习树的时候,整个人有点懵懵的。后来在经过pta等一系列的练习,逐渐有了一些感悟。但是总的来说,在这方面还是有些欠缺,感觉最近写代码的时候,从开始编写,到调试,最后成功的提交,要花费不少时间,感觉自己在处理题目的效率上并没有很大的提升,可能也是我平时对于一些问题没有仔细研究的原因。总之,在这块上还是很薄弱。实验课上看了同学的博客以及老师讲解到的一些写法,正在努力赶上。
2.阅读代码
2.1 题目及解题代码
2.1.1 该题的设计思路
- 如图,这是一棵二叉树。我们可以知道,如果一个树的左子树与右子树镜像对称,那么这棵二叉树是对称的。
- 那么,我们的目标就是判断这颗二叉树的左右子树是否镜像对称。当一棵二叉树要满足镜像对称,只需要满足以下条件:
- 当两棵树处于对称位置的时候,它们的根结点相同。
- 对称位置的两棵树,其中一棵二叉树的左子树与另一颗二叉树的右子树相同,反之亦然。
2.1.2该题的伪代码
public boolean isSymmetric(TreeNode root) {
return isMirror(root, root);
}
public boolean isMirror(TreeNode t1, TreeNode t2) {
if (两个结点同时为null) return true;
if (t1结点为null或者t2结点为null) return false;
否则,(即两个结点都不为null)
return (t1.val == t2.val)
&& 将t1结点的右孩子与t2结点的左孩子传入函数递归isMirror(t1.right, t2.left)
&& 将t1结点的左孩子与t2结点的右孩子传入函数递归isMirror(t1.left, t2.right);
}
- 时间复杂度:O(n):其中 n 是树中结点的总数。
- 空间复杂度:O(n)
2.1.3运行结果
2.1.4分析该题目解题优势及难点
- 优势:思路简洁易懂,遍历递归整棵树来进行判断
- 难点:
- 树为镜像,并不是左右子树完全相同,需要在参数的传递已经对传入两个结点的判断上注意
- 递归调用的次数受树的高度限制。在最糟糕情况下,树是线性的,其高度为 O(n),占用的空间比较大。
2.2 题目及解题代码
2.2.1 该题的设计思路
1.思路:我们可以定义一个node
来先指向当前二叉树的根结点,通过访问当前结点的左右孩子,若有孩子结点不为1,则对该结点定义函数进行递归上述操作,当遍历完二叉树的时候,将不符合的结点对应的指针置为空
2.2.2该题的伪代码
定义pruneTree函数用于删除不符合题意的结点public TreeNode pruneTree(TreeNode root) {
return containsOne(root) ? root : null;
public boolean containsOne(TreeNode node) {
if 根结点为空
返回 false;
end if
定义a1为当前结点的左孩子的递归返回值 a1 = containsOne(node.left);
定义a2为当前结点的右孩子的递归返回值 a2 = containsOne(node.right);
if 左子树包含1(a1不等于0)
左孩子置空node.left = null;
if 右子树包含1(a2不等于0)
右孩子置空node.right = null;
(即在子树递归中出现了孩子结点不为0的结点,通过返回null对递归进行结束)
对结点进行判断并返回对应的1或者a1或者a2 return node.val == 1 || a1 || a2;
}
}
- 时间复杂度:O(N):其中N是树中结点的个数。
- 空间复杂度:O(H):其中H为树的高度。
2.2.3运行结果
2.2.4分析该题目解题优势及难点
- 优势:遍历递归的方法并不陌生,分为左右子树开始遍历,整体实现难度不高
- 难点:对于递归过程中返回值的处理。尤其是在对a1和a2的返回值以及最后一句结点都为0的判断,如何正确的写出适当的返回值来让题目实现是难点。
2.3 题目及解题代码
2.3.1 该题的设计思路
1.思路:题目需要遍历出一颗二叉树右视图结点元素,那么意味着我们需要的是这颗二叉树每一层的最右侧的结点,那么我们可以立即联想到我们所学过的二叉树的层次遍历。对这棵二叉树进行层次遍历,依次取出每一层的最后一个结点即可。
- 我们对每一层都从左到右访问。因此,通过只保留每个深度最后访问的结点,我们就可以在遍历完整棵树后得到每个深度最右的结点
2.3.2该题的伪代码
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public List<Integer> rightSideView(TreeNode root) {
创建一维数组接收结果值
List<Integer> ans = new ArrayList<>();
if (根节点为空)
return ans;
end if
创建队列进行层次遍历
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while (队列不为空)
队列长度赋值int len = queue.size();
// 这里先不给 node 赋值
创建接受目标结点指针TreeNode node = null;
for int i = 0 to len-1
在这里进行赋值,每次取出的都是当前层最后一个元素
node = queue.poll();
if (左孩子不为空)
queue.add(node.left);
end if
if (右孩子不为空)
queue.add(node.right);
end if
end for
ans.add(node.val);
end while
返回ans return ans;
}
}
- 时间复杂度:O(n)
- 空间复杂度:O(n)
2.3.3运行结果
2.3.4分析该题目解题优势及难点
- 优势:将我们所学的层次遍历运用到这题当中,使代码实现起来更加的方便,这样简化思路,使代码的可读性大大增强
- 难点:本题需要定义接收目标结点指针
node
,在for
循环之前定义为空,而后进入循环再给它赋值为每层最后一个结点,这样node
每次取出的就是一层中最后一个结点了,最后放入数组即可完成右视图遍历。
2.4 题目及解题代码
2.4.1 该题的设计思路
这道题是翻转等价二叉树,我们需要明确以下几种情况:
1.如果二叉树的两个结点 root1
,root2
值相等,那么他们的孩子是否相等这两个二叉树即为等价。
2.当 root1
,root2
两个结点的值不相等,那这两个二叉树的一定不等价。
PS:第3点和第4点为题解补充,我在看到这题时没有想到这一点...我在是看完题解和代码后后知道的这两点。
3.当 root1
和 root2
的值相等的情况下,需要继续判断 root1
的孩子节点是不是跟 root2
的孩子节点相当。因为可以做翻转操作,所以这里有两种情况需要去判断。
4.如果 root1
或者 root2
是 null
,那么只有在他们都为 null
的情况下这两个二叉树才等价;否则,只有一个结点为null
这两棵二叉树就不为等价。
2.4.2该题的伪代码
class Solution {
public boolean flipEquiv(TreeNode root1, TreeNode root2) {
if 两个根结点相等
return true;
end if
if (结点1 == null 或 结点2 == null 或 两结点值)
return false;
end if
考虑结点翻转,所以这里有两种情况需要去判断。
return (递归两结点的左孩子 && 递归两结点的右孩子 或 递归结点1的左孩子和结点2的右孩子 && 递归结点1的右孩子和结点2的左孩子);
}
}
- 时间复杂度:O(min(N1,H2)),其中 N1,N2 分别是二叉树
root1
,root2
的大小。 - 空间复杂度:O(min(H1,H2)),其中 H1,H2 分别是二叉树
root1
,root2
的高度。
2.4.3运行结果
2.4.4分析该题目解题优势及难点
优势:代码精简,思路明确,充分考虑所有可能出现的情况,可读性很强。
难点:在所有情况的处理上有难度,因为题目并未给出翻转的结点,所以,在判断结点的时候需要进行更多方面的考虑,需要将根结点进行比较,同时对结点的左右孩子进行判断。 -