DS博客作业03--树
这个作业属于哪个班级 | 数据结构--网络2011/2012 |
---|---|
这个作业的地址 | DS博客作业03--树 |
这个作业的目标 | 学习树结构设计及运算操作 |
姓名 | 林进源 |
0.PTA得分截图
1.本周学习总结(5分)
-
树的定义:树是由n个结点组成的有限集合
-
树的基本术语:
结点的度和树的度:树中某个结点的子树的个数称为该结点的度。树中所有结点的度中最大值为树的度。通常将度为m的树称为m次树。
分支结点和叶子结点:树中度不为的结点称为分支结点,树中度为0的叶子结点称为叶子结点或者终端结点。
路径与路径长度:树中任意两结点,存在一个结点序列实现结点到另一结点,该结点序列为路径。路径长度是该路径经过的结点数减一。
树的高度:所有结点中的最大层数。 -
树的性质
(1)树中的结点数为所有结点的度数和减一
(2)度为m的树中第i层最多有2的i-1次方个结点
1.1 二叉树结构
- 二叉树的定义:是n个结点的有限集合,它或为空树,或由一个根结点和两棵互不相交的左子树和右子树组成。二叉树不存在度大于2的结点。
- 满二叉树:一棵二叉树中,所有分支结点都有双分支结点,叶结点都集中在二叉树的最下一层。
高度为h的二叉树恰有2的n-1次方个结点。 - 完全二叉树:二叉树最多只有最下面两层的结点的度可以小于2,并且最下面一层的叶子结点排列在该层最左边的位置,这样的二叉树称为完全二叉树。
如果有度为1的结点,只有一种可能该结点只有左孩子
当结点个数为奇数时,无单分支。当叶子结点个数为偶数时,有单分支。
- 二叉树的性质:
(1)非空二叉树上的叶结点树等于双分支结点树加1
(2)所有结点的度之和=n-1=n1+2*n2=n0+n1+n2。
(3)若编号为i的结点有左孩子结点,左孩子编号为2i,右孩子编号为2i+1。(前提为i从1开始)
1.1.1 二叉树的2种存储结构
- 顺序存储结构:
例:某二叉树的层次遍历为ABD#C#E,则其顺序存储结构如图:
利用数组下标对二叉树的结点进行编号,编号从i=1开始。数组i的表示树的第i个结点,其父亲为i/2,左孩子为2i,右孩子为2i+1。
优缺点:对于完全二叉树而言,采用顺序存储结构可以节省存储空间,利用数组下标进行位置与结点的判断。但如果时一般的二叉树,不能保证度全为2,需要构造多余的空间进行存放,造成空间的浪费。
- 链式存储结构
如图:
利用一个链表来存储二叉树,需定义一个结构体。
typedef struct node
{
ElemType data;
struct node *lchild;
struct node *rchild;
}BTNode;
优缺点:对于一般的二叉树,利用的存储空间相对较少,访问结点的孩子较方便,但访问指定的结点需要进行的扫描较多。
1.1.2 二叉树的构造
(1)知道一棵二叉树的先序遍历和中序遍历
例:
开始利用先序遍历判断首结点后,带入到中序遍历中,将其分为左中序和右中序,并对照字母顺序将先序遍历分为左先序和右先序,后先判断一边,利用先序遍历找到根结点,带入中序遍历中,观察该结点在该序列中位于上一结点的左侧还是右侧,对应其为左孩子还是右孩子,依次进行后再判断另一侧。
(2)知道一棵二叉树的后序遍历和中序遍历
利用后序遍历找到首结点,带入到中序遍历中,将其分为左中序和右中序,并对照字母顺序将先序遍历分为左后序和右后序,后先判断一边,利用后序遍历找到根结点,带入中序遍历中,观察该结点在该序列中位于上一结点的左侧还是右侧,对应其为左孩子还是右孩子,依次进行后再判断另一侧。
(3)利用构建结构体进行构建树链表存放数据
(4)利用数组下标顺序存放树的各结点,通过下标记录和判断位置并找到左右孩子
1.1.3 二叉树的遍历
(1)层次遍历
伪代码:
if (判断树是否为空)
{
return;
}
q.push(T);
while (q队列不为空)
{
if (q.front()左孩子不为空)
{
进队;
}
if (q.front()右孩子不为空)
{
进队;
}
输出q.front();
q.pop();
}
代码:
if (T == NULL)
{
return;
}
q.push(T);
while (!q.empty())
{
if (q.front()->lchild != NULL)
{
q.push(q.front()->lchild);
}
if (q.front()->rchild != NULL)
{
q.push(q.front()->rchild);
}
cout << q.front;
q.pop();
}
利用队列进行结点的存放以及转移,先将首结点放入队列,后用while循环将队首的结点判断是否有孩子并存放入队,再将队首元素出队,实现层次遍历。
(2)先序遍历
void PreOrder(BTNode *b)
{
if (b != NULL)
{
cout << b->data;
PreOrder(b->lchild);
PreOrder(b->rchild);
}
}
(3)中序遍历
void PreOrder(BTNode *b)
{
if (b != NULL)
{
PreOrder(b->lchild);
cout << b->data;
PreOrder(b->rchild);
}
}
(4)后序遍历
void PreOrder(BTNode *b)
{
if (b != NULL)
{
PreOrder(b->lchild);
PreOrder(b->rchild);
cout << b->data;
}
}
1.1.4 线索二叉树
规定当某个结点的左指针为空时,该指针指向这个序列中该结点的前驱结点,当右指针为空时,指向该序列中该结点的后继结点。指向前驱和后继结点的指针为线索。
需要定义标志域进行判断是否指向孩子还是前驱和后继结点
中序线索二叉树特点:中序线索二叉树好处在于遍历二叉树不需要递归,所有结点只需遍历一次,时间和空间的利用效率高。
在中序线索二叉树查找前驱和后继:先找到最左边的节点,然后判断其右子树是否为线索,如果是线索,那么就遍历后继结点,如果右子树是右孩子,那么就进入到右孩子的最左边的节点,进行同样的判断,直到遍历完了整棵树为止。
1.1.5 二叉树的应用--表达式树
构建表达式树:树中叶子结点均为操作数,分支结点均为运算符,构建运算符栈和存放树根栈。
思路如下:遇到操作数入栈,遇到运算符则判断优先级是否比栈顶的运算符低,若低则出栈,对应的操作数栈出两个元素与运算符构建结点建树
伪代码:
while (遍历表达式)
{
若为操作数,生成树结点,进栈1;
若为运算符:
若优先级大于栈顶运算符, 进栈2;
若小于,出栈2,栈1出两元素建树;
若相等,出栈2;
}
计算表达式树:
思路如下:构建函数进行递归运算,先判断结点是否为操作数,并转为数字,后定义a和b分别计算左树和右树,进行递归运算,需要用switch判断运算符,转化为对应的运算。
伪代码:
double EvaluateExTree(BTree T)
{
double a, b;
if (T为叶子结点)
{
转数字;
}
a = EvaluateExTree(T->rchild);
b = EvaluateExTree(T->lchild);
switch (T->data)
{
分别判断 + -*/ 并返回相应情况;
}
}
代码实现:
void InitExpTree(BTree &T,string str)
{
stack<BTree> s;
stack<char> op;
op.push('#');//结束标志
int i=0;
while(str[i])
{
if(!In(str[i]))
{
T = new BTNode;//构建新的树节点
T->data = str[i++];
T->lchild = T->rchild = NULL;//初始化
s.push(T);
}
else
{
switch(Precede(op.top(),str[i]))
{
case '<':
op.push(str[i]);
i++;
break;
case '=':
op.pop();
i++;
break;
case '>'://优先级小要出栈操作
T = new BTNode;
T->data = op.top();
T->rchild = s.top();//s出两元素作为T的左右孩子
s.pop();
T->lchild = s.top();
s.pop();
s.push(T);
op.pop();
break;
}
}
}
while(op.top()!='#')//当op还有元素时
{
T=new BTNode;
T->data = op.top();
T->rchild = s.top();
s.pop();
if(!s.empty())//奇数个时需要此操作避免越界
{
T->lchild = s.top();
s.pop();
}
s.push(T);
op.pop();
}
T = s.top();
}
double EvaluateExTree(BTree T)
{
double sum=0,a,b;
if(!T->lchild && !T->rchild)//判断为叶子结点
{
return T->data-'0';
}
b = EvaluateExTree(T->rchild);//左递归
a = EvaluateExTree(T->lchild);//右递归
switch(T->data)
{
case '+':
return a+b;
break;
case '-':
return a-b;
break;
case '*':
return a*b;
break;
case '/':
if(b==0)//除数为0时
{
cout << "divide 0 error!" << endl;
exit(0);
}
return a/b;
break;
}
}
1.2 多叉树结构
1.2.1 多叉树结构
(1)双亲存储结构:
typedef struct
{
ElemType data;
int parent;//存放双亲
};
利用该性质可以准确有效的找到父亲母亲,但不易找到孩子
(2)孩子链存储结构:
typedef struct node
{
ElemType data;
struct node *son;//孩子结点
};
找到某结点的孩子容易,但找双亲费劲,且树的度较大时存在较多的空指针域
(3)孩子兄弟链存储结构
typedef struct node
{
ElemType data;
struct node *son;//孩子结点
struct node *brother;//兄弟结点
};
结构相似于二叉树,其优点为可方便实现树和二叉树的转换,缺点是查找指定双亲结点较难,利用该结构可解决目录树问题。
1.3 哈夫曼树
1.3.1 哈夫曼树定义
设二叉树有n个带权值的叶子结点,从根结点到各个叶子结点的路径长度与相应的结点权值乘积的和,为二叉树的带权路径长度(WPL),具有最小带权路径长度的二叉树为哈夫曼树。
哈夫曼树可解决数据通信中的文字转二进制数字,此时二进制数字越少越好,故哈夫曼树起到很大的作用。同时也可解决程序判断的时间效率问题,利用哈夫曼树减少程序查找的次数以及时间,实现更好的优化。
构建二叉树的原则:权值越大的叶结点越靠近根结点,权值越小的叶结点越远离根节点。
哈夫曼树的性质:n=n0+n1+n2=n0+n2=2n0+1。
1.3.2 哈夫曼树的结构体
(1)顺序存储结构
typedef struct
{
char data;
double weight;//权重
int parent;
int lchild;
int rchild;
};
(2)链式存储结构
typedef struct node
{
char data;
double weight;//权重
struct node *parent;
struct node *lchild;
struct node *rchild;
};
1.3.3 哈夫曼树构建及哈夫曼编码
每次找到最小的两个结点组成树后再放入序列中重复比较操作,构建完树后,给每条边标上记号,左子树的边为0,右子树的边为1,后每个叶子结点的哈夫曼编码对应走过的边所做记号的序列。
1.4 并查集
(1)支持查找一个元素所属的集合以及2个元素各自专属的集合等运算,当给出(a,b)时,快速合并a和b所在的集合。在这种数据中,n个不同的元素被分为若干组,每组是一个集合,称之为并查集。
(2)可实现多个不同的集合或者树的合并,找到其中元素间的对应关系,例如等价问题和朋友圈问题。
(3)结构体:
typedef struct node
{
int data;
int rank;//对应的秩,进行高度合并
int parent;//双亲下标
}UFSTree;
(4)查找操作
int Find(USFTree t[], int x)//在x所在的子树查找集合编号
{
if (x != t[x].parent)//双亲不是自己
{
return Find(t, t[x].parent);//递归父母直至找到
}
else
{
return x;//找到自己
}
}
(5)合并操作
void UNION(UFSTRee t[], int x, int y)//合并x于y的集合
{
x = Find(t, x);//找到x的编号
y = Find(t, y);//找到y的编号
if (t[x].rank > t[y].rank)//y的秩较小
{
t[y].parent = x;//x作为y的双亲
}
else
{
t[x].parent = y;//y作为x的双亲
if (t[x].rank == t[y].rank)
{
t[y].rank++;
}
}
}
1.5.谈谈你对树的认识及学习体会。
首先要理解树和它所有的结点定义,之后开始扩展到其结构的定义,这一步很重要,定义结构里面的内容要考虑到是需要孩子还是父母还是兄弟甚至两个以上,这一步基本确定题目的解题思路,之后便是要思考这是什么样的树,如何去读并且存入,是要顺序还是链表存储结构,有时某种结构的代码实现较为轻松,在存放完树节点后,一般可能会需要我们用特定的某种遍历方式,此时我们应正确理解四种遍历的方式,以及如何去递归实现,递归这个思路在解决树的问题非常重要,还需要知道哪个为递归出口,函数的判定条件一般跟树的下一结点或者上一结点有关,可能需要用到队列或者栈来进行结点的移动判断,有时还需要改变结点的前驱和后继,生成新的树进行操作例如并查集这类问题。
2.PTA实验作业(4分)
2.1 输出二叉树每层节点
2.1.1 解题思路及伪代码
解题思路:构建正常二叉树的结构体,加入h高度,后用先序建树的方法存放树,后需要用层次遍历的方法,新建一个队存放,并建立一个哈希数组进行换行的判断,while判断队列是否为空,存放队头的左右孩子,判断元素的位置并且输出,后出队继续操作。
伪代码:
BTree CreateBTree(string str, int &i, int h)//存放树
{
length = str长度;
if (i == length) return NULL;
if (遇到'#') return NULL;
新建bt结点;
先序递归存放;
}
void level(BTree T)
{
建队q;
T入队;
if (T为空) return NULL;
static int A[10];
int flag = 1;//第一行
while (q不为空)
{
T左右孩子入队;
if (flag == 1 && 哈希对应数值为0)
{
输出;
flag = 0;
}
else if (flag == 0 && 哈希对应数值为0)
{
换行输出;
}
else
{
输出;
}
q出队;
}
}
2.1.2 总结解题所用的知识点
该题在层次遍历的基础上进行输出操作,需要用到先序建树以及层次遍历树以及递归函数,层次遍历树则需要引入队列的操作,控制换行输出则用到哈希数组。
2.2 目录树
2.2.1 解题思路及伪代码
解题思路:首先建立孩子兄弟链的结构,读入字符串并分离,判断是文件还是目录,分别进入各自的函数,文件函数通过对比优先级的高低,若高则改变指针的位置原来位置变为其孩子,低则作为孩子插入,相同则在其当前位置基础上进行操作,目录函数则是对兄弟链进行操作。
结点插入:
1.没有第一个孩子,直接生成第一个孩子结点。建立Catalog关系。修改当前指针
2.有孩子,则是否需要更改孩子:
(1) 结点存在,即数据相等且目录文件熟悉相等,则不新建孩子结点,当前指针更改为孩子位置。
(2) 如果孩子是文件,新节点是目录,则 更改孩子为新节点。修改当前指针
(3) 如果孩子文件属性同新节点,但是值比新节点大,则更改。修改当前指针
3.不是作为孩子插入,做兄弟插入情况:
(1) 没有兄弟,则插入新节点为兄弟。
(2) 有兄弟,找新节点插入位置,这是一个有序链表,
如果新节点属性和兄弟属性相等且值大于兄弟,则继续遍历下一个兄弟
如果新节点是文件,兄弟是目录,则继续遍历下一个兄弟
遍历中发现兄弟是文件,新节点是目录,提前退出循环。
(3) 遍历结束,兄弟节点值和新节点值相等,则不需要插入新节点,更新当前指针为兄弟。
(4) 插入新节点,更改新节点brother关系,并把插入位置的前一个节点的brother改为新节点。更新指针。
伪代码:
while (str.size() > 0)
{
查找字符串中是否有’\’,并记录下位置pos
if (没有)
说明是文件,则进入插入文件的函数
else
说明是目录,则进入插入目录的函数里
bt要跳到下一个目录的第一个子目录
while (bt!NULL&&bt->name != name)
bt = bt->Brother;//找到刚刚插入的目录,然后下一步开始建立它的子目录
将刚插进去的目录从字符串中去掉
}
//插入文件操作
if(情况1:bt为空,情况2:当前为文件,且优先级大于当前结点)
{
新建结点将原结点作为其兄弟;
}
if(相同)
{
返回;
}
作为兄弟插入;
//插入目录操作
转移目录位置
if(情况1:目录为空 情况2:当前结点为文件 情况3:优先级大)
{
新建结点原结点作为其兄弟;
}
if(相同)
{
返回;
}
作为兄弟插入;
2.2.2 总结解题所用的知识点
利用孩子兄弟链结构进行存储,比较优先级插入结点并且改变结点位置,以及先序和后序遍历方式,递归函数调用计算高度。
3.阅读代码(0--1分)
3.1 题目及解题代码
解题代码:
class Solution {
public:
int dfs(TreeNode* root, int prevSum)
{
if (root == nullptr)
{
return 0;
}
int sum = prevSum * 10 + root->val;//遇到结点就累加
if (root->left == nullptr && root->right == nullptr) //直到遇到叶子结点返回
{
return sum;
}
else //分别左递归和右递归
{
return dfs(root->left, sum) + dfs(root->right, sum);
}
}
int sumNumbers(TreeNode* root)
{
return dfs(root, 0);
}
};
3.2 该题的设计思路及伪代码
设计思路:从根节点开始,遍历每个节点,如果遇到叶子节点,则将叶子节点对应的数字加到数字之和。如果当前节点不是叶子节点,则计算其子节点对应的数字,然后对子节点递归遍历。
伪代码:
if (根结点为空)
{
return 0;
}
累加求和计算;
if (为叶子结点)
{
return sum;
}
else
{
进入左递归;
进入右递归;
}
3.3 分析该题目解题优势及难点
优势:利用深度搜索找到递归出口的叶子结点,过程中遇到的其他结点需要进行与前面结点的累加运算,返回过程中也需要进行累加运算。
难点:分析找到路径遍历并且如何利用递归进行累加运算,并且找到正确的递归返回出口。