| 这个作业属于哪个班级 | 数据结构--网络2011/2012 |
| ---- | ---- | ---- |
| 这个作业的地址 | DS博客作业03--树 |
| 这个作业的目标 | 学习树结构设计及运算操作 |
| 姓名 | 杨振鹏 |
0.PTA得分截图
1.本周学习总结
1.1 二叉树结构
- 1.1.1二叉树的2种存储结构
1.顺序结构:
用一组连续的存储单元存放二叉树中的结点,就是数组形式。从树根起,自上层至下层,每层自左至右地给所有结点编号
优点:可以根据编号,快速找到祖先及孩子,也可以根据数组大小判断高度
缺点:对于普通的二叉树而言,没有数据的结点也会留有空间,这样就会造成空间浪费
2.链式结构:
采用链表形式对数据进行存储,用链来表示相互关系
优点:可直接找到左右孩子,只储存有数据的结点,节省空间
缺点:不太容易找到祖先,不过可以再增加一个parent指针指向祖先 - 1.1.2二叉树的构造
1.顺序存储结构转二叉链
void CreateBTree(BTree &bt,string str, int i)
{
int len;
len = str.size();
bt = new TNode;
if (i > len || i < 1)
{
bt=NULL;return;
}
if (str[i] == '#')
{
bt=NULL;
return;
}
bt->data = str[i];
CreateBTree2(bt->lchild,str, 2 * i);
CreateBTree2(bt->rchild,str, 2 * i + 1);
}
2.先序遍历建二叉树
BTree CreatTree(string str, int &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;
}
3.先中
BTree CreatTree(int n, char* pre, char* mid)
{
if (n <= 0)return NULL;
BTree T;
char* p;
T = new BTNode;
T->data = *pre;//先序的第一个一定是根节点
T->lchild = NULL;
T->rchild = NULL;
for (p = mid; p < mid+n; p++)//中序找根节点,将左右子树分开
if (*p==*pre)break;
int i = p - mid;
T->lchild = CreatTree(i, pre + 1, mid);
T->rchild = CreatTree(n - 1 - i, pre + i + 1, mid + 1 + i);
return T;
}
4.后中
BTree CreatTree(int n, int* last, int* mid)
{
if (n <= 0)return NULL;
BTree T;
T = new BTNode;
T->data = last[n - 1];//后序的最后一个一定是根节点
T->lchild = NULL;
T->rchild = NULL;
int i;
for (i = 0; i < n; i++)//中序找根节点,将左右子树分开
if (mid[i] == last[n - 1])break;
T->lchild = CreatTree(i, last, mid);
T->rchild = CreatTree(n - 1 - i, last + i, mid + 1 + i);
return T;
}
- 1.1.3二叉树的遍历
先序遍历:
void PreOrder(BTree bt)
{
if (bt != NULL) {
cout << " " << bt->data; //根
PreOrder(bt->lchild);//左
PreOrder(bt->rchild);//右
}
}
中序遍历:
void MidOrder(BTree bt)
{
if (bt != NULL){
MidOrder(bt->lchild);//左
cout << bt->data<<" ";//根
MidOrder(bt->rchild);//右
}
}
后序遍历:
void PostOrder(BTree bt)
{
if (bt != NULL) {
PostOrder(bt->lchild);//左
PostOrder(bt->rchild);//右
cout << " " << bt->data;//根
}
}
层次遍历:
void Level(BTree BT)
{
BTree b;
q.push(BT);
while (!q.empty()) {
b=q.front();
q.pop();
cout << " " << b->Data;
if (b->lchild)q.push(b->lchild);
if (b->rchild)q.push(b->rchild);
}
}
- 1.1.4线索二叉树
在二叉树的结点上加上线索的二叉树称为线索二叉树。每个节点有两个指针域,n个结点总共有2n个指针域,非空链域为n-1个,空链域有n+1个
结构体:
typedef struct node
{ ElemType data; //结点数据域
int ltag,rtag; //增加的线索标记
struct node *lchild; //左孩子或线索指针
struct node *rchild; //右孩子或线索指针
} TBTNode;
中序线索二叉树特点:
遍历二叉树时不需要递归,所有节点只需遍历一次
既没有递归也没有用栈,空间效率得到提高,时间复杂度O(n)
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二叉树的应用--表达式树
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.2 多叉树结构
- 1.2.1 多叉树结构
双亲存储结构:
typedef struct
{
ElemType data;//结点的值
int parent;//指向双亲的位置
}PTree[MaxSize];
孩子链存储结构:
typedef struct node
{
ElemType data;//结点的值
struct node* sons[MaxSons];//指向孩子结点
}TSonNode;
孩子兄弟链存储结构:
孩子兄弟链存储结构是为每个结点设计3个域,即一个数据域,一个指向该节点的左边的第一个孩子结点(长子)的指针域,一个指向该节点的下一个兄弟结点的指针域。
typedef struct tnode
{
ElemType data;//结点的值
struct tnode* son;//指向孩子结点
struct tnode* brother;//指向兄弟结点
}TSBNode;
特点:
-
每个结点固定两个指针域,且两指针有序,不能混淆
-
方便实现树与二叉树的相互转换
-
寻找双亲结点比较麻烦,需要从树的根结点开始逐个结点比较查找
-
1.2.2 多叉树遍历
先序遍历:
访问根节点,按照从左到右的顺序先根遍历根节点的每一棵子树
先根遍历序列的第一个元素即为根节点对应的结点值
结果:ABDIKEJFCGLMOHNP
1.3 哈夫曼树
-
1.3.1 哈夫曼树定义
在许多应用中经常将树中的结点赋予一个有某种意义的数值,称此数值为该结点的权。
从根节点到该节点之间的路径长度与该节点上权的乘积称为结点的带权路径长度。
树中所有叶子节点的带权路径长度之和称为该树的带权路径长度,通常记为:WPL=∑WiLi
n0:叶子节点的个数
wi:第i个叶子结点的权值
li:根到第i个叶子节点的路径长度
- 1.3.2 哈夫曼树的结构体
typedef struct
{
char data;//结点值
int weight;//权重
int parent;//双亲结点
int lchild;//左孩子结点
int rchild;//右孩子结点
}HTNode, * HTree;
- 1.3.3 哈夫曼树构建及哈夫曼编码
例{1,2,3,4}求WPL:
WPL=41+32+23+13=19
1.4 并查集
定义:并查集支持查找一个元素所属的集合以及两个元素各自专属的集合的合并等运算。在这种数据类型中,n个不同的元素被分为若干组。每组是一个集合,这种集合叫分离集合,称之为并查集。
结构体定义:
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输出二叉树每层节点
思路:建立树,求树高,高决定循环次数,遍历树,在对应层数的循环中输出对应的结点
伪代码:
CreateBTree建树
GetHeight求树高
for 1 to 树高
PreOrder输出每层结点
end for
知识点:
先序遍历递归建树
利用队列进行层次遍历
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
}
知识点:
结点插入树,分为孩子和兄弟,采用孩子兄弟链
后序遍历目录树,对每个结点的子目录和子文件进行排序
3.阅读代码
思路:
首先,考虑实现一个简化的函数 maxGain(node),该函数计算二叉树中的一个节点的最大贡献值,具体而言,就是在以该节点为根节点的子树中寻找以该节点为起点的一条路径,使得该路径上的节点值之和最大。
具体而言,该函数的计算如下。
空节点的最大贡献值等于 00。
非空节点的最大贡献值等于节点值与其子节点中的最大贡献值之和(对于叶节点而言,最大贡献值等于节点值)。
叶节点 99、1515、77 的最大贡献值分别为 99、1515、77。
得到叶节点的最大贡献值之后,再计算非叶节点的最大贡献值。节点 2020 的最大贡献值等于 20+\max(15,7)=3520+max(15,7)=35,节点 -10−10 的最大贡献值等于 -10+\max(9,35)=25−10+max(9,35)=25。
上述计算过程是递归的过程,因此,对根节点调用函数 maxGain,即可得到每个节点的最大贡献值。
根据函数 maxGain 得到每个节点的最大贡献值之后,如何得到二叉树的最大路径和?对于二叉树中的一个节点,该节点的最大路径和取决于该节点的值与该节点的左右子节点的最大贡献值,如果子节点的最大贡献值为正,则计入该节点的最大路径和,否则不计入该节点的最大路径和。维护一个全局变量 maxSum 存储最大路径和,在递归过程中更新 maxSum 的值,最后得到的 maxSum 的值即为二叉树中的最大路径和。
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;
}
};
复杂度分析
时间复杂度:O(N)O(N),其中 NN 是二叉树中的节点个数。对每个节点访问不超过 22 次。
空间复杂度:O(N)O(N),其中 NN 是二叉树中的节点个数。空间复杂度主要取决于递归调用层数,最大层数等于二叉树的高度,最坏情况下,二叉树的高度等于二叉树中的节点个数。
- 这题目的难点在于理解题意和转化题意