这个作业属于哪个班级 | 数据结构--网络2011,2012(集美大学) |
---|---|
这个作业的地址 | C博客作业03--树 |
这个作业的目标 | 学习树结构设计及运算操作 |
姓名 | 张官德 |
0.PTA得分截图
1.本周学习总结
1.1 二叉树结构
1.1.1 二叉树的2种存储结构
1. 顺序存储
顺序结构实现二叉树时,采用一个一维数组来存储所有结点,需要将所有结点按照在树中的位置安排成一个恰当的序列,使其能反应结点之间相互的逻辑关系,通常使用编号的方法;
例如:
若二叉树为完全二叉树则上述方法,可以很好的利用存储空间,基本没有浪费,且对于结点的查找是很方便的;
但是当二叉树为一般二叉树时,如想要使用顺序结构存储则必须增加虚拟结点,使其变为完全二叉树,像下面这样:
这样一来则需要浪费一部分存储空间,极端情况下,若二叉树是一分叉的(每个节点只有一个子节点),将造成极大的空间浪费。
所以采用链式存储结构能合理有效地分配内存空间问题
每个结点由一个数据域和两个指针域组成,共三个部分,如下图所示:
但寻找他的双亲结点,则需要遍历所有结点,比较麻烦。另外,当树的度较大时存在较多的空指针域。
1.1.2二叉树的构造
先序序列和后序序列构造二叉树
BTNode* CreatBTl(char* pre, char* in, int n)
//pre 存放先序序列,in存放中序序列,n为二叉树结点个数,返回构造二叉树的根
{
BTNode* b;
char* p;
int k;
if (n <= 0) return NULL;
b = (BTNode*)malloc(sizeof(BTNode)); //创建树结点b
b->data = *pre;
for (p = in; p < in + n; p++) {//在中序序列中找到等于*pre字符的位置k
if (*p == *pre)//pre指向根结点
{
break;//在in中找到后退出循环
}
}
k = p - in//确定很结点在in中的位置
b->lchilde = CreateBTl(pre + 1, in, k);//递归构造左子树
b->rchild = CreateBTl(pre + k + 1, p + 1, n - k - 1);//递归构造右子树
return b;
}
实际上,先序序列的作用是确定一棵二叉树的根结点,中序序列的作用是确
定左右子树的中序序列,进而确定左右子树的先序序列。
中序序列和后序序列构造二叉树
BTNode* CreateBT2(char* post, char* in, int n)//*post存放后序序列,n为二叉树结点个数,本算法执行后返回构造的二叉树的根节点指针*b
{
BTNode* b;
char r, * p;
int k;
if (n <= 0)
{
return NULL;
}
r = *(post + n - 1);///根节点值
b = (BTNode*)malloc(sizeif(BTNode));创造二叉树结点b
b->data = r;
for (p = in; p < in + p; p++)//在in中查找根节点
{
if (*p == r)
{
break;
}
}
k = p - in;//k为根节点在in中的下标
b->lchild = CreateBT2(post, in,k);//递归构造左子树
b->rchild = CreateBT2(post + k, p + 1, n - k - 1);//递归构造右子树
return b;
}
二叉树的后序序列遍历过程是:左→右→根。中序序列遍历过程是:左→根→右。
已知后序序列可以唯一确定根结点,即:后序序列的最后一个结点就是根结点。确定完根结点之后,根据中序序列可以确定根结点的左子树和右子树,即:在中序序列中根结点的左边是左子树,根结点的右边是右子树。
1.1.3 二叉树的遍历###
先序遍历
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 PrintTree(BTree BT)//层次遍历二叉树
{
BTree ptr;//遍历二叉树
queue<BTree>qu;
qu.push(BT);//根结点进栈
while (!qu.empty())
{
ptr = qu.front();
qu.pop();
cout << ptr->data;
if (ptr->lchild != NULL)
qu.push(ptr->lchild);
if (ptr->rchild != NULL)
qu.push(ptr->rchild);
}
}
1.1.4 线索二叉树###
概念
对于具有n个结点的二叉树,当采用二叉链存储结构时,每个结点具有两个指针域,总共有2n个指针域
,又由于只有n-1个结点被有效指针所指向(根结点没有),则共有2n-(n-1)=n+1个空链域。
利用二叉树中的空指针域 来存放在某种遍历次序下的前驱和后继 ,这种指针叫“线索”。这种加上了线索的二叉树称为线索二叉树。
根据遍历次序的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。
线索二叉树声明
typedef struct node
{ ElemType data; //结点数据域
int ltag,rtag; //增加的线索标记
struct node *lchild; //左孩子或线索指针
struct node *rchild; //右孩子或线索指针
} TBTNode; //线索树结点类型定义
中序线索二叉树算法
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 二叉树的应用--表达式树
- 表达式树如何构造
将中缀表达式转换为后缀表达式
step1:初始化一个栈和一个后缀表达式字符串
step2:从左到右依次对中缀表达式中的每个字符进行以下处理,直到表达式结束
如果字符是‘(’,将其入栈
如果字符是数字,添加到后缀表达式的字符串中
如果字符是运算符,先将栈顶优先级不低于该运算符的运算符出栈,添加到后缀表达式中,再将该运算符入栈。注意,当‘(’在栈中时,优先级最低
如果字符是‘)’,将栈顶元素出栈,添加到后缀表达式中,直到出栈的是‘(’
step3:如果表达式结束,但栈中还有元素,将所有元素出栈,添加到后缀表达式中
void InitExpTree(BTree& T, string str) //建二叉表达式树
{
stack<char>op;
stack<BTree>t;
int i = 0;
BTree p = NULL, a, b;//结点
while (str[i])
{
if (!In(str[i]))
{
p = new BTNode;
p->data = str[i];
p->lchild = NULL;
p->rchild = NULL;
t.push(p);
}
else
{
if (op.empty())
{
op.push(str[i]);
}
else
{
switch (Precede(op.top(), str[i]))
{
case'<':
op.push(str[i]); break;
case'=':
op.pop(); break;
case'>':
a = t.top();
t.pop();
b = t.top();
t.pop();
CreateExpTree(p, b, a, op.top());
op.pop();
t.push(p);
i--;
break;
}
}
}
i++;
}
while (!t.empty() && !op.empty())
{
b = t.top();
t.pop();
a = t.top();
t.pop();
CreateExpTree(p, a, b, op.top());
op.pop();
t.push(p);
}
T = p;
}
- 计算表达式树
double EvaluateExTree(BTree T)//计算表达式树
{
if (!T)//
return -1;
double x1, x2, result;
if (!T->lchild && !T->rchild)//若果是叶子结点,也就是操作数,返回数值
return T->data - '0';
if (T->lchild)//访问左子树
x1 = EvaluateExTree(T->lchild);
if (T->rchild)//访问右子树
x2 = EvaluateExTree(T->rchild);
char op = T->data;//得到运算符
switch (op)//进行运算
{
case '+':result = x1 + x2; break;
case '-':result = x1 - x2; break;
case '*':result = x1 * x2; break;
case '/':
if (x2 == 0)//检查除数是否为零
{
cout << "divide 0 error!" << endl;
exit(0);
}
else result = x1 / x2; break;
}
return result;
}
1.2 多叉树结构
1.2.1 多叉树结构
- 双亲存储结构
typedef struct
{
ElemType data;//结点的值
int parent;//指向双亲的位置
}PTree[MaxSize];
- 这种存储结构是一种顺序存储结构,用一组连续空间存储树的所有结点,同时在每个结点中附设一个下标(伪指针)指示其双亲结点的位置。
声明:
#define MAX_TREE_SIZE 100
struct PTNode{
TElem Type data;
int parent; //双亲位置域
};
struct PTree{
PTNode nodes[MAX_TREE_SIZE];
int r,n; //根的位置,结点数
};
- 孩子链表存储结构
typedef struct node
{
ElemType data; //结点的值
struct tnode *sons[MaxSons]; //指向孩子结点
}TSonNode;
孩子兄弟链结构体
typedef struct tnode
{
ElemType data; //结点的值
struct tnode *son; //指向兄弟
struct tnode *brother; //指向孩子结点
}TSBNode;
寻找双亲结点比较麻烦,需要从树的根结点开始逐个结点比较查找
1.2.2 多叉树遍历
先序遍历
访问根节点
按照从左到右的顺序先根遍历根节点的每一棵子树
先根遍历序列的第一个元素即为根节点对应的结点值
1.3 哈夫曼树##
1.3.1 哈夫曼树定义
哈夫曼树的基本概念 最优二叉树,也称哈夫曼(Haffman)树,是指对于一组带有确定权值的叶结点,构造的具有最小带权路径长度的二叉树
哈夫曼树主要用在数据的压缩如JPEG格式图片,在通信中我们可以先对发送的数据进行哈夫曼编码压缩数据提高传输速度。
查询优化:在工作中我们我们身边放许多工具,由于空间限制我们不能把所有工具放在我们最容易拿到的地方,所有我们把使用频率最高的工具放在最容易的位置。同样的道理在查询的时候我们把查询频率最高的数据建立索引,这些都是使用了哈夫曼算法的思想。
1.3.2 哈夫曼树的结构体
结构体
typedef struct
{
char data;//结点值
double weight;//权重
int parent;//双亲结点
int lchild;//左孩子结点
int rchild;//右孩子结点
}HTNode;
哈夫曼树构建及哈夫曼编码
- 哈夫曼树构建
void CreateHT(HTNode ht[], int n)
{
int i, k, lnode, rnode;
double 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 - 2; i++)
{
min1 = min2 = 32767;
lnode = rnode = -1;
for (k = 0; k <= i - 1; k++)
if (ht[k].parent == -1)
{
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[i].weight = ht[lnode].weight + ht[rnode].weight;
ht[i].lchild = lnode; ht[i].rchild = rnode;
ht[lnode].parent = i; ht[rnode].parent = i;
}
}
哈夫曼编码
结构体
typedef struct
{ char cd[N]; //存放当前节点的哈夫曼码
int start; //表示
} HCode;cd[start..n0]部分是哈夫曼码
算法
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 MAKE_SET(UFSTree t[],int n)//初始化并查集树
{
int i;
for (i = 1;i <= n;i++)
{
t[i].data = i; //1数据为该人的编号
t[i].rank = 0; //秩初始化为0
t[i].parent = i; //双亲初始化指向自已
}
}
- 查找
int FIND_SET(UFSTree t[], int x) //在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.5.谈谈你对树的认识及学习体会
- 树结构化抽象为形象,巧妙存储关系,树型存储结构类似于家族的族谱,各个结点之间也同样可能具有父子、兄弟、表兄弟的关系,
但是学习过程感觉有点难了,尤其是递归方面,很难自己创立一个合适的算法,过硬的套用,经常难解其意。
2.PTA实验作业
2.1 二叉树
- 二叉树叶子结点带权路径长度和
代码:
- 解题思路:
建树
定义wpl存放树的权
定义h存放树的高
利用递归来计算带权路径长度和
伪代码:
main
{
BTree bt
int h,wpl
建树
GetWPL
输出wpl
}
wpl
{
if 孩子节点不空
wpl = wpl + (bt->data - '0') * h
endif
GetWPL(bt->lchild, h + 1, wpl)
GetWPL(bt->rchild, h + 1, wpl)
}
-
2.1.2 总结解题所用的知识点
1.递归先序创造树结构
2.层次遍历 -
2.2 目录树
代码:
- 解题思路
要用左孩子右兄弟的二叉链表存储
注意同层目录排在文件前,同类按字典顺序输出
伪代码:
对结构体进行定义;
初始化树,建立根节点root;
输入字符串,进行建树;
void CreatTree(Tree& bt, string str, int i)//建树,
{
新建结点temp,ptr;初始化结点;
切割字符串;新节点name为该段字符;
if 该段字符串为目录,isfile改为false;
if (temp为文件)
InitFile(temp, bt);//插入文件
else //temp为目录
InitList(temp, bt);//插入目录
CreatTree(temp, str, i);
}
void InitList(Tree& temp, Tree& bt)//插入目录
{
定义结构体指针btr来遍历二叉树bt;
btr = bt->child;//btr先指向bt的孩子;
/*先对第一个兄弟结点进行判断*/
if (没有第一个孩子|| btr为文件 || 第一个孩子字典序大于该结点)//可插入
进行插入temp->brother = btr;bt->child = temp;//修改孩子指针
else if (二者相等)
直接使temp指向btr;
else //查找兄弟节点
while (btr->brother != NULL)
if (兄弟节点为文件 || 兄弟节点字典序大于该节点)
找到可插入位置,break;
else if (二者相等)
直接使temp指向btr->brother;break;
else
btr = btr->brother;//遍历下一兄弟结点;
end if
end while
if (btr->brother为空 || btr->brother->name != temp->name)
进行插入操作:temp->brother = btr->brother;btr->brother = temp;
end if
end if
}
void InitFile(Tree& temp, Tree& bt)//对文件temp找一个可插入位置
{
定义结构体指针btr来遍历二叉树bt;
btr = bt->child;//btr先指向bt的孩子;
if (第一个孩子为空 || btr为文件 && 结点字典序大于等于该节点)
进行插入,修改bt的孩子指针;
else //判断兄弟结点
while (btr->brother != NULL)
if (btr->brother为文件 && 兄弟节点字典序大于该节点)
找到可插入位置,break;
else
btr = btr->brother;//遍历下一个兄弟结点
end if
end while
对temp进行插入操作:temp->brother = btr->brother;btr->brother = temp;
end if
}
2.1.2 总结解题所用的知识点
1.创建孩子兄弟链
2.后序遍历目录树,排序每个结点的子目录和子文件
3.先序遍历进行输出