数据结构与算法(C语言版)P8---树、二叉树、森林

news2024/11/25 7:05:15

【本节目标】

  • 树概念及结构。
  • 二叉树概念及结构。
  • 二叉树常见OJ题练习。

1、树概念及结构

1.1、树的概念

树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一颗倒挂的树,也就是说它是根朝上、而叶朝下的。

  • 有一个特殊的结点,称为根结点,根节点没有前驱节点。
  • 除根节点外,其余节点被分为成M(M>0)个互不相交的集合T1、T2、…、Tm,其中每个集合Ti(1<=i<=m)又是一棵结构与树类似的子树。每颗子树的根节点有且只有一个前驱,可以有0个或多个后继。
  • 因此树是递归定义的。

在这里插入图片描述

在这里插入图片描述

概念:

  • 没有父节点的节点称为根节点。
  • 没有子节点的节点称为叶节点。

在这里插入图片描述

  • 子树是不相交的。
  • 除了根节点外,每个节点有且仅有一个父节点。
  • 一棵N个节点的树有N-1条边。

下面来说一下树的常见概念:以下图为例
在这里插入图片描述

结点的度:一个节点含有的子树的个数称为该节点的度。如上图:A节点的度为6。

叶(子)结点或终端结点:度为0的节点成额为叶节点。如上图:B、C、H、I…等节点为叶节点。

非终端结点或分支结点:度不为0的节点。如上图:D、E、F、G…等节点为分支节点。

双亲结点或父结点:若一个节点含有子节点,则这个节点称为其子节点的父节点。如上图:A是B的父节点。

孩子结点或子结点:一个节点含有的子树的根节点称为该节点的子节点。如上图:B是A的子节点。

兄弟结点:具有相同父节点互称为兄弟节点。如上图:B、C是兄弟节点。

树的度:一棵树中,最大节点的度称为树的度。如上图:树的度为6。

结点的层次:从根开始定义起,根为第一层,根的子节点为第2层,以此类推。

树的高度或深度:树中节点的最大层次。如上图:树的高度为4。

结点的祖先:从根到该结点所经分支上的所有节点。如上图:A是所有节点的祖先。A、E、J是Q的祖先。

子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙。I、J、P、Q是E的子孙。

森林:由m(m>0)棵树互不相交的多棵树的集合称为森林。(数据结构中的学习并查集本质就是森林)。

树一定是森林,单森林不一定是树。

1.2、树的表示

树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦,实际中树有很多种表示方法,如:双亲表示法,孩子表示法,孩子兄弟表示法等等。我们这里就简单的了解其中最常用的__孩子兄弟表示法__。

typedef  int DataType;
struct Node
{
	struct Node* _firstChild1;           //第一个孩子节点
	struct Node* _pNextBrother;         //指向其下一个兄弟节点
	DataType _data;                    //节点中的数据域
};

在这里插入图片描述

1.3、树在实际中的运用(表示文件系统的目录树结构)

在这里插入图片描述

2、二叉树概念及结构

为什么需要二叉树呢?

首先二叉树有以下特点:

  • 二叉树的结构最简单,规律性最强。
  • 可以证明,所有树都能转为唯一对应的二叉树,不失一般性。

普通树(多叉树)若不转化为二叉树,则运算很难实现。

二叉树在树结构的应用中起这非常重要的作用,因为对二叉树的许多操作算法简单,而任何树都可以与二叉树相互转换,这样就解决了树的存储结构及其运算中存在的复杂性。

2.1、概念

一棵二叉树是节点的一个有限集合,该集合或者为空,或者由一个根节点加上两棵称为左子树和右子树的二叉树组成。

二叉树的特点:

​ 1、每个节点最多由两颗子树,即二叉树不存在度大于2的节点。

​ 2、二叉树的子树有左右之分,其子树的次序不能颠倒。

在这里插入图片描述

2.2、二叉树的性质

性质一:在二叉树的第i层上至多有2^(i-1)个结点(i>=1)。第i层上至少有1个结点。

性质二:深度为k的二叉树至多有2^k - 1个结点(k>=1)。深度为k时至少有k个结点。

性质三:对任何一棵二叉树,如果度为0的叶节点个数为n0,度为2的分支节点个数为n2,则有n0 = n2+1。

如下图演示:

在这里插入图片描述

如上图所示:度为0的节点个数有8个(n0)。度为2的节点有7个(n2),所以n0 = n2+1。

我们看个图:
在这里插入图片描述

如上图所示:度为0的节点有两个(n0):F、E。度为2的节点有1个(n2):A。

所以:n0 = n2+1。

性质四:若规定根节点的层数为1,具有n个节点的满二叉树的深度h为h=log2 N(N是总结点个数)。

2.3、特殊的二叉树

2.3.1、满二叉树

满二叉树:一个二叉树,如果每一层的节点数都达到最大值,则这个二叉树就是满

二叉树。也就是说,如果一个二叉树的层数为k,且节点总数示(2^k)-1,则它就是满二叉树。

【性质】:满二叉树中度为1的节点最多为1个。度为1的个数要么为0要么为1。

所以满二叉树可以使用数组进行存储。

在这里插入图片描述

2.3.2、完全二叉树

完全二叉树:完全二叉树示效率很高的数据结构,完全二叉树是由满二叉树引出来的。对于深度为k的,由n个节点的二叉树,当且仅当其每一个节点都与深度为k的满二叉树中编号从1至n的节点一一对应时称为完全二叉树。要注意的是__满二叉树是一种特殊的完全二叉树__。

换句话说,完全二叉树就是:假设树的高度为h。

  • 前h-1层都是满的。最后一层可以全满,也可以不满。
  • 如果最后一层不满,要求最后一层结点从左向右都是连续的。

关于完全二叉树的性质:

性质一:具有n个结点的完全二叉树的深度为[log2 N] + 1。

N代表完全二叉树的结点总数。

[x]:称作x的底,表示不大于x的最大整数。加入x=3.14,那[x]=3。

例题:如下图,求完全二叉树的深度

在这里插入图片描述

可以看到此完全二叉树的结点总数为12,那直接套公式:[log2 N] + 1,log2 N约等于3.x,所以[log2 N]的结果为3,然后再加1,最终结果为4。所以此完全二叉树的深度为4。

性质二:探讨双亲节点和子节点的关系。

  • 如果i=1,则结点i是二叉树的根,无双亲,如果i>1,则其双亲是结点[i/2]。

  • 如果双亲节点编号是i,那么此双亲结点的左节点编号为2i,右节点编号是2i+1。

