树
这个作业属于哪个班级 | 数据结构--网络2011/2012 |
---|---|
这个作业的地址 | DS博客作业03--树 |
这个作业的目标 | 学习树结构设计及运算操作 |
姓名 | 王小雨 |
0.PTA得分截图
1.本周学习总结
1.1二叉树结构
1.1.1二叉树的2种储存结构
完全二叉树顺序存储
下标从1开始
ElemType SqBTree[Maxsize];
SqBTree bt="ABCDEF####";
非完全二叉树顺序存储
先用空结点补全成为完全二叉树,然后对结点进行编号
ElemType SqBTree[Maxsize];
SqBTree bt="ABD#C#E######F";
链式存储
typedef struct node{
ElemType data;
struct node *lchild,*rchild;//指向左子树和右子树
}BTNode;
typedef BTNode *BTree;
在二叉链中:有n个结点,则有2n个指针域,有n-1个分支数,n-1个非空指针域,2n-(n-1)=n+1个空指针域
顺序存储和链式存储的区别和优缺点:
顺序存储时,逻辑上相邻的数据元素,其物理存放地址也相邻。顺序存储的优点是存储密度大,存储空间利用率高;缺点是插入或删除元素时不方便。
链式存储时,相邻数据元素可随意存放,但所占存储空间分两部分,一部分存放结点值,另一部分存放表示结点间关系的指针。链式存储的优点是插入或删除元素时很方便,使用灵活。缺点是存储密度小,存储空间利用率低。
1.1.2二叉树的构造
1.顺序存储转二叉链
BTree CreateBTree(string str,int i)
{
if(i>字符串长度||i<=0)return NULL;//递归出口
if(str[i]=='#')return NULL;
创建根结点bt,bt->data=str[i];
创建左子树:
bt->lchild=CreateBTree(str,2*i);
创建右子树:
bt->rchild=CreateBTree(str,2*i+1);
return bt;
}
BTree CreateBTree(string str,int i)
{
int len=str.size();
BTree bt;
bt=new TNode;
if(i>len||i<=0)return NULL;
if(str[i]=='#')return NULL;
bt->data=str[i];
bt->lchild=CreateBTree(str,2*i);//i从1开始
bt->rchild=CreateBTree(str,2*i+1);
return bt;
}
bt->lchild=CreateBTree(str,2*i+1);//i从0开始
bt->rchild=CreateBTree(str,2*i+2);
2.先序遍历建二叉树
ABD#G###CEH###F#I##
BTree CreatTree(string str,int &i)//i要加引用符,才能使i一直增
{
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;
}
同时给定一棵二叉树的先序和中序序列(先序和后序序列)就能唯一确定 二叉树
例:已知先序为ABDGCEF,中序为DGBAECF
由先序知,A为根节点,再从中序中找到A,得DGB为左子树,ECF为右子树;先序中B在最前面,得B为根节点,以此类推
方法:先在先序或后序中找到根节点,然后在中序中找到该节点位置,得到它的左右子树
BTree CreateBT(char *pre,char *in,int n)//字符指针,pre指向先序序列,in指向中序序列 n为序列长度
{
递归口:若n<=0,返回空,递归结束
1.创建根节点BT
BT->data=*pre
2.查找根节点在中序序列中的位置k
3.创建左子树:BT->lchild=CreatBT(pre+1,in,k)
4.创建右子树:BT->rchild=CreatBT(pre+k+1,in+k+1,n-k-1)
}
3.已知先序和中序
BTree CreateBT(char *pre,char *in,int n)
{
BTNode *s;char *p;int k;
if(n<=0)return NULL;
s=new BTNode;//创建根节点
s->data=*pre;
for(p=in;p<in+n;p++)//在中序中找*pre的位置k
{
if(*p==*pre)
break;
}
k=p-in;//指针相减为两个元素相差的单元,k为int型
s->lchild=(CreateBT(pre+1,in,k));//构造左子树
s->rchild=(CreateBT(pre+k+1,p+1,n-k-1));//构造右子树
return s;
}
4.已知先序和后序
BTree CreateBT(char *post,char *in,int n)
{
BTNode *s;char *p;int k;
if(n<=0)return NULL;
s=new BTNode;//创建根节点
s->data=*(post+n-1);//后序序列的最后一个结点
for(p=in;p<in+n;p++)
{
if(*p==*(post+n-1))
break;
}
k=p-in;
s->lchild=(CreateBT(post,in,k));//构造左子树
s->rchild=(CreateBT(post+k,p+1,n-k-1));//构造右子树
return s;
}
1.1.3二叉树的遍历
先序遍历:访问根结点,先序遍历左子树,先序遍历右子树
void PreOrder(BTree bt)
{
if(bt!=NULL)
{
printf("%c",bt->data);
PreOrder(bt->lchild);
PreOrder(bt->rchild);
}
}
递归调用从根结点开始,遍历完所有结点后在根节点结束,每个结点访问2遍
中序遍历:左根右
我们直到递归是来回的过程,对于恰好有两个子节点(子节点无节点)的节点来说。只需要访问一次左节点,访问根,访问右节点。即可。
而如果两侧有节点来说。每个节点都要满足中序遍历的规则。我们从根先访问左节点。到了左节点这儿左节点又变成一颗子树,也要满足中序遍历要求。所以就要先访问左节点的左节点(如果存在)。那么如果你这样想,规则虽然懂了。但是也太复杂了。那么我们借助递归。因为它的子问题和根节点的问题一致,只是范围减小了。所以我们使用递归思想来解决。
那么递归的逻辑为:考虑特殊情况(特殊就直接访问)不进行递归否则递归的访问左子树(让左子树执行相同函数,特殊就停止递归输出,不特殊就一直找下去直到最左侧节点。)——>输出该节点—>递归的访问右子树.
在遍历左子树和右子树的时候也要按照中序遍历的规则
void PreOrder(BTree bt)
{
if(bt!=NULL)
{
PreOrder(bt->lchild);
printf("%c",bt->data);
PreOrder(bt->rchild);
}
}
后序遍历:左右根
void PreOrder(BTree bt)
{
if(bt!=NULL)
{
PreOrder(bt->lchild);
PreOrder(bt->rchild);
printf("%c",bt->data);
}
}
层次遍历(用到队列)
初始化队列,将根节点进队
while(队列不空)
{
出列一个结点*p,访问它;
若他有左孩子,将左孩子进队;
若有右孩子,将右孩子进队;
}
A进队,A出队,访问A,然后B,C进队;B出队,访问B,然后D进队;C出队,访问C,然后E,F进队....
1.1.4线索二叉树
若结点有左孩子,则lchild指向其左孩子;否则lchild指向其直接前驱
若结点有右孩子,则rchild指向其右孩子,否则rchild指向其直接后继
为避免混淆,增加两个标志域
Tag=0,说明有孩子;Tag=1,没有孩子
typedef struct node{
ElemType data;
int ltag,rtag;
struct node *lchild;
struct node *rchild;
}TBTNode;
中序线索二叉树再增加一个头结点
1.头结点左孩子指向根节点
2.右孩子为线索,指向最后一个孩子
3.遍历序列时头结点为第一个结点的前驱,最后一个结点的后继
左根右
任一个结点的后继:
结点有右孩子,则为右子树最左孩子结点
无右孩子,则为后继线索指针指向结点
任一个结点的前驱:
结点有左孩子,则为左子树最右孩子结点
无左孩子,则为前驱线索指针指向结点
找中序遍历的第一个结点:左子树上最左下(没有左子树)的结点
1.1.5二叉树的应用--表达式树
将中缀表达式转换为二叉树
操作数都是叶子节点,运算符都是分支节点
需要用到两个栈,一个栈放运算符,一个放树根栈
while(遍历表达式)
{
若为操作数,生成树节点,入树根栈
若为运算符:
若优先级>栈顶运算符,入运算符栈
若小于,出栈,树根栈弹出两个结点与它建树,新生成的树根入树根栈
若相等,出栈运算符栈
}
while结束后还要看看运算符栈是否已空
1.2多叉树结构
1.2.1多叉树结构
顺序存储(双亲)
typedef struct{
ElemType data;
int parent;//指向双亲的位置
}PTree[maxsize];
找父亲容易,找孩子不容易
孩子链
typedef struct node{
ElemType data;
struct node *sons[maxsons];//指向孩子结点
}TSonNode;
sons要申请度最大的指针区域,导致空指针太多;找父亲不容易
孩子兄弟链
typedef struct tnode{//只有两个指针域
elemtype data;
struct tnode *son;//第一个孩子结点
struct tnode *brother;//兄弟结点
}TSBNode;
1.2.2多叉树遍历
1.3哈夫曼树
1.3.1哈夫曼树定义
设二叉树有n个带权值的叶子节点,那么从根节点到各个叶子节点的路径长度与相应结点权值乘积的和,叫做二叉树的带权路径长度
具有最小带权路径长度的二叉树称为哈夫曼树(最优树)
构造哈夫曼树
原则:
权值越大的叶节点越靠近根节点;权值越小的叶节点越远离根节点
过程:
根据给定的n个权值,构造n个只有根节点的二叉树作为一个集合,从中选取根节点权值最小和次小的两颗二叉树作为左右子树构造一棵新的二叉树,这棵新的二叉树的根节点权值为其左右子树根节点权值之和。在原来的n棵二叉树集合中删除作为左右子树的两棵二叉树,并将新建立的二叉树加入进去。重复上述步骤,当集合中只剩下一棵二叉树时,这棵二叉树就是要建立的哈夫曼树
特点:已知n0个叶子节点
因为每次都是两棵树合并,所以没有单分支结点,n1=0
总结点数n=2n0-1
1.3.2哈夫曼树的结构体
typedef struct{
char data;//节点值
float weight;//权值
int parent;//双亲结点
int lchild;//左孩子结点
int rchild;//右孩子结点
}HTNode;
1.3.3哈夫曼树构建及哈夫曼编码
哈夫曼树构建
运用结构体数组,已知n个叶子节点,所以总结点数为2n-1,数组长度为2n-1;
1.初始化数组ht:所有2n-1个结点的parent,lchild,rchild域置为-1;输入n个叶子节点的data,weight值
2.构造非叶子节点h[i],放在ht[n]ht[2n-2]中:从ht[0]ht[i-1]中找出根节点(parent为-1)最小的两个结点ht[lnode]和ht[rnode],他们的双亲结点置为ht[i],且ht[i].weight=ht[lnode].weight+ht[rnode].weight,直到2n-1个非叶子结点处理完
void CreateHT(BTNode ht[],int n){
int i,j,k,lnode,rnode;float min1,min2;
for(i=0;i<2*n-1;i++)//所有结点初始化
{
ht[i].parent=ht[i].lchild=ht[i].rchild=-1;
}
for(i=n;i<2*n-1;i++)//构造非叶子结点
{
min1=min2=123;lnode=rnode=-1;
for(k=0;k<=i-1;k++)//查找该结点前最小的两个结点
{
if(ht[k].parent==-1)//parent若已被修改,说明该结点已被构造,已被删除
{
if(ht[k].weight<min1){
min2=min1;rnode=lnode;min1=ht[k].weight;lnode=k;}
else if(ht[k].weight<min2){
min2=ht[k].weight;rnode=k;}
}
ht[lnode].parent=i;ht[rnode].parent=i;//修改parent
ht[i].weight=ht[lnode].weight+ht[rnode].weight;//权相加
ht[i].lchild=lnode;ht[i].rchild=rnode;//修改左右孩子
}
}
哈夫曼编码
编码目标:总编码长度最短
让待传字符串中出现次数较多的字符采用尽可能短的编码
要设计长度不等的编码,则必须使任一字符的编码都不是另一个字符的编码的前缀;如00和0010
哈夫曼编码:根节点到叶子节点经过路径组成的0,1序列,做分支用0,右分支用1
typedef struct{
char cd[N];//存放当前结点的哈夫曼码
int start;//哈夫曼码在cd中的起始位置
}HCode;
根据哈夫曼树求对应的哈夫曼编码
void CreatrHCode(HTNode ht[].NCode hcd[],int n){
int i,f,c;HCode hc;
for(i=0;i<n;i++)
{
hc.start=n;c=i;f=ht[i].parent;//起始位置在最后,c保存当前结点
while(f!=-1)//循环直到没有双亲结点即到达树根结点
{
if(ht[f].lchild==c)hc.cd[hc.start--]=='0';//当前结点是左孩子结点,起始位置前移
else hc.cd[hc.strat--]=='1';
c=f;f=ht[f].parent;
}
hc.strat++;
hcd[i]=hc;
}
}
1.4并查集
并查集是一种用于管理分组的数据结构。它具备两个操作:(1)查询元素a和元素b是否为同一组 (2) 将元素a和b合并为同一组。
集合查找:用数组查找需要全部遍历,用树查找可以减少查找次数即为树的高度。同一个集合的标志就是根parent一样
集合合并:高度低的树作为另一棵树的子树
并查集的优势:并查集是若干个不相交集合,能够实现较快的合并和判断元素所在集合的操作,应用很多。
顺序存储:
typedef struct node{
int data;//结点对应编号
int rank;//秩,子树高度
int parent;//双亲下标
}UFCTree;
注:根节点的父亲是自己
并查集树初始化
int i;
for(i=1;i<=n;i++){
t[i].data=i;//数据为其编号
t[i].rank=0;
t[i].parent=i;//都指向自己
}
查找元素所属集合
int FIND(UFSTree t[],int x){
if(x!=t[x].parent)return(FIND(t,t[x].parent));//双亲不是自己
else return(x);
}
两个元素各自所属的集合合并
x=FIND(t,x);//查找x所在集合树的编号
y=FIND(t,y);//查找y所在集合树的编号
if(t[x].rank>t[y].rank)
t[y].parent=x;//把y连到x结点上
else{
t[x].parent=y;
if(t[x].rank==t[y].rank) t[y].rank++;
}
1.5谈谈你对树的认识及学习体会
树结构结合了另外两种数据结构的优点:一个是有序数组,另外一个是链表。 树结构的查询的速度和有序数组一样快,树结构的插入数据和删除数据的速度也和链表一样快。
树的知识点很多,要对树的构造和遍历等具体操作有很好的掌握才能把它更好的应用到题目中去,所以还是要多多练习~
2.PTA实验作业
2.1输出二叉树每层结点
#include<iostream>
#include<string>
#include<queue>
using namespace std;
typedef char ElementType;
typedef struct node {
ElementType data;
struct node* lchild, * rchild;
}BTNode;
typedef BTNode* BTree;
BTree CreatTree(string str, int& i);
void LevelTree(BTree T);
int main()
{
string str;
cin >> str;
int i = 0;
BTree bt;
bt = CreatTree(str, i);//先序建二叉树
LevelTree(bt);
}
BTree CreatTree(string str, int& i)//i要加引用符,才能使i一直增
{
BTree bt;
if (i > str.length() - 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;
}
void LevelTree(BTree T)
{
queue<BTree>qu;
qu.push(T);
BTree num;
num = T;
int count = 1;//层数
if (num == NULL)
{
cout << "NULL";
return;
}
while (!qu.empty())
{
int length = qu.size();
cout << count << ":";
for (int i = 0; i < length; i++)
{
num = qu.front();
qu.pop();
cout << num->data << ",";
if (num->lchild != NULL)
qu.push(num->lchild);
if (num->rchild != NULL)
qu.push(num->rchild);
}
cout << endl;//一层结束,换行,层数加1
count++;
}
}
2.1.1解题思路及伪代码
先根据输入的字符串先序遍历建树,然后层次遍历二叉树输出每层的结点
定义一个队列qu
根节点入队
定义一个树指针num
num=T
int count=1层数
if num为空 return
while(队列不空){
int length=队列长度
cout << count << ":"
for i=0 to length
{
取队头元素给num;出队
cout << num->data << ","
if左孩子不为空 左孩子入队;if右孩子不为空 右孩子入队
}
cout<<endl;
count++;
}
2.2.2知识点
1.先序遍历建树相关代码
BTree CreatTree(string str, int& i)//i要加引用符,才能使i一直增
{
BTree bt;
if (i > str.length() - 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;
}
2.输出每层结点时,因为每层结点数不一样,在输出每层最后一个结点后要换行,所以我用到了队列
2.2目录树
2.2.1解题思路及伪代码
typedef struct node*BTree;
typedef struct node
{
string name;
BTree Catalog;
BTree Brother;
Bool flag;
}BTnode;
void DealStr(string str, BTree bt)
{
while(str.size()>0)
{
查找字符串中是否有’\’,并记录下位置pos
if(没有)
说明是文件,则进入InsertFile的函数
else
说明是目录,则进入Insertcatalog的函数里
bt=bt->catalog;
同时bt要跳到下一个目录的第一个子目录去,因为刚刚Insertcatalog的函数是插到bt->catalog里面去
while(bt!NULL&&bt->name!=name)
bt=bt->Brother;//找到刚刚插入的目录,然后下一步开始建立它的子目录
str.erase(0, pos + 1);把刚插进去的目录从字符串中去掉
}
}
2.2.2总结知识点
1.用了孩子兄弟链的结构
2.目录插入