0.PTA得分截图
1.本周学习总结
1.1 二叉树
1.1.1 二叉树的两种存储结构
1. 顺序存储:用一组连续的存储单元存放二叉树中的结点,就是数组形式。从树根起,自上层至下层,每层自左至右地给所有结点编号,
优点:可以根据编号,快速找到祖先及孩子,也可以根据数组大小判断高度。
缺点:顺序存储,会将所有结点数据都储存,更适合完全二叉树,
对于普通的二叉树而言,没有数据的结点也会留有空间,这样就会造成空间浪费,
例如下图:
2. 链式存储:采用链表形式对数据进行存储,用链来表示相互关系
优点:可直接找到左右孩子,只储存有数据的结点,节省空间。
缺点:不太容易找到祖先,不过可以再增加一个parent指针指向祖先。
1.1.2 二叉树的构造
二叉树一般是将顺序存储的转化为链式
结构体如下
typedef struct node{
ElemType data;
struct node *lchild;
struct node *rchild;
}BTNode,*BTree;
1. 顺序存储结构转二叉链
根据双亲与孩子的关系---->双亲2是左孩子下标,双亲2+1是右孩子下标,
进行递归调用,建立二叉树。
/*函数设计*/
BTree CreatTree(string str, int& i)
{
int len = str.size();
BTree bt;
/*递归出口*/
if (i > len - 1||i<0) return NULL;
if (str[i] == '#')return NULL;
bt = new BTNode;
bt->data = str[i];
bt->lchild = CreatTree(str, 2*i);//左右递归,建立左右孩子
bt->rchild = CreatTree(str, 2*i+1);
return bt;
}
2. 先序建树---根左右
建树时按照先建立根,再建立左子树,最后右子树的方式进行建树
所以采用递归的方式建树
递归出口---字符串结束,或者碰到#
BTree CreatTree(string str, int& i)//先序遍历建树
{
int len = str.size();
BTree bt;
/*递归出口*/
if (i > len - 1) return NULL;
if (str[i] == '#')return NULL;
bt = new BTNode;
bt->data = str[i];
bt->lchild = CreatTree(str, ++i);//左右递归
bt->rchild = CreatTree(str, ++i);
return bt;
}
3. 层次建树--队列
一层一层的建立,则需要储存每层数据,就需要队列与之搭配完成建树
void creatbintree(BTree& bt, string s)
{
int i = 1;
BTree p;
bt = new BTNode;
if (s[i] == '#') {//如果第一个节点为空,就直接返回空树
bt = NULL;
return;
}
else {//创建根节点
bt->data = s[i];
bt->lchild = bt->rchild = NULL;
q.push(bt); //根节点入队
}
while (!q.empty()) { //当队列不为空
p = q.front();
q.pop();
i++;
p->lchild =new BTNode;//创建左孩子
if (s[i] == '#') p->lchild = NULL; //左孩子为空
else {
p->lchild->data = s[i];
p->lchild->lchild = p->lchild->rchild = NULL;
q.push(p->lchild); //左孩子入队
}
p->rchild = new BTNode;//创建右孩子
i++;
if (s[i] == '#') p->rchild = NULL; //右孩子为空
else {
p->rchild->data = s[i];
p->rchild->lchild = p->rchild->rchild = NULL;
q.push(p->rchild); //右孩子入队
}
}
}
4. 括号法建树--栈
例如:A(B(D(,G)),C(E,F))
- 单个字符:结点的值
- (:表示一棵子树的开始
- ):表示一棵子树的结束
- ,:表示一棵右子树的开始
void CreateTree(BTree& b, char str[])
{
char ch;
BTree stack[MaxSize], p;//stack[MaxSize]为指针数组,其每一个元素都为指向bitnode这种结构的指针,p为临时指针
int top = -1, k, j = 0; //top为栈顶指针、k决定谁是左、右孩子、j为str指针
while ((ch = str[j++]) != ' '){
switch (ch){
case '(':
top++;
stack[top] = p;//根节点入栈
k = 1; //1为左孩子
break;
case ',':
k = 2; //2为右孩子
break;
case ')':
top--; //根节点出栈
break;
default:
p = new BTNode;
p->data = ch;
p->lchild = p->rchild = NULL;
if (b == NULL) b = p; //树为空时
else{//树非空时
switch (k){
case 1:
stack[top]->left = p; //根节点的左孩子
break;
case 2:
stack[top]->right = p; //根节点的右孩子
break;
}
}
}
}
}
此外,还可以根据给出先序和中序,或中序和后序得到二叉树 |
先序+中序
根据先序序列可得,根节点在开头,
根据中序序列可得,根节点在中间,
于是通过中序序列得到左右两支树
BTree CreatTree(int n, char* pre, char* mid)
{
if (n <= 0)return NULL;
BTree T;
char* p;
T = new BTNode;
T->data = *pre;//先序的第一个一定是根节点
T->lchild = NULL;
T->rchild = NULL;
for (p = mid; p < mid+n; p++)//中序找根节点,将左右子树分开
if (*p==*pre)break;
int i = p - mid;
T->lchild = CreatTree(i, pre + 1, mid);
T->rchild = CreatTree(n - 1 - i, pre + i + 1, mid + 1 + i);
return T;
}
后序+中序
根据后序序列可得,根节点在结尾,
根据中序序列可得,根节点在中间,
于是通过中序序列得到左右两支树
BTree CreatTree(int n, int* last, int* mid)
{
if (n <= 0)return NULL;
BTree T;
T = new BTNode;
T->data = last[n - 1];//后序的最后一个一定是根节点
T->lchild = NULL;
T->rchild = NULL;
int i;
for (i = 0; i < n; i++)//中序找根节点,将左右子树分开
if (mid[i] == last[n - 1])break;
T->lchild = CreatTree(i, last, mid);
T->rchild = CreatTree(n - 1 - i, last + i, mid + 1 + i);
return T;
}
注意:没有先序+后序建树,两者虽然都可以确定根的位置,但没办法将左右子树分开,无法唯一确定二叉树
1.1.3 二叉树的遍历
- 按照对应的遍历关系(根左右,左根右,左右根)进行遍历
- 每到一个节点时,将该结点作为根节点!!!
1. 先序遍历---根左右
void PreOrder(BTree bt)
{
if (bt != NULL) {
cout << " " << bt->data; //访问根结点
PreOrder(bt->lchild);//遍历左支
PreOrder(bt->rchild);//遍历右支
}
}
2. 中序遍历---左根右
void MidOrder(BTree bt)
{
if (bt != NULL){
MidOrder(bt->lchild);//左
cout << bt->data<<" ";//根
MidOrder(bt->rchild);//右
}
}
3. 后序遍历---左右根
void PostOrder(BTree bt)
{
if (bt != NULL) {
PostOrder(bt->lchild);//左
PostOrder(bt->rchild);//右
cout << " " << bt->data;//访问根结点
}
}
4. 层次遍历---一层一层,从左向右
层次遍历借助了队列,将该层的元素从左到右储存,再输出
void Level(BTree BT)
{
BTree b;
q.push(BT);
while (!q.empty()) {
b=q.front();
q.pop();
cout << " " << b->Data;
//递归
if (b->lchild)q.push(b->lchild);
if (b->rchild)q.push(b->rchild);
}
}
1.1.4 线索二叉树
为了方便查找孩子,前驱,祖先,引入线索指针----线索二叉树
- 若结点有左子树,则lchild指向其左孩子;否则,lchild指向其直接前驱(即线索)
- 若结点有右子树,则rchild指向其右孩子;否则,rchild指向其直接后继(即线索)
LTag:1. 若 LTag=0, lchild域指向左孩子
2. 若 LTag=1, lchild域指向其前驱
RTag:1. 若 RTag=0, rchild域指向右孩子
2. 若 RTag=1, rchild域指向其后继
//结构体
typedef struct node{
ElemType data;//结点数据域
int ltag,rtag;//增加的线索标记
struct node* lchild;//左孩子或线索指针
struct node* rchild;//右孩子或线索指针
}TBTNode;//线索树结点类型定义
中序线索二叉树特点
遍历二叉树时不需要递归,所有节点只需遍历一次!!!
既没有递归也没有用栈,空间效率得到提高,时间复杂度O(n)
TBTNode* pre;//全局变量
TBTNode* CreatThread(TBTNode* b)//中序线索化二叉树
{
TBTNode* root;
root = (TBTNode*)malloc(sizeof(TBTNode)); //创建头结点
root->ltag = 0; root->rtag = 1; root->rchild = b;
if (b == NULL) root->lchild = root; //空二叉树
else{
root->lchild = b;
pre = root; //pre是*p的前驱结点,供加线索用
Thread(b); //中序遍历线索化二叉树
pre->rchild = root; //最后处理,加入指向头结点的线索
pre->rtag = 1;
root->rchild = pre; //头结点右线索化
}
return root;
}
void Thread(TBTNode*& p)//对二叉树b进行中序线索化
{
if (p != NULL){
Thread(p->lchild); //左子树线索化
if (p->lchild == NULL) { //前驱线索化
p->lchild = pre; p->ltag = 1;
} //建立当前结点的前驱线索
else p->ltag = 0;
if (pre->rchild == NULL){ //后继线索化
pre->rchild = p; pre->rtag = 1;
} //建立前驱结点的后继线索
else pre->rtag = 0;
pre = p;
Thread(p->rchild); //递归调用右子树线索化
}
}
1.1.5 二叉树的应用--表达式树
表达式当然想到前面的栈啦,存操作数,存运算符
伪代码
void InitExpTree(BTree& T, string str) //建二叉表达式树
{
定义两个stack st ss分别存树与运算符
'#'进ss栈//后面函数以#作为结束标志
while (str[i]) {
if str[i]是数字//In()//函数进行判断
{
直接建树T,左右孩子置空
T进st栈
}
else {//运算符
比较ss栈顶与str[i]的优先级//Precede()
< str[i]优先级高,直接进ss栈
> ss栈顶元素出栈,创建新结点,再让st的元素作为新结点的子树,最后新结点入st栈
= ss栈顶出栈
}
}
while (ss栈顶元素不是#) {
ss栈顶元素出栈,创建新结点,再让st的元素作为新结点的子树
最后新结点入st栈
}
}
double EvaluateExTree(BTree T)//计算表达式树
{
double a, b//两个操作数
递归出口
T的左右孩子都为空 返回T的data
a= EvaluateExTree(T->lchild)
b= EvaluateExTree(T->rchild)
对于T->data
+:return a+b
-:return a-b
*:return a*b
/:当b==0除数为零 cout << "divide 0 error!"
b!=0 return a/b
}
1.2 多叉树结构
1.2.1 多叉树结构
1. 顺序存储--双亲存储结构
typedef struct{
ElemType data; //结点的值
int parent; //指向双亲的位置
}PTree[MaxSize];
缺点:找双亲容易找孩子难
2. 孩子连存储结构
typedef struct node{
ElemType data; //结点的值
struct node* sons[MaxSons]; //指向孩子结点
}TSonNode;
缺点:找孩子容易找双亲难
于是就诞生了两者结合的方法--孩子兄弟链
3. 孩子兄弟链
typedef struct tnode{
ElemType data; //结点的值
struct tnode* son; //指向兄弟
struct tnode* brother; //指向孩子结点
}TSBNode;
1.2.2 多叉树遍历
1.先序遍历
先根,然后孩子(从左到右)
先从根节点A开始,根之后,从最左的孩子开始遍历
以B为根,遍历完后又有它的孩子,开始遍历B的最左的子树,
再以E为根,遍历后,看它的孩子,E,没有孩子,则返回B
以B为根,孩子遍历到F, F没有孩子,返回B
B的子树遍历完,再返回A
A的下一个子树C,开始遍历,如此循环下去,遍历完时,回到了A
1.3 哈夫曼树
1.3.1 哈夫曼树定义
哈夫曼树:又称最优二叉树,给定N个权值作为N个叶子结点,构造一棵二叉树,该树的带权路径长度是最小的二叉树。
哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
WPL=(sum)(W_i)(L_i)
每次都找最小的两个数据进行合并,再重新排序,如此循环下去,建立哈夫曼树 |
例如:给定一组数据{1,2,3,4},求WPL
WPL=41+32+23+13=19
1.3.2 哈夫曼树的结构体及建构
结构体
/*顺序结构*/
typedef struct node{
char data;//节点值
float weight;//权重
int parent;//双亲节点
int lchild;//左孩子节点
int rchild;//右孩子节点
}HTNode;
/*链式结构*/
typedef struct node
{
char data; //结点数据
float weight; //权重
struct node* parent; //双亲节点
struct node* lchild; //左孩子
struct node* rchild; //右孩子
struct node* next; //指向下一个节点
}HTNode;
建立哈夫曼树
- 给定的n个权值存在hf中
- 从hf中选出权值最小和次小的两个数据作为左右结点,构造成新节点,新节点的权值等于左右节点之和
- 将最小和次小的两个数据删除,并将新结点的权值加到hf中
- 重复上面操作,直到最后剩余一个节点
void CreatHFT(HTNode hf[], int n)//建立哈夫曼树
{
int min1, min2, lnode, rnode;
for (int i = 0; i < 2 * n - 1; i++)//初始化
hf[i].parent = hf[i].lchild = hf[i].rchild = -1;
for (int i = n; i < 2 * n - 1; i++) {
min1 = min2 = MAX;//找到最小和次小
lnode = rnode = -1;//最小和次小对应下标
for (int j = 0; j <= i - 1; j++) {
if (hf[j].parent == -1) {
if (hf[j].weight < min1) {
min2 = min1;
min1 = hf[j].weight;
lnode = j;
}
else if (hf[j].weight < min2) {
min2 = hf[j].weight;
rnode = j;
}
}
hf[lnode].parent = hf[rnode].parent = i;
hf[i].weight = hf[lnode].weight + hf[rnode].weight;
hf[i].lchild = lnode; hf[i].rchild = rnode;
}
}
}
该操作时间复杂度为O((n^2));
除上述操作外,找最小和次小,还可以通过sort,堆排,优先队列等一些比较快的排序进行优化
- sort--O(nlogn)
C++STL库中一个很好用的自动排序函数,在头文件#include中
sort (first, last,cmp);//对容器或普通数组中区间[first,last)内的元素按照cmp的规则进行,一般默认以元素值的大小做升序排序。
//对 [first, last) 区域内的元素做默认的升序排序
void sort (RandomAccessIterator first, RandomAccessIterator last);
//按照指定的 comp 排序规则,对 [first, last) 区域内的元素进行排序
void sort (RandomAccessIterator first, RandomAccessIterator last, Compare comp);
- 优先队列--O(nlogn)
与一般的队列一样需要头文件#include,和queue不同的就在于我们可以自定义其中数据的优先级, 让优先级高的排在队列前面,优先出队
优先队列具有队列的所有特性,包括基本操作,只是在这基础上添加了内部的一个排序,它本质是一个堆实现的
定义方式:priority_queue<Type, Container, Functional>
Type 就是数据类型,
Container 就是容器类型(Container必须是用数组实现的容器,比如vector,deque等等,但不能用 list。STL里面默认用的是vector)
Functional 就是比较的方式,当需要用自定义的数据类型时才需要传入这三个参数,使用基本数据类型时,只需要传入数据类型,默认是大顶堆
//升序队列
priority_queue <int,vector<int>,greater<int> > q;
//降序队列
priority_queue <int,vector<int>,less<int> >q;
//greater和less是std实现的两个仿函数(就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),
//这个类就有了类似函数的行为,就是一个仿函数类了)
//也可以自定义排序方式
1.3.2 哈夫曼编码
在远程通讯中,要将待传字符转换成由二进制的字符串,为了让编码长度减小,出现了哈弗曼编码
例如:设一段文本中包含字符{a, b, c, d, e},其出现频率相应为{3, 2, 5, 1, 1}。
则每个字母的哈夫曼编码是
结构体
typedef struct
{
char cd[N];//存放当前节点的哈夫曼码
int star;//哈夫曼码在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;
}
}
1.4 并查集
并查集:是一种树型的数据结构,顾名思义,是用于合并查找的集合,用于多个可以不相关的树的合并与查找。
对于大量数据根据某些特征进行合并,查找,不仅空间需要很大,时间效率也比较低。
而并查集,可以将数据压缩成树的形式,树的高度不高,就能大大提高运算效率。
结构体
typedef struct node {
int data;//结点对应的编号
int rank;//结点秩,子树的高度
int parent;//结点对应的双亲下表
}UFSTree;
初始化
void MakeSet(UFSTree t[], int n)//初始化
{
for (int i = 1; i <= n; i++) {
t[i].data = i;
t[i].rank = 0;
t[i].parent = i;
}
}
查找
int FindSet(UFSTree t[], int x)//找双亲
{
if (x != t[x].parent)return FindSet(t, t[x].parent);
else return x;
}
合并
void Union(UFSTree t[],int x, int y)//合并x,y
{
x = FindSet(t, x);
y = FindSet(t, y);
if (t[x].rank > t[y].rank)t[y].parent = x;
else {
t[x].parent = y;
if (t[x].rank == t[y].rank)
t[y].rank++;
}
}
并查集应用--朋友圈
#include<iostream>
#define MaxSise 30005
using namespace std;
typedef struct node {
int data;//结点对应的编号
int rank;//结点秩,子树的高度
int parent;//结点对应的双亲下表
}UFSTree;
UFSTree t[MaxSise];
int a[MaxSise],f[MaxSise];
void MakeSet(UFSTree t[], int n)//初始化
{
for (int i = 1; i <= n; i++) {
t[i].data = i;
t[i].rank = 0;
t[i].parent = i;
}
}
int FindSet(UFSTree t[], int x)//找双亲
{
if (x != t[x].parent)return FindSet(t, t[x].parent);
else return x;
}
void Union(UFSTree t[],int x, int y)//合并x,y
{
x = FindSet(t, x);
y = FindSet(t, y);
if (t[x].rank > t[y].rank)t[y].parent = x;
else {
t[x].parent = y;
if (t[x].rank == t[y].rank)
t[y].rank++;
}
}
int main()
{
int n, m,num,maxx=0,parent;
cin >> n >> m;//总人数,询问次数
MakeSet(t, n);//初始化
for (int i = 0; i < m; i++) {//m个俱乐部
cin >> num;//num个成员
for (int s = 1; s <= num; s++)cin >> a[s];//暂时存在数组中
for (int j = 1; j <= num; j++){
for (int k = j + 1; k <= num; k++) {
Union(t, a[j], a[k]); //对每一个朋友圈里的任意两个人并在一起
}
}
}
for (int i = 1; i <= n; i++) { //找到最大朋友圈人数
parent = FindSet(t, i);
f[parent]++;
maxx=max(maxx, f[parent]);
}
cout << maxx;
return 0;
}
1.5.谈谈你对树的认识及学习体会。
- 树是非线性结构,但也需要线性结构进行辅助完成,例如层次遍历需要队列,表达式二叉树需要栈进行辅助,对整体把握性要求较高
- 尽量自己多动手画图,才能真正理解,懂得怎么遍历实现的
- 学习一些算法优化的方法,需要多了解一些STL库中的东西,sort,堆排等等
2.PTA实验作业
2.1 输出二叉树每层节点
2.1.1 解题思路及伪代码
解题思路
建立二叉树
层次输出需要借助队列暂时储存每层元素,要有一个该层结束的标志---b==eb
还需要有每层的高度
伪代码
void LevelOrder(BTree bt)
{
BTree b,eb;//结尾所在的树枝eb
b=eb=bt;//最开始都在根部
queue<BTree>q;//队列
if bt 空 cout<<"NULL"; return ;
bt进队列
while q不空
//每层结束
if b==eb//当前的树是该层最后一个时
cout<<h++;//可以用flag 控制换行与不换行
eb=q.back();//更新eb
b=q.front();//从最左侧开始
q.pop();
cout<< b->data << ",";
if b左孩子存在 左孩子进队列;
if b右孩子存在 右孩子进队列;
end while
}
2.1.2 总结解题所用的知识点
- 前面的线性结构队列,也可以在非线性结构利用,毕竟非线性结构也是由很多线性结构拼接的
- 二叉树的层次遍历要牢记
- 利用队列将该层的头与尾结合,控制输出每层
2.2 目录树
2.2.1 解题思路及伪代码
解题思路
- 结构体设计--孩子兄弟链
- 区分目录与文件--flag 0是文件,1是目录
- 每行有多个目录或文件名称,/隔开--substr()函数
- 不管文件还是目录,都要按照字典序排序--string类型的比较字符
- 目录下面是文件--定义1>0
伪代码
主要介绍建树的函数,分割字符串就不细讲了。。。
Position insert(BTree root, string s, int flag)
{
if 根节点没有左孩子,root->child=NULL 直接插入s到根节点的左孩子处
定义tmpNode指向左孩子,father指向根
whilie(当前优先级比tmpNode的低 或者 相同但字典序比tmpNode大)从头开始找插入位置
father与tmpNode更新为下一个
一直循环,找到插入位置
end while
循环结束后有三种情况
1. 到了末尾也没有找到
if tmpNode==NULL 直接插入链尾
return Node
2. 目录中已经存在该文件或目录
return tmpNode
3. 找到了插入位置
是否为第一个结点,插入情况不同
return Node
}
2.2.2 总结解题所用的知识点
- 旧知识:分割字符串,substr()
- 指针头插法与尾插法,插入新结点
- string字符直接比较字典序
3.阅读代码
3.1 题目及解题代码
//官方题解
int maxGain(TreeNode* node) {
if (node == nullptr) {
return 0;
}
// 递归计算左右子节点的最大贡献值
// 只有在最大贡献值大于 0 时,才会选取对应子节点
int leftGain = max(maxGain(node->left), 0);
int rightGain = max(maxGain(node->right), 0);
// 节点的最大路径和取决于该节点的值与该节点的左右子节点的最大贡献值
int priceNewpath = node->val + leftGain + rightGain;
// 更新答案
maxSum = max(maxSum, priceNewpath);
// 返回节点的最大贡献值
return node->val + max(leftGain, rightGain);
}
int maxPathSum(TreeNode* root) {
maxGain(root);
return maxSum;
}
3.2 该题的设计思路及伪代码
二叉树路径:被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
所以一定要有父亲节点才能出现子树
对树进行一次遍历,每次比较当前数据加与不加的数值,找到最大的数值
int maxGain(TreeNode* node)
{
结点为空 返回0
递归计算左子树与右子树的最大值
只取大于零的结点
leftGain = max(maxGain(node->left), 0);
rightGain = max(maxGain(node->right), 0);
priceNewpath= node->val+ rightGain+ leftGain//根左右的值
max()比较当前的maxSum与priceNewpath的值,更新maxSum
返回最后的 node->val + max(leftGain, rightGain);
}
- 时间复杂度:O(N)O(N),其中 NN 是二叉树中的节点个数。对每个节点访问不超过 22 次。
- 空间复杂度:O(N)O(N),其中 NN 是二叉树中的节点个数。空间复杂度主要取决于递归调用层数,最大层数等于二叉树的高度,最坏情况下,二叉树的高度等于二叉树中的节点个数。
3.3 分析该题目解题优势及难点。
优势
- 对于负数的处理采用max(0,x);将所有负数成功去掉,不影响最后结果
- 将整棵树分解成一个最小的二叉树,祖先(a),左右子树(b,c),所以路径长度有可能是a+b+c;b+a;c+a
难点 - `理解题意和转化题意,题目比较难以理解和转化
- 可能思路想错,根节点会忽视 ,无法正确形成一条路径`