2.4、二叉树的存储结构

二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。链式结构又分:二叉链,三叉链。

在这里插入图片描述

2.4.1、顺序存储:

顺序结构存储就是使用__数组来存储__,一般使用数组只适合表示完全二叉树(包含满二叉树),因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储,关于堆在后面的章节会专门讲解。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。

完全二叉树存储:

在这里插入图片描述

非完全二叉树存储:

在这里插入图片描述

例题:二叉树结点数值采用顺序存储结构,如图所示。画出二叉树结构

在这里插入图片描述

解题思路:画出满二叉树的图,按照序号一次填入。

在这里插入图片描述

2.4.2、链式存储

二叉树的链式存储结构是指:用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。通常的方法是链表中的每一个节点由三个域组成,数据域和左右指针域,左右指针分别用来给出该节点左孩子和右孩子所在的链接点的存储地址。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,后面课程学到高阶数据结构,如:红黑树等会用到三叉链。

在这里插入图片描述

在这里插入图片描述

//二叉链
struct Node
{
	struct Node* _firstChild1;           //指向当前节点的左孩子
	struct Node* _pNextBrother;         //指向当前节点的右孩子
	DataType _data;                    //节点中的数据域
};

//三叉链
struct BinaryTreeNode
{
	struct BinTreeNode* pParent;     //指向当前节点的双亲
	struct BinTreeNode* pLeft;       //指向当前节点的左孩子
	struct BinTreeNode* pRight;      //指向当前节点的右孩子
	int data;                    //节点中的数据域
};

在n个结点的二叉链表中,有n+1个空指针域。

3、二叉树的一些操作

首先我们在看待二叉树时,应该是这样看待:任何一颗二叉树有三个部分:

  • 根节点
  • 左子树
  • 右子树

在这里插入图片描述

下面我们将要使用的算法是:

​ 分治算法:分而治之,把大问题分成类似子问题,子问题再分为子问题。知道子问题不在可分割。

3.1、二叉树链式结构的遍历

所谓遍历(Traversal)是指沿着某条搜索路线,依次对树中每个节点均作一次且只做一次访问。访问节点所做的操作依赖于具体的应用问题。遍历是二叉树上最重要的运算之一,是二叉树上进行其它运算之基础。

在这里插入图片描述

__前序/中序/后序的递归结构遍历:__是根据访问节点操作发生位置而命名的。

​ 1、NLR:前序遍历(Preorder Traversal称为先序遍历)——访问根节点的操作发生在遍历其左右子树之 前。

​ 2、LNR:中序遍历(Inorder Traversal)——访问根节点的操作发生在遍历其左右子树之中(间)。

​ 3、LRN:后序遍历(Postorder Travedsal)——访问根节点的操作发生在遍历其左右子树之后。

由于被访问的节点必是某子树的根,所以__N(Node)、L(Left subtree)和R(Right subtree)又可解释为:根、根的左子树和根的右子树。__NLR、LNR、LRN分别又称为:先根遍历、中根遍、后根遍历。

前序,中序,后序遍历又叫做深度优先遍历。

下面我们以图示,来说明__前序遍历、中序遍历、后序遍历__

在这里插入图片描述

前序遍历(先根):访问顺序:A—>B—>D NULL NULL—>E NULL NULL—>C NULL NULL。

  • 先放问A,然后访问A的左子树,也就是P1部分。
  • P1部分,先访问B,然后访问B的左子树,也就是D部分,由于D的左子树和右子树都为NULL。所以B的左子树访问结束。之后再访问B的右子树,也就是E部分,由于E的左子树和右子树都为NULL。所以B的右子树访问结束。
  • 拿到这个时候A的左子树访问完毕,接着访问A的右子树,也就是P2部分。右C的左子树和右子树都为NULL。
  • 所以整个二叉树访问完毕。

中序(中根):左子树 根 右子树

访问顺序:NULL D NULL—>B—>NULL E NULL—>A—>NULL C NULL。

简化顺序:D B E A C。

后序(后根):左子树 右子树 根

访问顺序:NULL NULL D—>NULL NULL E—>B —>NULL NULL C—>A。

简化顺序:D E B C A。

代码实现:

#include <stdio.h>
#include <stdlib.h>

typedef int BTDataType;

typedef struct BinaryTreeNode
{
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
	char data;
}BTNode;

//前序
void PrevOrder(BTNode* root)
{
	//判断根节点是否为空,为空直接返回
	if (root == NULL)
	{
		return;
	}

	printf("%c ", root->data);
	PrevOrder(root->left);
	PrevOrder(root->right);

}

//中序
void InOrder(BTNode* root)
{
	if (root == NULL)
		return;

	InOrder(root->left);
	printf("%c ", root->data);
	InOrder(root->right);
}

//后序
void PostOrder(BTNode* root)
{
	if (root == NULL)
		return;

	PostOrder(root->left);
	PostOrder(root->right);
	printf("%c ", root->data);
}

int main()
{
	BTNode* A = (BTNode*)malloc(sizeof(BTNode));
	A->data = 'A';
	A->left = NULL;
	A->right = NULL;

	BTNode* B = (BTNode*)malloc(sizeof(BTNode));
	B->data = 'B';
	B->left = NULL;
	B->right = NULL;

	BTNode* C = (BTNode*)malloc(sizeof(BTNode));
	C->data = 'C';
	C->left = NULL;
	C->right = NULL;

	BTNode* D = (BTNode*)malloc(sizeof(BTNode));
	D->data = 'D';
	D->left = NULL;
	D->right = NULL;

	BTNode* E = (BTNode*)malloc(sizeof(BTNode));
	E->data = 'E';
	E->left = NULL;
	E->right = NULL;

	A->left = B;
	A->right = C;
	B->left = D;
	B->right = E;
	
    printf("前序:");
	PrevOrder(A);
	printf("\n");
	
    printf("中序:");
	InOrder(A);
	printf("\n");

    printf("后序:");
	PostOrder(A);
	printf("\n");
	return 0;
}

输出:

在这里插入图片描述

  • 时间复杂度:O(n) //每个结点只访问一次。
  • 空间复杂度:O(n) //栈占用的最大辅助空间。

3.2、通过使用遍历统计的方法来计算二叉树节点个数

void TreeSize(BTNode* root, int* psize)
{
	if (root == NULL)
	{
		return;
	}
	else
	{
		++(*psize);
	}
	TreeSize(root->left,psize);
	TreeSize(root->right,psize);
}

