DS博客作业03--树
| 这个作业属于哪个班级 |
| ---- | ---- | ---- |
| 这个作业的地址 |
| 这个作业的目标 | 学习树结构设计及运算操作 |
| 姓名 | 吴慧敏 |
🔅0.PTA得分截图
🔅1.本周学习总结(5分)
1.1 二叉树结构
-
二叉树的定义
是n(n>0)个结点的有限集合,它或为空树(n=0),或由一个根结点和至多棵称为根的左子树和右子树的互不相交的二叉树组成。
❗注意:二叉树中不存在度大于2的结点,并且二叉树的子树有左子树和右子树之分。
-
二叉树性质
①性质1:非空二叉树上叶节点数等于双分支节点数加1。
②性质2:在二叉树的第i层上至多有2^(i-1)个结点(i≥1)。【满二叉树时候最多】
③高度为h的二叉树至多有2^h-1个节点(h≥1)。
④具有n个结点的完全二叉树的深度必为[㏒₂n]+1。
-
二叉树的5种基本形态
①空树
②只含根结点
③右子树为空树
④左子树为空树
⑤左右子树均不为空树
-
两种特殊的二叉树
①满二叉树:
在一棵二叉树中:
如果所有分支结点都有双分结点;
并且叶结点都集中在二叉树的最下一层。
🔸注:高度为n的二叉树恰好有2ⁿ-1个结点。
②完全二叉树(实际上是对应的满二叉树删除叶结点层最右边若干个结点得到的。)
深度为k的,有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应没有单独的右分支结点
🔸注:深度为n的完全二叉树结点个数h:2ˆ(n-1)-1<h<=2ⁿ-1
完全二叉树性质(含n为结点)
①性质1 在二叉树的第 i 层至多有 2^(i -1)个结点。(i>=1)
②性质2 深度为 k 的二叉树至多有 2^(k-1)个结点(k >=1)。
③性质3 对任何一棵二叉树T, 如果其叶结点数为n0, 度为2的结点数为 n2,则n0=n2+1。
④性质4 具有 n (n>=0) 个结点的完全二叉树的深度为+1
⑤性质5 如将一棵有n个结点的完全二叉树自顶向下,同层自左向右连续为结点编号0,1, …, n-1,则有:
1)若i = 0, 则 i 无双亲, 若i > 0, 则 i 的双亲为」(i -1)/2」
2)若2i+1 < n, 则i 的左子女为 2i+1,若2i+2 < n, 则 i 的右子女为2i+2
3)若结点编号i为偶数,且i != 0,则左兄弟结点i-1.
4)若结点编号i为奇数,且i != n-1,则右兄弟结点为i+1.
5)结点i 所在层次为」log2(i+1) 」
满二叉树和完全二叉树的区别
满二叉树是叶子一个也不少的树,而完全二叉树虽然前n-1层是满的,但最底层却允许在右边缺少连续若干个结点。满二叉树是完全二叉树的一个特例。
1.1.1 二叉树的2种存储结构
顺序存储结构
二叉树的顺序存储结构也就是将树这种有指向的二维数据像二维数组一样存在一个线性的一维的数据结构中(一般用于完全二叉树,避免内存浪费)
以B结点为例:
1.B结点在数组中的下标为1
2.B结点左子树D在数组中的下标为3
3.B结点右子树E在数组中的下标为4
设B结点下标为i时:
父亲结点的下标为i/2
左子树的下标为2i+1
右子树的下标为2(i+1)
🔺注:一般的二叉树先用空结点补全成为完全二叉树,然后对结点标号。
eg.
用一个数组存储
ElemType SqBTree[MaxSize];
SqBTree bt="#ABD#C#E######F";
- 优点:
1.具有按元素序号随机访问的特点。
2.方法简单,各种高级语言中都有数组,容易实现。
3.不用为表示节点间的逻辑关系而增加额外的存储开销。 - 缺点:
1.空间利用率太低。对于普通二叉树来说,在最坏的情况下,一个深度为k且只有k个k个结点的单支树(树中不存在度为2的结点)却需要2ˆk-1的一维数组。
2.查找、插入删除不方便。
3.存储空间固定,可扩展性差。需要预先分配足够大的存储空间,估计过大,可能会导致顺序表后部大量闲置;预先分配过小,又会造成溢出。
链式存储结构
在链式存储结构中,每个结点的结构如下,即一个存储数据的变量与两个指向孩子的指针域。
利用指针域我们可以存储非完全二叉树,如下:
代码层面对节点的描述:
typedef struct node
{ ElemType data;
struct node *lchild, *rchild;
}BTNode;
typedef BTNode *BTree;
- 优点:
1.空间利用率较高
2.插入、删除运算方便。
3.可扩展性强。 - 缺点:
1.链式存储存储密度小。
2.不适合查找。
1.1.2 二叉树的构造
1.二叉树的顺序存储结构转成二叉链
将二叉树的顺序存储结构转成二叉链,要先把二叉树补成满二叉树,之后进行递归建树。
(i如果从1开始)
BinTree CreatBinTree(string str, int i)//递归建树
{
BinTree bt;
if (i > str.size()-1||i <= 0) return NULL;
if (str[i] == '#') return NULL;
bt = new TNode;
bt->Data = str[i];
bt->Left = CreatBinTree(str, i * 2);
bt->Right = CreatBinTree(str, i * 2 + 1);
return bt;
}
🔸注意:如果i从0开始,注意孩子和父亲关系,即左孩子为2i+1,右孩子为2i+2
2.先序遍历递归建树
先建立根节点,再先序建立左子树,最后先序建立右子树。
BinTree CreatBinTree(string str, int& i)//递归建树
{
BinTree bt;
if (i > str.size() - 1) return NULL;
if (str[i] == '#') return NULL;
bt = new TNode;
bt->Data = str[i];
bt->Left = CreatBinTree(str, ++i);
bt->Right = CreatBinTree(str, ++i);
return bt;
}
3.层次法建树
层次法建树不需要事先知道树的遍历结果,只要事先确定需要建一棵什么样的二叉树即可。
这里的层次法建树是采用数组下标与队列的方法。
eg.
其从左往右层次遍历顺序为:F C E A D H G B M
接下来开始按照这个顺序建二叉树:
ⅰ先确定二叉树的结构体:
typedef struct Tree
{
char data;
Tree *L;//左子树指针
Tree *R;//右子树指针
}Tree;
ⅱ建树代码:这里要注意Root与T的区别,Root是整棵树的根节点,T是新建节点的根节点,用队列与输入下标确定新建节点的位置。
void CreatTree(Tree* &Root,int n)//建树,Root是返回的树根
{
int i=0;
queue<Tree*>Q;
Tree *T;//建树过程中的每个结点的根
while(i<n)
{
char elem;
cin>>elem;
if(i==0)
{
Root=new Tree();
Root->data= elem;
i++;
Q.push (Root);
continue;
}
T=Q.front ();//每次循环都将队头赋值给,将要建立的结点的祖先结点
if(elem=='#')
{
if(i%2==1)//按输入顺序,奇数为左结点
T->L =NULL;
if(i%2==0)//按输入顺序,偶数为右结点
{
T->R =NULL;
Q.pop ();//每一次建完右结点,其祖先结点就没用了,出队列
}
i++;
}
else
{
Tree *TMP;
TMP=new Tree();
TMP->data =elem;
if(i%2==1)//左
{
T->L =TMP;
}
if(i%2==0)//右
{
T->R =TMP;
Q.pop ();//每一次建完右结点,其祖先结点就没用了,出队列
}
Q.push (TMP);//将建立的结点入队列,elem=‘#’的空结点不用入
i++;
}
}
}
ⅲ完整的代码:
#include<iostream>
#include<queue>
#include<stack>
using namespace std;
typedef struct Tree
{
char data;
Tree *L;
Tree *R;
}Tree;
void CreatTree(Tree* &Root,int n)//建树,Root是返回的树根
{
int i=0;
queue<Tree*>Q;
Tree *T;//建树过程中的每个结点的根
while(i<n)
{
char elem;
cin>>elem;
if(i==0)
{
Root=new Tree();
Root->data= elem;
i++;
Q.push (Root);
continue;
}
T=Q.front ();//每次循环都将队头赋值给,将要建立的结点的祖先结点
if(elem=='#')
{
if(i%2==1)//按输入顺序,奇数为左结点
T->L =NULL;
if(i%2==0)//按输入顺序,偶数为右结点
{
T->R =NULL;
Q.pop ();//每一次建完右结点,其祖先结点就没用了,出队列
}
i++;
}
else
{
Tree *TMP;
TMP=new Tree();
TMP->data =elem;
if(i%2==1)//左
{
T->L =TMP;
}
if(i%2==0)//右
{
T->R =TMP;
Q.pop ();//每一次建完右结点,其祖先结点就没用了,出队列
}
Q.push (TMP);//将建立的结点入队列,elem=‘#’的空结点不用入
i++;
}
}
}
void PreTree(Tree* T)//先序遍历
{
if(!T)
return ;
else
{
cout<<T->data <<" ";
PreTree(T->L );
PreTree(T->R );
}
}
void InoTree(Tree* T)//中序遍历
{
if(!T)
return ;
else
{
PreTree(T->L );
cout<<T->data <<" ";
PreTree(T->R );
}
}
int main()
{
Tree *T=NULL;
int n;
cin>>n;
CreatTree(T,n);
PreTree(T);
InoTree(T);
return 0;
}
4.先序遍历和中序遍历构造二叉树
step1.确定根结点的位置:根节点在前序遍历中的位置(通过遍历前序遍历序列,比较每一个节点与中序遍历中的第一个节点即根节点可知)。
step2.确定左子树的节点数:左子树的节点数,因为一旦找到前序遍历中根节点的位置,就找到左右子树的分界点,也就是说,前序遍历中根节点左边的都是左子树节点,可以通过遍历知道左子树的节点数。
step3.确定右子树的节点数:同step2。
5.后序遍历和中序遍历构造二叉树
1.第一步:先找到根节点,由后序遍历可知根节点是后序遍历数组中的最后一个元素。
2.第二步,找到根节点值在中序遍历数组中的位置。
3.第三步:可以找到左子树在数组中的长度。
eg.
中序遍历 inorder = [9,3,15,20,7]
后序遍历 postorder = [9,15,7,20,3]
返回如下的二叉树:
3
/
9 20
/
15 7
- 思路:
1.找到根结点
2.找到根结点值在中序遍历数组中的位置。
3.可以找到左子树在数组中的长度,如下图所示:
由上述可知:9为根节点的左子树,15,20,7节点为根节点的右子树的节点,这是需要找到右子树的根节点,然后去找到右子树的左子树和右子树的右子树来构造根节点下的右子树。
于是我们在15,20,7中重复上述的一二三步,如下图所示:
1.1.3 二叉树的遍历
- 二叉树遍历的概念:
二叉树的遍历是指按照一定次序访问树中所有节点,并且每个结点仅被访问一次的过程。它是最基本·1的运算,是二叉树中所有其它运算的基础。
eg.
先(根)序遍历(根左右):A B D H E I C F J K G
中(根)序遍历(左根右) : D H B E I A J F K C G
后(根)序遍历(左右根) : H D I E B J K F G C A
层次遍历:A B C D E F G H I J K -
1.先序遍历(根左右)
先序遍历二叉树的过程是:
①访问根结点;
②先序遍历左子树;
③先序遍历右子树。
- 具体代码:
void PreOrder(BTree bt)
{
if (bt != NULL)
{
printf("%c", bt->data);//访问根节点
PreOrder(bt->lchild);//先序遍历左子树
PreOrder(bt->rchild);//先序遍历右子树
}
}
🔺注意:
1.递归调用从根节点开始,到根节点结束;
2.每个结点访问两遍。
-
2.中序遍历(左根右)
中序遍历二叉树的过程是:
①中序遍历左子树;
②访问根节点;
③中序遍历右子树。
- 具体代码:
void InOrder(BTree bt)
{
if (bt != NULL)
{
InOrder(bt->lchild);//中序遍历左子树
printf("%c", bt->data);//访问根节点
InOrder(bt->rchild);//中序遍历右子树
}
}
-
3.后序遍历(左右根)【最后一个结点是根节点】
后序遍历二叉树的过程是:
①后序遍历左子树;
②后序遍历右子树;
③访问根节点。
- 具体代码:
void PostOrder(BTree bt)
{
if (bt != NULL)
{
PostOrder(bt->lchild);//遍历左子树
PostOrder(bt->rchild);//遍历右子树
printf("%c", bt->data);//访问根节点
}
}
-
4.层次遍历
即按层,从上到下,从左到右遍历。
核心思想:核心思想:每次出队一个元素,就将该元素的孩子节点加入队列中,直至队列中元素个数为0时,出队的顺序就是该二叉树的层次遍历结果。
- 具体代码
queue<Bitree> TreeQueue; //使用队列
TreeQueue.push(tree); //先将队头元素加入队列
Bitree p = tree;
while (!TreeQueue.empty()) //循环判断队列是否未空,若不空则
{
p = TreeQueue.front(); //获取队列头节点,并出队列
TreeQueue.pop();
printf(" %c ", p->data); //打印队列元素
if (p->Lchild) //如果该节点有左节点
{
TreeQueue.push(p->Lchild); //加入队列
}
if (p->Rchild) //如果该节点有右节点
{
TreeQueue.push(p->Rchild); //加入队列
}
}
eg.
这棵二叉树的层次遍历次序为:A、B、C、D、F、G
具体思路方法:
- 初始状态下,队列中只保留根节点的元素:
- 当A出队时,将A的孩子节点加入队列中:
3.重复上面的动作,队首元素出队时,将孩子节点加入队尾.......
1.1.4 线索二叉树
线索二叉树的定义
对于n个结点的二叉树,在二叉链存储结构中有n+1个空链域,利用这些空链域存放在某种遍历次序下该结点的前驱结点和后继结点的指针,这些指针称为线索,加上线索的二叉树称为线索二叉树。
这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。
注意:线索链表解决了无法直接找到该结点在某种遍历序列中的前驱和后继结点的问题,解决了二叉链表找左、右孩子困难的问题。
- 线索化二叉树
▶左标志 ltag:
ltag=0,表示lchild指向左孩子结点
ltag=1,表示lchild指向前驱结点
▶右标志 rtag
rtag=0,表示rchild指向右孩子结点
rtag=1,表示rchild指向后继结点
线索二叉树的设计
建立线索二叉树,或者说对二叉树线索化,实质上就是遍历一棵二叉树。在遍历过程中,访问结点的操作是检查当前的左,右指针域是否为空,将它们改为指向前驱结点或后续结点的线索。为实现这一过程,设指针pre始终指向刚刚访问的结点,即若指针p指向当前结点,则pre指向它的前驱,以便设线索。
另外,在对一颗二叉树加线索时,必须首先申请一个头结点,建立头结点与二叉树的根结点的指向关系,对二叉树线索化后,还需建立最后一个结点与头结点之间的线索。
- 结点代码实现
#define TElemType int//宏定义,结点中数据域的类型
//枚举,Link为0,Thread为1
typedef enum PointerTag{
Link,
Thread
}PointerTag;
//结点结构构造
typedef struct BiThrNode{
TElemType data;//结点数据域
struct BiThrNode* lchild,*rchild;//左孩子,右孩子指针域
PointerTag Ltag,Rtag;//标志域,枚举类型
}BiThrNode,*BiThrTree;
中序线索二叉树
- 中序线索二叉树的特点
在线索二叉树中再加入一个头节点:
①头节点的左孩子指向根节点
②右孩子为线索,指向最后一个孩子
③遍历序列第一个结点前驱为头节点,最后一个结点后继为头节点 - 如何在中序线索二叉树查找前驱和后继
ⅰ寻找结点前驱
观察线索二叉树的示意图,如果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 二叉树的应用--表达式树
先序遍历表达式树,得到的是前缀表达式
中序遍历表达式树,得到的是中缀表达式
后序遍历表达式树,得到的是后缀表达式
构造表达式树(栈的应用)
如下图就是一个表达式树
表达式树的叶子是操作数,内部是操作符。
上面所对应的表达式是
(a+b)(c(d+e));
对该树进行后序遍历得到后缀表达式
ab+cde+**,
这里实现的是如何根据一个后缀表达式构造出其相应的表达式树。
- 算法思想:主要是栈的应用。时间复杂度是O(n),n是后缀表达式长度。从前向后依次扫描后缀表达式,如果是操作数就建立一个单节点树,并把其指针压入栈。如果是操作符,则建立一个以该操作符为根的树,然后从栈中依次弹出两个指针(这两个指针分别指向2个树),作为该树的左右子树。然后把指向这棵树的指针压入栈。直到扫描完后缀表达式。构造最后栈中就会只有一个指针,这个指针指向构造的表达式树的根节点。
如何计算表达式树
采用递归的方式来计算表达式树。
1.2 多叉树结构
1.2.1 多叉树结构
(1)双亲存储结构(顺序存储):
typedef struct
{
ElemType data;//结点的值
int parent;//指向双亲的位置
}PTree[MaxSize];
- 优缺点:找父亲容易,但找孩子部容易。
(2)孩子链存储结构:
typedef struct node
{
ElemType data;//结点的值
struct node *sons[MaxSons];//指向孩子结点
}TSonNode;
- 优缺点:
1.空指针太多;
2.找到某结点的孩子容易,但找父亲不容易。
(3)孩子兄弟链存储结构
是为每个结点设计3个域:
1.一个数据元素域;
2.第一个孩子结点指针域;
3.一个兄弟结点指针域。 - 结点的类型声明如下:
typedef struct node
{
ElemType data; //结点的值
struct tnode *son;//指向孩子结点
struct tnode *brother;//指向兄弟结点
}TSBNode;
🔺注:
1.每个结点固定只有两个指针域;
2.结构相似于二叉树,其优点为可方便实现树和二叉树的转换;
3.缺点是查找指定双亲结点较难,利用该结构可解决目录树问题。
1.2.2 多叉树遍历
先序遍历做法
eg.给定一个 N 叉树,返回其节点值的前序遍历。
例如,给定一个 3叉树 :
返回其前序遍历: [1,3,5,6,2,4]。
1.3 哈夫曼树
1.3.1 哈夫曼树定义
哈夫曼树又称为最优树。
-
什么是哈夫曼树(最优树)?
哈夫曼树又称最优二叉树,是一种带权路径长度最短的二叉树。 -
哈夫曼树的特点:
已知n0个叶子结点,哈夫曼树总结点树n?
1.没有单分支节点,n1=0(因为每次两棵树合并)。
2.n和n0关系?
n=n0+n1+n2=n0+n2=2n0-1。
-
基本术语:
⑴路径和路径长度:
在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1。
⑵结点的权及带权路径长度:
若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积。
⑶树的带权路径长度:
树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL。 -
哈夫曼树的应用:
⑴哈夫曼编码
eg.假如有A,B,C,D,E五个字符,出现的频率(即权值)分别为5,4,3,2,1
所以各字符对应的编码为:A->11,B->10,C->00,D->011,E->010
1.3.2 哈夫曼树的结构体
(1)顺序存储结构
typedef struct
{
char data; //节点值
double weight; //权重
int parent; //双亲节点
int lchild; //左孩子节点
int rchild; //右孩子节点
}HTNode;
(2)链式存储结构
typedef struct node
{
char data;
double weight;//权重
struct node *parent;
struct node *lchild;
struct node *rchild;
};
1.3.2 哈夫曼树构建及哈夫曼编码
哈夫曼树构建
- 构造哈夫曼树的原则:
①权值越大的叶结点越靠近根结点。
②权值越小的叶结点越远离根结点。
eg.
假设有n个权值,则构造出的哈夫曼树有n个叶子结点。 n个权值分别设为 w1、w2、…、wn,哈夫曼树的构造规则为:
- 将w1、w2、…,wn看成是有n 棵树的森林(每棵树仅有一个结点);
- 在森林中选出根结点的权值最小的两棵树进行合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;
- 从森林中删除选取的两棵树,并将新树加入森林;
- 重复(02)、(03)步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树。
以{5,6,7,8,15}为例,来构造一棵哈夫曼树。
哈夫曼编码
eg.假如有A,B,C,D,E五个字符,出现的频率(即权值)分别为5,4,3,2,1
所以各字符对应的编码为:A->11,B->10,C->00,D->011,E->010
1.4 并查集
①并查集是一种简单的用途广泛的集合,并查集是若干个不相交集合。在并查集中,每个分离集合对应的一棵树,称为分离集合对应的一棵树,称为分离集合树。整个并查集也就是一棵分离集合森林。
②能够实现较快的合并和判断元素所在集合的操作,应用很多,如其求无向图的连通分量个数、最小公共祖先、带限制的作业排序,还有最完美的应用:实现Kruskar算法求最小生成树。
1.结构体:
typedef struct node
{
int data;//结点对应人的编号
int rank;//对应的秩,进行高度合并
int parent;//结点对应双亲下标
}UFSTree;
🔸注:根节点父亲是自己。
2.并查集树的初始化
void MAKE_SET(UFSTree t[],int n)//初始化并查集树
{ int i;
for(i=1;i<=n;i++)
{ t[i].data=i; //数据为该人的编号
t[i].rank=0; //秩初始化为0
t[i].parent=i; //双亲初始化指向自己
}
}
3.查找操作
int FIND_SET(USFTree t[], int x) //在x所在的子树查找集合编号
{
if (x != t[x].parent) //双亲不是自己
{
return FIND_SET(t, t[x].parent);//递归父母直至找到
}
else
{
return x; //双亲是自己,返回x
}
}
4.合并操作
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
}
}
}
对于 n个人,合并算法的时间复杂度=查找的时间复杂度O(log₂n)。
1.5谈谈你对树的认识及学习体会。
1.树的定义
- 树是一种数据结构,它是由n(n>=1)个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
- 树(tree)是包含个节点,条边的有穷集,其中:
(1)每个元素称为节点(node);
(2)有一个特定的节点被称为根节点或树根(root);
(3)除根节点之外的其余数据元素被分为个互不相交的集合,其中每一个集合本身也是一棵树,被称作原树的子树(subtree)。
2.基本术语
- 根:即根结点(没有前驱);
- 叶子:即终端结点(没有后继),度为0;
- 双亲节点(或父节点):若一个节点含有子节点,则这个节点称为其子节点的父节点,即上层的那个结点(直接前驱);
- 孩子节点(或子节点):即下层结点的子树的根(直接后继);
- 兄弟节点:同一双亲下的同层结点(孩子之间互称兄弟);
- 节点的祖先:从根到该节点所经分支上的所有节点;
- 子孙:以某节点为根的子树中任一节点都称为该节点的子孙,即该结点下层子树中的任一结点;
- 结点:即树的数据元素;
- 结点的度:结点挂接的子树数,分支数目;
- 结点的层次:从根到该结点的层数,从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
- 终端结点:即度为0的结点,即叶子;
- 分支节点(或非终端节点):即度不为0的节点(也称为内部结点);
- 树的度:一棵树中,最大的节点的度称为树的度;
- 树的深度(或高度):指所有结点中最大的层数;
- 堂兄弟节点:双亲在同一层的节点互为堂兄弟;
- 森林:指m棵不相交的树的集合(例如删除1后的子树个数);
- 有序树:结点各子树从左至右有序,不能互换(左为第一);
- 无序树:结点各子树可互换位置。
3.我的学习体会:
- 树型结构是一种非线性结构(一对多)。
- 空集合也是树,称为空树,空树中没有节点。
- 树可说是一类非常特殊的图,由于其特殊的性质,科学家们创造许多独属于树的算法,方便处理树形结构,也通过树形结构解决了大量算法问题,数据储存问题和程序调用的问题。
🔅2.PTA实验作业(4分)
2.1 二叉树---输出二叉树每层节点
- 具体代码:
#include<iostream>
#include<string>
#include<queue>
using namespace std;
typedef struct node
{
char data;
struct node* lchild;
struct node* rchild;
}BTNode,*BTree;
BTree CreatTree(string str, int& i);
void LayerNode(BTree bt);
BTree CreatTree(string str, int& i)
{
BTree bt;
int len = str.size();
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;
}
void LayerNode(BTree bt)
{
int layer = 1, flag = 0;
queue<BTree>qtree;
BTree tNode;
BTree node, lastNode;
node = lastNode = bt;
if (bt)
qtree.push(bt);
else
{
cout << "NULL" << endl;
return;
}
while (!qtree.empty())
{
if (flag == 0)
{
cout << "1:";
flag = 1;
}
node = qtree.front();
if (node->rchild)
tNode = node->rchild;
else if (node->lchild)
tNode = node->lchild;
cout << node->data << ",";
if (node->lchild)
qtree.push(node->lchild);
if (node->rchild)
qtree.push(node->rchild);
qtree.pop();
if (node == lastNode && !qtree.empty())
{
cout << endl;
cout << ++layer << ":";
lastNode = tNode;
}
}
}
int main()
{
string str;
cin >> str;
int i = 0;
BTree tree = CreatTree(str, i);
LayerNode(tree);
return 0;
}
2.1.1 解题思路及伪代码
-
解题思路:
对于输入的一行字符串,首先要将它进行先序遍历递归建树,之后再按照层次输出所在层及其包含节点。 -
伪代码:
BTree CreatTree(string str, int& i)//建树
{
len=str//长度
if(遍历完所有结点)
return NULL;
if(遇到空结点)
return NULL;
新建bt结点;
先序递归存放;
}
void LayerNode(BTree bt)
{
layer = 1,flag=0;
建队qtree;
if(bt不为空)
bt入队;
else
{
输出NULL;
}
while(qtree不为空)
{
if(flag==0)
{
输出“1:”;
置flag=1;
}
if(左孩子不为空)
{
tNode=左孩子;
}
else if(右孩子不为空)
{
tNode=右孩子;
}
输出
if (左孩子不为空)
进队;
if (右孩子不为空)
进队;
弹出队首元素;
if (node在lastNode的位置且队不为空)
{
换行输出层数;
更新LastNode,LastNode=tNode;
}
}
}
2.1.2 总结解题所用的知识点
①先序遍历递归建树;
②引用队列进行层次遍历二叉树;
③控制换行时,利用LastNode标记末尾结点的位置,方便知道每一层的结点在哪里停止。
2.2 目录树
2.2.1 解题思路及伪代码
- 解题思路:
①建立孩子兄弟链的结构体,建根结点;
②对输入的字符串进行切割,分离文件名和目录名;判断结点是文件还是目录;
③插入结点,建立目录树;
④比较插入结点优先级
高:改变原来指针位置为新插入结点的孩子;
低:新结点作为孩子插入;
相同:对兄弟链进行操作。 - 伪代码:
void DealStr(string str, BTree bt)
{
while (str.size() > 0)
{
查找字符串中是否有’\’,并记录下位置pos
if (pos == -1)
说明是文件,则进入插入文件的函数
else
说明是目录,则进入插入目录的函数里
bt要跳到下一个目录的第一个子目录
while (bt不为空且bt->name != name)
bt = bt->Brother;//找到刚刚插入的目录,然后下一步开始建立它的子目录
将刚插进去的目录从字符串中去掉
}
}
//插入文件操作InsertFile
if(bt为空 或者 当前为文件,且优先级大于当前结点)
{
新建结点将原结点作为其兄弟;
}
if(和当前文件名相同)
{
return bt;
}
作为兄弟插入;
return bt;
//插入目录操作Insertcatalog(
转移目录位置
if(目录为空 或者 当前结点为文件 或者优先级大)
{
新建结点将原结点作为其兄弟;
}
if(与当前名字相同)
{
返回 bt_Ca;
}
作为兄弟插入;
返回 bt_Ca;
2.2.2 总结解题所用的知识点
①利用孩子兄弟链结构进行存储;
②结构体重利用flag队结点进行其实目录还是文件的判断;
③比较插入结点优先级(因为可能需要改变结点位置);
④先序,后序遍历方式;
⑤使用递归函数来计算高度。
🔅3.阅读代码(0--1分)
3.1 题目及解题代码
题目:二叉树的中序遍历
- 解题代码:
class Solution {
public:
void inorder(TreeNode* root, vector<int>& res) {
if (!root) {
return;
}
inorder(root->left, res);
res.push_back(root->val);
inorder(root->right, res);
}
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
inorder(root, res);
return res;
}
};
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/binary-tree-inorder-traversal/solution/er-cha-shu-de-zhong-xu-bian-li-by-leetcode-solutio/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
3.2 该题的设计思路及伪代码
- 设计思路:
定义 inorder(root) 表示当前遍历到 root 节点的答案,那么按照定义,我们只要递归调用 inorder(root.left) 来遍历 root 节点的左子树,然后将 root 节点的值加入答案,再递归调用inorder(root.right) 来遍历 root 节点的右子树即可,递归终止的条件为碰到空节点。
中序遍历:左 - 打印 - 右,递归函数实现。
终止条件:当前节点为空时
函数内:递归的调用左节点,打印当前节点,再递归调用右节点
- 伪代码:
class Solution {
void inorder(root,res)
{
if(碰到空节点){
返回,递归终止;、
}
递归调用inorder(root.left)遍历左子树
将root节点的值加入答案
递归调用inorder(root.right)遍历右子树
}
3.3 分析该题目解题优势及难点
- 解题优势:
时间和空间复杂度较低。
时间复杂度:O(n)(其中 n为二叉树节点的个数。二叉树的遍历中每个节点会被访问一次且只会被访问一次。)
空间复杂度:O(h),h 是树的高度(空间复杂度取决于递归的栈深度,而栈深度在二叉树为一条链的情况下会达到 O(n) 的级别。) - 难点:暂无。