数据结构(七)---树

目录

一.树的基本概念

二.树的性质

三.二叉树

1.二叉树的基本概念

2.特殊的二叉树

(1)满二叉树

(2)完全二叉树

(3)二叉排序树

(4)平衡二叉树

3.二叉树的性质

4.完全二叉树的性质 

5.二叉树的存储结构

(1)顺序存储

(2)链式存储

6.二叉树的遍历

7.二叉树的层序遍历

8.由遍历序列构造二叉树

考法1:前序+中序遍历序列

考法2:后序+中序遍历序列

考法3:层序+中序遍历序列

9.线索二叉树

(1)线索二叉树的作用

(2)线索二叉树的存储结构

线索二叉树的定义:

中序线索化:

先序线索化:

后序线索化:

10.根据线索二叉树找前驱/后继

(1)中序线索二叉树

•找中序后继

•找中序前驱

(2)先序线索二叉树

•找先序后继

•找先序前驱

(3)后序线索二叉树

•找后序前驱

•找后序后继


如有错误,还望佬们指正!!💖💖💖

一.树的基本概念

1.空树:结点数为0的树

2.非空树:至少有一个结点的树

•有且仅有一个根节点

•没有后继的结点称为“叶子结点”(或终端结点)

•有后继的结点称为“分支结点”(或非终端结点)

•除了根节点外,任何一个结点都有且仅有一个前驱

•每个结点可以有0个或多个后继

•除了跟节点外,其余结点可以分为互不相交的有限集合,其中每个集合又是一棵树,并且称为根结点的子树

同理,子树也可以划分为多个互不相交的集合,所以树是一种递归定义的数据结构

结点之间的关系:

祖先结点:从该结点出发到根节点路径上所有的结点都是祖先结点。

例如上图,F的祖先结点为"父亲","爷爷"
子孙结点:从该结点出发,其所有的分支结点。

例如上图,“父亲”的子孙结点为:"你","F","K","L"
双亲结点(父节点):一个结点的直接前驱。

例如,“F”和“你”的父结点都是“父亲”
孩子结点:一个结点的直接后继。

例如,“父亲”的孩子结点是“F”和“你”
兄弟结点:“F”和“你”互为兄弟结点。

路径与路径长度:

路径是某个结点到达另一个结点的路径,只能从上往下,例如“爷爷”结点到"你"结点时有路径的,而"你"到"爷爷"结点是没有路径的。

路径长度就是经过了几条边:“爷爷”结点到"你"结点的路径长度为2

结点、树的属性描述:

结点的层次(深度)--从上往下数(默认从1开始)

例如,B的深度为2
结点的高度--从下往上数

例如,B的高度为3
树的高度(深度)--总共多少层

例如,上图树的高度为4
结点的度--有几个孩子(分支)

例如,D结点的度为3
树的度--各结点的度的最大值

例如,上图的树的度为3

有序树:从逻辑上看,树中结点的各子树从左至右是有次序的,不能互换。

无序树:从逻辑上看,树中结点的各子树从左至右是无次序的,可以互换

树和森林:

森林是m(m>=0)棵互不相交的树的集合。

二.树的性质

1.结点数=总度数+1

总的结点数等于各个结点的度数之和+1,由于根结点是没有前驱结点的,所以各个结点的度数相加后还需+1。

2.度为m的数与m叉树的区别

树的度--各结点的度的最大值
m叉树--每个结点最多只能有m个孩子的树

例如,度为3的树,表示这棵树中至少有一个结点的度数为3,而3叉树则规定了每个结点至多有3个孩子,就算所有结点度数都小于3,也是一个合法的三叉树,当然也可以有3叉空树。

所以对于一个度为m的树而言,其至少有m+1个结点。

总结如下:

3.度为 m 的树第 i 层至多有 m^{i-1} 个结点(i>=1)

如图,这是一棵度为3的树,第一层只能有根结点,由于树的度为3,所以他最多有3个孩子,也就是第二层最多为3,第二层的每个结点又最多各有3个孩子,以此类推:

m叉树第 i 层至多有 m^{i-1} 个结点(i>=1)

由于m叉树也规定了每一个结点至多有m个孩子,所以结论和度为m的树是一样的

4.高度为 h 的 m 叉树至多有 \frac{m^h-1}{m-1} 个结点。

将每一层至多的结点数相加即可。等比数列求和就可以得到\frac{m^h-1}{m-1}

5.高度为 h 的m叉树至少有 h 个结点

m叉树只规定了每个结点孩子数量的上限是多少,没有规定下限,所以高度为h的m叉树至少有h个结点。

而对于度为m的数而言,高度为h、度为m的树至少有 h+m-1 个结点。

度为m的树中,至少要保证一个结点的孩子结点的数量为m,所以度为m的树至少有h+m-1个结点

6.具有n个结点的m叉树的最小高度为\left \lceil log_{m}(n(m-1)+1) \right \rceil

整理一下得到:

m^h-1<n(m-1)+1\leq m^h

对三个部分分别取对数:

h-1< log_{m}(n(m-1)+1) \leq h

由于中间部分要大于h-1,所以h最小为

h= \left \lceil log_{m}(n(m-1)+1) \right \rceil

三.二叉树

1.二叉树的基本概念

二叉树是n(n>=0)个结点的有限集合:
①或者为空二叉树,即n=0。
②或者由一个根结点和两个互不相交的被称为根的左子树右子树组成。左子树和右子树又分别是一棵二叉树。

①每个结点至多只有两棵子树

②左右子树不能颠倒(二叉树是有序树)

注意二叉树和度为2的有序树的区别:度为2的有序树表示这棵树中至少有一个结点他的孩子结点有两个。二叉树则是每个结点最多只能有2个孩子结点。

二叉树的五种状态如下图所示:

2.特殊的二叉树
(1)满二叉树

一棵高度为h,且含有2^h-1个结点的二叉树。通俗来讲就是,除了最下面的叶子结点外,其他结点都有两个分支。

例如下图,第一层有1个结点,第二层有2个结点,第三层有4个结点,第四层有8个结点,高度为4的满二叉树,含有的结点个数就为:2^4-1=15个结点(等比数列求和)。

满二叉树的特点:

①只有最后一层有叶子结点

②不存在度为1的结点

③按层序从1开始编号,结点i的左孩子为2i,右孩子为2i+1;结点 i 的父节点为 \left \lfloor i/2 \right \rfloor (如果有的话)

(2)完全二叉树

