树
这个作业属于哪个班级 | 数据结构--网络2012 |
---|---|
这个作业的地址 | DS博客作业03--树 |
这个作业的目标 | 学习树结构设计及运算操作 |
姓名 | 陈垚嘉 |
1.本周学习总结(5分)
1.1 二叉树结构
1.1.1 二叉树的2种存储结构
顺序存储结构
顺序存储结构其实就是在一个连续存储单元里,从根结点开始,像对完全二叉树编号的顺序一样,把我们的二叉树的内容存储在一个一维数组中,一般顺序存储结构是拿来存储完全二叉树的,但是也可以拿来存储一般的二叉树,只是要按照完全二叉树的规则来编号,如果没有的就存0,如下图所示:
#define Maxsize 100
typedef char Datatype;
typedef struct
{ Datatype bt[Maxsize];
int btnum;
}Btseq;
链式存储结构
由二叉树的定义可知道,我们链式存储结构至少包含三个部分:数据域和两个指针域,一个指向左孩子,另一个指向右孩子,我们称这种链表为二叉链表,另外一种就是我们还可以添加一个指针域,指向其父亲结点,我们称为三叉链表,如下图所示:
#define datatype char
typedef struct node
{ Datatype data;
struct node *lchild,*rchild;
}Bitree;
树的顺序存储和链式存储结构优缺点。
1.顺序存储时,相邻数据元素的存放地址也相邻(逻辑与物理统一);要求内存中可用存储单元的地址必须是连续的。
优点:存储空间利用率高。缺点:插入或删除元素时不方便。
2.链式存储时,相邻数据元素可随意存放,但所占存储空间分两部分,一部分存放结点值,另一部分存放表示结点间关系的指针
优点:插入或删除元素时很方便,使用灵活。缺点:存储空间利用率低。
1.1.2 二叉树的构造
(1)构造二叉树,就是根据两个遍历序列(数组)推算出二叉树的结构。
这两个遍历序列必须有一个是中序遍历序列,另一个可以是前序/后序/层次遍历序列。
(2)
1.通过前/后序遍历得到根节点,在中序遍历中找到根节点的位置 i
2.通过i 划分中序遍历中的左右子树,划分 前/后序遍历 中的左右子树
3.递归生成左子树和右子树
1.1.3 二叉树的遍历
先序遍历
前序遍历:若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子书。
(2). 中序遍历
中序遍历:若二叉树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后访问根结点,最后中序遍历右子树。
(3). 后序遍历
后序遍历:若二叉树为空,则空操作返回,否则从左到右先叶子结点后结点的方式遍历访问左右子树,最后访问根结点。
(4). 层序遍历
层序遍历:若二叉树为空,则空返回,否则从树的第一层,即根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。
1.1.4 线索二叉树
(1)线索二叉树概述
在二叉树的节点上加上线索的二叉树称为线索二叉树。对二叉树以某种遍历方式(如先序、中序、后序或层次等)进行遍历,使其变为线索二叉树的过程称为对二叉树进行线索化。
通过考察各种二叉链表,不管二叉树的形态如何,空链域的个数总是多过非空链域的个数。准确的说,n 个节点的二叉链表共有 2n 个链域,其中非空链域为 n-1 个,但空链域却有 n+1 个。如下图所示,二叉树有 6 个节点,其中非空链域的个数为 7 个。
利用这些空链域存放在某种遍历次序下该结点的前驱结点和后继结点的指针,这些指针称为线索,加上线索的二叉树称为线索二叉树。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。
上图的二叉树的中序遍历结果是 {8, 3, 10, 1, 14, 6},但是 8, 10, 14, 6 这四个节点的左右指针并没有完全利用上。其实我们可以利用它们的左右指针,让各个节点可以指向自己的前后节点。如下图所示就是原二叉树线索化后生成的中序线索二叉树。
中序线索二叉树
线索化:中序线索二叉树,就按照中序遍历,遍历的过程中在当前节点的左或右空指针域中添加前驱或后继节点。为了保留遍历过程中访问节点的前驱与后继关系,需要设置一个前一节点变量priorNode始终指向刚访问过的节点。
中序线索二叉树的遍历
分两步,1获取中序遍历的首节点。2获取中序线索化二叉树中当前节点的后继节点。只要完成这两步就可以遍历中序线索化二叉树。
获取中序遍历的首节点:从根节点开始沿着左指针不断向左下搜索,直到找到最左的节点。最左的节点指该节点左指针为空。
获取中序线索化二叉树中当前节点的后继节点:有两种情况
1.当前节点存在右子树:则从该右子树根节点开始不断向左下搜索,找到找到最左的节点。该节点即当前节点的后继。
2.当前节点不存在右子树:则其后继节点即为其右指针域中后继线索所指节点。
下面是步骤图
1.2 多叉树结构
1.2.1 多叉树结构
多差树结构体
typedef struct node
{
char data; // 节点数据
struct node *fir, *bro; // 指向第一个子节点和下一个兄弟节点的指针
}TB;
我们把上述儿子兄弟的节点表达方式成为多叉树的二叉链表达。比如我们的多叉树长这个样子:
则它的二叉链表达为:
每个节点的依旧只有左右节点,但是它们的实际含义已经和二叉树节点的左右节点不一样了。比如当前我们考察的节点为node,则二叉链中node的左节点(node.fir)代表node的第一个子节点,可以发现图中E的左节点为F,说明F是E的第一个子节点;C的左节点为NULL,说明C没有子节点。
1.2.2 多叉树遍历
先序遍历
多叉树的前序遍历写法和二叉树的一模一样
1.3 哈夫曼树
1.3.1 哈夫曼树定义
哈夫曼又称最优二叉树。是一种带权路径长度最短的二叉树。它的定义如下。
假设有n个权值{w1,w2,w3,w4...,wn},构造一棵有n个节点的二叉树,若树的带权路径最小,则这颗树称作哈夫曼树。
1.哈夫曼树主要用在数据的压缩如JPEG格式图片,在通信中我们可以先对发送的数据进行哈夫曼编码压缩数据提高传输速度。
2.查询优化:在工作中我们我们身边放许多工具,由于空间限制我们不能把所有工具放在我们最容易拿到的地方,所有我们把使用频率最高的工具放在最容易的位置。同样的道理在查询的时候我们把查询频率最高的数据建立索引,这些都是使用了哈夫曼算法的思想。
什么是哈夫曼树?,哈夫曼树解决什么问题?
1.3.2 哈夫曼树的结构体
typedef struct HTNode
{
int weight;
int parent,lch,rch;
}HTNode,*HafumanTree;
1.3.2 哈夫曼树构建及哈夫曼编码
(1)构造哈夫曼树
假设有n个权值,则构造出的哈夫曼树有n个叶子结点。 n个权值分别设为 w1、w2、…、wn,则哈夫曼树的构造规则为:
(1) 将w1、w2、…,wn看成是有n 棵树的森林(每棵树仅有一个结点);
(2) 在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;
(3)从森林中删除选取的两棵树,并将新树加入森林;
(4)重复(2)、(3)步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树。
如:对 下图中的六个带权叶子结点来构造一棵哈夫曼树,步骤如下:
(2)哈夫曼编码
如上文所示的哈夫曼编码如下:
a 的编码为:00
b 的编码为:01
c 的编码为:100
d 的编码为:1010
e 的编码为:1011
f 的编码为:11
1.4 并查集
(1)并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题(即所谓的并、查)。比如说,我们可以用并查集来判断一个森林中有几棵树、某个节点是否属于某棵树,朋友圈是否重复等。
主要构成:
并查集主要由一个整型数组pre[ ]和两个函数find( )、join( )构成。
数组 pre[ ] 记录了每个点的前驱节点是谁,函数 find(x) 用于查找指定节点 x 属于哪个集合,函数 join(x,y) 用于合并两个节点 x 和 y 。
(2)并查集的基本操作
1.结构体
#define MAX 10000
struct Node
{
int parent; // 集合index的类别
int data; // 集合index的数据类型
int rank; // 集合index的层次,通常初始化为0
}node[MAX];
2.查找
int get_Parent(int x)
{
if(node[x].parent==x)
return x;
return get_Parent(node[x].parent);
}
3.合并集合
void Union(int x,int y)
{
x=get_Parent(x);
y=get_Parent(y);
if(node[x].rank>node[y].rank)
node[y].parent=x;
else
node[x].parent=y;
if(node[x].rank==t[y].rank)
node[y].rank++;
}
4.初始化
void init(int i){
node[i].parent=i; // 初始化的时候,一个集合的parent都是这个集合自己的标号。
// 没有跟它同类的集合,那么这个集合的源头只能是自己了。
node[i].rank=0;
}
int parent[max];
int rank[max];
int data[max];
void init(int i)
{
set[i]=i;
rank[i]=0;
}
1.5.谈谈你对树的认识及学习体会。
二叉树还是比链表的难一些,在这一章,在写代码时候,一定用理解局部变量与全局变量的使用,要不然很容易不过。做题的时候感觉思路很简单,但是写着写着就会发现某个地方有问题,然后慢慢的通过思考上网找资料等等,慢慢的考虑的也更加全面了。关键在于想与不想,做与不做,只要肯做,一定会有收获的
2.PTA实验作业(4分)
此处请放置下面2题代码所在码云地址(markdown插入代码所在的链接)。如何上传VS代码到码云
2.1 二叉树
二叉树叶子结点带权路径长度和
2.1.1 解题思路及伪代码
思路:算法思想:对于求带权路径长度我们只需要找到叶子结点以及叶子结点所在的层数即可
void GetWpl(BTree BT,int &wpl,int h)
{
定义h表示深度
先判断是否是叶子节点
if(BT->Left==NULL&&BT->Right==NULL)
{
wpl=深度*当前data的值
深度归零
}
h++
若是空节点,return
非空非叶
递归访问其左右子树
GetWpl(BT->Left,wpl,h);
GetWpl(BT->Right,wpl,h);
}
}
2.1.2 总结解题所用的知识点
感觉没什么,就是创树然后找到叶子节点和他们的高度
2.2 目录树
2.2.1 解题思路及伪代码
解题思路:先设计目录树结构体,进行初始化树;建立孩子兄弟链的结构,读入字符串并分离,判断是文件还是目录,分别进入各自的函数,文件函数通过对比优先级的高低,若高则改变指针的位置原来位置变为其孩子,低则作为孩子插入,相同则在其当前位置基础上进行操作,目录函数则是对兄弟链进行操作。
伪代码:
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 总结解题所用的知识点
1)使用到孩子兄弟链结构
(2)结构体中增加isfile判断一个结点是目录还是文件
(3)需要考虑是否有第一个孩子,孩子是目录还是文件,二者字典序大小等情况
(4)插入的顺序要求:目录在文件前面,字典序小的在前面
3.阅读代码(0--1分)
3.1 题目及解题代码
1.题目
路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
路径和 是路径中各节点值的总和。
给你一个二叉树的根节点 root ,返回其 最大路径和 。
class Solution {
private:
int maxSum = INT_MIN;
public:
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;
}
};
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/binary-tree-maximum-path-sum/solution/er-cha-shu-zhong-de-zui-da-lu-jing-he-by-leetcode-/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
设计思路:
首先,考虑实现一个简化的函数 maxGain(node),该函数计算二叉树中的一个节点的最大贡献值,具体而言,就是在以该节点为根节点的子树中寻找以该节点为起点的一条路径,使得该路径上的节点值之和最大。
具体而言,该函数的计算如下。
1.空节点的最大贡献值等于 0。
2.非空节点的最大贡献值等于节点值与其子节点中的最大贡献值之和(对于叶节点而言,最大贡献值等于节点值)。
根据函数 maxGain 得到每个节点的最大贡献值之后,如何得到二叉树的最大路径和?对于二叉树中的一个节点,该节点的最大路径和取决于该节点的值与该节点的左右子节点的最大贡献值,如果子节点的最大贡献值为正,则计入该节点的最大路径和,否则不计入该节点的最大路径和。维护一个全局变量 maxSum 存储最大路径和,在递归过程中更新 maxSum 的值,最后得到的 maxSum 的值即为二叉树中的最大路径和。
伪代码
定义maxSum=INT_MIN
int maxGain(TreeNode*node)
{
if node==NULL
then
return 0
// 递归计算左右子节点的最大贡献值
// 只有在最大贡献值大于 0 时,才会选取对应子节点
定义leftGain=max(maxGain(node->left),0)
定义rightGain=max(maxGain(node->right),0)
// 节点的最大路径和取决于该节点的值与该节点的左右子节点的最大贡献值
定义priceNewpath=node->val+leftGain+rightGain
maxSum=max(maxSum,priceNewpath)
返回node->val+max(leftGain,rightGain)
}
int maxPathSum(TreeNode*root)
{
maxGain(root)
返回maxSum
}
3.2复杂度分析
时间复杂度:O(N)O(N),其中 NN 是二叉树中的节点个数。对每个节点访问不超过 22 次。
空间复杂度:O(N)O(N),其中 NN 是二叉树中的节点个数。空间复杂度主要取决于递归调用层数,最大层数等于二叉树的高度,最坏情况下,二叉树的高度等于二叉树中的节点个数。
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
3.3 分析该题目解题优势及难点。
(1)这题目的难点在于理解题意和转化题意
(2)对于二叉树中的一个节点,该节点的最大路径和取决于该节点的值与该节点的左右子节点的最大贡献值,于是可以通过维护一个全局变量 maxSum 存储最大路径和
(3)对每个结点访问次数不超过两次,提升效率
(4)要时刻记得递归函数的功能,该题中递归函数maxGain的功能是获取以当前节点开始的最大路径和,处理递归只需要处理好递归出口和return返回值即可,然后以该递归函数的功能直接调用它。而求该题所获取的答案在递归函数中更新是因为恰巧递归函数中遍历到当前节点时可以获取左右子树的最大路径和,因此可以在此处计算下获得题目要求的答案,这也是为什么更新的是一个全局变量。