int main()
{
    //计算以A为根节点的二叉树节点个数
	int Asize = 0;
	TreeSize(A, &Asize);
	printf("以A为根节点的节点个数为:%d\n", Asize);
    
    //计算以B为根节点的二叉树节点个数
	int Bsize = 0;
	TreeSize(B, &Bsize);
	printf("以B为根节点的节点个数为:%d\n", Bsize);
}

输出:

在这里插入图片描述

3.3、通过分治的思路来计算二叉树节点个数

int TreeSize(BTNode* root)
{
	return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}

printf("以A为根节点的节点个数为:%d\n", TreeSize(A));
printf("以B为根节点的节点个数为:%d\n", TreeSize(B));

输出:

在这里插入图片描述

分析:如下图:

在这里插入图片描述

3.4、通过遍历统计的方法计算二叉树中叶子节点的个数

void TreeLeafSize(BTNode* root, int* psize)
{
	if (root->left == NULL && root->right == NULL)
	{
		++(*psize);
	}
	else
	{
		TreeLeafSize(root->left, psize);
		TreeLeafSize(root->right, psize);
	}
}

int main()
{
    int a = 0;
	TreeSize(A, &a);
	printf("%d\n",a);
}

3.5、通过分治的思路来计算二叉树中叶子节点的个数

int TreeLeafSize(BTNode* root)
{
	if (root == NULL)
		return 0;
	if (root->left == NULL && root->right == NULL)
		return 1;
	return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}

int main()
{
    printf("%d\n", TreeLeafSize(A));
}

3.6、复制二叉树(递归)

核心思路:

  • 如果是空树,递归结束。
  • 否则,申请新节点空间,复制根节点
    • 递归复制左子树。
    • 递归复制右子树。

代码实现:

#include <stdio.h>
#include <stdlib.h>

typedef struct BinaryTreeNode
{
    struct BinaryTreeNode* left;
    struct BinaryTreeNode* right;
    char data;
}BTNode;

//暴力创建二叉树
BTNode* CreateTree()
{
    BTNode* A = (BTNode*)malloc(sizeof(BTNode));
    A->data = 'A';
    A->left = NULL;
    A->right = NULL;

    BTNode* B = (BTNode*)malloc(sizeof(BTNode));
    B->data = 'B';
    B->left = NULL;
    B->right = NULL;

    BTNode* C = (BTNode*)malloc(sizeof(BTNode));
    C->data = 'C';
    C->left = NULL;
    C->right = NULL;

    BTNode* D = (BTNode*)malloc(sizeof(BTNode));
    D->data = 'D';
    D->left = NULL;
    D->right = NULL;

    BTNode* E = (BTNode*)malloc(sizeof(BTNode));
    E->data = 'E';
    E->left = NULL;
    E->right = NULL;

    A->left = B;
    A->right = C;
    B->left = D;
    B->right = E;

    return A;
}

//复制二叉树
void CopyTree(BTNode* root,BTNode** copy_root)
{
    if (root == NULL)
    {
        //如果主二叉树为空,那就将副二叉树指控,也就是说不复制了。
        *copy_root = NULL;
        return 0;
    }
    else
    {
        *copy_root = (BTNode*)malloc(sizeof(BTNode));
        (*copy_root)->data = root->data;
        CopyTree(root->left, &(*copy_root)->left);
        CopyTree(root->right, &(*copy_root)->right);
    }
}

//中序遍历
void InOrder(BTNode* root)
{
    if (root == NULL)
    {
        return;
    }
    InOrder(root->left);
    printf("%c ", root->data);
    InOrder(root->right);
}

//后序遍历
void PostOrder(BTNode* root)
{
    if (root == NULL)
    {
        return;
    }
    PostOrder(root->left);
    PostOrder(root->right);
    printf("%c ", root->data);
}

int main()
{
    BTNode* root = CreateTree();
    BTNode* copy_root;

    CopyTree(root, &copy_root);

    printf("中序遍历:");
    InOrder(copy_root);

    printf("\n");

    printf("后序遍历:");
    PostOrder(copy_root);
    return 0;
}

3.7、二叉树的深度

核心思想:

  • 如果是空树,则深度为0。
  • 否则,递归计算左子树的深度记为m,递归计算右子树的深度记为n,二叉树的深度则为m与n的较大者加1。

代码展示:

int maxDepth(struct TreeNode* root){
    if (root == NULL)
    {
        return 0;
    }
    int leftDepth = maxDepth(root->left);
    int rightDepth = maxDepth(root->right);
    return leftDepth > rightDepth ? leftDepth+1 : rightDepth+1;
}

3.8、二叉树的销毁

这里直接说结论:使用后序遍历进行销毁

核心代码实现:

//二叉树的销毁
void DestroyTree(struct TreeNode* root)
{
    if (root == NULL)
    {
        return;
    }

    DestroyTree(root->left);
    DestroyTree(root->right);

    free(root);
    root = NULL;
}

4、二叉树层序遍历的实现(非递归实现)

4.1、利用队列实现层序遍历

上面我们进行二叉树的遍历都是用递归的方法(前序,中序,后序),又叫深度优先遍历。

那可不可以使用非递归的方法来遍历二叉树呢?可以!!!(在上面我们也写了2个案例)。

此方法叫做:层序遍历,广度优先遍历。

这里借助__队列实现(先进先出)。__

层序遍历的作用是将二叉树,从上到下,从左到右依次遍历。如下图遍历的结果是A->B->C->D->E->F->G->H。

这种方法的核心思路就是:上一层带下一层。

具体实现方法,如下图:

在这里插入图片描述

1、首先有个队列,先把节点A放进去。

在这里插入图片描述

2、取出A节点,注意:重点来了。我们说核心思路就是:上一层带下一层。因为A连接下一层的B,C节点。所以把A取出来之后,先把B,C节点放进队列中去。

在这里插入图片描述

3、然后取出节点B,由于B连接的下层有:D,E节点。所以在取出B节点之后,先把D,E节点放进队列中。

在这里插入图片描述

4、然后将C结点取出,由于C左右子树为F,G结点。所以在取出C结点后,在把F,G进栈,如下图:

在这里插入图片描述

就这样以此类推,实现效果。

这里简化代码量,使用一个简单的二叉树(如下),和上面的原理一样,就是少创建几个二叉树结点。

在这里插入图片描述

4.2、代码全放在一个原文件中