当且仅当其每个结点都与高度为 h 的满二叉树中编号为 1~n 的结点一一对应时,称为完全二叉树。也就是说可以在满二叉树的基础上,将一些编号更大的结点去掉。

假若现在只去掉13号结点,那么按照层次的编号规则,7号结点的孩子结点编号分别为13,14,但是在满二叉树中,对应结点的编号为14,15,不能一一对应,所以这棵树不是完全二叉树。

所以如果某个结点只有一个孩子,那么一定是左孩子

:满二叉树是一种特殊的完全二叉树,但是完全二叉树未必是满二叉树。

完全二叉树的特点:

①只有最后两层可能有叶子结点。

②最多只有一个度为1的结点。
③按层序从1开始编号,结点i的左孩子为2i,右孩子为2i+1;结点 i 的父节点为 \left \lfloor i/2 \right \rfloor (如果有的话),这一点和满二叉树一样。

④总共有n个结点,i\leq \left \lfloor n/2 \right \rfloor为分支结点,i> \left \lfloor n/2 \right \rfloor为叶子结点。

(3)二叉排序树

二叉排序树。一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树:

1.左子树上所有结点的关键字均小于根结点的关键字;

2.右子树上所有结点的关键字均大于根结点的关键字。

3.左子树和右子树又各是一棵二叉排序树。

例如想要插入新元素68,那么可以与结点依次比较,最终确定插入的位置:

① 68>19,与其右孩子进行比较,② 68>50,与其右孩子进行比较,③ 68>66,继续与其右孩子进行比较,④ 68<70,将其作为70的左孩子。

二叉排序树可用于元素的排序和搜索。

(4)平衡二叉树

树上任一结点的左子树和右子树的深度之差不超过1。

例如下图,对于根结点而言,其左子树深度为2,右子树深度为3,那么对于根结点,满足平衡二叉树的条件。

同理,以66作为根节点的子树也满足这一条件。所有的结点都满足这一条件,那么这棵树是平衡二叉树。

而下图所示的二叉树,对于根结点而言,其左子树深度为1,右子树深度为6,深度之差超过1了,所以这棵树不是平衡二叉树。

对于平衡二叉树而言,其搜索效率是较高的。例如,寻找70结点,对于左边的平衡二叉树而言,只需要2步,对于右边的树而言,则需要6步。

3.二叉树的性质

(1)设非空二叉树中度为0、1和2的结点个数分别为n0、n1和n2,则n0=n2+1,即叶子结点比二分支结点多一个。

假设树中结点总数为n,则

①n=n0 + n1 + n2(二叉树中只可能有度为0,1,2的结点)

n=n_1+2n_2+1(树的结点树=总度数+1)

度为1的结点有n1个,度为2的结点有n2个,由于度为2的每个结点又有两个孩子结点所以为2n_2,再加上一个根结点:n=n_1+2n_2+1

② - ① 得到:n0=n2+1

可以用下面两个图验证一下:

(2)二叉树第 i 层至多有 2^{i-1} 个结点(i>=1),之前讲过m叉树第 i 层至多有 m^{i-1} 个结点(i>=1)

(3)高度为 h 的二叉树至多有 2^h-1 个结点( 满二叉树 ),之前讲过高度为h的m叉树至多有\frac{m^h-1}{m-1}个结点,把m=2,即可得到2^h-1

4.完全二叉树的性质 

(1)具有n个( n >0 )结点的完全二叉树的高度h为\left \lceil log_2(n+1) \right \rceil 或 \left \lfloor log_2n \right \rfloor+1

解法1

高为 h 的满二叉树共有 2^h-1 个结点

高为 h-1的满二叉树共有 2^{h-1}-1个结点

推出:2^{h-1}-1<n\leq 2^h-1

进一步地:2^{h-1}<n+1\leq 2^h

得到:h-1<log_2(n+1)\leq h

最后得到 :h=\left \lceil log_2(n+1) \right \rceil

解法2

高为 h-1的满二叉树共有 2^{h-1}-1个结点,完全二叉树要比这样的满二叉树至少多一个结点

也就是:2^{h-1}个结点

高为 h 的满二叉树共有 2^h-1 个结点,若在此基础上+1,这样的完全二叉树的高度为h+1,所以完全二叉树高度的范围:

2^{h-1}\leq n< 2^h

同时取对数:h-1\leq log_2n<h

刚好小于等于则:h-1=\left \lfloor log_2n \right \rfloor

最后得到:h=\left \lfloor log_2n \right \rfloor+1

同理,第i个结点所在层次为\left \lceil log_2(n+1) \right \rceil 或 \left \lfloor log_2n \right \rfloor+1

(2)对于完全二叉树,可以由结点数n推出度为0、1和2的结点个数为n0、n1和n2

完全二叉树最多只有一个度为1的结点,即:n1=0或1

二叉树中,n0=n2+1,那么n0+n2一定是奇数

若完全二叉树有 2k 个(偶数)个结点,则必有n1=1, n0=k,n2=k-1

若完全二叉树有 2k-1 个(奇数)个结点,则必有n1=0,n0=k,n2=k-1

总结:

5.二叉树的存储结构
(1)顺序存储
#define MaxSize 100
struct TreeNode{
    ElemType value;    //结点中的数据元素
    bool isEmpty;    //结点是否为空
};

TreeNode t[MaxSize];
//定义一个长度为 MaxSize 的数组t,按照从上至下、从左至右的顺序依次存储完全二叉树中的各个结点

如下图所示:可以让第一个位置空缺,保证数组下标和结点编号一致。由于使用静态数组存储结点,所以二叉树中的结点数量是有限的。

根据完全二叉树的性质:

•i 的左孩子为2i

•i 的右孩子为2i+1

•i 的父节点为\left \lfloor i+2 \right \rfloor

•i 所在的层次为\left \lceil log_2(n+1) \right \rceil 或 \left \lfloor log_2n \right \rfloor+1

若完全二叉树中共有n个结点,则:

•判断 i是否有左孩子:2i<=n,因为i的左孩子为2i,若2i存在(2i<=n),则说明有左孩子

•判断 i是否有右孩子:与左孩子同理,判断2i+1<=n

•判断 i是否有叶子/分支结点:判断 i>\left \lfloor n/2 \right \rfloor

对于普通二叉树而言,结点的编号不能反映出结点的逻辑关系,所以可以把二叉树的结点编号与完全二叉树对应起来。并放到数组的相应位置中。

