DS博客作业03--树
| 这个作业属于哪个班级 | 数据结构--网络2011/2012 |
| ---- | ---- | ---- |
| 这个作业的地址 | DS博客作业03--树 |
| 这个作业的目标 | 学习树结构设计及运算操作 |
| 姓名 |廖浩轩|
0.PTA得分截图
1.本周学习总结
1.1 二叉树结构
1.1.1 二叉树的2种存储结构
顺序存储结构:
把一个满二叉树自上而下、从左到右顺序编号,依次存放在数组内,可得到图6.8(a)所示的结果。设满二叉树结点在数组中的索引号为i,那么有如下性质。
(1)如果i = 0,此结点为根结点,无双亲。
(2)如果i > 0,则其双亲结点为(i -1) / 2 。(注意,这里的除法是整除,结果中的小数部分会被舍弃。)
(3)结点i的左孩子为2i + 1,右孩子为2i + 2。
(4)如果i > 0,当i为奇数时,它是双亲结点的左孩子,它的兄弟为i + 1;当i为偶数时,它是双新结点的右孩子,它的兄弟结点为i – 1。
(5)深度为k的满二叉树需要长度为2 k-1的数组进行存储。
通过以上性质可知,使用数组存放满二叉树的各结点非常方便,可以根据一个结点的索引号很容易地推算出它的双亲、孩子、兄弟等结点的编号,从而对这些结点进行访问,这是一种存储二叉满二叉树或完全二叉树的最简单、最省空间的做法。
为了用结点在数组中的位置反映出结点之间的逻辑关系,存储一般二叉树时,只需要将数组中空结点所对应的位置设为空即可,其效果如图6.8(b)所示。这会造成一定的空间浪费,但如果空结点的数量不是很多,这些浪费可以忽略。
一个深度为k的二叉树需要2 k-1个存储空间,当k值很大并且二叉树的空结点很多时,最坏的情况是每层只有一个结点,再使用顺序存储结构来存储显然会造成极大地浪费,这时就应该使用链式存储结构来存储二叉树中的数据。
#define Maxsize 100
typedef struct TNode {
char tree[Maxsize]; //数组存放二叉树中的节点
int parent; //表示双亲结点的下标
}TNode, * BTree;
链式存储结构:
二叉树的链式存储结构可分为二叉链表和三叉链表。二叉链表中,每个结点除了存储本身的数据外,还应该设置两个指针域left和right,它们分别指向左孩子和右孩子(如图6.9(a)所示)。
当需要在二叉树中经常寻找某结点的双亲,每个结点还可以加一个指向双亲的指针域parent,如图6.9(b)所示,这就是三叉链表。
二叉树还有一种叫双亲链表的存储结构,它只存储结点的双亲信息而不存储孩子信息,由于二叉树是一种有序树,一个结点的两个孩子有左右之分,因此结点中除了存放双新信息外,还必须指明这个结点是左孩子还是右孩子。由于结点不存放孩子信息,无法通过头指针出发遍历所有结点,因此需要借助数组来存放结点信息。图6.10(a)所示的二叉树使用双亲链表进行存储将得到图6.11所示的结果。由于根节点没有双新,所以它的parent指针的值设为-1。
双亲链表中元素存放的顺序是根据结点的添加顺序来决定的,也就是说把各个元素的存放位置进行调换不会影响结点的逻辑结构。由图6.11可知,双亲链表在物理上是一种顺序存储结构。
二叉树存在多种存储结构,选用何种方法进行存储主要依赖于对二叉树进行什么操作来确定。而二叉链表是二叉树最常用的存储结构,下面几节给出的有关二叉树的算法大多基于二叉链表存储结构。
typedef struct TNode { //二叉树结点由数据域,左右指针组成
char data;
struct TNode* lchild;
struct TNode* rchild;
}TNode, * BTree;
1.1.2 二叉树的构造
二叉树构造:顺序存储结构转二叉链,先序遍历,先序+中序序列,中序+后序序列
先序+中序序列:前序遍历第一位数字一定是这个二叉树的根结点。中序遍历中,根结点讲序列分为了左右两个区间。左边的区间是左子树的结点集合,右边的区间是右子树的结点集合。
void CreateTree(BiTree& T, char* pre, char* infix, int n)
{
int i;
char* pi;
if (n <= 0)
{
T = NULL;
return;
}
T = new BiTNode;
T->data = *pre;
for (pi = infix; pi < infix + n; pi++)
{
if (*pi == T->data)
{
break;
}
}
i = pi - infix;
CreateTree(T->lchild, pre+1, infix, i);
CreateTree(T->rchild, pre+i+1, pi + 1, n - i - 1);
}
中序+后序序列:找到根结点(后序遍历的最后一位)在中序遍历中,找到根结点的位置,划分左右子树,递归构建二叉树。思路和先序+中序序列遍历一样。
void CreateTree(BiTree& T, int* post, int* infix, int n)
{
int i;
int* pi;
if (n <= 0)
{
T = NULL;
return;
}
T = new BiTNode;
T->data = *(post + n - 1);
for (pi = infix; pi < infix + n; pi++)
{
if (*pi == *(post + n - 1))
{
break;
}
}
i = pi - infix;
CreateTree(T->lchild, post, infix, i);
CreateTree(T->rchild, post + i, pi + 1, n - i - 1);
}
1.1.3 二叉树的遍历
二叉树的遍历:前序,中序,后序,层次
前序:二叉树先序遍历的实现思想是:访问根节点;访问当前节点的左子树;若当前节点无左子树,则访问当前节点的右子树。
void PreOrderTraverse(BiTree T)
{
if (T)
{
cout<<T->data<<" ";
PreOrderTraverse(T->lchild);
PreOrderTraverse(T->rchild);
}
}
中序:二叉树中序遍历的实现思想是:访问当前节点的左子树;访问根节点;访问当前节点的右子树。
void INOrderTraverse(BiTree T)
{
if (T)
{
INOrderTraverse(T->lchild);
cout<<T->data<<" ";
INOrderTraverse(T->rchild);
}
}
后序:二叉树后序遍历的实现思想是:从根节点出发,依次遍历各节点的左右子树,直到当前节点左右子树遍历完成后,才访问该节点元素。
void PostOrderTraverse(BiTree T)
{
if (T)
{
PostOrderTraverse(T->lchild);
PostOrderTraverse(T->rchild);
cout<<T->data<<" ";
}
}
层次:层次遍历是从上到下、从左到右依次遍历每个结点。通过队列的应用,从根结点开始,将其左孩子和右孩子入队,然后根结点出队,重复次操作,直到队为空,出队顺序就是层次遍历的结果。
void LevelTravesal(BiTree T)
{
queue<BiTree>qu;
BiTree BT;
int flag = 0;
if (T == NULL)
{
cout << "NULL";
return;
}
qu.push(T);
while (!qu.empty())
{
BT = qu.front();
if (flag == 0)
{
cout << BT->data;
flag = 1;
}
else
{
cout << " " << BT->data;
}
qu.pop();
if (BT->lchild != NULL)
{
qu.push(BT->lchild);
}
if (BT->rchild != NULL)
{
qu.push(BT->rchild);
}
}
}
1.1.4 线索二叉树
线索二叉树:
二叉链存储结构时,每个结点都有两个指针,一共有2n个指针域,有效指针域为n-1个,空指针域有n+1个,可以利用空指针域指向该线性序列的前驱和后继指针,这称为线索。
若结点有左子树,则其lchild域指向左孩子,否则指向直接前驱;若结点有右子树,则其rchild域指向右孩子,否则指向直接后继。
为了避免混淆,再增加两个标志域LTag和RTag。若LTag=0,lchild域指向左孩子;若LTag=1,lchild域指向其前驱;若RTag=0,rchild域指向右孩子;若RTag=1,rchild域指向其后继。
存储结构
typedef struct Node
{
ElemType data;
int LTag,RTag;
struct Node *lchild,*rchild;
}TBTNode;
寻找结点前驱:观察线索二叉树的示意图,如果LTag=1,直接找到前驱,如果LTag=0,则走到该结点左子树的最右边的结点,即为要寻找的结点的前驱。
binThiTree* preTreeNode(binThiTree* q) {
binThiTree* cur;
cur = q;
if (cur->LTag == true) {
cur = cur->lchild;
return cur;
}
else{
cur = cur->lchild;//进入左子树
while (cur->RTag == false) {
cur = cur->rchild;
}//找到左子树的最右边结点
return cur;
}
}
寻找结点后继:观察线索二叉树示意图,如果RTag=1,直接找到后继,如果RTag=0,则走到该结点右子树的最左边的结点,即为要寻找的结点的后继。
binThiTree* rearTreeNode(binThiTree* q) {
binThiTree* cur = q;
if (cur->RTag == true) {
cur = cur->rchild;
return cur;
}
else {
//进入到*cur的右子树
cur = cur->rchild;
while (cur->LTag == false) {
cur = cur->lchild;
}
return cur;
}
}
1.1.5 二叉树的应用--表达式树
如图所示为表达式3x2+x-1/x+5的二叉树表示。树中的每个叶结点都是操作数,非叶结点都是运算符。
对该二叉树分别进行先序、中序和后序遍历,可以得到表达式的三种不同表示形式。
前缀表达式+-+3xxx/1x5
中缀表达式3xx+x-1/x+5
后缀表达式3xx**x+1x/-5+
表达式树的构建和输出
#include<stdio.h>
#include<string.h>
typedef struct binode
{
char data[4];
int h;
int depth;
struct binode *lchild,*rchild;
}binode,*bitree;
char d[100][100];
int q=0,num1;
void creatbitree(bitree &T,int y,int num)
{
if(d[q][0]=='#') {T=NULL;q++;}
else
{
T=new binode;
if(y==1) T->h=1;
else T->h=0;
T->depth=++num;
strcpy(T->data,d[q++]);
creatbitree(T->lchild,1,T->depth);
creatbitree(T->rchild,0,T->depth);
}
}
void travel(bitree T)
{
int i;
if(T!=NULL)
{
if(T->data[0]=='+'||T->data[0]=='-'||T->data[0]=='*'||T->data[0]=='/')
{
printf("(");
travel(T->lchild);
printf("%s",T->data);
travel(T->rchild);
printf(")");
}
else
printf("%s",T->data);
}
}
int ldepth(bitree T)
{
if(T==NULL)
return 0;
num1=ldepth(T->lchild);
return num1+1;
}
int rdepth(bitree T)
{
if(T==NULL)
return 0;
num1=rdepth(T->rchild);
return num1+1;
}
int main()
{
char a[500];
bitree T;
while(gets(a)!=NULL)
{
int i,j=0,k=0;
q=0;
for(i=0;a[i]!='\0';i++)
{
if(a[i]!=' ')
d[j][k++]=a[i];
else
{
d[j][k++]='\0';
//puts(d[j]);
k=0;
j++;
}
}
d[j++][k++]='\0';
//printf("%d\n",j);
creatbitree(T,2,0);
travel(T);
printf("\n");
}
}
1.2 多叉树结构
1.2.1 多叉树结构
多叉树是指一个父节点可以有多个子节点,但是一个子节点依旧遵循一个父节点定律,通常情况下,二叉树的实际应用高度太高,可以通过多叉树来简化对数据关系的描述。
多叉树结构
typedef struct node_t
{
char* name; // 节点名
int n_children; // 子节点个数
int level; // 记录该节点在多叉树中的层数
struct node_t** children; // 指向其自身的子节点,children一个数组,该数组中的元素时node_t*指针
} NODE; // 对结构体重命名
1.2.2 多叉树遍历
给定一个 N 叉树,返回其节点值的前序遍历。
返回其前序遍历: [1,3,5,6,2,4]。
class Solution {
public List<Integer> res = new ArrayList<Integer>();
public List<Integer> preorder(Node root) {
if(root == null)
return res;
res.add(root.val);
for(Node child : root.children){
preorder(child);
}
return res;
}
1.3 哈夫曼树
1.3.1 哈夫曼树定义
哈夫曼树:给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
解决问题:
哈夫曼静态编码,哈夫曼动态编码
在数据通信中,需要将传送的文字转换成二进制的字符串,用0,1码的不同排列来表示字符。例如,需传送的报文为“AFTER DATA EAR ARE ART AREA”,这里用到的字符集为“A,E,R,T,F,D”,各字母出现的次数为{8,4,5,3,1,1}。现要求为这些字母设计编码。
1.3.2 哈夫曼树的结构体
typedef struct HuffmanTree
{
ELEMTYPE weight;
ELEMTYPE id; // id用来主要用以区分权值相同的结点,这里代表了下标
struct HuffmanTree* lchild;
struct HuffmanTree* rchild;
}HuffmanNode;
typedef struct{ //哈夫曼树结构体
int weight; //输入权值
int parent,lchild,rchild; //双亲节点,左孩子,右孩子
}HNodeType;
1.3.3 哈夫曼树构建及哈夫曼编码
哈夫曼树构建:
假设有n个权值,则构造出的哈夫曼树有n个叶子结点。 n个权值分别设为 w1、w2、…、wn,则哈夫曼树的构造规则为:
(1) 将w1、w2、…,wn看成是有n 棵树的森林(每棵树仅有一个结点);
(2) 在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;
(3)从森林中删除选取的两棵树,并将新树加入森林;
(4)重复(2)、(3)步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树。
如下图做解释:
哈夫曼编码:
利用哈夫曼树求得的二进制编码称为哈夫曼编码。树中从根到每个叶子节点都有一条路径,对路径上的各分支约定指向左子树的分支表示”0”码,指向右子树的分支表示“1”码,取每条路径上的“0”或“1”的序列作为各个叶子节点对应的字符编码,即是哈夫曼编码。
如下图做解释:
A,B,C,D对应的哈夫曼编码分别为:111,10,110,0
1.4 并查集
并查集:
并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。这一类问题近几年来反复出现在信息学的国际国内赛题中。其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高,根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。
并查集是一种树型的数据结构,用于处理一些不相交集合(disjoint sets)的合并及查询问题。常常在使用中以森林来表示。
结构体
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);
}
合并操作也是很简单的,先找到两个集合的代表元素,然后将前者的父节点设为后者即可。
1.5 谈谈你对树的认识及学习体会
本章主要学习了树的结构,属于非线性结构。线性结构是一对一关系,而树结构是一对多的关系。树结构有二叉树,线索二叉树和哈夫曼树,二叉树由一个根节点和左子树和右子树组成。线索二叉树是利用空余的指针指向结点前驱,而哈夫曼树利用带权路径解决最优问题。树结构的运算大部分都是递归运算,所以只有用好递归才能更加的熟悉树的操作。一般递归体分为左子树和右子树,递归口为结点为空或者其他条件。对树结构有了一定的学习后,发现代码调试时很困难,因为有些复杂了,但也为多对多关系打了基础。
2.PTA实验作业
2.1 二叉树
2.1.1 解题思路及伪代码
int CalculateWpl(BTree bt,int h) //计算叶子结点带权路径长度和
if bt为空 do
return 0
end if
if bt->lchild为空 and bt->rchild为空 do //叶子结点
return bt->data*h //bt->data要转化为整型
end if
return 递归左子树和右子树
2.1.2 总结解题所用的知识点
递归遍历每个结点,碰到叶子结点计算wpl大小,算出最后总的wpl值
主要就是wpl值的计算和树结点的遍历
2.2 目录树
2.2.1 解题思路及伪代码
首先要注意的是目录和文件输出的优先级不同,优先输出目录,其次是文件。后面跟了\的就是目录,没有\的是文件。我用结构体中的ismulu区分目录和文件:ismulu=1表示目录,ismulu=0表示文件。
样例中每一层可能有多个结点,属于多叉树,这里通过父子-兄弟表示法,将多叉树转化为二叉树进行表示。父子关系用结点-左孩子表示,兄弟关系用结点-右孩子表示。于是样例转化为二叉树表示如下:
每次读入一行,每一行里面,后一层结点总是作为前一层结点的左孩子(或左孩子的右孩子序列中的结点)插入。因此每一行的内层循环中总是要把q更新为前一层结点。
而读入不同行时,每一次都要将q重新置为根结点F,从根结点开始插入。
最后先序遍历输出各结点和相应的空格数。
2.2.2 总结解题所用的知识点
树的按情况输出和结点插入和先序遍历
多叉树转换为二叉树
3.阅读代码
3.1 题目及解题代码
题目
代码
3.2 该题的设计思路及伪代码
设计思路
当 root.val > R,那么修剪后的二叉树会出现在节点的左边。当root.val < L时,那么修剪后的二叉树会出现在节点的右边。否则,修剪树的两边。
时间复杂度:O(N)。
空间复杂度:O(N)。
伪代码
struct TreeNode* trimBST(struct TreeNode* root, int L, int R){
root为空时,返回NULL
若root->val < L
返回trimBST(root->right, L, R);
若R < root->val
返回trimBST(root->left, L, R);
接着找左孩子root->left = trimBST(root->left, L, R);
接着找右孩子root->right = trimBST(root->right, L, R);
}
3.3 分析该题目解题优势及难点
优势:利用递归的算法,减少了代码量,复杂度也相对较小。
难点:修减时如何替代前一个结点。