0.展示PTA总分
1.本周学习总结
1.1 总结树及串内容
串的BF\KMP算法
BF算法
int index(SqString s,SqString t)
{ int i=0, j=0;
while (i=t.length)
return(i-t.length); //返回匹配的第一个字符的下标
else
return(-1); //模式匹配不成功
}
-
BF算法的分析
1.算法在字符比较不相等,需要回溯(即i=i-j+1):即退到s中的下一个字符开始进行继续匹配。
2.最好情况下的时间复杂度为O(m)。
3.最坏情况下的时间复杂度为O(n×m)。
4.平均的时间复杂度为O(n×m)。 -
所以,虽然BF算法实现起来简单,但是在主串指针进行回溯的时候,运行时间会大大提高,使得整个算法的运行效率降低,所以并不是匹配串的最佳算法。
KMP算法
- KMP算法较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=t.length)
return(i-t.length); //返回匹配模式串的首字符下标
else
return(-1); //返回不匹配标志
}
- KMP算法的改进:将
next
改为nextval
在KMP算法的匹配中,依然存在缺陷,即相同的字符出现重复匹配的问题。因此,我们将KMP算法进一步改进。
改进后的KMP算法,较原来的算法来说,省略了一些不必要的操作,进一步提高模式匹配的效率。
二叉树存储结构、建法、遍历及应用
二叉树的定义
二叉树是有限的结点集合。这个集合或者是空,或者是由一个根节点华人两颗互不相交的称为左子树和右子树的二叉树组成。
两种特殊的二叉树
- 满二叉树
- 完全二叉树
二叉树的性质
- 性质1 非空二叉树上叶结点数等于双分支结点数加1。
- 性质2 非空二叉树上第i层上至多有2^(i-1)个结点(i≥1)。
- 性质3 高度为h的二叉树至多有2^(h)-1个结点(h≥1)。
二叉树的存储结构
- 顺序存储结构
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;
queueQ;
Tree *T;//建树过程中的每个结点的根
while(i>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<data<<" ";
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
- 中序遍历
void InOrder(BiTree T)//中序递归遍历
{
if(T!=NULL)
{
InOrder(T->lchild);
cout<data<<" ";
InOrder(T->rchild);
}
}
- 后序遍历
void PostOrder(BiTree T)//后序递归遍历
{
if(T!=NULL)
{
PostOrder(T->lchild);
PostOrder(T->rchild);
cout<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;
- 孩子兄弟链存储结构的每个结点固定只有两个指针域,若寻找双亲结点,会比较麻烦。
树的运算操作
- 树的运算主要分为三大类:
- 查找满足某种特定关系的结点,如查找当前结点的双亲结点等;
- 插入或删除某个结点,如在树的当前结点上插入一个新结点或删除当前结点的第i个孩子结点等;
- 遍历树中每个结点。
树的遍历
- 先根遍历
- 后根遍历
- 层次遍历
- 树与森林的互相转化
- 森林:n(n>0)个互不相交的树的集合称为森林。
- 因此,删去树的根结点,即可把树变为森林;同理,给n颗独立的树加上一个结点,同时把这n棵树作为该结点子树,那么森林就转化为了一棵树。
- 在生活中,很多具有层次关系的数据都可以抽象为树,例如:
- 汽车产品库:车-品牌-车系-配置
- 公司组织架构:董事长-CXO-总监-经理-主管-员工
因此,公司组织架构可以用下图来描述:
所以,如果对整个公司的人员进行梳理,那么就涉及到对上述架构的树进行遍历。而进行遍历,就涉及遍历的方式,这就利用到了上述的几种不同的树的遍历方法;同时,在树的遍历过程中,对于每个结点的处理等等,又可以与之前的栈与队列结合(例如PTA-树-7-4 jmu-ds-输出二叉树每层节点),因此,树在应用方面覆盖了各个层面与知识。
线索二叉树
n个结点的二叉链表中含有n+1(2n-(n-1)=n+1)个空指针域。利用二叉链表中的空指针域,存放指向结点在某种遍历次序下的前驱和后继结点的指针(这种附加的指针称为"线索")。
- 概念
这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。 - 本质
二叉树的遍历本质上是将一个复杂的非线性结构转换为线性结构,使每个结点都有了唯一前驱和后继(第一个结点无前驱,最后一个结点无后继)。对于二叉树的一个结点,查找其左右子女是方便的,其前驱后继只有在遍历中得到。为了容易找到前驱和后继,有两种方法。一是在结点结构中增加向前和向后的指针fwd和bkd,这种方法增加了存储开销,不可取;二是利用二叉树的空链指针。现将二叉树的结点结构重新定义如下:
- 线索化
对二叉树线索化,实质上就是遍历一棵二叉树。在遍历过程中,访问结点的操作是检查当前的左,右指针域是否为空,将它们改为指向前驱结点或后续结点的线索。为实现这一过程,设指针pre始终指向刚刚访问的结点,即若指针p指向当前结点,则pre指向它的前驱,以便设线索。
另外,在对一颗二叉树加线索时,必须首先申请一个头结点,建立头结点与二叉树的根结点的指向关系,对二叉树线索化后,还需建立最后一个结点与头结点之间的线索。 - 分类
哈夫曼树、并查集
哈夫曼树
-
定义
设二叉树具有n个带权值的叶结点,那么从根结点到各个叶结点的路径长度与相应结点权值的乘积的和,叫做二叉树的带权路径长度。
对于相同的叶结点来说,可以构造出很多种不同的二叉树,而这些二叉树种,具有最小带权路径长度的二叉树就称为哈夫曼树。因此哈夫曼树也被称为最优树。
-
构造哈夫曼树
- 原则:
- 权值越大的叶结点越靠近根结点。
- 权值越小的叶结点越远离根结点。
- 过程:
(1)在二叉树集合F中选取根结点的权值最小和次小的两棵二叉树作为左、右子树构造一棵新的二叉树,这棵新的二叉树根结点的权值为其左、右子树根结点权值之和。
(2)在集合F中删除作为左、右子树的两棵二叉树,并将新建立的二叉树加入到集合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
并查集
- 概念
并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题。常常在使用中以森林来表示。 - 结构体的定义
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):因为我们需要遍历整个树一次,所以总的运行时间为 O(n),其中 n 是树中结点的总数。
- 空间复杂度:O(n):当树为线性的时候,为O(n)。
2.1.3运行结果
2.1.4分析该题目解题优势及难点
- 优势:思路简洁易懂,遍历递归整棵树来进行判断
- 难点:
- 树为镜像,并不是左右子树完全相同,需要在参数的传递已经对传入两个结点的判断上注意
- 递归调用的次数受树的高度限制。在最糟糕情况下,树是线性的,其高度为 O(n),占用的空间比较大。
2.2 题目及解题代码
2.2.1 该题的设计思路
1.思路:构造一个获取当前节点最大深度的方法 depth(root)
,通过比较此子树的左右子树的最大高度差abs(depth(root.left) - depth(root.right))
,来判断此子树是否是二叉平衡树。若树的所有子树都平衡时,此树才平衡。
2.2.2该题的伪代码
class Solution:
def 判断平衡函数isBalanced(self, root: TreeNode) -> bool:
根节点为空,返回true if not root: return True
进入计算深度函数返回绝对值差return abs(self.depth(root.left) - self.depth(root.right)) <= 1 and \
递归判断该结点的左子树是否平衡self.isBalanced(root.left) and 递归判断该结点的右子树是否平衡self.isBalanced(root.right)
def 计算深度函数depth(self, root):
根节点为空 返回高度0 if not root: return 0
返回左右子树中较高子树的高度 return max(self.depth(root.left), self.depth(root.right)) + 1
- 时间复杂度:O(Nlog2N):最差情况下,
isBalanced(root)
遍历树所有节点,占用O(N);判断每个节点的最大高度depth(root)
需要遍历各子树的所有节点,子树的节点数的复杂度为 O(log2N)。 - 空间复杂度 O(N):最差情况下(树退化为链表时),系统递归需要使用O(N)的空间。
2.2.3运行结果
2.2.4分析该题目解题优势及难点
- 优势:方法普遍,适用于大部分的题目,而且容易想到,实现起来难度较低。
- 难点:这道代码难点不多,但是不是此题的最优解法。由顶至底的方法虽然简单,但是在运行过程中,会产生大量的计算,导致时间复杂度高。
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 rightSideView(TreeNode root) {
创建一维数组接收结果值
List ans = new ArrayList<>();
if (根节点为空)
return ans;
end if
创建队列进行层次遍历
Queue 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
是 null
,那么只有在他们都为 null
的情况下这两个二叉树才等价。
3.如果 root1
,root2
的值不相等,那这两个二叉树的一定不等价。
4.当 root1
和 root2
的值相等的情况下,需要继续判断 root1
的孩子节点是不是跟 root2
的孩子节点相当。因为可以做翻转操作,所以这里有两种情况需要去判断。
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分析该题目解题优势及难点
优势:代码精简,思路明确,充分考虑所有可能出现的情况,可读性很强。
难点:在所有情况的处理上有难度,因为题目并未给出翻转的结点,所以,在判断结点的时候需要进行更多方面的考虑,需要将根结点进行比较,同时对结点的左右孩子进行判断。 -