代码实现:这里的队列使用前面所学写的队列。并且所有代码都在一个源文件中:

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>

//typedef struct BinaryTreeNode* QDataType;

typedef struct BinaryTreeNode
{
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
	char data;
}BTNode;

typedef BTNode* QDataType;

typedef struct QueueNode
{
	struct QueueNode* next;
	QDataType data;
}QNode;

typedef struct Queue
{
	QNode* head;
	QNode* tail;
}Queue;




void QueueInit(Queue* pq);
void QueueDestroy(Queue* pq);
void QueuePush(Queue* pq, QDataType x);
void QueuePop(Queue* pq);     //在对头删除数据
QDataType QueueFront(Queue* pq);    //取对头的数据
QDataType QueueBack(Queue* pq);    //取对尾的数据
int QueueSize(Queue* pq);          //计算队列中数据个数
bool QueueEmpty(Queue* pq);



void QueueInit(Queue* pq)
{
	assert(pq);
	pq->head = NULL;
	pq->tail = NULL;
}

QNode* BuyQueueNode(QDataType x)
{
	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	if (newnode == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

void QueueDestroy(Queue* pq)
{
	assert(pq);
	QNode* cur = pq->head;
	while (cur != NULL)
	{
		QNode* next = cur->next;
		free(cur);
		cur = next;
	}
	pq->head = pq->tail = NULL;
}

void QueuePush(Queue* pq, QDataType x)  //插入数据,其实就是尾插
{
	assert(pq);
	QNode* newnode = BuyQueueNode(x);
	if (pq->head == NULL)
	{
		pq->head = pq->tail = newnode;
	}
	else
	{
		pq->tail->next = newnode;
		pq->tail = newnode;

	}
}

void QueuePop(Queue* pq)     //在对头删除数据
{
	assert(pq);
	//防止pq->head == NULL,而导致程序崩溃。
	assert(!QueueEmpty(pq));
	QNode* next = pq->head->next;
	free(pq->head);
	pq->head = next;
	if (pq->head == NULL)
	{
		pq->tail = NULL;
	}
}

QDataType QueueFront(Queue* pq)    //取对头的数据
{
	assert(pq);
	assert(!QueueEmpty(pq));
	return pq->head->data;
}

QDataType QueueBack(Queue* pq)    //取对尾的数据 
{
	assert(pq);
	assert(!QueueEmpty(pq));
	return pq->tail->data;
}
int QueueSize(Queue* pq)          //计算队列中有多少的数据
{
	int count = 0;
	QNode* cur = pq->head;
	while (cur != NULL)
	{
		count++;
		cur = cur->next;
	}
	return count;
}

bool QueueEmpty(Queue* pq)
{
	assert(pq);
	return pq->head == NULL;
}

void LevelOrder(BTNode* root)
{
	Queue qq;
	QueueInit(&qq);
	if (root)
	{
		QueuePush(&qq, root);
	}
	while (!QueueEmpty(&qq))
	{
		BTNode* front = QueueFront(&qq);
		QueuePop(&qq);
		printf("%c ", front->data);

		if (front->left)
		{
			QueuePush(&qq, front->left);
		}

		if (front->right)
		{
			QueuePush(&qq, front->right);
		}
	}
	printf("\n");
	QueueDestroy(&qq);
}

int main()
{
	BTNode* A = (BTNode*)malloc(sizeof(BTNode));
	A->data = 'A';
	A->left = NULL;
	A->right = NULL;

	BTNode* B = (BTNode*)malloc(sizeof(BTNode));
	B->data = 'B';
	B->left = NULL;
	B->right = NULL;

	BTNode* C = (BTNode*)malloc(sizeof(BTNode));
	C->data = 'C';
	C->left = NULL;
	C->right = NULL;

	BTNode* D = (BTNode*)malloc(sizeof(BTNode));
	D->data = 'D';
	D->left = NULL;
	D->right = NULL;

	BTNode* E = (BTNode*)malloc(sizeof(BTNode));
	E->data = 'E';
	E->left = NULL;
	E->right = NULL;

	A->left = B;
	A->right = C;
	B->left = D;
	B->right = E;

	LevelOrder(A);
}

4.3、代码分布放

queuqe.h

#pragma once

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>

typedef struct BinaryTreeNode
{
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
	char data;
}BTNode;

typedef BTNode* QDataType;

typedef struct QueueNode
{
	QDataType data;
	struct QueueNode* Next;
}QNode;

typedef struct Queue
{
	struct QueueNode* head;
	struct QueueNode* tail;
}Queue;

//队列初始化
void QueueInit(Queue* pq);

//销毁
void QueueDestroy(Queue* pq);

//扩容
QNode* BuyQueueNode(QDataType x);

//对尾插入数据
void QueuePush(Queue* pq, QDataType x);

//队头删除数据
void QueuePop(Queue* pq);

//取队尾数据
QDataType QueueBack(Queue* pq);

//取对头数据
QDataType QueueFront(Queue* pq);

//统计队列元素个数
int QueueSize(Queue* pq);

//判断队列是否为空
bool QueueEmpty(Queue* pq);

queue.c

#include "queue.h"

//队列初始化
void QueueInit(Queue* pq)
{
	assert(pq);
	pq->head = NULL;
	pq->tail = NULL;
}

//销毁
void QueueDestroy(Queue* pq)
{
	assert(pq);
	QNode* cur = pq->head;
	while (cur)
	{
		QNode* next = cur->Next;
		free(cur);
		cur = next;
	}
	pq->head = pq->tail = NULL;
}

//扩容
QNode* BuyQueueNode(QDataType x)
{
	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	if (newnode == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	newnode->data = x;
	newnode->Next = NULL;
	return newnode;
}

//对尾插入数据
void QueuePush(Queue* pq, QDataType x)
{
	QNode* newnode = BuyQueueNode(x);

	if (pq->head == NULL)
	{
		pq->head = pq->tail = newnode;
	}
	else
	{
		pq->tail->Next = newnode;
		pq->tail = newnode;
	}
}

//队头删除数据
void QueuePop(Queue* pq)
{
	assert(pq);
	assert(!QueueEmpty(pq));
	QNode* next = pq->head->Next;
	free(pq->head);
	pq->head = next;
	if (pq->head == NULL)
	{
		pq->tail = NULL;
	}
}

//取队尾数据
QDataType QueueBack(Queue* pq)
{
	assert(pq);
	assert(!QueueEmpty(pq));
	return pq->tail->data;
}

//取对头数据
QDataType QueueFront(Queue* pq)
{
	assert(pq);
	assert(!QueueEmpty(pq));
	return pq->head->data;
}

//统计队列元素个数
int QueueSize(Queue* pq)
{
	assert(pq);
	assert(!QueueEmpty(pq));
	int count = 0;
	QNode* cur = pq->head;
	while (cur)
	{
		count++;
		cur = cur->Next;
	}
	return count;
}

//判断队列是否为空
bool QueueEmpty(Queue* pq)
{
	assert(pq);
	return pq->head == NULL;
}

test.c

#include "queue.h"

void LevelOrder(BTNode* root)
{
	Queue qq;
	QueueInit(&qq);
	if (root)
	{
		QueuePush(&qq, root);
	}
	while (!QueueEmpty(&qq))
	{
		BTNode* front = QueueFront(&qq);
		QueuePop(&qq);
		printf("%c ", front->data);

		if (front->left)
		{
			QueuePush(&qq, front->left);
		}

		if (front->right)
		{
			QueuePush(&qq, front->right);
		}
	}
	printf("\n");
	QueueDestroy(&qq);
}

int main()
{
	BTNode* A = (BTNode*)malloc(sizeof(BTNode));
	A->data = 'A';
	A->left = NULL;
	A->right = NULL;

	BTNode* B = (BTNode*)malloc(sizeof(BTNode));
	B->data = 'B';
	B->left = NULL;
	B->right = NULL;

	BTNode* C = (BTNode*)malloc(sizeof(BTNode));
	C->data = 'C';
	C->left = NULL;
	C->right = NULL;

	BTNode* D = (BTNode*)malloc(sizeof(BTNode));
	D->data = 'D';
	D->left = NULL;
	D->right = NULL;

	BTNode* E = (BTNode*)malloc(sizeof(BTNode));
	E->data = 'E';
	E->left = NULL;
	E->right = NULL;

	A->left = B;
	A->right = C;
	B->left = D;
	B->right = E;

	LevelOrder(A);

	return 0;
}

5、二叉树的建立

按先序遍历序列建立二叉树的二叉链表。

例,已知先序序列为:ABCDEGF。

核心思想:

  • 从键盘输入二叉树的结点信息,建立二叉树的存储结构。
  • 在建立二叉树的过程中按照二叉树先序方式建立。

如果单单给出一个先序,也许会有多种接表结构,就比如:ABCDEGF。会有下面两种(不仅限于这两种):

在这里插入图片描述

那我们到底想要建立那种结构呢?换句话说如果我想建立第一种二叉树呢?其实也很简单,我们将结点左右子树的NULL的地方在表示出来就行了。这里就用#标识NULL吧。那就需要按照下列顺序读入字符:ABC##DE#G##F###。

我们在建立二叉树之后,在输出中序和后序的结果来验证。

中序:C B E G D F A。

后序:C G E F D B A。

知道了实现思想,下面来看看代码实现:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>

typedef struct BinaryTreeNode
{
    struct BinaryTreeNode* left;
    struct BinaryTreeNode* right;
    char data;
}BTNode;

//创建二叉树
void CreateTree(BTNode** root)
{
    char ch;
    scanf("%c", &ch);
    if (ch == '#')
    {
        *root = NULL;
    }
    else
    {
        *root = (BTNode*)malloc(sizeof(BTNode));
        if (*root == NULL)
        {
            printf("malloc fail\n");
            exit(-1);
        }
        (*root)->data = ch;
        
        //那这里对应的也需要传地址。
        CreateTree(&(*root)->left);
        CreateTree(&(*root)->right);
    }
}

//中序遍历
void InOrder(BTNode* root)
{
    if (root == NULL)
    {
        return;
    }
    InOrder(root->left);
    printf("%c ", root->data);
    InOrder(root->right);
}

//后序遍历
void PostOrder(BTNode* root)
{
    if (root == NULL)
    {
        return;
    }
    PostOrder(root->left);
    PostOrder(root->right);
    printf("%c ", root->data);
}

int main()
{
    BTNode* T;
    CreateTree(&T); //注意这里需要传递结构体指针的地址,所以是个二级指针。

    printf("中序遍历:");
    InOrder(T);

    printf("\n");

    printf("后序遍历:");
    PostOrder(T);
    return 0;
}

输出:

在这里插入图片描述

6、线索二叉树

为什么要研究线索二叉树?

当用二叉链表作为二叉树的存储结构时,可以很方便的找到某个结点的左右孩子;但一般情况下,无法直接找到该节点在某种遍历序列中的前驱和后继结点。

那如何寻找特定遍历序列中二叉树结点和前驱和后继?

解决方法:

  • 通过遍历寻找-------费时间。
  • 每个结点再增设前驱、后继指针域------增加了存储负担。
  • 利用二叉链表中的空指针域(本章研究)。

【结论】:具有n个结点的二叉链表中,有n+1个指针域为空。

结论剖析:具有n个结点的二叉链表中,一共有2n个指针域;因为n个结点中有n-1个孩子,即2n个指针域中,有n-1个用来指示结点的左右孩子,其余n+1个指针域为空。

利用二叉链表中的空指针域:

​ 如果某个结点的左孩子为空,则将空的左孩子指针域改为__指向其前驱__;如果某结点的右孩子为空,则将空的 右孩子指针域改为__指向其后继。__

这种__改变指向的指针称为“线索”。__

那加上线索的二叉树称为__线索二叉树(Threaded Binary Tree)。__

对二叉树按某种遍历次序使其变为线索二叉树的过程叫__线索化。__

那如何实现线索化呢?如下二叉树,其中序遍历:CBEGDFA。

在这里插入图片描述

注意,在强调一遍此二叉树的中序遍历为:__C B E G D F A。__下面我们要根据此中序遍历进行线索化。

如下图,链接结构:

在这里插入图片描述

但是要注意:不是所有的二叉树线索化都是看中序遍历的顺序。而是要求什么样的遍历就按照什么样的遍历来。

为了区分lrchid和rchild指针到底是指向孩子指针,还是指向前驱或者后继的指针,对二叉链表每个结点增设两个标志域ltag和rtag,并约定:

  • ltag = 0;lchild指向该结点的左孩子。
  • ltag = 1;lchild指向该结点的前驱。
  • rtag = 0;rchild指向该结点的右孩子。
  • rtag = 1;rchild指向该结点的后继。

这样,二叉树结点的结构为:

在这里插入图片描述

结构实现,如下:

typedef struct BinaryTreeNode
{
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
    int ltag;
    int rtag;
	char data;
}BTNode;

下面我们再来看个二叉树:要求先序线索二叉树。

先序序列:A B C D E。

在这里插入图片描述

那线索化的结果就如下:

在这里插入图片描述

练习:

画出以下二叉树对应的中序线索二叉树。

该二叉树中序遍历结果为:H D I B E A F C G。

在这里插入图片描述

可以看到H没有前驱,G没有后继。那H的左子树和G的右子树结点就置空吗?

可以置空。但我们还可以利用起来。

为了避免悬空态,增设一个头结点。这个头结点顾名思义就在根节点A的头上。

增设一个头结点:

头结点中的ltag=0;lchild指向根节点。

头结点中的rtag=1;rchild指向遍历序列中最后一个结点。

然后再将上图二叉树的H、G结点置空的域都指向结点A。

这样以来:

遍历序列中第一个结点的lchild域和最后一个结点的rchild域都指向头结点。

如下图:

在这里插入图片描述

7、搜索二叉树

实际上我们单纯的学习二叉树没有太多的用处。学习二叉树主要是用于搜索二叉树的。如下图:

在这里插入图片描述

任何一棵树,左子树都比根要小,右子树都比根要大。

搜索中查找一个数,最多查找高度次。

时间复杂度:O(N)。

8、树和森林

首先我们先来回顾一下什么是树,什么是森林。

树:

  • 树是n(n>=0)个结点的有限集。若n=0,称为空树。
  • 若n>0:
    • 有且仅有一个特定的称为根(root)的结点。
    • 其余结点可分为m(m>=0)个互不相交的有限集T1,T2,T3,…,Tm。

在这里插入图片描述

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

在这里插入图片描述

8.1、树的存储结构

8.1.1、双亲表示法

实现:定义结构数组,存放树的的结点,每个结点含两个域。

数据域:存放结点本身信息。

双亲域:指示本结点的双亲结点在数组中的位置。

这样听起来有点抽象,我们来说个示例,给如下数组,写出树的结构。

在这里插入图片描述

那根据上面所描述的规则,我们就可以写出此树的结构了,如下:

在这里插入图片描述

特点:找双亲容易,找孩子难。

C语言的类型描述:

typedef struct PTNode
{
    Type data;
    int parent;       //双亲位置域
}PTNode;

树的结构:

#define MAX_TREE_SIZE 100
typedef struct
{
    PTNode node[MAX_TREE_SIZE];
    int r,n;     //根节点的位置和结点个数。
}PTree;

8.1.2、孩子链表

把每个结点的孩子结点排列起来,看成是一个线性表,用单链表存储,则n个结点有n个孩子链表(叶子结点的孩子链表为空)。而n个头指针又组成一个线性表,用顺序表(含n个元素的结构数组)存储。

听起来依然抽象,我们来看示例:

在这里插入图片描述

孩子结点结构:

typedef struct CTNode
{
    int child;       //用来存放单链表结点中child结点的下标值
    struct CTNode* Next;       //指向下一个结点
}*ChildPtr;

双亲结点结构:

typedef struct
{
    Type data;        //用来存放结点的值
    ChildPtr firstchild;          //用来存放第一个孩子结点的指针。
}CTBox;

树结构:

typedef struct
{
 	CTBox nodes[MAX_TREE_SIZE];
    int n,t;         //结点树和根节点位置。
}CTree;

特点:找孩子容易,找双亲难。

8.1.3、孩子兄弟表示法

孩子兄弟表示法又名:二叉树表示法或叉链表表示法。

实现:用二叉链表作树的存储结构,链表中每个结点的两个指针域分别指向其第一个孩子结点和下一个兄弟结点。

结构描述:

typedef struct CSNode
{
    Type data;
    struct CSNode *firstchild,*nextbother;     
    //firstchild指向其第一个孩子结点,*nextbother指向下一个兄弟结点
}CSNode,*CSTree;

下面来看个示例:

在这里插入图片描述

上面补充:B的兄弟结点有两个,A,C。但为什么B的右指针域不指向A,而指向C呢?那是因为,我们强调是找__下一个兄弟结点__,A是B的上一个兄弟节点,C才是B结点的下一个兄弟节点,所以B的右指针域指向结点C。

现在如果想找到结点C的路径是这样的:根据根节点R的firstchild指针域找到结点A,然后根据A结点的nextbother指针域找到B,最后在根据B结点的nextbother指针域找到C即可。

8.2、树与二叉树的转换

将树转化为二叉树进行处理,利用二叉树的算法来实现对树的操作。

那如何操作呢?其实可以发现一个对应关系:

由于树和二叉树都可以用二叉链表做存储结构,则以二叉链表作媒介可以导出树与二叉树之间的一个对应关系。

那是如何对应的呢?如下:

【说明一下这里树的存储结构采用孩子兄弟法。】

在这里插入图片描述

下面来详细说明树和二叉树的转换。

8.2.1、树转换为二叉树:

  • 加线:在树的原始结构中兄弟结点直接每有联系,但转为二叉树前,需要将兄弟结点之间加线。

    在这里插入图片描述

  • 抹线:对每个结点,除了其左孩子外,去除其与其余孩子之间的关系。

如下图,A有三个孩子结点B C E,现在只将A和其左子树,也就是B链接,不和C E链接了。又因为在第一步中兄弟结点之间加线了,所以B又和C链接了。所以树转换为二叉树了。

在这里插入图片描述

  • 旋转:以树的根节点为轴心,将整数顺时针转45°。

总结为一句口诀:兄弟相连留长子。

练习:将树转换为二叉树

在这里插入图片描述

(1)兄弟结点之间连线:

在这里插入图片描述

(2)除了其左孩子外,去除其与其余孩子之间的关系:

在这里插入图片描述

(3)以树的根节点为轴心,将整数顺时针转45°:

在这里插入图片描述

这样就完成了树转换为二叉树的过程。

8.2.2、二叉树转换为树

核心步骤:

  • 加线:若p结点是双亲结点的左孩子,则将p的右孩子,右孩子的右孩子…沿分支找到的所有右孩子,都与p的双亲用线连起来。
  • 抹线:抹掉原二叉树中双亲与右孩子之间的连线。这里有个简单的规律:上一步加多少条线,那这一步就会去掉多少条线。
  • 调整:将结点按层次排序,形成树结构。

这个过程就是上面树转为二叉树的逆操作。

口诀:左孩右右连双亲,去掉原来右孩线。

练习:将二叉树转换为树。

在这里插入图片描述

(1)加线:

在这里插入图片描述

(2)抹线,抹掉原二叉树中双亲与右孩子之间的连线。(上一步加了5条线,那这一步需要去除5条线)

在这里插入图片描述

(3)调整:同一层次的结点给调整到同一行。

在这里插入图片描述

这样就完成了二叉树转换为树的过程。

8.3、森林与二叉树的转换

8.3.1、森林转换为二叉树

核心步骤:

  • 将各棵树分别转换成二叉树。
  • 将每棵树的根节点用线相连。
  • 以第一颗树根结点为二叉树的根,再以根节点为轴心,顺时针旋转,构成二叉树型结构。

口诀:树变二叉根相连。

练习:将森林转换为二叉树

在这里插入图片描述

(1)将各棵树分别转换成二叉树。(这里不在具体介绍树转换为二叉树的过程了)

在这里插入图片描述

(2)将每棵树的根节点用线相连。

在这里插入图片描述

(3)旋转

在这里插入图片描述

8.3.2、二叉树转换为森林

核心步骤:

  • 抹线:将二叉树中根节点与其右孩子连线,及沿右分支搜索到的所有右孩子间连线全部抹掉,使之变成孤立的二叉树。
  • 还原:将孤立的二叉树还原成树。

口诀:去掉全部右孩线,孤立二叉再还原。

练习:将二叉树转换为森林

在这里插入图片描述

(1)去掉全部右孩线。

在这里插入图片描述

(3)还原,将每个二叉树变为树(这里不在具体介绍树二叉转换为树的过程了)

在这里插入图片描述

8.4、树和森林的遍历

8.4.1、树的遍历

前面学习到二叉树有四种遍历方式:先序、中序、后序,层序遍历。

而树的遍历有三种方式(没有中序遍历)。

1、先根(次序)遍历:

​ 若树不为空,则先访问根节点,然后依次先根遍历各棵子树。

2、后跟(次序)遍历:

​ 若树不为空,则先依次后根遍历各棵子树,然后访问根节点。

3、按层次遍历:

​ 若树不为空,则自上而下自左至右访问树中每个结点。

下面给出一个树(如下),我们来写出先根遍历、后跟遍历、层次遍历的顺序。

在这里插入图片描述

先根遍历:A B C D E。

后跟遍历:B D C E A 。

层序遍历:A B C E D。

8.4.2、森林的遍历

将森林看作由三部分构成:

  • 森林中第一颗树的根节点。
  • 森林中第一颗树的子树森林。
  • 森林中其它树构成的森林。

在这里插入图片描述

森林的遍历方式也有三种:(根据访问森林中第一部分的顺序而区分)

  • 先序遍历(先访问第一部分)。
  • 中序遍历(先访问第一棵树的子树森林,再访问第一部分,最后访问其它树构成的森林)。
  • 后序遍历(最后访问第一部分)。

先序遍历:

若森林不空,则:

  • 访问森林中第一棵树的根节点。
  • 先序遍历森林中第一颗树的子树森林。
  • 先序遍历森林中(除第一棵树之外)其余树构成的森林。

即:依次从左至右对森林中的每一棵树进行先根遍历。

中序遍历:

若森林不空,则:

  • 中序遍历森林中第一棵树的子树森林。
  • 访问森林中第一棵树的根节点。
  • 中序遍历森林中(除第一棵树之外)其余树构成的森林。

即:依次从左至右对森林中的每一棵树进行后根遍历。

练习:给一个森林,如下图,进行森林的遍历

在这里插入图片描述

先序遍历的结果:A B C D E F G H I J。

先序遍历的过程分析:

(1)首先分为三部分:

在这里插入图片描述

(2)遍历第一部分,得到A结点。再访问第二部分,第二部分又是个森林,那B C D 结点又是森林中的每个树,那就按照树的遍历方法来,B是B这个树的根结点,遍历B树的根节点,那就得到了B结点。那同理得到C结点,D结点。至此第二部分遍历完毕。

(3)最后再访问第三部分,第三部分又可以分为三部分(如下):

在这里插入图片描述

然后访问第一部分,得到E结点。再访问第二部分,F是个子树森林,遍历此子树森林,得到此子树森林的根节点F。最后再访问第三部分。然后还需要再分为三部分…,这里不在细分,直接写结果了,最后得到了G H I J结点。

所以最终得到此森林的__先序遍历:A B C D E F G H I J。__

练习:基于上面问题,写出森林的中序遍历

访问顺序:先访问第二部分,在访问第一部分,最后访问第三部分。

在这里插入图片描述

中序遍历结果:B C D A F E H J I G。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

再看dockerfile指令用法:80分钟一口气学完docker+k8s!带你掌握docker+k8s所有核心知识点,全程干货,无废话!

dockerfile实践 需求&#xff1a;通过dockerfile&#xff0c;构建nginx镜像&#xff0c;且容器运行后&#xff0c;生成的页面显示“书季&#xff0c;要攒劲哦~” #1、穿件dockerfile,注意文件名必须是这个。 [rootHadoop2 learn_docker]# cat dockerfile FROM nginx RUN echo…

linux opensuse使用mtk烧录工具flashtool

环境 linux发行版&#xff1a;opensuse leap 15.5 工具&#xff1a;SP_Flash_Tool_Selector_exe_Linux_v1.2316.00.100.rar 或其他版本 目标&#xff1a;mtk设备 下载链接 https://download.csdn.net/download/zmlovelx/88382784 或网络搜索。 使用 opensuse可直接解压后使…

HOMER7配置告警

概述 HOMER是一款100%开源的针对SIP/VOIP/RTC的抓包工具和监控工具。 HOMER是一款强大的、运营商级、可扩展的数据包和事件捕获系统&#xff0c;是基于HEP/EEP协议的VoIP/RTC监控应用程序&#xff0c;并可以使用即时搜索、处理和存储大量的信令、RTC事件、日志和统计信息。 …

【数据仓库设计基础(二)】维度数据模型

文章目录 一. 概述二. 维度数据模型建模过程三. 维度规范化四. 维度数据模型的特点五. 维度数据模型1. 星型模式1.1&#xff0e;事实表1.2&#xff0e;维度表1.3&#xff0e;优点1.4&#xff0e;缺点1.5&#xff0e;示例 2. 雪花模式2.1&#xff0e;数据规范化与存储2.2&#x…

PreMaint设备管理系统:实现制药企业的CSV合规性

在当今数字化时代&#xff0c;制药企业越来越依赖计算机化系统来支持其各个方面的运营&#xff0c;从研发到生产再到质量控制。然而&#xff0c;这些系统的使用不仅需要高效性和可靠性&#xff0c;还需要符合法规要求&#xff0c;尤其是药品生产质量管理规范&#xff08;Good M…

新零售革命:可视化助力零售业焕发新生

一、什么是新零售&#xff1f; 新零售是一种融合了传统零售业和数字科技的商业模式&#xff0c;旨在提升零售业的效率、便捷性和个性化。它将线上和线下的零售渠道结合在一起&#xff0c;通过数字技术、大数据分析、人工智能等手段&#xff0c;实现了以下几个主要特征和目标&a…

图像语义分割 FCN图像分割网络详解

图像语义分割 FCN图像分割网络详解 0、介绍1、VGG16网络结构2、转置卷积3、FCN-32S、FCN-16S&#xff0c;FCN-8S网络结构4、损失函数5、膨胀卷积6、FCN(Backbone-ResNet-50)6.1 项目框架6.2 ResNet50网络结构6.3 FCN(Backbone-ResNet-50)网络结构6.4 FCN(Backbone-ResNet-50)模…

基于SpringBoot的大学生就业招聘系统的设计与实现

目录 前言 一、技术栈 二、系统功能介绍 求职信息管理 首页 招聘信息管理 岗位申请管理 岗位分类 企业管理 三、核心代码 1、登录模块 2、文件上传模块 3、代码封装 前言 随着信息互联网信息的飞速发展&#xff0c;大学生就业成为一个难题&#xff0c;好多公司都舍不…

【RV1103】Luckfox Pico RV1103 开发记录

文章目录 对比uboot的差别Linux的差别其他差别编译命令对比板级配置选择spi-nand flashemmc/SD 卡spinand flash烧录差别由于没有原理图--引脚分析 对比 linux defconfiglinux dtsuboot defconfiguboot fragmentluckfox-picosd/tf (emmc)luckfox_rv1106_linux_defconfigrv1103…

澳大利亚海运价格下半年走势

随着全球疫情的逐渐缓解&#xff0c;国际贸易开始逐步恢复。在这个过程中&#xff0c;澳大利亚作为全球重要的贸易伙伴&#xff0c;其海运价格也成为了市场关注的焦点。本文将从下半年的市场预期、影响因素以及行业动态等方面&#xff0c;对澳大利亚海运价格走势进行分析展望。…

祝贺莱佛士学生在ASDA2023设计大赛中获得最高奖项

莱佛士一直主张学生们积极参与各种国际知名的设计大赛&#xff0c;也会竭尽所能为学生们的参赛提供途径与指导&#xff0c;本次的American Standard Design Award&#xff08;ASDA&#xff09;2023设计大赛也不例外。 ASDA2023设计大赛&#xff0c;推广以用户为中心的设计理念…

极简非凡react hooks+arcoDesign+vite后台管理模板

最近捣鼓了一个vite4搭建react18后台模板&#xff0c;搭载了字节团队react组件库Arco Design&#xff0c;整体编译运行顺滑衔接。支持多种模板布局、暗黑/亮色模式、国际化、权限验证、多级路由菜单、tabview标签栏快捷菜单、全屏控制等功能。 使用技术 "arco-design/web…

攀登数字化高峰,中小企业如何找“搭子”?

相信大多数人都认可&#xff0c;中小企业数字化&#xff0c;是一条充满未知和艰辛的征程。 这个过程&#xff0c;不是租几台云服务器、开发几个APP那么简单&#xff0c;而是一个对组织架构、业务环节、基础设施、商业模式等进行量身定制、长期迭代的体系化工程&#xff0c;需要…

船用白炽照明灯具

声明 本文是学习GB-T 3027-2012 船用白炽照明灯具. 而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 1 范围 本标准规定了船用白炽照明灯具(以下简称灯具)的要求、试验方法、检验规则、标识、包装和储 存等。 本标准适用于电源电压在250V 以下的交流…

uni-app使用iconfont字体图标

先iconfont选择好自己需要的图标 添加至项目 下载字体文件到本地 将下载的文件解压缩到工程目录static文件夹下 定义好iconfont.css文件的font-face声明,修改好引入的url地址 打开App.vue文件 ,引入static下刚才修改的iconfont.css字体图标文件 完成上线的步骤后就可以全局使用…

matplotlib绘图实现中文宋体的两种方法(亲测)

方法一&#xff1a;这种方法我没有测试。 第一步 找宋体字体 &#xff08;win11系统&#xff09; 2.matplotlib字体目录&#xff0c;如果不知道的话&#xff0c;可以通过以下代码查询&#xff1a; matplotlib.matplotlib_fname() 如果你是Anaconda3 安装的matplotlib&#x…

不同组合地下管线的地质雷达响应特征分析

不同组合地下管线的地质雷达响应特征分析 前言 以混凝土管线为例&#xff0c;建立水平相邻管线电性模型&#xff0c;管径为60cm&#xff0c;中心埋深为70cm&#xff0c;管线长度为150cm&#xff0c;分别建立管线圆心相距150cm的两根相邻双管线和三管线模型&#xff0c;进行二…

SQL血缘解析原理

根据sql解析获取到表到表, 字段到字段间的关系,即血缘关系。实际上这是从sql文本获取到数据流的过程。 大致步骤如下&#xff1a; 1.sql文本进行词法分析 2.sql语法分析获取到AST抽象语法树 3.访问AST抽象语法树根据语法结构推测出数据的流向,例如create as select from 这种结…

使用x64dbg手动脱UPX壳(UPX4.1.0)

本文选用的壳是4.1.0的UPX壳 将加壳的exe文件拖入x64dbg 打开符号&#xff0c;进入第一个sample.exe 进入后在第一个位置下断点&#xff0c;按下F9运行 继续按下F9 单步运行到此处&#xff0c;发现只有RSP变红&#xff0c;根据ESP定律&#xff0c;进行下面的操作 所谓定律就像…

【配置conda环境】新版pycharm导入新版anaconda环境

最近下载了新版pycharm和新版anaconda&#xff0c;并且在命令行创建了环境&#xff0c;想着在pycharm里面导入环境。结果现在的导入方式发生了变化。 之前是通过导入Python.exe进行的。 现在&#xff1a; 当我们点击进去之后&#xff0c;会发现找不到python.exe了。 具体什么…