DS博客作业03--树
0.PTA得分截图
1.本周学习总结
1.1 总结树及串内容
1.1.1串的BF\KMP算法
- 串的BF算法
1.基本思想:从主串S的第一个字符开始与模式串T的第一个字符进行比较,若相等则两串继续比较后续字符;若不相等,则从主串S的第二个字符和模式串的第一个字符进行比较,(即若不同,S的初始位置
+1,T则一直从第一个字符开始与S比较),重复以上操作。若全部遍历比较完T串,则成功在S串中找到字串T;若主串S中的剩余字符不足以与模式串T比较,则匹配失败。
2.优点:较容易理解
3.缺点:利用计算机这个完美工具人的典例,每次S串与T串不同时只是机械地将主串右移一位,费时。
- 串的KMP算法
1.个人对PTT上关于KMP算法的解释不太能理解,就四处翻翻其他人介绍KMP算法的博客,其实KMP算法实质上跟BF算法差不多,都是若字符匹配成功则主串与模式串移动,直到模式串被匹配完或主串的剩余字
符不足以与模式串T比较,不同之处在于若字符不同KMP算法对主串的移动位数更为灵活,而不是像BF算法只是机械地将主串右移一位。
2.KMP算法对主串S的移动位数公式:移动位数 = 已匹配的字符数 - 对应的部分匹配值
3.部分匹配值:对模式串的"前缀"和"后缀"的最长的共有元素的长度
4.前缀与后缀:"前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。
5.部分匹配值举例:以"ABCDABD"为例,可得
即
6.优点:比BF算法省时多了。
7.缺点:较难理解。
1.1.2树
- 相关概念:
1.树:由n(n≥0)个结点组成的有限集合,当n=0时为空树。
2.根结点:有且只有一个,无前驱节点。
3.除根结点外,每个结点有且仅有一个前驱结点。
4.每个结点可以有零个或多个后继结点。
5.结点的度与树的度:树中一个结点的子树的个数称为该结点的度。树中各结点的度的最大值称为树的度,通常将度为m的树称为m次树或者m叉树。其中以二叉树较为普遍。
6.分支结点与叶结点:度不为零的结点称为非终端结点,又叫分支结点。度为零的结点称为终端结点或叶结点(或叶子结点)。度为1的结点称为单分支结点;度为2的结点称为双分支结点,依此类推。
7.孩子结点、双亲结点和兄弟结点:在一棵树中,每个结点的后继,被称作该结点的孩子结点(或子女结点)。相应地,该结点被称作孩子结点的双亲结点(或父母结点)。具有同一双亲的孩子结点互为
兄弟结点。
8.树的高度:树中结点的最大层次称为树的高度(或树的深度)。
9.森林:n(n>0)个互不相交的树的集合称为森林。
10.对树的操作普遍要用到递归算法。
- 一些性质及公式
1.树中的结点数等于所有结点的度数之和加1。
2.度为m的树中第i层上至多有m^(i-1)个结点(i≥1)。
3.高度为h的m次树至多有(m^h-1)/(m-1)个结点。
4.具有n个结点的m次树的最小高度为logm(n(m-1)+1)(取整)。
- 树的应用
如二叉树、哈夫曼树、并查集等,以下详细展开~
1.1.3二叉树
- 二叉树的存储结构
1.顺序存储结构:用一组连续的存储单元存放二叉树中的结点,如下图
【一棵二叉树】---------------------------------【改造后的完全二叉树】
【二叉树顺序存储状态】
代码:
#define Maxsize 100
typedef struct
{
char bt[Maxsize];
int btnum;
}Btseq;
2.链式存储结构:用链表来表示一棵二叉树,如下图
【一棵二叉树】----------------------------------【二叉链表存储结构】
代码:
typedef struct BiTNode
{
TElemType data;//数据域
struct BiTNode *lchild,*rchild;//左右孩子指针
struct BiTNode *parent;
}BiTNode,*BiTree;
还有一种二叉树的链式存储结构,可用于查找父亲节点,这样的链表结构,通常称为三叉链表,如下图:
- 二叉树建法
1.先序建法:先序遍历字符串如:ABC##DE#G##F### 其中“#”表示的是空格,代表一棵空树。
代码:
BTNode* InitExpTree(string str)
{
if (str[i] == '#')
{
i++;
return NULL;
}
else
{
BTree temp = new BTNode;
temp->data = str[i];
i++;
temp->lchild = InitExpTree(str);
temp->rchild = InitExpTree(str);
return temp;
}
}
中序、后序同理。
2.用先序和中序创建二叉树:通过先序和中序字符串建树。
代码:
*BtNode* CreateTree(char ps[],char is[],int n )
{
if(n<=0)
return NULL;
BtNode* p=(BtNode*)malloc(sizeof(BtNode));
int i=0;
int m;
while(i<n)
{
if(ps[0]==is[i])
{
m=i;
break;
}
++i;
}
if(i>=n)
return NULL;
p->data=ps[0];
p->leftchild=CreateTree(ps+1,is,m);
p->rightchild=CreateTree(ps+m+1,is+m+1,n-m-1);
return p;
}
3.用中序和后序创建二叉树
代码:
BtNode* CreateTree(char is[],char ls[],int n)
{
if(n<=0)
return NULL;
BtNode *p=(BtNode*)malloc(sizeof(BtNode));
int i=0;
int m;
while(i<n)
{
if(ls[n-1]==is[i])
{
m=i;
break;
}
++i;
}
if(i>=n)
return NULL;
p->data=ls[n-1];
p->leftchild= CreateTree(is,ls,m);
p->rightchild= CreateTree(is+m+1,ls+m,n-m-1);
return p;
}
4.层次建树:输入一行字符串表示二叉树的顺序存储结构,比如字符串“#ABCD#EF#G##H##I”,#代表空节点。第一个#不使用
代码:
BTNode* InitExpTree(string str, int i)
{
BTree R;
if (i > str.size()-1)
return NULL;
if (str[i] == '#')
return NULL;
R = new BTNode;
R->data = str[i];
R->lchild = InitExpTree(str, 2 * i);
R->rchild = InitExpTree(str, 2 * i + 1);
return R;
}
- 二叉树的操作
1.二叉树的中序遍历
代码:
void PrintfTree(BTree& T)
{
if (T != NULL)
{
PrintfTree(T->lchild);
cout << T->data << " ";
PrintfTree(T->rchild);
}
return;
}
先序与后序遍历同理。
2.二叉树的层次遍历
代码:
void PrintfTree(BTree& T)
{
queue<BTree> Q;
BTree temp;
if (T == NULL)
{
cout << "NULL";
return;
}
else
{
Q.push(T);
while (!Q.empty())
{
temp = Q.front();
if (temp->lchild != NULL)
Q.push(temp->lchild);
if (temp->rchild != NULL)
Q.push(temp->rchild);
Q.pop();
if (Q.empty())
cout << temp->data;
else
cout << temp->data << ' ';
}
}
}
3.求树的高度
代码:
int Depth(BiTree T)
{
if (T == NULL)
return 0;
else
{
int m = Depth(T->lchild);
int n = Depth(T->rchild);
if (m > n)
return (m + 1);
else
return (n + 1);
}
}
4.统计二叉树的结点个数
代码:
int NodeCount(BiTree T)
{
if (T == NULL)
return 0;
else
return NodeCount(T->lchild) + NodeCount(T->rchild) + 1;
}
5.统计二叉树的叶结点个数
代码:
int LeafCount(BiTree T)
{
if (!T)
return 0;
if (!T->lchild && !T->rchild)
return 1;
else
return LeafCount(T->lchild) + LeafCount(T->rchild);
}
6.输出从每个叶子结点到根结点的路径
代码:
void PrintAllPath(BiTree T, char path[], int pathlen)
{
int i;
if (T != NULL)
{
path[pathlen] = T->data; //将当前结点放入路径中
if (T->lchild == NULL && T->rchild == NULL)
{//叶子结点
for (i = pathlen; i >= 0; i--)
cout << path[i] << " ";
cout << endl;
}
else
{
PrintAllPath(T->lchild, path, pathlen + 1);
PrintAllPath(T->rchild, path, pathlen + 1);
}
}
}
7.交换二叉树每个结点的左孩子和右孩子
代码:
void ExChangeTree(BiTree& T)
{//构造函数,使用递归算法进行左右结点转换
BiTree temp;
if (T != NULL)
{//判断T是否为空,非空进行转换,否则不转换
temp = T->lchild;
T->lchild = T->rchild;//直接交换节点地址
T->rchild = temp;
ExChangeTree(T->lchild);
ExChangeTree(T->rchild);
}
}
1.1.4线索二叉树
- 大概:增加了两个标志域ltag和rtag,以下是建立线索的规则:
(1)ltag为0时指向该结点的左孩子,为1时指向中序遍历序列中该结点的前驱结点;
(2)rtag为0时指向该结点的右孩子,为1时指向中序遍历序列中该结点的后继结点。
可得
- 代码
- 线索二叉树存储结构定义
2.线索化
BiTree pre; //全局变量,始终指向刚刚访问过的结点
void InThreading(BiTree p) //中序遍历进行中序线索化
{
if(p)
{
InThreading(p->lchild); //递归左子树线索化
if(!p->lchild) //没有左孩子
{
p->ltag = Thread; //前驱线索
p->lchild = pre; //左孩子指针指向前驱
}
if(!pre->rchild) //没有右孩子
{
pre->rtag = Thread; //后继线索
pre->rchild = p; //前驱右孩子指针指向后继(当前结点p)
}
pre = p;
InThreading(p->rchild); //递归右子树线索化
}
}
3.遍历线索树
//t指向头结点,头结点左链lchild指向根结点,头结点右链rchild指向中序遍历的最后一个结点。
int InOrderThraverse_Thr(BiTree t)
{
BiTree p;
p = t->lchild; //p指向根结点
while(p != t) //空树或遍历结束时p == t
{
while(p->ltag == Link) //当ltag = 0时循环到中序序列的第一个结点
p = p->lchild;
printf("%c ", p->data); //显示结点数据,可以更改为其他对结点的操作
while(p->rtag == Thread && p->rchild != t)
{
p = p->rchild;
printf("%c ", p->data);
}
p = p->rchild; //p进入其右子树
}
return OK;
}
1.1.5哈夫曼树
- 相关名词介绍
1.路径:在一棵树中,一个结点到另一个结点之间的通路,称为路径。
2.路径长度:在一条路径中,每经过一个结点,路径长度都要加1。
3.结点的权:给每一个结点赋予一个新的数值,被称为这个结点的权。
4.结点的带权路径长度:指的是从根结点到该结点之间的路径长度与该结点的权的乘积。
5.树的带权路径长度:树中所有叶子结点的带权路径长度之和。通常记作 “WPL”。
- 代码
1.哈夫曼树存储结构定义
typedef struct
{
int weight;//结点权重
int parent, left, right;//父结点、左孩子、右孩子在数组中的位置下标
}HTNode, *HuffmanTree;
2.哈夫曼树的构建
假设有n个结点,构成的二叉树的集合为F,则构造一棵含有n个叶子结点的哈夫曼树的步骤如下:
(1)从F中选取两棵根结点权值最小的树作为左右子树构造一棵新的二叉树,其新的二叉树的权值为其左右子树根结点权值之和;
(2)从F中删除上一步选取的两棵二叉树,将新构造的树放到F中;
//HT为地址传递的存储哈夫曼树的数组,w为存储结点权重值的数组,n为结点个数
void CreateHuffmanTree(HuffmanTree *HT, int *w, int n)
{
if(n<=1) return; // 如果只有一个编码就相当于0
int m = 2*n-1; // 哈夫曼树总节点数,n就是叶子结点
*HT = (HuffmanTree) malloc((m+1) * sizeof(HTNode)); // 0号位置不用
HuffmanTree p = *HT;
// 初始化哈夫曼树中的所有结点
for(int i = 1; i <= n; i++)
{
(p+i)->weight = *(w+i-1);
(p+i)->parent = 0;
(p+i)->left = 0;
(p+i)->right = 0;
}
//从树组的下标 n+1 开始初始化哈夫曼树中除叶子结点外的结点
for(int i = n+1; i <= m; i++)
{
(p+i)->weight = 0;
(p+i)->parent = 0;
(p+i)->left = 0;
(p+i)->right = 0;
}
//构建哈夫曼树
for(int i = n+1; i <= m; i++)
{
int s1, s2;
Select(*HT, i-1, &s1, &s2);
(*HT)[s1].parent = (*HT)[s2].parent = i;
(*HT)[i].left = s1;
(*HT)[i].right = s2;
(*HT)[i].weight = (*HT)[s1].weight + (*HT)[s2].weight;
}
}
- 哈夫曼树的应用
1.哈夫曼编码
介绍:在数据通信中,需要将传送的文字转换成二进制的字符串,用0,1码的不同排列来表示字符。为使不等长编码为前缀编码(即要求一个字符的编码不能是另一个字符编码的前缀),可用字符集中的每个
字符作为叶子结点生成一棵编码二叉树,为了获得传送报文的最短长度,可将每个字符的出现频率作为字符结点的权值赋予该结点上,显然字使用频率越小权值越小,权值越小叶子就越靠下,于是频率小编
码长,频率高编码短,这样就保证了此树的最小带权路径长度效果上就是传送报文的最短长度。下图体现了哈夫曼编码的过程:
代码
void HuffmanTree::encode(Node* root, string code)
{
if (root->left == NULL&&root->right == NULL) //叶子结点
cout << root->val << " 被编码为 " << code << endl;
if (root->left != NULL)
{
code += "0"; //左子树,编码code添加'0'
encode(root->left, code);
code.erase(code.end()-1); //删除上一步添加的'0'
}
if (root->right != NULL)
{
code += "1"; //右子树,编码code添加'1'
encode(root->right, code);
code.erase(code.end()-1); //删除上一步添加的'1'
}
}
1.1.5并查集
1.用有根树来表示集合,树中的每个结点包含集合的一个成员,每棵树表示一个集合。多个集合形成一个森林,以每棵树的树根作为集合的代表,并且根结点的父结点指向其自身,树上的其他结点都用一个父指针表示它的附属关系。在并查集中,每个分离集合对应的一棵树,称为分离集合树。整个并查集也就是一棵分离集合森林。
如下图,4个集合{1,2,3,4}、{5,6,7}、{8,9}、{10},分别以4、7、9和10表示对应集合的编号。
代码
const int maxn = 1000 + 100;
int par[maxn]; //父亲, 当par[x] = x时,x是所在的树的根
int Rank[maxn]; //树的高度
//初始化n个元素
void init(int n)
{
for (int i = 0; i < n; i++)
{
par[i] = i;
Rank[i] = 0;
}
}
//查询树的根
int find(int x)
{
if (par[x] == x)
return x;
else
return par[x] = find(par[x]);
}
//合并x和y所属集合
void unite(int x, int y)
{
x = find(x);
y = find(y);
if (x == y)
return;
if (Rank[x] < Rank[y])
par[x] = y;
else
{
par[y] = x;
if (Rank[x] == Rank[y]) Rank[x]++; //如果x,y的树高相同,就让x的树高+1
}
}
//判断x和y是否属于同一个集合
bool same(int x, int y)
{
return find(x) == find(y);
}
int main()
{
int n;
scanf("%d", &n);
init(n);
int data, p;
cout << "输入数据: \n";
for (int i = 0; i < n; i++)
{
scanf("%d%d", &data, &p);
par[data] = p;
Rank[p]++;
}
cout << "输入合并集合: \n";
int p1, p2;
cin >> p1 >> p2;
unite(p1, p2);
cout << "查询是否属于一个集合: \n";
cin >> p1 >> p2;
if (same(p1, p2))
puts("same");
else
puts("diff");
return 0;
}
2.应用
畅通工程:某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府“畅通工程”的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只
要互相间接通过道路可达即可)。问最少还需要建设多少条道路?输入两个正整数,分别是城镇数目N ( < 1000 )和道路数目M;随后的M行对应M条道路,每行给出一对正整数,分别是该条道路直接连通
的两个城镇的编号。为简单起见,城镇从1到N编号。如:
Sample Input
4 2
1 3
4 3
3 3
1 2
1 3
2 3
5 2
1 2
3 5
999 0
0
Sample Output
1
0
2
998
思路:要求所有独立的集合连接起来还需要几条路,那只要找到独立集合个数-1即可
代码
#include<stdio.h>
int father[1005];
int Find(int x)
{
while(x!=father[x])
x=father[x];
return x;
}
void Combine(int a,int b)
{
int fa=Find(a);
int fb=Find(b);
if(fa!=fb)
father[fa]=fb;
}
int main()
{
int n,m;
int i;
int a,b;
while(~scanf("%d",&n))
{
if(n==0)
break;
scanf("%d",&m);
int sum=0;
for(i=1;i<=n;i++)
father[i]=i;
for(i=0;i<m;i++)
{
scanf("%d%d",&a,&b);
Combine(a,b);
}
for(i=1;i<=n;i++)
{
if(father[i]==i)
sum++;
}
printf("%d\n",sum-1);
}
return 0;
}
1.2.对树的认识及学习体会。
没入本专业前,我就从一个段子里听说过树的大名了,树具有独特的“一对多”结构,与现实生活中的许多例子相同,(比如族谱、目录之类),可见其具有较强的实际应用性。可也就是这独特的甚至有点“
反计算机”的“一对多”性质导致树在编程过程中难以让编程人员看到它的树形结构,给程序纠错带来不小难度。总的来说,还是要打好编程基础。
2.阅读代码
2.1.1 题目及解题代码
- 题目
- 代码
2.1.2 该题的设计思路
-
思路:由题意可得:满足左<=根<=右即正确,因此,可引入上下边界,判断左节点时,仅上界变化(故新上界为val),判断右节点时,仅下界变化(新下界为val)。
-
时间复杂度:O(N),每个结点访问一次。
-
空间复杂度:O(N),跟进了整棵树
2.1.3 伪代码
bool fun(struct TreeNode* root, long low, long high) {
if 根为空 then
返回正确
定义num,取根值
end if
if 根小于左,或根大于右 then
返回错误
end if
左右孩子进入递归,并赋予上下界新值
}
bool isValidBST(struct TreeNode* root){
调用fun函数
}
2.1.4 运行结果
2.1.5分析该题目解题优势及难点
-
优势:代码量较少,简洁明了;用了 long 定义num防止 INT_MAX 溢出;没有用到三叉链表而是用巧妙的递归解决了根与左右孩子的比较问题。
-
难点:难点在根与左右孩子的比较上,我初看题目的时候想到可能要用三叉链表,或者设几个全局变量之类的,没想到原作者很灵活地用递归解决了这个比较问题。
2.2.1 题目及解题代码
- 题目:
-
解题代码
pair<int, int> dfs(TreeNode *root) { if (root == nullptr) return { 0, 0 }; end if auto left_pair = dfs(root->left); auto right_pair = dfs(root->right); return { root->val + left_pair.second + right_pair.second, max(left_pair.first, left_pair.second) + max(right_pair.first, right_pair.second) }; } int rob(TreeNode* root) { auto p = dfs(root); return max(p.first, p.second); }
2.2.2 该题的设计思路
- 思路:由于不能选择相邻的两个节点,所以对于一个子树来说,有两大种情况:(1)包含当前根节点(2)不包含当前根节点。
情况(1):包含根节点,即不能选择左右儿子节点,这种情况的最大值为:当前节点 + 左儿子情况2 + 右二子情况2。
情况(2):不包含根节点,即可以选择左右儿子节点,所以有四种可能:
a.左儿子情况1 + 右儿子情况1
b.左儿子情况1 + 右儿子情况2
c.左儿子情况2 + 右儿子情况1
d.左儿子情况2 + 右儿子情况2
综合来说情况(2)就是,(左儿子情况1, 左儿子情况2)取最大 + (右儿子情况1, 右儿子情况2)取最大。
所以可以调用pair,将这两种情况存入pair型变量,最后再比较出该pair型变量的最大值即可。
-
时间复杂度:O(N),每个结点访问一次。
-
空间复杂度:O(N),跟进了整棵树
2.2.3 伪代码
pair<int, int> dfs(TreeNode *root) {
if 根为空 then
返回 { 0, 0 }
end if
根的左孩子进入递归,返回值赋予left_pair
根的左孩子进入递归,返回值赋予right_pair
返回 { 当前节点 + 左儿子情况2 + 右二子情况2, (左儿子情况1, 左儿子情况2)取最大 + (右儿子情况1, 右儿子情况2)取最大 };
}
int rob(TreeNode* root) {
调用dfs函数,赋予p
取p的最大值作为最终结果返回
}
2.2.4 运行结果
2.2.5分析该题目解题优势及难点
-
优势:代码量较少,递归设计的很巧妙。原作者逻辑清晰,将问题划分为两种大情况,即上文提到的情况1和情况2,调用pair,将这两种情况存入pair型变量,最后再比较出该pair型变量的最大值即可。
-
难点:由于不能选择相邻的两个节点,所以就有了多种组合,而且还要比较出各个组合之间的最大的一组,难点就在此。
2.3.1 题目及解题代码
- 题目:
-
解题代码
vector<int> largestValues(TreeNode* root) { vector<int> res; if (root == NULL) return res; queue<TreeNode*> myque; myque.push(root); while ( ! myque.empty() ){ int level_size = myque.size(); int max = INT_MIN; for (int i = 0; i < level_size; i++){ TreeNode *node = myque.front(); myque.pop(); max = node->val > max ? node->val : max; if (node->left != NULL) myque.push(node->left); if (node->right != NULL) myque.push(node->right); } res.push_back(max); } return res; }
2.3.2 该题的设计思路
-
思路:采用层次遍历求出每行最大值即可。
-
时间复杂度:O(N^2),每个结点访问一次,且有循环嵌套比较大小。
-
空间复杂度:O(N),跟进了整棵树
2.3.3 伪代码
vector<int> largestValues(TreeNode* root) {
vector<int> res;
queue<TreeNode*> myque;
if 根为空
返回 res;
根入队
end if
while ( 队不空 )
取队长度赋予level_size
定义max
for i = 0 to i < level_size
取队首赋予node,并出队
比较node的值与max大小,大者为max新值
if node左孩子不为空 then node左孩子入队
if node右孩子不为空 then node右孩子入队
end for
max进入res
end while
return res;
}
2.3.4 运行结果
2.3.5分析该题目解题优势及难点
-
优势:利用了层次遍历时队的长度等于该层的结点个数这一特性,成功比较出了各层的最大值,值得学习。
-
难点:如何确定各层的有效结点个数是个小难点。
2.4.1 题目及解题代码
- 题目:
-
解题代码:
void dfs(TreeNode* root, int v, int d, int k) { if (root == NULL) return; if (k == d - 1) { TreeNode* lnode = new TreeNode(v); TreeNode* rnode = new TreeNode(v); lnode->left = root->left; rnode->right = root->right; root->left = lnode; root->right = rnode; return; } dfs(root->left, v, d, k + 1); dfs(root->right, v, d, k + 1); } TreeNode* addOneRow(TreeNode* root, int v, int d) { if (d == 1) { TreeNode* node = new TreeNode(v); node->left = root; return node; } dfs(root, v, d, 1); return root; }
2.4.2 该题的设计思路
- 思路:与在链表中增加一个新结点类似,如图所示:
-
时间复杂度:O(N),最坏情况每层都访问。
-
空间复杂度:O(N)
2.4.3 伪代码
void dfs(TreeNode* root, int v, int d, int k) {//k为层数
if 根为空 then return
end if
if 找到插入层 then
开辟两个左右孩子结点,并同时赋予值
两个新结点分别指向根结点的左右孩子
根结点指向左右孩子
return;
end if
根左孩子进入递归,k+1
根右孩子进入递归,k+1
}
TreeNode* addOneRow(TreeNode* root, int v, int d) {
if d为1 then
开辟新根结点并指向原根结点后返回新根结点
end if
d不为1则进入dfs函数
返回原根结点
}
2.4.4 运行结果
2.4.5分析该题目解题优势及难点
-
优势:用时较少,思路清晰,易于理解,其中的开辟新结点并赋值语句值得学习,减少了代码量。
-
难点:只要会在链表中插入新结点,我觉得本题就没什么大难度。