这个作业属于哪个班级 | 数据结构--网络2012 |
---|---|
这个作业的地址 | DS博客作业03--树 |
这个作业的目标 | 学习树结构设计及运算操作 |
姓名 | 李兴果 |
0.PTA得分截图
❤️.学习总结 |
❤️.二叉树结构 |
1.1.1 二叉树的2种存储结构
二叉树:每个结点最多有两个子树的有序树
性质1:二叉树第i层上的结点数目最多为2i-1
性质2:深度为k的二叉树至多有2k-1个结点
性质3:包含n个结点的二叉树的高度至少为(log2n)+1
性质4:在任意一棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则n0=n2+1
(1)树的顺序存储
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储,二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树
可以让第一个位置为空,保证数组下标和结点编号一致
typedef struct
{
ElemType data;
int parent;//结点是否为空
}PTree[MaxSize];
- 优点:找父亲容易,读取指定的节点的时效率较高
- 缺点:找孩子不容易,非完全二叉树时浪费空间
(2)树的链式存储
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址,链式结构又分为二叉链和三叉链
typedef struct BTnode
{
ElemType data;
struct BTnode * lchild, * rchild;//左右孩子指针
struct BTnode *parent;//父亲指针
}BTnode,*BTree;
-
缺点:读取指定节点的时候效率偏低
-
优点:相对二叉树比较大的时候浪费空间较少
-
孩子链存储结构
typedef struct BTnode
{
ElemType data;
struct BTnode *sons[MaxSons];//指向孩子结点
}TSonNode;
缺点:空指针多 不易找父亲
- 孩子兄弟链存储结构
typedef struct BTnode
{
ElemType data;
struct BTnode *son;//指向孩子
struct BTnode *brother;//指向兄弟
}TSonNode;
- 缺点:每个结点固定只有两个指针域,找父亲不容易
1.1.2 二叉树的构造
(1)顺序存储结构转换为二叉链
代码实现:
//如果i从0开始时 注意孩子和父亲的关系
BTree CreareBTree(string str, int i)//递归法建立二叉树
{
BTree t;
t = new TNode;
int len;
len = str.size();
if (i > len - 1 || i <= 0)
return NULL;
if (str[i] == '#')
{
return NULL;
}
else
{
t->data = str[i];//存入
}
t->lchild = CreareBTree(str, 2 * i);//左
t->rchild = CreareBTree(str, 2 * i + 1);//右
return t;
}
(2)先序遍历递归建树
代码实现:
BTree CreareBTree(string str, int i)/
{
BTree t;
t = new TNode;
int len;
len = str.size();
if (i > len - 1 )
return NULL;
if (str[i] == '#')
{
return NULL;
}
else
{
t->data = str[i];//存入
}
t->lchild = CreareBTree(str, ++i);//左
t->rchild = CreareBTree(str, ++i);//右
return t;
}
(3)先序遍历序列和中序遍历序列构造二叉树
伪代码:
BTree CreateBT1(char *pre,char *in,int n)
{
若n<=0,返回空,递归结束
创建根节点BT
BT->data=*pre;
查找根结点在中序序列位置k
创建左子树
BT->lchild=CreateBT1(pre+1,in,k)
创建右子树
BT->rchild=CreateBT1(pre+k,in+k+1,n-k-1)
- 代码实现:
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++)
if(*p==*pre)
break;
k=p-in;
s->lchild= CreateBT(pre+k+1,in,k);
s->rchild=CreateBT(pre+k+1,p+1,n-k-1);
return s;
}
(4)后序遍历序列和中序遍历序列构造二叉树
伪代码:
BTree CreateBT2(char *post,char *in,int n)
{
若n<=0,返回空,递归结束
创建根节点s
s->data=*(post+n-1);
查找根结点在中序序列位置k
创建左子树
s->lchild=CreateBT2(post,in,k)
创建右子树
s->rchild=CreateBT2(post+k,p+1,n-k-1)
}
- 代码实现:
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++)
if(*p==*(post+n-1))
break;
k=p-in;
s->lchild= CreateBT2(post,in,k);
s->rchild=CreateBT2(pre+k,p+1,n-k-1);
return s;
}
1.1.3 二叉树的遍历
总结二叉树的4种遍历方式,如何实现。
- 注:先根与后跟遍历算法都是递归的
(1)先根遍历
若树不为空,则先访问根结点,然后依次先根遍历各课子树
(2)后根遍历
若树不为空,则先依次后根遍历各课子树,然后访问根结点
(3)层次遍历
若树不为空,则自上而下,自左自右访问树中每个结点
1.1.4 线索二叉树
(1)线索二叉树如何设计?
- 二叉链存储结构时,每个结点有两个指针域,总共有2n个指针域
- 有效指针域:n-1(根节点没指针指向)
- 空指针:n+1
1)线索二叉树性质:
增加两个线索标记,ltag和rtag
若ltag=0,lchild域指向左孩子,若ltag=1,lchild域指向其前驱(线索);
若rtag=0,rchild域指向右孩子,若rtag=1,rchild域指向其后继(线索);
- 结构体定义:
typedef struct node
{
Elem Type data;
int ltag,rtag;
struct node *lchild;//左孩子或线索指针
struct node *rchild;//右孩子或线索指针
}TBTNode;
(2)中序线索二叉树特点?如何在中序线索二叉树查找前驱和后继?
-
特点:
中序线索二叉树可以找到对应树每个节点的前驱和后继节点
1.头结点左孩子指向根节点
2.右孩子为线索,指向最后一个孩子
3.遍历序列第一个结点前驱为头结点,最后一个结点后继为头结点 -
找中序遍历的第一个结点
左子树上处于“最左下”(没有左子树)的结点 -
找中序线索化链表中结点的后继
若无右子树,则为后继线索所指结点
否则为其右子树最左那个结点
代码:
1.1.5 二叉树的应用--表达式树
void InitExpTree(BTree &T,string str)
{
遍历字符串str{
如果当前字符串为数字{
T->data=当前数字
入栈Q1
}
如果当前字符串为运算符{
与Q2栈顶比较优先级
优先级低入栈Q2
优先级相同Q2出栈
优先级高 T->data=Q2栈顶元素
}
}
}
double EvaluateExTree(BTree T)
{
double x,y;
x=递归调用左子树的值
y=递归调用右子树的值
根据data的运算符分别运算并返回结果
}
❤️.多叉树结构 |
1.2.1 多叉树结构
- 双亲存储结构
结构体定义:
typedef struct
{
ElemType data; //结点的值
int parent; //指向双亲的位置
}PTree[MaxSize];
-
缺点:找父亲容易,找孩子不容易
-
孩子链存储结构
结构体定义:
typedef struct node
{
ElemType data; //结点的值
struct tnode *sons[MaxSons]; //指向孩子结点
}TSonNode;
-
缺点:空指针太多,找父亲不容易
-
孩子兄弟链存储
结构体定义:
typedef struct tnode
{
ElemType data; //结点的值
struct tnode *son; //指向兄弟
struct tnode *brother; //指向孩子结点
}TSBNode;
每个结点固定只有两个指针域,找父亲不容易
(1)在一棵树中最常用的操作是查找某个结点的祖先结点,采用双亲存储结构最合适
(2)如最常用的操作是查找某个结点的所有兄弟,采用孩子链存储结构或者孩子兄弟链存储结构
1.2.2 多叉树遍历
定义:它是由n(n>=0)个有限结点组成一个具有层次关系的集合
-
先根遍历(递归)(根左右)
若树不空,则先访问根结点,然后依次先根遍历各棵子树
-
后根遍历(递归)(左右根)
若树不空,则先依次后根遍历各棵子树,然后访问根结点 -
层次遍历
若树不空,则自上而下、自左至右访问树中每个结点。
❤️.哈夫曼树 |
1.3.1 哈夫曼树定义
什么是哈夫曼树?哈夫曼树解决什么问题?
给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树
也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
-
构造原则:
-
权值越大的叶结点越靠近根结点;
-
权值越小的叶结点越远离根结点;
-
WPL:带权路径和
1.3.2 哈夫曼树的结构体
- 顺序结构
typedef struct
{ char data; //节点值
float weight; //权重
int parent; //双亲节点
int lchild; //左孩子节点
int rchild; //右孩子节点
} HTNode;
- 初始化哈夫曼树
typedef struct
{
int data;
int parent;
int lchild;
int rchild;
}HTNode,*HuffmanTree;
void CreateHTree(HuffmanTree &ht, int n)
{
int len;
len = 2 * n - 1;
ht = new HTNode[len];
}
1.3.2 哈夫曼树构建及哈夫曼编码
-
哈夫曼构造
(1)
例如:
频率表 A:60, B:45, C:13 D:69 E:14 F:5 G:3
第一步:找出字符中最小的两个,小的在左边,大的在右边,组成二叉树。在频率表中删除此次找到的两个数,并加入此次最小两个数的频率和。由频率表可知G,F最小,先删除FG,并返回两者的和8给频率表
重复以上动作....
每个 字符 的 二进制编码 为(从根节点 数到对应的叶子节点,路径上的值拼接起来就是叶子节点字母的应该的编码)
编码 |
---|
字符 |
A |
B |
C |
D |
E |
F |
G |
❤️.并查集 |
1.4什么是并查集?
并查集,在一些有N个元素的集合)应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。
合并查找的集合,用于多个可以不相关的树的合并与查找。
对于大量数据根据某些特征进行合并,查找,不仅空间需要很大,时间效率也比较低
1.4.1并查集解决问题
1)初始化:每个点看做一棵树 ,并且为每个树的树根;树根就是每个组别的代表。
2)查询:对于点对(a,b),通过a和b去向上查找他们的祖先节点直到树根,如果有相同的祖先节点,则他们在已经在一棵树下,属于同一组别。
3)合并:若不在同一组别,令其中一个点(比如a)所在树的根节点成为另一个点(比如b)的根节点的孩子。这样即便再查询到a,最终会判断认为a属于b的组别。
大树小树合并技巧: 小树变成大树的子树,会比大树变成小树的子树更加不易增加树高,这样可以减少查询次数。
- 并查集的结构体
typedef struct node
{ int data; //结点对应人的编号
int rank; //结点秩:子树的高度,合并用
int parent; //结点对应双亲下标
} UFSTree; //并查集树的结点类型
- 初始化
int fa[MAXN];
inline void init(int n)
{
for (int i = 1; i <= n; ++i)
fa[i] = i;
}
假如有编号为1, 2, 3, ..., n的n个元素,我们用一个数组fa[]来存储每个元素的父节点(因为每个元素有且只有一个父节点,所以这是可行的)。一开始,我们先将它们的父节点设为自己。
- 查询
int find(int x)
{
if(fa[x] == x)
return x;
else
return find(fa[x]);
}
用递归的写法实现对代表元素的查询:一层一层访问父节点,直至根节点(根节点的标志就是父节点是本身)。要判断两个元素是否属于同一个集合,只需要看它们的根节点是否相同即可。
- 合并
inline void merge(int i, int j)
{
fa[find(i)] = find(j);
}
❤️.谈谈你对树的认识及学习体会 |
3.1学习体会
-
1.遍历二叉树 是指以一定的次序访问二叉树中的每个结点。所谓 访问结点 是指对结点进行各种操作的简称。例如,查询结点数据域的内容,或输出它的值,或找出结点位置,或是执行对结点的其他操作。
-
2.对于一个二叉树的存储,首先想到的是能不能用一个数组顺序表存储,用数组下标映射每个节点的存储位置。事实是是可行的,但是存在着诸多问题。
对于完全二叉树是完全可行的,但是对于一般二叉树,就会出现空间浪费的问题。
树是一种重要的非线性数据结构,其定义是递归的,所以递归的调用是树中是很重要的 -
困难:操作多,对于伪代码到代码的转换不熟练,递归函数的运用不熟练只会生搬硬套。树的非递归算法掌握不熟练
❤️.PTA实验作业(4分) |
❤️.二叉树 |
2.1输出二叉树每层节点
2.1.1 解题思路及伪代码
- 解题思路:
先先序遍历二叉树
层次遍历,输出每层结点
是否为第一层结点,引用node和lastnode分别用于存放遍历中途结点的孩子结点并判断是否找到这一层的最后一个结点
运用两个指针记住当前结点位置和每层的最后一个结点位置,以便于当当前结点等于层最后结点时,输出换行并输出下一层结点
而每层的结点都要通过队列依次存入并且依次输出。
伪代码:
PrintfBTree(BTree bt)
{ 创建两个结点curnode,lastnode;//第一个结点,末尾结点
置行数level = 1;
if (树为空)
输出空,并返回;
q.push(bt)//入根
lastnode = bt
控制格式
当树不为空
{
curnode = q.front();//队头赋
if存在左孩子,入队
if存在右孩子,入队
if (curnode == lastnode)//一层遍历结束,修改
//赋队尾
控制队的长度至少大于1
出队
}
- 代码实现:
2.1.2 总结解题所用的知识点
- 运用queue库函数,运用队列存放元素
- 先序遍历和层次遍历
- 运用两指针存放当前结点位置和每层的最后一个结点位置,以便于当当前结点等于层最后结点时,输出换行并输出下一层结点;而每层的结点都要通过队列依次存入并且依次输出
2.2 目录树
2.2.1 解题思路及伪代码
- 分析
此题目文件树需要用左孩子右兄弟的二叉链表存储 - 建树
注意输出的顺序,即同层目录排在文件前,同类按字典顺序输出
*插入作为孩子
没有第一个孩子,直接生成第一个孩子结点
有孩子,则需要判断是否需要更改孩子
结点存在,即数据相等且目录文件相等,则不需要新建孩子结点,当前指针更改为孩子位置
若孩子为文件内,新结点为目录则更改孩子为新结点
若孩子文件属性为同新结点,但是值比新结点大,则需更改当前指针
-
插入作为兄弟
没有兄弟,则插入新结点为兄弟
有兄弟,找新节点插入位置
若新结点属性和兄弟属性相等且值大于兄弟,则继续遍历下个兄弟
若新节点为文件,兄弟为目录,则继续遍历下个兄弟
遍历中发现兄弟是文件,新结点是目录,退出循环
遍历结束,兄弟结点值和新结点值先弄个等,则不需要插入新结点,更改当前指针为兄弟
插入新结点,更改新结点兄弟关系,并把插入位置的前一个结点的兄弟改成新结点 -
伪代码
void CreatTree(Tree& bt, string str, int i)//建树,
{
设置字符
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 为空
if (兄弟节点为文件 || 兄弟节点字典序大于该节点)
找到可插入位置,break;
else if (二者相等)
直接使temp指向btr->brother;break;
else
遍历下一兄弟结点;
end if
end while
if (btr->brother为空 || btr->brother->name != temp->name)
进行插入temp
end if
}
void InitFile(Tree& temp, Tree& bt)//对文件temp找一个可插入位置
{
结点btr先指向bt的孩子;
if (第一个孩子为空 || btr为文件 && 结点字典序大于等于该节点)
进行插入,修改bt的孩子指针;
else //判断兄弟结点
if (btr->brother为文件 并且 兄弟节点字典序大于该节点)
找到可插入位置,break;
else
遍历下一个兄弟结点
end if
end while
temp进行插入
end if
}
2.2.2 总结解题所用的知识点
❤️.阅读代码(0--1分) |
3.1 题目及解题代码
给定一个二叉树,返回其节点值的锯齿形层次遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<List<Integer>>() ;
if(root == null) {
return res;
}
LinkedList<TreeNode> queue = new LinkedList<TreeNode>();
queue.push(root);
//true:从右往左, false:从左往右
boolean flag = false;
while(!queue.isEmpty()) {
int levelSize = queue.size();
List<Integer> list = new ArrayList<Integer>(levelSize);
while(levelSize-- > 0) {
TreeNode curNode = queue.poll();
list.add(curNode.val);
if(curNode.left != null) {
queue.offer(curNode.left);
}
if(curNode.right != null) {
queue.offer(curNode.right);
}
}
if(flag && list.size()>1) {
//需要翻转数组
Collections.reverse(list);
}
flag = !flag;
res.add(list);
}
return res;
}
3.2 该题的设计思路及伪代码
- 使用二叉树的层次遍历
3.3 分析该题目解题优势及难点
- 优势:层次遍历类似相关
- 难点:一层转换为另一层易出错