这个作业属于哪个班级 | 数据结构--网络2011/2012 |
---|---|
这个作业的地址 | DS博客作业03--树 |
这个作业的目标 | 学习树结构设计及运算操作 |
姓名 | 王鑫 |
0.PTA得分截图
前面学习了几种结构,都是线性存储,关系都是一对一的。现在我们学习了树结构,迈入了非线性结构的学习。
-
链表
-
树
树结构就像它的名字一样,在逻辑上来看,像是一棵树。根结点和分支结点的关系是一对多,这样分散开,就像是一颗倒着的树。
-
二叉树
树中有一种比较比较特殊,好看的树:二叉树。这种树每一个节点最多只有两个分叉,也就是两个结点,度为2.如果每个结点的度都是满两个,这样叫做满二叉树,若满二叉树最下面一层缺了几个,并且缺的是最右边的几个结点,这样叫做完全二叉树。满二叉树是完全二叉树的一种特殊形态。
-
1.非空二叉树上叶结点等于双分支结点数+1
叶子结点是树最下面的结点(无分支)的结点,所以叶子结点是结尾,每个链的结尾都必是叶子结点,不是双分支结点就只有一条链,一个叶子结点。 -
2.在二叉树的第i层上最多有2^(i-1)个结点(i>1)
最多的时候就是满二叉树状态,这时每一层的结点满足2^(i-1)的关系 -
3.高度为h的二叉树最多有2^k-1个结点(h>=1)
-
4.具有n个结点的完全二叉树的深度必为[log2n]+1
1.本周学总结
1.1二叉树结构
1.1.2二叉树结构的2种存储结构
二叉树有两种存储的结构,也和之前的结构差不多,一种是顺序存储,另一种就是链式存储。
1.顺序存储结构
1.对结点标号
2.数组存储
我们要用数组存储就需要对每一个节点存储,数组存储是顺序放置的,是一个一维的空间关系,用编号来知道我们不同节点之间的关系。
一般用#来表示空结点,遇到#设为NULL
ab#cd##ef#g####
- 优点:每个节点个关系可以通过数学计算得知,完全二叉树就很合适
- 缺点:对于普通的二叉树来说,节点可能没有每个都是两个度的。在最坏的情况,所有的度只有1时,空间利用率就太低了。
用数组查找、插入和删除不方便。
链式存储
采用链的方式存储
typedef struct TNode* Position;
typedef Position BinTree;/*二叉树类型*/
struct TNode {/*树结点定义*/
ElementType Data; /*结点数据*/
BinTree Left;/*指向左子树*/
BinTree Right;/*指向右子树*/
};
-
优点:空指针的个数是比较少的 2n-(n-1)=n+1
空间利用率是比顺序存储高
插入、删除比较方便。 -
缺点:各个节点的关系不好知道
1.1.2二叉树的遍历
二叉树的三种遍历
这三种都是用递归的方式访问的。
- 1.先序遍历:根,左子树,右子树
2.中序遍历:左子树,根,右子树
3.后序遍历:右子树,左子树,根
先序遍历
如果左子树还有,就会一直访问下去,直到NULL,再返回上一级调用的函数,来达到先序遍历的功能。
先序遍历构造二叉树
用不同节点在数组中的数学关系来创造它的左右孩子
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); //访问根结点
}
}
层次遍历
还有一个遍历的方式,一层一层的遍历下去
层次遍历我们要借用队列的结构,队列来保持一层一层的遍历
队列存放的都是遍历完的这一层的孩子们
直到队列为空,说明到了最后一层
层次遍历过程是:
初始化队列,先将根节点进队。
while (队列不空)
{
队列中出列一个节点* p, 访问它;
若它有左孩子节点,将左孩子节点进队;
若它有右孩子,将右孩子进队。
}
1.1.3二叉树的构造
双序列构造二叉树
同一颗二叉树(假设每个结点值唯一)具有唯一先序序列、中序序列和后序序列。
但不同的二叉树可能具有相同的先、中或后序序列。(序列中不包括空结点)
给定一颗二叉树的先序序列和中序序列、中序序列和后序序列就可以确定这棵二叉树。
先序+中序
通过先序我们可以知道根结点,
再因为中序遍历根在左右子树中间而确定左右子树
把左右子树的序列分开构造
左子树的序列就可以确定这个节点的左右子树
一直把原序列分成左右子树的序列,到最后就可以完全确定这棵二叉树
BTree CreateBT1(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++)//在中序中找为* ppos的位置k
if (*p == *pre)
break;
k = p - in;
s->lchild = CreateBT1(pre + 1, in, k);//构造左子树
s->rchild = CreateBT1(pre + k + 1, p + 1, n - k - 1);//右子树
return s;
}
中序+后序
这个和前面的方法一样,都是通过先确定根结点来分开左右子树的序列
BTRee CreateBT2(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++) //在中序中找为 * ppos的位置k
if (*p == *(post + n - 1))
break;
k = p - in;
s->lchild = CreateBT2(post, in, k); //构造左子树
s->rchild = CreateBT2(post + k, p + 1, n - k - 1); // 构造右子树return s;
}
这里构造二叉树都是采用分而再分,直到最后不能再分的的方式来构造二叉树
每次都把左右子树的两种序列传进下一次的函数中
分而再分,直到不能再分
如果只有先序和后序就不能分开左右子树,唯一能确定的只有根结点
如果没有先或后序就不能确定根结点而导致不能分开左右子树
所以要拿先+中或中+后这两种序列才可以确定二叉树
先序遍历建二叉树
- 就像先序遍历那样,
先创建新的结点,赋值
再创建左子树
创建右子树
ABD#G###CEH###F#I##
BinTree CreateBTree(string str,int &i,int h)
{
BinTree bt;
int len;
len = str.size();
bt = new TNode;
if (i >= len)return NULL;
if (str[i] == '#')
return NULL;
bt->Data = str[i];
bt->Left = CreateBTree(str,++i,h+1);
bt->Right = CreateBTree(str,++i,h+1);
return bt;
}
层次遍历法创建二叉树
要用上队列结构和层次遍历很像
队列里都是本次遍历要生成孩子的双亲结点
伪代码
1.1.4线索二叉树
- 二叉链存储结构时,每个结点有两个指针域,总共有2n个指针域
有效指针域:n-1(根结点没指针指向)
空指针域:n+1
用二叉链表的形式存储有些指针就会浪费,
所以我们把那些空的指针来存放序列的前驱和后继,这样就能利用上这些空间。
- 利用这些空链域,指向该线性序列中的“前驱”和“后继”的指针,称作线索。
若结点有左子树,则lchild指向其左孩子;
否则,lchild指向其直接前驱(即线索)
若结点有右子树,则rchild指向其右孩子;
否则,rchild指向其直接后驱(即线索)
为了更方便搞清楚是否有无线索,增加两个标志域
typedef struct node
{
Elemlype data;//结点数据域
int ltag,rtag;//增加的线索标记
struct node* lchild;//左孩子或线索指针
struct node* rchild;//右孩子或线索指针
}TBTNode;//线索树结点类型定义
LTag :若LTag=0,lchild域指向左孩子;
若LTag=0,Lchild域指向左孩子;
RTag:若RTag=1,rchild域指向其后继。
若RTag=1,rchild域指向其后继。
当没有孩子时,指向的是它的序列的前驱或后继
线索用虚线表示
线索要依据遍历来设计不同的序列不同的线索二叉树
**为避免悬空应增设一个头结点
1.1.5二叉树的应用--表达式树
1.2多叉树结构
1.2.1多叉树结构
双亲存储结构
每个节点都有指针指向双亲
找父亲容易,找孩子不容易
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;
每个结点固定只有两个指针域!!
类似二叉树
找父亲不容易
遍历时,
没有兄弟就访问孩子
有兄弟就一直访问兄弟,直到无兄弟执行上一条
树查找祖先节点,双亲存储结构
查找某个节点所有兄弟,孩子链存储结构、孩子兄弟存储结构
1.2.2多叉树遍历
和之前的二叉树遍历很像,分为3种
- 先根遍历
根左子树右子树
后跟遍历
左子树根右子树
层次遍历
自上而下,自左至右访问树中每个结点
先根和后跟都是递归算法
先根:ABEFCDGHIJK
后跟:EFBCIJKHGDA
层次:ABCDEFGHIJK
1.3哈夫曼树
1.3.1哈夫曼树定义
设二叉树具有n个带权值的叶子节点,那么从根节点到各个叶子节点的路径长度与相应节点权值的乘积的和,叫做二叉树的带权路径长度。
具有最小带小带权路径长度的二叉树称为哈夫曼树
树的不同分支带不同的权,树的不同的分布可能让我们遍历整棵树的查找效率很小
而哈夫曼树就是我们计算后,查找效率最大
1.3.2哈夫曼树的结构体
顺序
typedef struct {
char data;//节点值
float weight;//权重
int parent;//双亲节点
int lchild;//左孩子节点
int rchild;// 右孩子节点
}HTNode;
链式
typedef struct tagHFtree
{
char data; /*结点数据,以后用到*/
double weight; /*结点的权重*/
struct tagHFtree* parent; /*双亲结点*/
struct tagHFtree* lchild; /*左孩子*/
struct tagHFtree* rchild; /*右孩子*/
struct tagHFtree* next; /*指向下一个结点*/
}HFtree;
构造哈夫曼树的原则
- 权值越大的叶结点越靠近根结点
权值越小的叶结点越远离根结点
1.3.3哈夫曼树的构造
(1)根据给定的n个权值{w1, wg, ....w,},构造n棵只有根结点的二叉树。F={T,T,...,T}。
(2)在F中选取根结点的权值最小和次小的两棵二叉树作为左、右子树构造一棵新的二叉树,这棵新的二叉树根结点的权值为其左、右子树根结点权值之和。
(3)在集合F中删除作为左、右子树的两棵二叉树,并将新建立的二叉树加入到集合F中。
(4)重复(2)、(3)两步,当F中只剩下一棵二叉树时,这棵二叉树便是所要建立的哈夫曼树。
哈夫曼树的特点
1.没有单分支结点,因为都是两棵树的合并
2.n(总结点)=n0(分支数为0的结点)+n2(分支数为1的结点)=2n0-1
1.3.3哈夫曼编码
哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,哈夫曼编码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,该方法完全依据字符出现概率来构造异字头的平均长度最短的码字,有时称之为最佳编码,一般就叫做Huffman编码(有时也称为霍夫曼编码)。--百度百科
我们在传输信息的时候,信息越短,中间出的差错的可能性就越小。
这时候我们就会用哈夫曼编码让二进制的编码尽量短,但是可以辨认出来
让待传字符串中出现次数较多的字符采用尽可能短的编码则转换的二进制字符串便可以减少。
**关键:要设计长度不等的编码,则必须使任一字符的编码都不是另一个字符的编码的前缀。**
符合这种编码要求的编码方式称为前缀编码。
哈夫曼编码:根结点到叶子结点经过路径组成的0,1序列
A 的编码: 0
D 的编码: 10
B 的编码: 110
C 的编码: 111
得到哈夫曼编码的方法和创建哈夫曼树的方法一样
不过就在左支加上0,右支加上1
从根走到叶子0,1序列就是哈夫曼编码
1.4并查集
什么是并查集
并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。这一类问题近几年来反复出现在信息学的国际国内赛题中。其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高,根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。
并查集是一种树型的数据结构,用于处理一些不相交集合(disjoint sets)的合并及查询问题。常常在使用中以森林来表示。--百度百科
- 集合查找:在一棵高度较低的树中查找根结点的编号,所花的时间较少。同一个集合标志就是根(parent)是一样。
- 集合合并:两棵分离集合树A和B,高度分别为hA和hg,则若h>hs,应将B树作为A树的子树;否则,将A树作为B树的子树。总之,总是高度较小的分离集合树作为子树。得到的新的分离集合树C的高度hC=MAX {hA,h;+1}。
typedef struct node {
int data;
//结点对应人的编号
int rank; //结点秩:子树的高度,合并用
int parent;//结点对应双亲下标
}UFSTree;
根结点父亲是自己
查找一个元素所属的合集
int FIND_SET(UFSTree t[, int x)l// 在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)// ly结点的秩小于x结点的秩
t[y].parent = x;//将y连到x结点上,x作为y的双亲结点
else//结点的秩大于等于x结点的秩
{
t[x].parent = y;//将x连到y结点上,y作为x的双亲结点
if (t[x].rank == t[y].rank)//x和y结点的秩相同
tly].rank++;//y结点的秩增1
}
}
1.5对树的认识及学习体会
- 递归是树的特点,创建树什么的用的是递归,运用起来比较难以控制和神奇。
要设计好出口才能用的了递归。 - 树的数据关系就会比之前的数据关系更复杂,不是一对一,是一对多。就要处理好数据关系。
但这样让数据更为集中,线性的移长串,树就隔着几层就有关系了
2.PTA实验作业
2.1二叉树
2.1.1解题思路及伪代码
二叉树叶子结点带权路径长度和
解题思路
先创建树,
再把叶子的权重*(高度-1)
输出
伪代码
//创建树
BinTree CreateBTree(string str, int i)
{
申请新的node
如果 (i >= 总长)//递归出口
return NULL;
如果 str[i] == '#'return NULL;
node->data = str[i];//赋值给新节点
//递归的创建
node->lef= CreateBTree(str, 2 * i);
node->right= CreateBTree(str, 2 * i + 1);
}
void GetWpl(BinTree bt, int& wpl, int h)
{
if bt == NULL
return NULL;//递归出口
if(是叶子节点)
{
wpl=data *h//权重*高度,在这过程中遇到叶子结点就积累
}
//递归
GetWpl(bt->Left, wpl, h + 1);//h层数+1
GetWpl(bt->Right, wpl, h + 1);
}
2.1.2总结解题所用知识点
- 递归的调用,每次传入层数+1,和wpl做累积
- 递归出口的设置,可以跳出递归
2.2目录树
2.2.1解题思路及伪代码
解题思路
再找合适的地方插入
如果无孩子,直接插入
如果有孩子,遍历看看是否为已有的孩子
是:下一个节点
否:判断插入方法
伪代码
//建树
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.2.2总结解题所用的知识点
- 孩子兄弟链来判断两种情况
链表的插入,头插法等
string的使用,有的自带的函数,很方便
3.阅读代码
3.1题目及解题代码
3.2该题的设计思路及伪代码
设计思路
运用层次遍历,每次遍历完一层的最后一个节点就是最右边的节点
和输出每层节点这题的一样都是层次遍历
伪代码
while(队列不为空)
{
for (int i = 0; i < size; i++)
{
队列全部出队
if(node.left!=NULL)//左孩子不为空进队
push(node.left)
if(node.right != NULL)//右孩子不为空进队
push(node.right)
if(isize-1)//这层最后一个
加到数据组里
}
}
3.3分析该题目解题优势及难点
优点:思路清晰,明了。
难点:借助队列,思路上要小小的拐一个弯