DS博客作业03--树
0.PTA得分截图
1.本周学习总结
总结串和树的总结
串的定义
定义:由n个或多个字符组成的有限序列(这n个字符可以由字母,数字,或其它字符
组成)
串的分类:
·空串;含有零个字符的串,如:S=""(在双引号中没有任何的字符)
·空格串:只包含空格的串。注意和空串区分开,空格串中是有内容的,只不过包含的
是空格,且空格串中可以包含多个空格。例如,a =" "(包含3个空格)
·子串与主串:串中任意个连续字符组成的字符串叫做该串的子串,包含子串的串称
为主串
例:
对于三个串:a = "BEI",b = "BEIJING",c = "BJINGEI"
对于字符串 a 和 b 来说,由于 b 中含有连续的字符串 a
即可以称 a 是 b 的子串,b 是 a 的主串;而对于 c 和 a ,虽然 c 中
也含有 a 的全部字符,但不是连续的 “BEI” ,所以串 c 和 a 没有任何关
系。
串的BF\KMP算法
串的BF算法
·BF算法:普通的模式匹配算法。
·BF算法的思想就是将目标串S的第一个字符与模式串T的第一个字符进行匹配,若相
等,则继续比较S的第二个字符和 T的第二个字符;
·若不相等,则比较S的第二个字符和T的第一个字符,依次比较下去,直到得出最
后的匹配结果
举例:
开始匹配:
第一次匹配
H和L不相等,串s向下移一个,串T回到原位置
第二次匹配
L和O不相等,串S移向下一个位置,串T回到原位
第三次匹配
L和L匹配上了,将此位置记为A,串S移向下一个位置,串T也移向下一个位置,继续
匹配
第四次匹配
L和E不匹配,将串S移动到标记为A的下一个位置重新进行匹配,将串T回到原位
第五次匹配
L和L匹配上了,将此位置记为B,串S移向下一个位置,串T也移向下一个位置,继续
匹配
第六次匹配
E和E匹配上了,串S移向下一个位置,串T也移向下一个位置,继续匹配
第七次匹配
W和W匹配上了,串T全部匹配成功,一次匹配成功
串的BF算法实现
int Index(String S, String T, int pos)//返回子串T在主串S中第pos个字符之后的位置,若不存在,返0
{
int i, j;
i = pos;//用于主串S中当前位置下标,若pos不为1时,则从pos位置开始匹配
j = 1;//用于子串T多种当前位置下标值
while (i<=S[0]-T[0]+1&&j<=T[0])//若i的长度小于可匹配长度,且j小于T的长度时循环
{
if (S[i]==T[j])//两字符相等时继续匹配
{
j++;
i++;
}
else//指针后退重新匹配
{
i = i - j + 2; //注意这个索引的加2,i退回到上次匹配首位的下一个位置
j = 1;//j退回子串T的首位
}
}
if (j > T[0])
{
return i - T[0];
return 0;
}
}
时间复杂度
最坏的时间复杂度为O((n-m+1)*m)
串的KMP算法
·KMP算法的作用是在一个已知字符串中查找子串的位置,也叫做串的模式匹配
·对于主串:“abbacbbade”,字串:“abbade”
一般的匹配模式:
·从主串s 和子串t 的第一个字符开始,将两字符串的字符一一比对,如果出
现某个字符不匹配,主串回溯到第二个字符,子串回溯到第一个字符再进行
一一比对。如果出现某个字符不匹配,主串回溯到第三个字符,子串回溯到第
一个字符再进行一一比对…一直到子串字符全部匹配成功。
第一次匹配:
子串“abb”部分与主串相等,'d’不等,结束比对,进行回溯
第二次匹配:
开始时就不匹配,直接回溯
第三次匹配:
第四次匹配:
开始时就不匹配,直接回溯
第五次匹配:
匹配成功
操作的代码
int KmpSearch(char& s, char&p)
{
int i = 0;
int j = 0;
int slong = strlen(s);
int plong = strlen(p);
while (i < sLen && j < pLen)
{
//①如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++
if (j == -1 || s[i] == p[j])
{
i++;
j++;
}
else
{
//②如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]
//next[j]即为j所对应的next值
j = next[j];
}
}
if (j == pLen)
{
return i - j;
}
else
{
return -1;
}
}
求next数组
每一个字符前的字符串都有最长相等前后缀,而且最长相等前后缀的长度是我们移位的关键,
所以我们单独用一个next数组存储子串的最长相等前后缀的长度。而且next数组的数值只与
子串本身有关。
即next[i]=j,含义是:下标为i 的字符前的字符串最长相等前后缀的长度为j。
对于主串:"aaababaaab"和子串"ababaaab"
按照上述匹配:子串t= "ababaaab"的next数组为:
next[0]=-1;
next[1]=0;
next[2]=0;
next[3]=1;
next[4]=2;
next[5]=3;
next[6]=1;
next[7]=1;
nextval数组(KMP算法改进)
在一般的算法匹配时
·主串s=“aaaaabaaaaac”
·子串t=“aaaaac”
在这个例子中当‘b’与‘c’不匹配时应该‘b’与’c’前一位的‘a’比,这显然是不匹配的。
'c’前的’a’回溯后的字符依然是‘a’。
因此我们知道没有必要再将‘b’与‘a’比对了,因为回溯后的字符和原字符是相同的,
原字符不匹配,回溯后的字符自然不可能匹配。
但是KMP算法中依然会将‘b’与回溯到的‘a’进行比对。
因此我们通过引入一个nextval数组:
如果a位字符与它next值指向的b位字符相等,则该a位的nextval就指向b位的nextval值,
如果不等,则该a位的nextval值就是它自己a位的next值。
因此对于上一题的举例:
主串:"aaababaaab"和子串"ababaaab"
子串的next和nextval数组分别为:
二叉树
定义:是n(n>=0)个结点的有限集合,它或为空树(n=0),或由一个根结点和至多两棵称为根的左子
树和右子树的互不相交的二叉树组成(每个结点最多有两个子树的树结构)
二叉树的分类:空二叉树
完全二叉树:
若设二叉树的高度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大
个数,第h层有叶子结点,并且叶子结点都是从左到右依次排布
满二叉树:
除了叶结点外每一个结点都有左右子叶且叶子结点都处在最底层的二叉树
二叉树的性质:
· 在非空二叉树中,第i层的结点总数不超过2^(i-1)
· 深度为h的二叉树最多有2^h-1个结点,最少有h个结点;
· 对于任意一棵二叉树,如果其叶结点数为N0,而度数为2的结点总数为N2,则N0=N2+1;
· 具有n个结点的完全二叉树的深度为 [long2n]+1
·设有i个枝点,I为所有枝点的道路长度总和,J为叶的道路长度总和J=I+2i
二叉树的储存结构
·顺序储存:主要利用数组来储存
·链式储存:利用指针来进行储存
特点:
链式:用指针对应关系,以上的定义便于寻找左右孩子,但是不好找双亲
顺序:各个结点的关系可以根据下标求出来,但是会浪费较多的空间
结构体定义:
typedef struct Node
{ ElemType data;
struct node *lchild, *rchild;
}BTnode,*Btree;
二叉树的遍历
· 前序遍历
· 中序遍历
· 后序遍历
· 层次遍历
注意点:
·已知 前序遍历序列 和 中序遍历序列,可以唯一确定一颗二叉树
·已知 中序遍历序列和 后序遍历序列,可以唯一确定一颗二叉树
·已知 前序和后序 是不能确定一颗二叉树的
前序遍历:根-左-右
void PreOrder(BiTree T) //先序遍历
{
if(T != NULL)
{
T->data=H;
PreOrder(T->lchild); //访问左子节点
PreOrder(T->rchild); //访问右子节点
}
}
中序遍历:左-根-右
void PreOrder(BiTree T) //中序遍历
{
if(T != NULL)
{
PreOrder(T->lchild); //访问左子节点
T->data=H;
PreOrder(T->rchild); //访问右子节点
}
}
后序遍历:左-右-根
void PreOrder(BiTree T) //后序遍历
{
if(T != NULL)
{
PreOrder(T->lchild); //访问左子节点
PreOrder(T->rchild); //访问右子节点
T->data=H;
}
}
层序遍历:从根节点出发,依次访问左右孩子结点,再从左右孩子出发,依次它们的孩子结
点,直到节点访问完毕
void LevelOrder(BiTree T)
{
BiTree p = T;
queue<BiTree> queue;
queue.push(p);
while(!queue.empty()) //队列不空
{
p = queue.front();
cout << p->data << " ";
queue.pop();
if(p->lchild != NULL)
{
queue.push(p->lchild);
}
if(p->rchild != NULL)
{
queue.push(p->rchild);
}
}
}
求二叉树的高度
int GetHeight(BinTree BT)
{
int a;
int b;
if (BT == NULL)
{
return 0;
}
if (BT)
{
a=GetHeight(BT->Left);
}
if (BT)
{
b = GetHeight(BT->Right);
}
if (a >= b)
{
a=a+1;
return a;
}
else
{
b=b+1;
return b;
}
}
树
:是由n(n>=0)个有限结点组成一个具有层次关系的集合
定义:树是由根结点和若干颗子树构成的。树是由一个集合以及在该集合上定义的
一种关系构成的。集合中的元素称为树的结点,所定义的关系称为父子关系。
父子关系在树的结点之间建立了一个层次结构。在这种层次结构中有一个结
点具有特殊的地位,这个结点称为该树的根结点,或称为树根。
基本术语:
空集合也是树,称为空树。空树中没有结点。
结点的度:一个结点含有的子结点的个数称为该结点的度;
双亲结点或父结点:若一个结点含有子结点,则这个结点称为其子结点的父结点;
孩子结点或子结点:一个结点含有的子树的根结点称为该结点的子结点;
兄弟结点:具有相同父结点的结点互称为兄弟结点;
树的度:一棵树中,最大的结点的度称为树的度;
结点的层次:从根开始定义起,根为第1层,根的子结点为第2层,以此类推;
树的高度或深度:树中结点的最大层次;
结点的祖先:从根到该结点所经分支上的所有结点;
子孙:以某结点为根的子树中任一结点都称为该结点的子孙。
分类
无序树:树中任意节点的子结点之间没有顺序关系,这种树称为无序树,也称为自由树;
有序树:树中任意节点的子结点之间有顺序关系,这种树称为有序树;
二叉树:每个节点最多含有两个子树的树称为二叉树;
完全二叉树
满二叉树
哈夫曼树:带权路径最短的二叉树称为哈夫曼树或最优二叉树
树的遍历
·先根遍历
·后根遍历
·层次遍历
对于下面的这棵树:
·先根遍历:A, B, E, F, C, D, G, H, I, J, K
·后根遍历:E, F, B, C, I, J, K, H, G, D, A
·层次遍历:A, B, C, D, E, F, G, H, I, J, K
树的储存结构
双亲储存结构:
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 *son;//指向兄弟
struct tnode *brother;//指向孩子结点
} TSBNode;
哈夫曼树:
定义:
给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样
的二叉树为最优二叉树,也称为哈夫曼树。哈夫曼树是带权路径长度最短的树,权值较大的结
点离根较近。
树的带权路径长度:
求树中所有的叶结点的权值乘上其到根结点的路径长度。树的路径长度是从树根到
每一结点的路径长度之和,记为WPL=(W1*L1+W2*L2+W3*L3+...+Wn*Ln),
N个权值Wi(i=1,2,...n)构成一棵有N个叶结点的二叉树,相应的叶结点的路径
长度为Li(i=1,2,...n)。
·路径和路径长度
在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分
支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-
1。
·结点的权及带权路径长度
将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径
长度为:从根结点到该结点之间的路径长度与该结点的权的乘积。
哈夫曼树的构造过程:
(1)根据给定的n个权值{w1,w2,……wn},构造n棵只有根结点的二叉树。F={T1,T2,…Tn}。
(2)在F中选取根结点的权值最小和次小的两棵二叉树作为左、右子树构造一棵新的二叉树,
这棵新的二叉树根结点的权值为其左、右子树根结点权值之和。
(3)在集合F中删除作为左、右子树的两棵二叉树,并将新建立的二叉树加入到集合F中。
(4)重复(2)、(3)两步,当F中只剩下一棵二叉树时,这棵二叉树便是所要建立的哈夫曼
树。
哈夫曼树结构:
typedef struct
{ char data;//节点值
float weight;//权重
int parent;//双亲节点
int lchild;//左孩子节点
int rchild;//右孩子节点
} HTNode;
哈夫曼树应用:哈夫曼编码
void CreateHCode(HCode hcd[], HTNode ht[], int n0)
{
int i, f, c;
HCode hc;
for (i = 0; i < n0; i++)
{
hc.start = n0;
f = ht[i].parent;
c = i;
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++;
hcd[i] = hc;
}
}
并查集
并查集的基本操作
初始化
·把每个点所在集合初始化为其自身。通常来说,这个步骤在每次使用该数据结构时只需要执行
一次,无论何种实现方式,时间复杂度均为O(N)。
查找
·查找元素所在的集合,即根节点。
合并
·将两个元素所在的集合合并为一个集合。
·通常来说,合并之前,应先判断两个元素是否属于同一集合,这可用上面的“查找”操作实现
并查集支持以下三种操作:
Union (Root1, Root2) //合并操作
Find (x) //查找操作
UFSets (s) //构造函数
并查集采用顺序方法存储,结点的类型声明如下:
typedef struct node
{ int data;//结点对应人的编号
int rank; //结点秩:子树的高度,合并用
int parent; //结点对应双亲下标
} UFSTree;//并查集树的结点类型
查找一个元素所属的集合
int FIND_SET(UFSTree t[], int 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合并二叉树
代码
该题的伪代码
mergeTrees(TreeNode t1, TreeNode t2)
{
if(t1为空)
return t2;
if(t2为空)
return t1;
t1和t2的根节点相加;
t1的左子树和t2的左子树合并;
t1的右子树和t2的右子树合并;
}
解题思路
若n1和n2都存在,则只需要保留其中一个结点(如n1),将另一结点的值加到此结点上即可(如n1.val += n2.val)。
若n1或n2任一不存在,则合并后的二叉树对应位置上的结点就是存在的那个了。
若n1和n2都不存在,则合并后仍不存在。
空间复杂度O(n),时间复杂度O(n);
运行结果:
左叶子之和
代码
该题的解题思路:
这道题关键是递归
首先要想到遍历每个叶节点,如果是左节点就加上val,如果不是的话就不进入if
即只需在遍历的过程中判断是否到达了左叶子
到达左叶子的判断标准为:
是否是当前节点的左孩子
当前节点的左孩子是不是叶子节点
该题的伪代码
void dfs()
if(树空)
return 0;
if(找到左节点p->left==NULL&&flag=0)
将左节点加sum++![](https://img2020.cnblogs.com/blog/1775400/202004/1775400-20200419152914783-2040218749.png)
if(左数不空)
左递归 dfs(p->left);
if(右树不空)
右递归 dfs(p->right);
end
运行
平衡二叉树
带码
该题的解题思路
首先创建递归,根据返回值-1来判断
若满足,递归判断根节点左右子树是否满足定义(即对左右子树做1.)
若不返回,则返回true
先对传进来的结点判空,之后再分别递归的去遍历左右子树的高度,用 -1 来标记是否满足要求
最后判断左右子树的高度差是否小于 2 也就是不超过 1,如果满足就返回该高度并与 -1 比较即可。
该题的伪代码
该题用-1来判断
if(空)
return 0;
左递归遍历左子树left = recur(node.left);
if(left==-1找到)
return -1;
右递归遍历左子树left = recur(node.left);
if(right==-1找到)
return -1;
比较左右子树之差
运行
时间复杂度O(nlogn)
空间复杂度O(n)
二叉搜索树中第K小的元素
代码
该题的伪代码
函数dfs
if (树为空或是已找到)
return
if
调用dsf函数
在左子树中寻找第k个最小值
if 找到
根节点对应数据赋值给res
find=true
直接返回
end
调用dsf函数
在右子树中寻找第k个最小值
时间复杂度:O(n)
空间复杂度:O(n)
运行