0.PTA得分截图
1.本周学习总结(0-5分)
串的BFKMP算法
BF算法(暴力法,代码就不贴了)
先来看一个例子:给出两个字符串A和B,求解A中是否包含B?如果包含,包含了几个?
BF算法的原理是一位一位地比较,比较到失配位的时候,将B串的向后移动一个单位,再从头一位一位地进行匹配。
在比较到第6个字符时(字符索引:5),不匹配了,此时就要将B串后移一个单位,从头开始匹配(将原本指向A串第六个字符的指针i指向第二个字符,指向B串第六个字符的指针j重新指向B串开头):
然而此时我们一眼可以看出第二次匹配也是必然失败的,但计算机并不知道,它只会按照BF算法一位一位的比较下去(在很多情况下要比较很多位才能发现不匹配),这种暴力求解的算法效率是极低的,所以便有了三位神人,搞出了KPM
KPM算法(高效)
NEXT数组的求解:next行则记录了当前从B串头部到当前位置的这一子串的公共最大长。
首先我们要了解几个概念:前缀、后缀、相同前缀后缀的最大长度(为表述方便,下文均用公共最大长指代),为了直观一点,我们直接举例:
abcdef的前缀:a、ab、abc、abcd、abcde(注意:abcdef不是前缀)
abcdef的后缀:f、ef、def、cdef、bcdef(注意:abcdef不是后缀)
abcdef的公共最大长:0(因为其前缀与后缀没有相同的)
ababa的前缀:a、ab、aba、
ababababa的后缀:a、ba、aba、
babaababa的公共最大长:3(因为他们的公共前缀后缀中最长的为aba,长度3)
代码如下:
void GetNext(SqString t,int next[])
{ int j,k;
j=0;k=-1;next[0]=-1;
while (j<t.length-1)
{ if (k==-1 || t.data[j]==t.data[k])
{ j++;k++;
next[j]=k;
}
else k=next[k];
}
}
利用公共最大长对匹配过程进行优化
还是开头的例子,B串在第6个字符处(索引5)失配,此时我们可以确认的是B串的前五个字符已经匹配成功了,让我们根据上面那个表格查找一下已经匹配成功的子串的公共最大长吧(请注意是已经匹配成功的,我们在第6个字符处失配,所以应当去查找第五个字符或者说索引4的位置记录的公共最大长)
图解:
很明显,已匹配成功的子串(我们称之为C串吧)的公共最大长为2,即B串匹配成功的部分和A串失配处之前的一小部分子串都是C串,C串的公共最大长为2,C串最前面的两个字符(也就是B串的开头两个字符)和C串最后面的两个字符(也就是A串失配位前面两个字符)是相同的,这就意味着我们重新进行匹配的时候可以直接将B串的头部2个字符和A串匹配成功的部分的最后两个字符对齐。然后开始对比B串的第三个字符与A串的失配字符,进行新一轮的匹配
具体代码:
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); //返回不匹配标志
}
(有些步骤我也表达不清楚,反正大概是个意思就好了,反正叫我重新写一遍是肯定写不出来的,有时间看看熟悉熟悉吧)
二叉树存储结构、建法、遍历及应用
二叉树链式存储结构
typedef struct BTNode //定义树的结构体
{
char Data;
struct BTNode* Left;
struct BTNode* Right;
}BTNode,*BTree;
二叉树的顺序存储结构
typedef int SqBTree[MaxSize]
SqBTree bt="#uhf#jhjf##"
//缺点:空间浪费大,查找删除不方便,但对完全二叉树很适合
建法
1.顺序结构转化成二叉链代码:
void CreakBTree(BTree &bt, string str,int i)
{
if (i>len-1||str[i] == '#')//递归出口
{
bt = NULL;
return;
}
bt = new BTNode;
bt->Data = str[i];//写入节点数据
CreakBTree(bt->Left, str, 2*i);//建立他的左孩子
CreakBTree(bt->Right, str, 2 * i + 1);//建立它的右孩子
}
2.先序遍历递归建树:
BTree CreakBTree(string str,int &i)
{
BTree bt;
int len;//计算总长度
if (!str[i]) return NULL;
if (str[i] == '#')
return NULL;
bt = new BTNode;
bt->Data = str[i];//写入节点数据
bt->Left = CreakBTree(str, ++i);//建立左子树
bt->Right = CreakBTree(str, ++i);//建立右子树
return bt;
}
3.层次法建树----借助队列
//给一个层次遍历序列,建二叉树,有些节点没有左右孩子的补齐为0,代码较长,此处为伪代码
void CreatBTree(BTree& BT, string str)
{
BTreeT; int i = 0;
queue<BTree> Q;
将根节点入队列
while 层次建树
{
从队列出队一个树根节点
从str中取出一个字符
if为0,则左孩子为NULL,
else将str[i]作为根节点的左孩子,入队列,i++
从str中取出下一字符
if为0,则右孩子为NULL
else将str[i]作为根节点的右孩子,入队列,i++
}
}
4.括号法建树-----借助栈(此处只写伪代码)
void CreateBTNode(BTNode * &b,char *str)
{
创建一个顺序树栈
初始化b=NULL
while 遍历str字符串
{
switch (str[i])
case'(',有左孩子节点,进栈
case‘)'该棵子树建完,出栈
case','有右孩子
其他情况则是孩子节点
将孩子节点建成子树
if b为NULL,则b=p
else建立孩子节点
}
}
5.先序加中序建树
BTree CreatBTree(char *preBT, char *inBT, int N)//根据先序和中序建立二叉树
{
if (N <= 0) return NULL;
int k;
BTree bt;
bt = new BTNode;
bt->data = *preBT;
for (k = 0; k < N; k++)
{
if (inBT[k] == *preBT) break;
}
bt->Left = CreatBTree(preBT+1, inBT, k);
bt->Right = CreatBTree(preBT+k+1, inBT+k+1, N - k - 1);
return bt;
}
6.中序加后序建树
BTree CreatBTree(int* lastBT, int* inBT, int N)//根据先序和中序建立二叉树
{
if (N <= 0) return NULL;
int k;
BTree bt;
bt = new BTNode;
bt->data = *(lastBT+N-1);
for (k = 0; k < N; k++)
{
if (inBT[k] == *(lastBT+N-1)) break;
}
bt->Left = CreatBTree(lastBT, inBT, k);
bt->Right = CreatBTree(lastBT+k, inBT+k+1, N - k-1);
return bt;
}
二叉树遍历
先序遍历代码:
void PreOrder(BTree bt)
{ if (bt!=NULL)
{ printf("%c ",bt->data);
PreOrder(bt->lchild);
PreOrder(bt->rchild);
}
}
中序遍历代码:
void InOrder(BTree bt)
{ if (bt!=NULL)
{ InOrder(bt->lchild);
printf("%c ",bt->data);
InOrder(bt->rchild);
}
}
后序遍历代码:
void PostOrder(BTree bt)
{ if (bt!=NULL)
{ PostOrder(bt->lchild);
PostOrder(bt->rchild);
printf("%c ",bt->data);
}
}
借助队列层次遍历:
图解:
代码:
void OutLevel(BTree bt)//输出每层节点
{
queue<BTree> Q;
BTree tree;
Q.push(bt);//将根节点入栈
while (!Q.empty())
{
tree = Q.front();
if (tree->Left != NULL) Q.push(tree->Left);
if (tree->Right != NULL) Q.push(tree->Right);
Q.pop();
cout << tree->Data << ",";
}
}
}
非递归就不做描述了,结果与递归先序,中序,后序的结果是一样
应用主要包含了表达树求值,哈弗曼树以及以后的二叉搜索树等等的
树的结构、操作、遍历及应用
树的结构:
双亲存储结构
typedef struct //找父亲容易,找孩子不容易
{ ElemType data; //结点的值
int parent; //指向双亲的位置
} PTree[MaxSize];
孩子兄弟链结构
typedef struct tnode //找父亲不容易
{ ElemType data; //结点的值
struct tnode *son; //指向兄弟
struct tnode *brother; 2//指向孩子结点
} TSBNode;
孩子链结构:
typedef struct node//找父亲不容易
{ ElemType data; //结点的值
struct node *sons[MaxSons]; //指向孩子结点
} TSonNode;
树的三种遍历方式:
先根遍历:
void PreOrder(BTree bt)
{ if (bt!=NULL)
{
printf("%c ",bt->data); //访问根结点
PreOrder (bt->FirstChild); //先根遍历首子树
PreOrder (bt->Nextsibling); //先根遍历兄弟树
}
}
后根遍历:
void PostOrder(BTree bt)
{
PostOrder (bt->FirstChild); //后根遍历首子树
PostOrder (bt->Nextsibling); //后根遍历兄弟树
printf("%c ",bt->data); //访问根结点
}
层次遍历:树的层次遍历也与二叉树的层次遍历相差无几,只是将节点写入队列时,需要将下一层全部写入而已,此处就不做介绍了
树的应用可以用来创建目录树,以及语法树查重等等应用
线索二叉树
链式二叉树中有许多空的指针域,利用这些空链域,指向该线性序列中的“前驱”和“后继”的指针,称作线索,故将称为线索二叉树
线索化二叉树结构体定义
typedef struct node
{ ElemType data; //结点数据域
int ltag,rtag; /*增加的线索标记,ltag中0表示有左孩子节点,1表示指向其前驱
rtag中0表示有右孩子节点,1表示指向其后继*/
struct node *lchild; //左孩子或线索指针
struct node *rchild; //右孩子或线索指针
} TBTNode;
此处图片太大,导致变成歪的了,劳累学长了!
先序线索树图解:(注意线索只能用虚线)
以先序序列ABCDE为例
后序线索树图解:
中序线索二叉树;
二叉树与树、森林之间的转换
哈夫曼树、并查集
哈夫曼树的定义:具有最小带权路径长度的二叉树称为哈夫曼树
最小带权路径长度:设二叉树具有n个带权值的叶子节点,那么
从根节点到各个叶子节点的路径长度与相应节点权值的乘积的和,
叫做二叉树的带权路径长度
哈夫曼树的结构:
typedef struct HuffmanBTNode(顺序结构)
{
int parent;//双亲
float data;//节点值
int left;
int right;
}HuffmanBTNode,*HuffmanBTree;
哈夫曼树的建立过程代码:
void CreatHTree(HuffmanBTree& ht, int n)//初始化哈夫曼树
{
ht = new HuffmanBTNode[2 * n - 1];
}
void CreatHT(HuffmanBTree &ht, int n)
{
int i,k;
int lnode, rnode;//lnode记录最小值的下标,rnode记录次小值的下标
float min1, min2;
for (i = 0; i < n; i++)
{
cin >> ht[i].data;//输入叶子结点
}
for (i = 0; i < 2 * n - 1; i++)
{
ht[i].left = ht[i].right = ht[i].parent = -1; //所有节点的相关域置初值-1
}
for (i = n; i < 2 * n - 1; i++)//反复找寻最小值和次小值的节点,直到只剩一棵树
{
min1 = min2 = 2147483647; lnode = rnode = -1;
for (k = 0; k < i; k++)
{
if (ht[k].parent == -1)
{
if (ht[k].data < min1)
{
min2 = min1;
min1 = ht[k].data;
rnode = lnode;
lnode = k;
}
else if(ht[k].data<min2)
{
min2 = ht[k].data;
rnode = k;
}
}
}
ht[lnode].parent = ht[rnode].parent = i;//构造非叶子节点ht[i](存放在ht[n]~ht[2n-2]中)
ht[i].left = lnode;
ht[i].right = rnode;
ht[i].data = ht[lnode].data + ht[rnode].data;
}
}
哈夫曼编码:
左分支用“0”表示;右分支用“1”表示
图解:
结构体如下:
typedef struct
{ char cd[N]; //存放当前节点的哈夫曼码
int start; //哈夫曼码在cd中的起始位置
} HCode;
具体代码:
void CreateHCode(HTNode ht[],HCode hcd[],int n)
{ int i,f,c; HCode hc;
for (i=0;i<n;i++) //根据哈夫曼树求哈夫曼编码
{ hc.start=n;c=i; f=ht[i].parent;
while (f!=-1) //循环直到无双亲节点即到达树根节点
{ if (ht[f].lchild==c) //当前节点是左孩子节点
hc.cd[hc.start--]='0';
else //当前节点是双亲节点的右孩子节点
hc.cd[hc.start--]='1';
c=f;f=ht[f].parent; //再对双亲节点进行同样的操作
}
hc.start++; //start指向哈夫曼编码最开始字符
hcd[i]=hc;
}
}
并查集:查找一个元素所属的集合及合并2个元素各自专属的集合等运算
集合查找:在一棵高度较低的树中查找根结点的编号,所花的时间较少。同一个集合标志就是根(parent)是一样
集合合并:总是高度较小的分离集合树作为子树。得到的新的分离集合树C的高度hC =MAX{hA,hB+1}。
并查集用顺序存储结构,结构体如下:
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 t[x]为其根节点,则返回根节点
else 递归查找它的根节点的编号
}
两个元素各自所属的集合的合并
void UNION(UFSTree t[],int x,int y) //将x和y所在的子树合并
{
x=它所属根节点的编号
y=它所属根节点的编号
if t[x].rank>t[y].rank
将y连到x结点上,x作为y的双亲结点
else
将x连到y结点上,y作为x的双亲结点
if t[x]和t[y]的秩相同的,则y的秩加一
}
2.阅读代码(0--5分)
2.1 题目及解题代码
题目:
解题代码:
2.1.1 该题的设计思路
时间复杂度:O(n)
空间复杂度:O(n)
2.1.2 该题的伪代码
bool SymmetryBTree(BTree LBTree,BTree RBTree)//判断树是否对称
{
if 左右两树的节点都为空,返回true,递归出口
if 有一边为空,返回false,递归出口
if 对称的两个节点值相等 return SymmetryBTree(LBTree->Left, RBTree->Right) && SymmetryBTree(LBTree->Right, RBTree->Left)//递归,判断对称的树
else 返回false
}
2.1.3 运行结果
2.1.4分析该题目解题优势及难点
一般都是以一边一边的遍历,而这题是从两边同时递归遍历,判断镜像对称,提供了一种新的思路
难点就是递归难设计,但设计出来就感觉好简单
2.2 题目及解题代码
解题代码:
2.2.1 该题的设计思路
运用动态规划将大的问题分化成小的问题,从而解决一个大的问题。
先分析一个小问题,爷爷儿子父亲是一棵树,抢爷爷的,父亲的就不能抢;不抢爷爷的,则看父亲的钱多还是儿子的钱多,谁多抢谁。最后用递归将它合并在整棵二叉树,解决问题
时间复杂度:O(n)
空间复杂度:O(1)
2.2.2 该题的伪代码
void RobDP(struct TreeNode* root, int* dp0, int* dp1)
{
初始化抢和不抢左右子树的值为0
if 根为NULL,返回
RobDP(左子树),递归进入左子树
RobDP(右子树),递归进入右子树
计算不抢根节点时的值dp0; 不抢根节点时,儿子可以抢可以不抢,谁的钱多抢谁
计算抢根节点时的值dp1; //抢根节点时,儿子不能抢
}
2.2.3 运行结果
2.2.4分析该题目解题优势及难点。
优势:运用递归将一个复杂的问题变成了一个个小的问题,将小的问题转化成递归代码,即是本题的解决办法
难点:动态规划将大问题归纳成一个个小小的问题就是一个难点,将小问题转化为递归代码也是一个难的点
2.3 题目及解题代码
解题代码:
2.3.1 该题的设计思路
从问题的描述中,可以清楚地了解到,我们需要在给定树的每个结点处找到其坡度,并将所有的坡度相加以获得最终结果。
要找出任意结点的坡度,我们需要求出该结点的左子树上所有结点和以及其右子树上全部结点和的差值。
因此,为了找出解决方案,我们使用递归,在任何结点调用该函数,都会返回当前结点下面(包括其自身)的结点和。
借助于任何结点的左右子结点的这一和值,我们可以直接获得该结点所对应的坡度。
时间复杂度:O(n)
空间复杂度:O(n)
2.3.2该题的伪代码
int getsum(struct TreeNode* root, int* sum)
{
if 为NULL,返回0
left=getsum(root->left,sum)//计算出左节点的和
right = getsum(root->right, sum);//计算右节点的和
计算坡度和sum
返回左节点和右节点的值+根节点的值
}
2.3.3 运行结果
2.3.4分析该题目解题优势及难点。
优势:巧用主函数返回子树的和,而答案sum是全局量,最后直接输出sum
难点就是递归设计以及函数需要一边返回和,一边返回坡度,一起返回两个参数的点
2.4 题目及解题代码
题目:
解题代码:
2.4.1 该题的设计思路
反转一颗空树结果还是一颗空树。
对于一颗根为 rr,左子树为right, 右子树为left 的树来说,它的反转树是一颗根为 rr,左子树为right 的反转树,右子树为left的反转树的树
这便将一个大的问题转化成了一些小的问题,适合用递归来将它表达出来
时间复杂度:O(n)
空间复杂度:O(n)
2.4.2 该题的伪代码
void invert(struct TreeNode* root)
{
if判断是否为空节点
用中间变量交换两个节点的左右子树
if root左节点不为空,invert(root->left)
if root右节点不为空,invert(root->right)
}
2.4.3 运行结果
2.4.4分析该题目解题优势及难点。
本题算是经典的二叉树了,非常适合用递归来做
好多的递归,树的题目都是需要分析清楚小问题,解决清楚小问题,进而才能用递归完整的表达出整个大问题,达到解决问题的目的