数据结构(七)---二叉树

目录

一.树的基本概念

二.树的性质

三.二叉树

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

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

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

相关文章

CC软件防火墙和WEB应用防火墙哪个好

本文将从CC软件防火墙的定义、原理、功能以及应用方面进行全面探讨&#xff0c;旨在加深对CC软件防火墙的理解&#xff0c;并推动网络安全意识的普及。以及WEB应用防火墙二者之间的对比。让用户更了解两个形态产品并作出选择。 第一部分&#xff1a;CC软件防火墙的定义和原理 …

北京小米智能工厂

小米工厂智能化 小米集团昌平区的小米智能工厂二期&#xff0c;成为引领智能化制造的重要一环。这座工厂计划打造成为京津冀地区智能制造示范工厂和全球级的“灯塔工厂”。 工厂位于小米未来产业园区&#xff0c;占地81000平方米&#xff0c;年产能可达千万台智能手机&#xff…

创新指南 | 2024年企业如何十步打造最佳的数字化营销策略组合

营销是一个动态且不断变化的领域。顶级的数字营销策略随着消费者和技术趋势的变化而变化。这就是为什么每个公司都需要一个经过良好规划并具有明确里程碑和目标的营销策略。一旦你有了正确的计划&#xff0c;你实现为业务设定的目标的可能性就会大大增加。这意味着&#xff0c;…

提交链码-编辑前后端,调用链码功能

一 . 链码介绍 1.什么链码&#xff1f; • 链码是一段用 Go、Node.js 或者 Java 实现了规定接口的程序。链码在安全的Docker容器中运行&#xff0c; 与背书节点的进程隔离。通过应用程序提交的交易&#xff0c;链码初始化和管理账本状态。• 链码通常处理网络成员协商达成的业…

全网都在找的python+requests接口自动化测试框架实例详解教程

前言 Python是一种功能强大的编程语言&#xff0c;它可以用于自动化测试&#xff0c;特别是接口自动化测试。许多Python库都可以用于接口自动化测试&#xff0c;其中requests库是其中最受欢迎的库之一。 requests库可以用于发送HTTP请求并获取服务器响应&#xff0c;从而轻松…

spring常用注解(五)lombok库

一、介绍&#xff1a; 1、简介&#xff1a; Lombok是一个作用于编辑器和构建工具的 Java 库&#xff0c;可以对编写的 Java 代码进行增强&#xff0c;比如说不用再写实体类的 getter 方法&#xff0c;equals 方法而是自动生成&#xff0c;自动生成日志输出变量等等&#xff0…

uniapp 之 开发微信小程序入门详细指南

目录 配置运行设置&#xff08;编辑器的设置&#xff09;项目目录文件配置基础配置中的uniapp应用标识&#xff08;AppID&#xff09;配置微信小程序的AppID 总结 配置运行设置&#xff08;编辑器的设置&#xff09; 点击编辑器上方菜单栏 - 运行 - 运行到小程序模拟器 - 运行…

css利用transform:skew()属性画一个大屏的背景斜面四边形特效

在工作工程中需要写一个如下的大屏背景&#xff0c;是由几个斜面做成的效果 使用css transform function中的skew()方法实现画其中一个斜面&#xff0c;然后调整背景色实现 写一个div <div class"skew_container test-2"><div class"skew_container_it…

【Linux进程】守护进程

【Linux进程】守护进程 目录 【Linux进程】守护进程守护进程守护进程概念进程组和会话的概念 系统的守护进程函数 作者&#xff1a;爱写代码的刚子 时间&#xff1a;2024.4.27 前言&#xff1a;本篇博客将会介绍守护进程&#xff0c;以及进程组和会话的概念&#xff0c;如何变成…

【信息收集】WAF防火墙识别工具Wafw00f

★★免责声明★★ 文章中涉及的程序(方法)可能带有攻击性&#xff0c;仅供安全研究与学习之用&#xff0c;读者将信息做其他用途&#xff0c;由Ta承担全部法律及连带责任&#xff0c;文章作者不承担任何法律及连带责任。 1、了解WAF防火墙 Web应用防护系统&#xff08;也称为&…