对于普通二叉树,就不能采用之前完全二叉树中判断是否有左右孩子 或 是否是叶子结点的方法了: 只能通过isEmpty字段判断,例如5号结点的左孩子为10(2i),那么就检查10的isEmpty字段,若isEmpty为true,那么5号没有左孩子。

采用顺序存储的方式存储二叉树,可能导致大量空间的浪费。例如,最坏的情况:高度为h且只有 h 个结点的单支树 (所有结点只有右孩子),也至少需要2^h-1个存储单元。

所以二叉树的顺序存储结构,只适合存储完全二叉树。

(2)链式存储
//二叉树的结点
typedef struct BiTNode{
    ElemType data;
    struct BiTNode *lchild,*rchild;     //数据域
}BiTNode, *BiTree;    //左,右孩子指针

 

由于每个结点有2个指针域,那么n个结点就会对应2n个指针域,除了头结点外,其他结点都有前驱结点,所以n个结点的二叉链表共有 n+1(2n-(n-1)) 个空链域。这些空链域可以用于构造线索二叉树。

•定义一个二叉树:

struct ElemType{
    int value;
};

typedef struct BiTNode{
    ElemType data;
    struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;

// 定义一棵空树
BiTree root = NULL;

// 插入根节点
root = (BiTree)malloc(sizeof(BiTNode));
root->data.value = 0; // 假设根节点的值为 0,你可以根据实际情况修改
root->lchild = root->rchild = NULL;

// 插入新结点
BiTNode *p = (BiTNode *)malloc(sizeof(BiTNode));
p->data.value = 2;
p->lchild = NULL;
p->rchild = NULL;
root->lchild = p;    // 作为根节点的左孩子

对于二叉树的链式存储而言,找到指定结点p的左/右孩子非常容易,但是找到其父结点,则只能从根开始遍历寻找。如果需要经常访问某结点的父节点,可以再创建一个父节点指针:

//三叉链表
typedef struct BiTNode{
    ElemType data;    //数据域
    struct BiTNode *lchild,*rchild;    //左、右孩子指针
    struct BiTNode *parent;    //父节点指针
}BiTNode,*BiTree;

6.二叉树的遍历

二叉树的遍历规则

先序/根遍历:根左右(NLR)

中序/根遍历:左根右(LNR)

后序/根遍历:左右根(LRN)

例如下面这棵二叉树,其先序,中序,后序遍历分别为: 

对于做题方法,我之前有写过一篇:http://t.csdnimg.cn/kQQIg 

对于算数表达式的“分析树”,进行先序遍历,中序遍历和后序遍历的结果分别对应的是这个算数序列的前缀表达式,中缀表达式和后缀表达式。

先序遍历,中序遍历和后序遍历代码实现如下:

struct ElemType{
    int value;
};

typedef struct BiTNode{
    ElemType data;
    struct BiTNode *lchild,*rchild;    
}BiTNode,*BiTree;

void visit(BiTree node) {
    printf("%d ", node->data.value);
}

//先序遍历
void PreOrder(BiTree T){
    if(T!=NULL){
        visit(T);    //访问根结点
        PreOrder(T->lchild);    //递归遍历左子树
        PreOrder(T->rchild);    //递归遍历右子树
    }
}

//中序遍历
void PreOrder(BiTree T){
    if(T!=NULL){
        InOrder(T->lchild);    //递归遍历左子树
        visit(T);    //访问根结点
        InOrder(T->rchild);    //递归遍历右子树
    }
}

//后序遍历
void PreOrder(BiTree T){
    if(T!=NULL){
        PostOrder(T->lchild);    //递归遍历左子树
        PostOrder(T->rchild);    //递归遍历右子树
        visit(T);    //访问根结点
    }
}

这样递归实现的算法,空间复杂度(h+1),h指的是2叉树的高度,+1是因为最后一层的叶子结点下面还有空结点需要处理,也就是说空结点的信息也需要压入栈中进行处理。

如图所示,从根节点出发,一条路如果左边还有没走的路,优先往左边走,走到路的尽头(空结点)就往回走如果左边没路了,就往右边走,如果左、右都没路了,则往上面走。

可以观察到,从根结点出发的路径,会依次路过每个结点3次:

若第1次路过时就访问访问结点,则为先序遍历上图的灰色字表示访问顺序)。

若第2次路过时访问访问结点,则为中序遍历

若在第3次路过时访问结点,则为后序遍历

以上也是一种求遍历序列的方法,但是不如上面讲的方法简单。

求树的深度:

int treeDepth(BiTree T){
    if(T == NULL){
        return 0;
    }
    else {
        int l=treeDepth(T->lchild);
        int r=treeDepth(T->rchild);
        //树的深度=Max(左子树深度,右子树深度)+1
        return l>r ? l+1 : r+1;
    }
}

7.二叉树的层序遍历

二叉树的层序队列,需要一个队列辅助

① 根结点入队。

② 若队列非空,则队头结点出队,访问该结点,并将其左、右孩子插入队尾(如果有的话)。

③ 重复②直至队列为空。

以下图为例:

① 根结点入队:

② 队列非空,队头结点出队,访问该结点,并将其左、右孩子插入队尾:

③ B出队,访问B,并将B结点的左右孩子入队:

④ 不断重复上面步骤,直至队列为空:

代码如下:

//二叉树的结点(链式存储)
typedef struct BiTNode{
    char data;
    struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;

//链式队列结点
typedef struct LinkNode{
    BiTNode *data;    //存指针
    struct LinkNode *next;
}LinkNode;

typedef struct{    
    LinkNode *front,*rear;    //队头队尾
}LinkQueue;

//层序遍历
void LevelOrder(BiTree T){
    LinkQueue Q;
    InitQueue(Q);    //初始化辅助队列
    BiTree p;    
    EnQueue(Q,T);    //根结点入队
    while(!IsEmpty(Q)){    //队列不为空则循环
        DeQueue(Q,p);
        visit(p);    //访问出队结点
        if(p->lchild!=NULL)
            EnQueue(Q,p->lchild);    //左孩子入队
        if(p->rchild!=NULL)    
            EnQueue(Q,p->rchild);    //右孩子入队
    }

}

如果对队列的基本操作不熟悉可以看:

队列的基本操作

8.由遍历序列构造二叉树

在这篇博客中,我也有讲到:http://t.csdnimg.cn/foLhQ

在这里针对考研的题型做补充:

对于前序遍历,中序遍历,后序遍历或层次遍历任何一种遍历序列而言,都对应着多种二叉树形态,例如:

所以,若只给出一棵二叉树的前/中/后/层序遍历序列中的一种,不能唯一确定一棵二叉树。只有给出其中两种,才能唯一确定一棵二叉树,并且其中一种必须为中序遍历。因为需要根据中序遍历序列,划分左右子树,再根据其他的遍历序列确定左右子树的根节点。若是前序、后序、层序序列两两组合,一定不能推出一棵唯一的二叉树。例如:

考法1:前序+中序遍历序列

分析:

① 前序遍历中,第一个是根节点,所以A是根节点。

② 再看中序遍历序列,A确定为根节点,那么他左边的就是左子树对应的中序遍历序列,右边就是右子树对应的中序遍历序列(这里只有E一个节点)。

对应下图:

③ 先序遍历中根节点后面的三个对应节点,就是左子树的前序遍历序列。

根据相同的逻辑,D一定是根节点,再根据中序遍历序列,B一定是他的左孩子,C是他的右孩子。对应下图:

考法2:后序+中序遍历序列

跟后序+中序遍历原理类似。

分析:

①根据后序遍历的最后一个节点,判断根节点为“D”。

②那么中序遍历中,D的左边为左子树,D的右边为右子树。

③再看左子树"EAF",左子树在后序遍历的顺序为"EFA",那么左子树的根节点为"A",E为左孩子,F为右孩子。

④右子树"HCBGI"在后序遍历中的顺序为"HCIGB",同理B是根节点。

⑤由于"HC"的后序遍历序列为"HC",所以C是根节点,中序遍历中"H"在“C”的左边,所以"H"是"C"的左孩子。"GI"同理。

考法3:层序+中序遍历序列

分析

① 根据层序遍历序列的第一个节点,判断"D"是根节点。

② 在层序遍历中,访问完第一层之后,继续访问第二层的两个节点,即“AB”,所以"A","B"都是根节点。

从中序遍历可以看出,若A是根节点,那么“E”“F”分别是其左右孩子。同理,B的下一层,左边是HC,右边是GI。

③根据层序遍历序列,第三层为“EFCG”四个,再中序遍历序列可知C的左孩子为H,G的右孩子是I

再看一个例子:

根据上面的步骤得到树为:

9.线索二叉树
(1)线索二叉树的作用

对二叉树进行先序/中序/后序遍历之后,会得到一个遍历序列,虽然二叉树的数据元素是非线性的关系,但是通过遍历序列可以使二叉树的元素之间存在线性关系,例如下图中,B的前驱元素是G,后继元素是E。

注:一个二叉树中的数据元素只有1个前驱,0~2个后继。而上面说的前驱和后继是基于遍历序列的。

普通二叉树的缺点:

① 对二叉树而言,能否从G节点开始遍历整棵树呢?不能,因为G节点的只有指向其孩子的指针,没有指向双亲的指针。而对于线性的遍历序列而言,是可以从G开始遍历的。

② 对二叉树而言,要找到指定节点的前驱,必须从头进行一次完整的中序遍历,例如下图,想找到p的前驱,用指针q记录当前访问的结点,指针 pre 记录上一个被访问的结点。

按照中序遍历序列的顺序依次遍历,直到q==p,那么pre指向的就是p的前驱。

找p的后继同理,只需要让指针再移动依次,当pre=p时,q就为后继。

void findPre(BiTree T){
    if(T!=NULL)
        InOrder(T—>lchild);
        visit(T);
        InOrder(T->rchild);
}

//访问结点q
void visit(BiTNode *q){
    if (q==p)      //当前访问结点刚好是结点p
        final = pre;    //找到p的前驱
    else
        pre = q;    //pre指向当前访问的结点

}
//辅助全局变量,用于查找结点p的前驱
BiTNode *p;    //p指向目标结点
BiTNode * pre=NULL;    //指向当前访问结点的前驱
BiTNode * final=NULL;    //用于记录最终结果

若某些应用场景中,找前驱,找后继或者遍历操作十分频繁,那么就可以用线索二叉树

如何将普通二叉树转变为线索二叉树

之前说过n个结点的二叉树,有n+1个空链域。可用来记录前驱、后继的信息,例如G节点,可以将其左孩子指针指向D(前驱),右孩子指针指向B(后继)。再例如,D节点,他是中序遍历中第一个被访问的节点,所以其左孩子指针指向NULL,表示其没有前驱。

其他节点同理,线索化后就能得到中序线索二叉树:

指向前驱、后继的指针称为“线索”

线索指向中序前驱,即指定节点在中序遍历序列中的前驱,中序后继,指定节点在中序遍历序列中的后继。(后面所讲的“先序前驱”,“先序后继”,“后序前驱”,“后序后继”同理)

现在想找到某个节点的前驱和后继,只需要根据其前驱线索和后继线索就能找到了。但是若某个节点的右孩子指针指向的是他的右孩子,而不是后继,例如上图B,要怎么找后继呢?

(2)线索二叉树的存储结构
线索二叉树的定义:
//二叉树的结点(链式存储)
typedef struct BiTNode{
    ElemType data;
    struct BiTNode*lchild,*rchild;
}BiTNode,*BiTree;
//二叉链表

//线索二叉树结点
typedef struct ThreadNode{
    ElemType data;
    struct ThreadNode *lchild,*rchild;
    int ltag,rtag;    //左、右线索标志
}ThreadNode,*ThreadTree;
//线索链表

当tag=0时,说明指针指向的是孩子,ltag=0,指针指向左孩子,rtag=0,指针指向右孩子。当tag=1时,说明指针指向的是线索,ltag=1,前驱线索,rtag=1,后继线索。 

同理,先序线索二叉树为:

增加tag值后为:

后序线索二叉树为:

增加tag后:

中序线索化:
typedef struct ThreadNode{
    ElemType data;
    struct ThreadNode *lchild,*rchild;
    int ltag,rteg;    //左、右线索标志

}ThreadNode, *ThreadTree;

//全局变量,指向当前访问结点的前驱    
ThreadNode *pre =NULL;

//中序遍历二叉树,一边遍历一边线索化
void InThread(ThreadTree T){
    if(T!=NULL){
        InThread(T->lchild);  
        visit(T);
        InThread(T->rchild);
    }
}

void visit(ThreadNode *g){
    if(q->lchild==NULL){    //左子树为空,建立前驱线索
        q->lchild=pre;
        q->ltag=1;
    }
    if(pre!=NULL && pre->rchild==NULL){
        pre->rchild=q;   //建立前驱结点的后继线索
        pre->rtag=1;
    }
    pre=q;
}

① 若当前结点的左孩子为空,那么建立前驱线索,刚开始前驱为pre=NULL。

② 由于pre ==NULL,跳过第二个循环,并且将pre指向下一个节点。

③ 接下来访问G节点,由于G节点左指针为空,所以将其左指针指向pre。并将pre指向下一个节点。

④接下来访问B节点,B节点左孩子非空,但他的pre指针指向的节点G,右孩子是空的,那么就将G节点的右孩子指针线索化,指向其后继,也就是q(pre->rchild=q,第2个循环)。

其他节点的线索化同理,当访问到最后一个节点后,pre=q指向最后一个节点。之后就不会再访问其他节点了,那最后一个节点的右孩子也是空的,应该也要将其线索化才对,怎么做呢?

检查pre 的 rchild 是否为 NULL,如果是,则令rtag=1。

即:

if(pre->rchild==NULL)

        pre->rtag=1;

typedef struct ThreadNode{
        ElemType data;
        struct ThreadNode *lchild,*rchild;
        int ltag,rtag;
}

//全局变量
ThreadNode *pre=NULL;

void CreateInThread(ThreadNode *T){
    pre=NULL;
    if(T!=NULL){
        InThread(T);
        if(pre->rchild==NULL)
            pre->rtag=1; //处理遍历的最后一个节点
    }
}

//中序遍历二叉树,一边遍历一边线索化
void InThread(ThreadTree T){
    if(T!=NULL){
        InThread(T->lchild);  
        visit(T);
        InThread(T->rchild);
    }
}

void visit(ThreadNode *g){
    if(q->lchild==NULL){    //左子树为空,建立前驱线索
        q->lchild=pre;
        q->ltag=1;
    }
    if(pre!=NULL && pre->rchild==NULL){
        pre->rchild=q;   //建立前驱结点的后继线索
        pre->rtag=1;
    }
    pre=q;
}

王道书中的代码:

//pre设为引用类型,就相当于在函数外设置了一个全局变量
void InThread(ThreadTree p,ThreadTree &pre){
    if(p!=NULL){
        //递归线索化左子树
        InThread(p->lchild,pre);

        if(p->lchild==NULL){
            p->lchild=pre;
            p->ltag=1;
        }
        if(pre!=NULL && pre->rchild==NULL){
            pre->rchild=p;
            pre->rtag=1;
        }
        pre=p;
        //递归线索化右子树
        InThread(p->rchild,pre);
    }   
}

void CreateInThread(Thread T){
    ThreadTree pre=NULL;
    if(T!=NULL){
        InThread(T,pre);
        pre->rchild=NULL;
        pre->rtag=1;
    }
}

上面的代码在处理最后一个节点时,为什么没有判断rchild=NULL:

因为中序遍历中,访问顺序为"左根右",最后一个被访问的节点一定没有右孩子。 

先序线索化:
//全局变量
ThreadNode *pre=NULL;

//先序遍历二叉树,一边遍历一遍线索化
void PreThread(ThreadTree T){
    if(T!=NULL)
        visit(T);
        PreThread(T->rchild);    
        PreThread(T->lchild);
   
}

void visit(ThreadNode *q){
    if(q->lchild==NULL){
        q->lchild=pre;
        q->ltag=1;
    if(pre!=NULL && pre->rchild==NULL)
        pre->rchild=q;
        pre->rtag=1;
    }
    pre=q;
}

这个代码有一个问题:

在访问完第3个节点后,D的前驱线索指向B,接下来会继续处理这个节点的左子树,但是我们已经把D的左孩子指针指向了B,所以处理左子树时,q指针会再次指回B,这样访问节点会进入死循环。

所以修改PreThread函数,通过ltag来判断lchild是左孩子还是前驱线索:

void PreThread(ThreadTree T){
    if(T!=NULL)
        visit(T);
        if(T->ltag==0)    //lchild不是前驱线索
            PreThread(T->lchild);    
        PreThread(T->rchild);
}

完整代码如下:

//全局变量
ThreadNode *pre=NULL;

//先序遍历二叉树,一边遍历一遍线索化
void PreThread(ThreadTree T){
    if(T!=NULL)
        visit(T);
        if(T->ltag==0)    //lchild不是前驱线索
            PreThread(T->lchild);    
        PreThread(T->rchild);
}

void visit(ThreadNode *q){
    if(q->lchild==NULL){
        q->lchild=pre;
        q->ltag=1;
    if(pre!=NULL && pre->rchild==NULL)
        pre->rchild=q;
        pre->rtag=1;
    }
    pre=q;
}

void CreatePreThread(ThreadTree T){
    pre=NULL;
    if(T!=NULL){
        PreThread(T);
        if(pre->rchild==NULL)
            pre->rtag=1;    //处理遍历的最后一个节点
    }
}

王道书中的先序线索化,处理逻辑是相同的:

void PreThread(Threadtree p,ThreadTree &pre){
//访问根节点
    if(p!=NULL){
        if(p->rchild==NULL)
            p->lchild=pre;
            p->ltag=1;
    }
    if(pre!=NULL && pre->rchild==NULL){
        pre->rchild=p;
        pre->rtag=1;
    }
    pre=p;
//访问左子树
    if(p->ltag==0)
        PreThread(p->lchild,pre);
//访问右子树
    PreThread(p->rchild,pre);
}

void CreatePreThread(ThreadTree T){
    ThreadTree pre=NULL;
    if(T!=NULL){
        PreThread(T,pre);
        if(pre->rchild==NULL)    
            pre->rtag=1;
    }
}

后序线索化:
//全局变量
ThreadNode *pre=NULL;

void CreatePostThread(ThreadTree T){
    pre=NULL;
    if(T!=NULL){
        PostThread(T);
        if(pre->rchild==NULL)
            pre->rtag=1;
    }
}

//后遍历二叉树,一边遍历一边线索化
void PostThread(ThreadTree T){
    if(T!=NULL){
        PostThread(T);
        PostThread(T);
        visit(T);
    }
}

void visit(ThreadNode *q){
    if(q->lchild==NULL){
        q->lchild=pre;
        q->ltag=1;
    }
    if(pre!=NULL && pre->rchild==NULL){
        pre->rchild=q;
        pre->rtag=1;
    }
    pre=q;
}

在王道书中的后续线索化:

//后序线索化
void PostThread(ThreadTree p,ThreadTree &pre){
    if(p!=NULL){
        PostThread(p->lchild,pre);//递归,线索化左子树
        PostThread(p->rchild,pre);//递归,线索化右子树
        //访问根节点
        if(p->lchild==NULL){
            p->lchild=pre;
            p->ltag=1;
        }
        if(pre!=NULL && pre->rchild==NULL){
            pre->rchild=p;
            pre->rtag=1;
        }
        pre=p;
    }
}

void CreatePostThread(ThreadTree T){
    ThreadTree pre=NULL;
    if(T!=NULL){
        PostThread(T,pre);
//一定不要忘记处理最后一个节点
        if(pre->rchild==NULL)
            pre->rtag==1;
    }
}

为什么在先序遍历中会出现死循环问题,而在后序遍历和中序遍历中不会:

因为后序遍历(左右根),中序遍历(左根右)中,在处理根节点时,其左孩子一定已经处理完了。而在先序遍历(根左右)中,需要先处理根节点,再处理左孩子。处理根节点时,会将其左孩子指针指向前驱,那么再处理左孩子时,则当前又访问回前驱节点,从而导致节点的遍历陷入死循环。

10.根据线索二叉树找前驱/后继
(1)中序线索二叉树
•找中序后继

在中序线索二叉树中找到指定结点*p的中序后继 next:

① 若p->rtag==1,则next=p->rchid;

② 若p->rtag==0,那么指定节点一定有右孩子,也就是右子树非空。按照“左根右”的规则,右子树中第一个被访问的结点,就是p的后继。也就是p的右子树中最左下角的结点。

//找到以p为根的子树中,第一个被中序遍历的结点
ThreadNode *Firstnode(ThreadNode *p){
    //循环找到最左下结点(不一定是叶节点)
    while(p->ltag==0)
        p=p->lchild;
    return p;
}

//在中序线索二叉树中找到结点p的后继结点
ThreadNode *Nextnode(ThreadNode *p){
    if(p->rtag=0)
        return Firstnode(p->rchild);
    else
        return p->rchild;    //rtag=1直接返回后继线索
}

//对中序线索二叉树进行中序遍历(利用线索实现的非递归算法)
void Inorder(ThreadNode *T){
    for(ThreadNode *p=Firstnode; p!=NULL; p=Nextnode(p))
        visit(p);
}
•找中序前驱

在中序线索二叉树中找到指定结点*p的中序前驱 pre:

①若 p->ltag==1,则 pre= p->lchild
②若 p->ltag==0,则p一定左孩子,按照"左根右"的规则,结点p的前驱一定是左子树中按照中序遍历最后被访问的结点,即pre=p的左子树中最右下的结点

//找到以p为根的子树中,最后一个被中序遍历的结点
ThreadNode *Lastnode(ThreadNode *p){
    //循环找到最右下结点(不一定是叶子节点)
    while(p->rtag==0)
        p=p->rchild;
    return p;
}

//在中序线索二叉树中找到结点p的前驱结点
ThreadNode *Prenode(ThreadNode *p){
    //左子树中最右下结点
    if(p->ltag==0)
        return Lastnode(p->lchild);
    else 
        return p->lchild;    //ltag==1直接返回前驱线索

//对中序线索二叉树进行中序遍历
void RevInorder(ThreadNode *T){
    for(ThreadNode *p=Lastnode(T); p!=NULL; p=Prenode(p))
        visit(p);
}
(2)先序线索二叉树
•找先序后继

在先序线索二叉树中找到指定结点*p的先序后继next

①若 p->rtag==1,则next = p->rchild
②若 p->rtag==0,则说明p一定有右孩子,假设p结点有左孩子,那么按照”根左右“的规则,p的后继一定是左子树中第一个被访问的结点 ,即左孩子。

假设没有左孩子,那么按照"根右"的规则,p的后继是右子树中第一个被访问的结点,即右孩子。

// 在先序线索二叉树中找到结点p的后继结点
ThreadNode *Nextnode(ThreadNode *p) {

    if (p->rtag == 0){
        if(p->lchild!=NULL)        // 如果p有左孩子
            return p->lchild;
        else        // 如果p没有左孩子,返回右孩子
            return p->rchild;
        }
    else
        return p->rchild;    //rtag=1直接返回后继线索    
}

// 对先序线索二叉树进行先序遍历
void Preorder(ThreadNode *T) {
    ThreadNode *p = T;
    while (p != NULL) {
        visit(p);  // 访问当前节点
        p = Nextnode(p); // 移动到下一个节点
    }
}
•找先序前驱

在先序线索二叉树中找到指定结点*p的先序前驱 pre

① 若 p->ltag==1,则next = p->lchild
② 若 p->ltag==0,那么p结点一定有左孩子,但是按照"根左右"的规则,p左右子树的所有结点只可能是p的后继,不可能是p的前驱。所以不可能在其左右子树中找到p的前驱。

所以在先序线索中找先序前驱,只能从头开始进行先序遍历。

这个结论是建立在二叉链表的基础上的,如果是三叉链表,也就是有指向父节点的指针,那么:

如果能找到p的父结点,且p是左孩子,按照"根左右"的规则,p结点一定是在父节点之后就被访问的结点。所以p的父节点一定是其前驱。

如果能找到p的父节点,且p是右孩子,其左兄弟为空。按照"根右"的规则,p的父节点一定为p的前驱。

 如果能找到p的父节点,且p是右孩子,其左兄弟非空。按照"根左右"的规则,p的前驱为 左兄弟子树中最后一个被先序遍历的结点。

想要找左兄弟子树中最后一个被先序遍历的结点,则对于左子树而言,一定要优先往有右边的路走,走到尽头,再往左子树走;若没有右边的路,先往左子树走,左子树中一旦碰到有右边的路,就往右边走。例如下图,C的前驱结点为H。

如果p是根节点,则p没有先序前驱。

//找到以p为根的子树中,最后一个被先序遍历的结点
ThreadNode *Lastnode(ThreadNode *p){
    //循环找到最右下结点(不一定是叶子节点)
    while(p->rtag==0)
        p=p->rchild;
    return p;
}

//在先序线索二叉树中找到结点p的前驱结点
ThreadNode *Prenode(ThreadNode *p){
    if (p->rtag == 0){
        if (p->parent == NULL) {
            printf("p为根结点,没有前驱!");
            return NULL;
        } else if (p->parent->lchild == p) {
            // p是父节点的左孩子,返回父节点
            return p->parent;
        } else {
            // p是父节点的右孩子,且父节点没有左孩子,返回父节点
            if (p->parent->lchild == NULL) {
                return p->parent;
            } else {
                // p是父节点的右孩子,且父节点有左孩子,返回左孩子为根的子树中最后一个被先序遍历的结点
                return Lastnode(p->parent->lchild);
            }
        }
    }
    else
        return p->rchild;    //rtag=1直接返回后继线索    
}

//对先序线索二叉树进行先序遍历
void RevPreorder(ThreadNode *T){
    for(ThreadNode *p=Lastnode(T); p!=NULL; p=Prenode(p))
        visit(p);
}
(3)后序线索二叉树
•找后序前驱

在后序线索二叉树中找到指定结点*p的后序前驱 pre

①若 p->ltag==1,则 pre = p->lchild
②若 p->ltag==0,则p结点一定有左孩子,假设有右孩子,按照后序遍历中"左右根"的规则,p结点的后序前驱一定是右子树中按照后序遍历最后一个被访问到的结点。那么p的右孩子就是p的后序前驱。

若p结点没有右孩子,按照"左根"的规则,p结点的前驱是左子树当中按照后序遍历最后一个被访问的结点,也就是p的左孩子

//在后序线索二叉树中找到结点p的前驱结点
ThreadNode *Prenode(ThreadNode *p){

    if(p->ltag==0){
        //如果有右孩子,那么右孩子就是p的前驱
        if(p->rchild!=NULL)
            return p->rchild;
        else
            return p->lchild;    //如果没有右孩子,那么左孩子就是p的前驱
    }
    else 
        return p->lchild;    //ltag==1直接返回前驱线索
}

//对后序线索二叉树进行后序遍历
void RevPostorder(ThreadNode *T){
    ThreadNode *p = T;
    while (p != NULL) {
        visit(p);  // 访问当前节点
        p = Prenode(p); // 移动到下一个节点
    }
}
•找后序后继

在后序线索二叉树中找到指定结点*p的后序后继 next

①若 p->rtag==1,则next = p->rchild
②若 p->rtag==0,说明p结点一定有右孩子,按照"左右根"的规则,p的左右子树中所有结点只可能是他的前驱,不可能是他的后继结点,所以不能在p的左右子树中找到后序后继。

若想要在后序线索二叉树中找到后序后继,只能从头开始先序遍历。

同理,如果用三叉链表,即可以找到p节点的父节点:

如果能找到p的父节点,且p是右孩子。那么p的后序后继一定是其父节点。

如果能找到p的父节点,且p是左孩子,其右兄弟为空。那么p的后序后继也是p的父节点。

如果能找到p的父节点,且p是左孩子,其右兄弟非空,p的后序后继为右兄弟子树中第一个被后序遍历的节点。

如何找到右兄弟子树中第一个被后序遍历的节点?

对于右子树而言,一定要优先走左边的路,如果左边的路走到尽头,就走右子树,如果没有左子树,就先走右边的路,遇到左子树就走左边。如下图所示可知,B的后序后继为G。

如果p是根节点,则p没有后序后继。因为在后序遍历中,根节点一定是最后一个被访问的。

// 找到以p为根的子树中,第一个被后序遍历的结点
ThreadNode *Firstnode(ThreadNode *p){
    // 循环找到最左下结点(不一定是叶节点)
    while(p->ltag == 0)
        p = p->lchild;
    return p;
}

// 在后序线索二叉树中找到结点 p 的后继结点
ThreadNode *Nextnode(ThreadNode *p){
    if(p->rtag == 0){  
        if(p->parent == NULL){  
            printf("p为根结点,没有后继!");
            return NULL;
        } else if(p->parent->rchild == p){    //如果p为父结点的右结点
            return p->parent;
        } else{
            if(p->parent->lchild == p && p->parent->rchild == NULL){    
            //如果p为父结点的左结点,且p的父结点的右结点为空
                return p->parent;
            } else{
            //如果p为父结点的左结点,且p的父结点的右结点不为空
                return Firstnode(p->rchild);   
            }
        }
    } else{
        return p->rchild;    // rtag=1 直接返回后继线索
    }
}

// 对后序线索二叉树进行后序遍历
void Postorder(ThreadNode *T){
    for(ThreadNode *p = Firstnode(T); p != NULL; p = Nextnode(p))
        visit(p);
}

总结:

中序线索二叉树找前驱和后继都是可以的,但在先序线索二叉树中不能找到前驱,只能从头开始进行先序遍历,除非使用三叉链表。同理,后序线索二叉树不能找到后继,只能从头开十四进行后序遍历,除非使用三叉链表。


最后,不懂*和&的区别及应用场景的可以看看这篇:http://t.csdnimg.cn/6eprE

不懂.和->的区别的话,推荐看看这篇:http://t.csdnimg.cn/l5oL2

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/569872.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【ARMv9 DSU-120 系列 -- Utility bus 详细介绍 2】

文章目录 ARM DSU-120DSU-120 Utiity BusCluster and core PPUPPU寄存器的访问性PPU寄存器的作用系统组件基地址ARM DSU-120 DSU-120 Utiity Bus 在ARMv9架构中,DSU-120(Dynamic Shared Unit 120)是一个关键组件,用于管理核心和系统组件之间的通信与协作。某些系统组件寄存…

嵌入式Linux driver开发实操(二十一):linux device driver basic设备驱动程序基础

linux的架构框图: 内核空间是内核(即操作系统的核心)执行(即运行)并提供其服务的地方。 用户空间是执行用户应用程序的地方。 内核模块是可以根据需要加载和卸载到内核中的代码片段。它们扩展了内核的功能,而无需重新启动系统。自定义代码可以通过两种方法添加到Linux内…

『FPGA通信接口』串行通信接口-IIC(1)接口协议

文章目录 1.IIC简介1.1 IIC概述1.2 总线结构 2.IIC通信协议2.1消息种类2.2通信过程 3.开发思路4.仿真分析5.传送门 1.IIC简介 1.1 IIC概述 IIC即 Inter-Integrated Circuit(集成电路总线&#xff09;&#xff0c;是由 Philips 半导体公司在八十年代初设计出来的一种简单、双向…

字节跳动(社招)三面算法原题

TikTok 喘息 继上月通过强制剥离 TikTok 法案后&#xff0c;美国众议院在当地时间 20 日下午以 360 票赞成 58 票反对通过了新的法案&#xff1a;剥离 TikTok 的期限由生效后 165 天调整至 270 天之内&#xff0c;即今年 11 月的美国总统大选后。 之前我们讲过&#xff0c;TikT…

【文件上传与包含漏洞综合利用】DVWA-文件上传-难度:High

实验过程和结果 步骤1&#xff1a;尝试直接上传php木马&#xff0c;失败&#xff0c;截图如下&#xff1a; 步骤2&#xff1a;将php木马后缀改为jpeg尝试上传&#xff0c;依旧失败&#xff0c;截图如下&#xff1a; 步骤3&#xff1a;将真实的jpeg图片1.jpeg上传&#xff0c;成…

云原生Service Mesh服务网格简单介绍

serviceMesh是什么 Service Mesh是一个用于处理服务间通信的基础设施层&#xff0c;旨在实现云原生应用复杂服务拓扑中的可靠请求传递。其基本构成是一组与应用一起部署的轻量级网络代理&#xff0c;这些代理对应用来说是透明的。Service Mesh通过统一的方式来控制和处理服务间…

基于openwrt交叉编译opencv4.9.0版本

源码包的获取 源码获取有两种方式&#xff0c;一种是通过编译时在makefile指定它的git地址&#xff0c;在编译时下载&#xff0c;这种很依赖网速&#xff0c;网速不好时&#xff0c;编译会失败。另一种是我们将源码的压缩包下载到本地&#xff0c;放到我们的SDK中&#xff0c;…

UltraScale+的10G/25G Ethernet Subsystem IP核使用

文章目录 前言一、设计框图1.1、xxv_ethernet_01.2、xxv_ethernet_0_sharedlogic_wrapper1.3、xxv_ethernet_0_clocking_wrapper1.4、xxv_ethernet_0_common_wrapper 二、IP核配置三、仿真四、上板测速五、总结 前言 前面我们学习了很多基于XILINX 7系列的高速接口使用&#x…

CC攻击频发,企业如何做好网络安全,该怎么防护能免遭CC攻击?

在当前网络现状下&#xff0c;随着信息技术的飞速发展&#xff0c;网络攻击手段也愈发多样化和复杂化。其中&#xff0c;CC攻击作为一种针对Web应用层的拒绝服务攻击&#xff0c;其危害日益凸显&#xff0c;对企业和个人造成了严重的威胁。下面我们就从多个角度详细分享关于CC攻…

SpringCloudAlibaba入门学习笔记20240408~20240424

跟学b站“图灵架构师”SpringCloudAlibaba入门教程 系统架构演化进程 单体应用架构>垂直应用架构>分布式架构>SOA架构>微服务架构 1、针对微服务架构&#xff1a; 如何管理众多小服务&#xff1f;(服务治理 注册中心[服务注册 发现 剔除])nacos 众多小服务之间如…

智能驾驶+网络安全

在智能驾驶场景下&#xff0c;安全问题一直是一个持续热点。 针对车机模块不被黑客利用Linux的漏洞攻击&#xff0c;可以采取以下几种方式来提高安全性&#xff1a; 安全设计和防护&#xff1a;在设计车机模块时&#xff0c;需要考虑安全性&#xff0c;并采取相应的安全防护措施…

【Yolov系列】Yolov5学习(一):大致框架

一、Yolov5网络结构 Yolov5特点&#xff1a; 合适于移动端部署&#xff0c;模型小&#xff0c;速度快 Yolov5骨干结构&#xff1a;CSPDarknet53网络Yolov5主要有Yolov5s、Yolov5m、Yolov5l、Yolov5x四个版本。这几个模型的结构基本一样&#xff0c;不同的是depth_multiple模型…

C++ | Leetcode C++题解之第42题接雨水

题目&#xff1a; 题解&#xff1a; class Solution { public:int trap(vector<int>& height) {int n height.size();if (n 0) {return 0;}vector<int> leftMax(n);leftMax[0] height[0];for (int i 1; i < n; i) {leftMax[i] max(leftMax[i - 1], he…

SpringMVC 源码剖析

SpringMVC 源码剖析 0 从源码角度分析SpringMVC执行流程 // 前端控制器&#xff0c;SpringMVC最核心的类 public class DispatcherServlet extends FrameworkServlet {// 前端控制器最核心的方法&#xff0c;这个方法是负责处理请求的&#xff0c;一次请求&#xff0c;调用一次…

数据类型总结

1 引言 在计算机的世界里&#xff0c;数据类型是被人类定义出来的&#xff0c;方便人去更好地理解、辨别数据。计算机只能识别二进制数&#xff0c;不可能要求写代码时&#xff0c;只是输入一些0/1的东西。通过定义数据类型&#xff0c;可以让人和计算机更好地“沟通”&#x…

图像处理|关于二维傅里叶变换的学习笔记(实用版)

因为图像至少是2D的&#xff0c;所以在数字图像处理中使用的都是2D-傅里叶变换。 1.什么是傅里叶变换(DFT)&#xff1f;傅里叶变换是将图像从空间域转换到频域&#xff0c;其逆变换是将图像从频域转到空间域。 物理意义是&#xff1a; 2.频谱图怎么看&#xff1f;傅里叶频谱图…

亚马逊、ozon、美客多等平台的测评技术核心:提升跨境电商业绩的关键要素

现今&#xff0c;越来越多的跨境卖家开始深入了解测评自养号这一领域&#xff0c;他们希望通过优化运营来降低成本并增加利润。在整个测评工作中&#xff0c;测评技术是非常关键的一环。只有不断学习、保持冷静&#xff0c;我们才能不断提升测评能力&#xff0c;从而获得更多机…

Bentley二次开发教程22-文件及模型管理-材质、图层

材质 材质主要用于对元素进行材质贴图&#xff0c;以表现实际的材料样式。材质表中包含材质表&#xff0c;材质面板以及材质。而其属性记录了反射等多种属性以表达实际材质效果。 创建材质 当我们需要创建自定义的材质时&#xff0c;对应的&#xff0c;需要依次创建材质表&…

JavaScript:js实现在线五子棋人机(人人)对弈

在线五子棋人机对弈 全部使用前端技术,使用HTML,CSS以及JS进行实现. 棋盘在后端就是一个15*15的二维数组 页面设计 页面设计的比较粗糙 主要使用js自带的canvas画布进行绘画 HTML代码如下: <div class"outer"><canvas id"canvas" height&qu…

男生一般穿什么裤子好看?五大爆款男装精选测评!

男生裤子要怎么选才能找到适合自己的裤子呢&#xff1f;这肯定是大家选裤子时经常出现的一个疑问了&#xff0c;现在的市面上虽然款式风格非常多&#xff0c;但是由于品牌鱼龙混杂的原因&#xff0c;不同的裤子质量也参差不齐。为了帮助各位男同胞能选到适合自己的裤子&#xf…