【Pytorch】(十三)模型部署: TorchScript

文章目录 &#xff08;十三&#xff09;模型部署: TorchScriptPytorch动态图的优缺点TorchScriptPytorch模型转换为TorchScripttorch.jit.tracetorch.jit.scripttrace和script的区别总结trace 和script 混合使用保存和加载模型 &#xff08;十三&#xff09;模型部署: TorchScr…

基于java+springboot+vue实现的医疗挂号管理系统(文末源码+Lw)203

摘 要 在如今社会上&#xff0c;关于信息上面的处理&#xff0c;没有任何一个企业或者个人会忽视&#xff0c;如何让信息急速传递&#xff0c;并且归档储存查询&#xff0c;采用之前的纸张记录模式已经不符合当前使用要求了。所以&#xff0c;对医疗挂号信息管理的提升&#x…

Pytorch 之torch.nn初探 卷积--Convolution Layers

任务描述 本关任务&#xff1a; 本关提供了一个Variable 类型的变量input&#xff0c;按照要求创建一 Conv1d变量conv&#xff0c;对input应用卷积操作并赋值给变量 output&#xff0c;并输出output 的大小。 相关知识 卷积的本质就是用卷积核的参数来提取原始数据的特征&a…

OpenHarmony语言基础类库【@ohos.util.Stack (线性容器Stack)】

ohos.util.Stack (线性容器Stack) Stack基于数组的数据结构实现&#xff0c;特点是先进后出&#xff0c;只能在一端进行数据的插入和删除。 Stack和[Queue]相比&#xff0c;Queue基于循环队列实现&#xff0c;只能在一端删除&#xff0c;另一端插入&#xff0c;而Stack都在一…

[Qt的学习日常]--信号和槽

前言 作者&#xff1a;小蜗牛向前冲 名言&#xff1a;我可以接受失败&#xff0c;但我不能接受放弃 如果觉的博主的文章还不错的话&#xff0c;还请点赞&#xff0c;收藏&#xff0c;关注&#x1f440;支持博主。如果发现有问题的地方欢迎❀大家在评论区指正 本期学习&#xff…

PyQt6 优化操作:建立侧边栏,要求可拖拽改变宽度,可用按钮控制侧边栏的展开和收起

1. 官方文档 QSplitter — PyQt Documentation v6.6.0 2. 效果展示 可拖拽改变宽度比例 点击按钮快速收起或展开侧边栏 点击按钮&#xff0c;侧边栏收起&#xff0c;同时按钮图标变为向左箭头 (对应展开功能)&#xff0c;再次点击按钮&#xff0c;侧边栏展开&#xff0c;同…

Pycharm新建工程时使用Python自带解释器的方法

Pycharm新建工程时使用Python自带解释器的方法 新建Project时最好不要新建Python解释器&#xff0c;实践证明&#xff0c;自己新建的Python解释器容易出现各种意想不到的问题。 那么怎样使用Python安装时自带的解释器呢&#xff1f; 看下面的三张截图大家就清楚了。 我的Pyth…

英智数字孪生机器人解决方案,赋能仓库物流模式全面升级

工业机械臂、仓储机器人、物流机器人等模式的机器人系统在现代产业中扮演着愈发重要的角色&#xff0c;他们的发展推动了自动化和智能化水平的提高&#xff0c;有助于为制造业、物流业、医疗保健业和服务业等行业创造新效率并提升人们的生活质量。 行业面临的挑战 机器人开发、…

Windows安装Elasticsearch 7.9.2

1 下载 https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.9.2-windows-x86_64.zip 2 配置 进入config目录&#xff0c;打开elasticsearch.yml文件&#xff0c;给集群和节点配置名称。 cluster.name: my-es node.name: node-1 3 启动 打开bin目录&am…

Docker之常见FAQ记录清单

一、前言 本文记录Docker使用过程中遇见的问题&#xff0c;供后续回顾参考。 关联资源&#xff1a;网络Docker博客、官方FAQ、文档、Docker 从入门到实践、中文社区、riptutorial 二、问题及处理记录 2.1、docker容器内没有vi,nano等编辑器 1&#xff09;如果宿主机本地有&a…