目录
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
1.有一个特殊的节点,称为根节点,根节点没有前驱节点
2.除根节点外,其余节点被分为M(M>0)个互不相交的集合T1,T2,T3..Tm,其中每一个集合Ti(1<=i<=m)又是一颗结构与树类似的子树。每颗子树的根节点有且只有一个前驱,可以有0个或多个后继
3.因此,树是递归定义的


注意:树形结构中,子树之间不能有交集,否则就不是树形结构


节点的度:一个节点含有的子树的个数称为该节点的度;如上图:A的度为6,D的度为1,E的度为2,B的度为0。
叶节点或终端节点:度为0的节点称为叶节点;如上图,B,C,H,I,K,L,M,N,P,Q为叶节点。
非终端节点或分支节点:度不为0的节点;如上图:A,D,E,F,G,J为分支节点。
双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;如上图,A是B的父节点。
孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;如上图,B,C,D,E,F,G是A的孩子节点。
兄弟节点:具有相同父节点的节点互称为兄弟节点;如上图,B,C互为兄弟节点。
树的度:一棵树中,最大的节点的度称为树的度;如上图,节点的度最大为6,故树的度为6。
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推。
树的高度或深度:树中节点的最大层次;如上图,树的高度为4。堂兄弟节点:双亲在同一层的节点互为堂兄弟节点;如上图,H,I互为堂兄弟节点。
节点的祖先:从根到该节点所经分支上的所有节点;如上图,P的祖先为J,E,A。
子孙:以某节点为根的子树中任一节点都称为该节点的子孙;如上图,所有节点都是A的子孙。
森林:由m(m>0)棵互不相交的树的集合称为森林;
typedef int DataType;
struct Node
{
struct Node* _firstChild1; // 第一个孩子结点
struct Node* _pNextBrother; // 指向其下一个兄弟结点
DataType _data; // 结点中的数据域
};

表示文件系统的目录树结构

一颗二叉树是节点的一个有限集合,该集合:1.或者为空,2有一个根节点加上两颗分别称为左子树和右子树的二叉树组成

从上图可以看出:
1. 二叉树不存在度大于2的结点 2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
对于任意的二叉树都是由以下几种情况复合而成:
现实中的二叉树:



可以看到,满二叉树每一层都是满的,即除了叶子结点外,其他所有的节点有且只有两个孩子,完全二叉树则可以看做是满二叉树从最后一层从右往左删掉了若干个节点,由于是从右往左,故完全二叉树不可能存在一个节点只有右孩子而没有左孩子的情况
1. 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2的i-1次方个结点.
证明:最多的情况下即这一层类似于满二叉树的节点数,满二叉数从根节点开始,下一层的节点数是上一层的2倍,第一层为1个即2的0次方,第2层为2个即2的1次方,第3层为4个即2的2次方,以此类推,第i层为2的i-1次方
2. 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是2的h次方-1
证明:深度h即由h层,由性质1得每一层的最大节点数为2的i-1次方,每一层的和:1+2+4+8+16+...+2的h-1次方,可以看出这是一个等比数列求和,由等比数列求和公式求得最大节点数为2的h次方-1
3. 对任何一棵二叉树, 如果度为0的叶结点个数为n0 , 度为2的分支结点个数为n2 ,则有 n0= n2+1
证明:从节点个数和边个数关系证明。除了根节点之外,其余n-1每个节点都有一个父节点,即每个节点上面都有一条边,有n-1个节点则总共有n-1条边,又度为1的节点下面有一条边,度为2的节点下面有两条边,度为0的节点即叶节点下面没有边
设度为2的节点数为n2,度为1的节点为n1,度为0的节点为n0,总节点数为n,则边数为n-1
有:n0+n1+n2=n(节点个数关系)
n1*1+n2*2=n-1(边个数关系)
联立上式即可得n0=n2+1
4. 若规定根节点的层数为1,具有n个结点的满二叉树的深度 h= log2(n+1)(ps :是log以2 为底,n+1为对数)
证明:由性质2可得深度为h的满二叉树的节点数n为2的h次方-1,即2的h次方-1=n,解得h=log2(n+1)
5. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有
双亲节点:若i>0,i位置节点的双亲节点:(i-1)/2;i=0,即为根节点,无双亲节点;
左孩子:若2i+1<n,则其左孩子存在,左孩子编号为2i+1,否则无左孩子
右孩子:若2i+2<n.则其右孩子存在,右孩子编号为2i+2,否则无右孩子
二叉树一般有两种存储方式,一种为顺序结构,一种为链式结构

从上图可以看出,二叉树的顺序存储即按照从上到下从左到右的顺序依次将节点的值放到数组中即可


typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
{
struct BinTreeNode* _pLeft; // 指向当前节点左孩子
struct BinTreeNode* _pRight; // 指向当前节点右孩子
BTDataType _data; // 当前节点值域
}
// 三叉链
struct BinaryTreeNode
{
struct BinTreeNode* _pParent; // 指向当前节点的双亲
struct BinTreeNode* _pLeft; // 指向当前节点左孩子
struct BinTreeNode* _pRight; // 指向当前节点右孩子
BTDataType _data; // 当前节点值域
};
可以看到,链式存储分为两种方式分别为二叉链和三叉链,二叉链即节点有两个指针域分别指向左孩子和右孩子,而三叉链在二叉链的基础上多了一个指针域指向父节点
如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<=K2i+2 ,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:
1.堆中某个节点的值总是不大于或不大于其父节点的值
2.堆总是一颗完全二叉树
通过上图我们看到,堆在数组的存储中时按照从上到下从左至右的顺序存储在数组中的,因此我们可以总结出父节点和孩子节点下标的关系 :
1.通过父亲节点找孩子节点:
leftchild=parent*2+1
rightchild=parent*2+2
2.通过孩子节点找父节点parent=(child-1)/2(左右孩子都符合)
堆的实现主要设计向上调整算法、向下调整算法、堆的构建、堆的插入、堆的删除等接口函数
下面依次来看这些接口函数,默认建立大堆
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}Heap;
以某一个孩子节点开始,让它与自己的父节点比较,当孩子节点大于父亲节点时,显然这不符合大堆的性质,于是需要将两个节点的值交换,原来的父节点变为孩子节点,之前的孩子节点往上成为父节点,新的父节点作为孩子节点依然需要与上一个父节点比较,直到所有节点都符合大堆的性质,原来的孩子节点再调整的过程中不断往上走,故称为向上调整,如图所示

代码设计和过程:
函数的参数为一个数组和孩子节点下标,首先通过孩子节点下标算出父节点的下标,接着进行两者的比较,如果孩子节点大于父节点则两者交换,并更新孩子节点,再算出新的父节点,以此循环直到符合大堆的性质或者孩子节点已经为堆顶则调整结束
void AdjustUp(HPDataType* a, int child)
{
assert(a);
int parent = (child - 1) / 2;//通过孩子节点下标找到父节点下标
while (child>0)//调整结束的条件
{
if (a[child] > a[parent])//孩子节点大于父节点则调整
{
Swap(&a[child], &a[parent]);//交换两个节点的值
child = parent;//孩子节点更新
parent = (child - 1) / 2;//再求出新的父节点
}
else
{
break;//如果不需要调整则直接退出
}
}
}
以某一个父亲节点开始,让它与自己的孩子节点比较,当父节点小于最大的孩子节点时,(为什么要与最大的孩子节点交换,如果将较小的孩子换上去成为父节点,此父节点依然小于最大孩子节点)显然这不符合大堆的性质,于是需要将两个节点的值交换,最大孩子节点变为父节点,之前的父节点往下成为孩子节点,新的孩子节点作为父节点依然需要与下一个孩子节点比较,直到所有节点都符合大堆的性质,原来的父亲节点再调整的过程中不断往下走,故称为向下调整,如图所示
向下调整算法有一个前提:左右子树必须是一个堆,才能调整

代码设计和过程:
函数的参数为一个数组以及数组的大小和父节点下标,首先通过父节点下标算出最大孩子节点的下标,接着进行两者的比较,如果父节点小于最大孩子节点则两者交换,并更新父节点,再算出新的最大孩子节点,以此循环直到符合大堆的性质或者父节点已经走到最后则调整结束
void AdjustDown(HPDataType* a, int n, int parent)
{
assert(a);
int child = parent * 2 + 1;//通过父节点算出最大孩子节点,假设最大孩子为左节点
while (child<n)
{
//如果右节点大于左节点,则将最大孩子设为右节点
if (child+1<n&&a[child + 1] > a[child])
{
child++;
}
if (a[child] >a[parent])//父节点小于最大孩子节点则调整
{
Swap(&a[child], &a[parent]);//将父节点与最大孩子节点交换
parent = child;//更新父节点
child = parent * 2 + 1;//再算出新的最大孩子节点
}
else
{
break;//不需要调整则直接退出
}
}
}
给定一个数组,我们可以看做是完全二叉树,我们可以通过向上调整和向下调整两种方式建堆,并对两种方法进行分析和比较
向上调整建堆:
从第二层开始(堆顶可以视为一个大堆),对每一个节点应用向上调整算法,直到最后一个节点应用完向上调整算法
时间复杂度:O(n*logn),证明如图所示

向下调整建堆:
从倒数第二层开始(每个叶节点可以视为一个大堆),对倒数第一个非叶子节点应用向下调整算法,直到最后堆顶应用完向下调整算法
时间复杂度:O(n),证明如下图所示

通过上述分析,向下调整建堆的时间复杂度更优,因此在后续的建堆过程中采用向下调整建堆
堆的插入是首先将节点插入到数组即堆的尾部,再对该节点应用向上调整即可
过程如图所示

void HeapPush(Heap* hp, HPDataType x)
{
assert(hp);
if (hp->size == hp->capacity)//当前容量等于最大容量则进行扩容
{
int newcapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;//第一次相当于开辟空间,后续扩容至2倍
HPDataType* tmp = (HPDataType *)realloc(hp->a, sizeof(HPDataType) * newcapacity);
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
hp->a = tmp;
hp->capacity = newcapacity;
}
hp->a[hp->size] = x;//插入到数组尾部
hp->size++;//当前容量加1
AdjustUp(hp->a, hp->size-1);//对新节点采用向上调增算法
}
堆的删除过程:首先将堆顶与最后一个节点交换,然后当前容量-1,在对堆顶应用向下调整算法
删除过程如图所示:

堆的初始化即让指向数组的指针置为空,当前容量和最大容量置为0即可
void HeapInit(Heap* hp)
{
assert(hp);
hp->a = NULL;
hp->size = hp->capacity = 0;
}
堆的销毁即释放指针指向的空间,并让指针置为空,当前容量和最大容量置为0即可
void HeapDestory(Heap* hp)
{
assert(hp);
free(hp->a);
hp->a = NULL;
hp->size = hp->capacity = 0;
}
全部代码:
#include"Heap.h"
void HeapInit(Heap* hp)
{
assert(hp);
hp->a = NULL;
hp->size = hp->capacity = 0;
}
void HeapCreate(Heap* hp, HPDataType* a, int n)
{
assert(hp);
HPDataType*tmp = (HPDataType*)malloc(sizeof(HPDataType) * n);
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
hp->a = tmp;
hp->size = hp->capacity = n;
memcpy(hp->a, a, sizeof(HPDataType) * n);
//已有一个数组,向下调整建堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(hp->a, n, i);
}
}
void HeaPPrint(Heap* hp)
{
assert(hp);
for (int i = 0; i < hp->size; i++)
{
printf("%d ", hp->a[i]);
}
printf("\n");
}
void Swap(HPDataType* a, HPDataType* b)
{
HPDataType tmp = *a;
*a = *b;
*b = tmp;
}
void AdjustUp(HPDataType* a, int child)
{
assert(a);
int parent = (child - 1) / 2;//通过孩子节点下标找到父节点下标
while (child>0)//调整结束的条件
{
if (a[child] > a[parent])//孩子节点大于父节点则调整
{
Swap(&a[child], &a[parent]);//交换两个节点的值
child = parent;//孩子节点更新
parent = (child - 1) / 2;//再求出新的父节点
}
else
{
break;//如果不需要调整则直接退出
}
}
}
void AdjustDown(HPDataType* a, int n, int parent)
{
assert(a);
int child = parent * 2 + 1;//通过父节点算出最大孩子节点,假设最大孩子为左节点
while (child<n)
{
//如果右节点大于左节点,则将最大孩子设为右节点
if (child+1<n&&a[child + 1] < a[child])
{
child++;
}
if (a[child] <a[parent])//父节点小于最大孩子节点则调整
{
Swap(&a[child], &a[parent]);//将父节点与最大孩子节点交换
parent = child;//更新父节点
child = parent * 2 + 1;//再算出新的最大孩子节点
}
else
{
break;//不需要调整则直接退出
}
}
}
void HeapPush(Heap* hp, HPDataType x)
{
assert(hp);
if (hp->size == hp->capacity)//当前容量等于最大容量则进行扩容
{
int newcapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;//第一次相当于开辟空间,后续扩容至2倍
HPDataType* tmp = (HPDataType *)realloc(hp->a, sizeof(HPDataType) * newcapacity);
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
hp->a = tmp;
hp->capacity = newcapacity;
}
hp->a[hp->size] = x;//插入到数组尾部
hp->size++;//当前容量加1
AdjustUp(hp->a, hp->size-1);//对新节点采用向上调增算法
}
void HeapPop(Heap* hp)
{
assert(hp);
assert(hp->size > 0);
Swap(&hp->a[0], &hp->a[hp->size - 1]);
hp->size--;
AdjustDown(hp->a, hp->size, 0);
}
HPDataType HeapTop(Heap* hp)
{
assert(hp);
return hp->a[0];
}
int HeapSize(Heap* hp)
{
assert(hp);
return hp->size;
}
int HeapEmpty(Heap* hp)
{
assert(hp);
return hp->size == 0;
}
void HeapDestory(Heap* hp)
{
assert(hp);
free(hp->a);
hp->a = NULL;
hp->size = hp->capacity = 0;
}
void PrintTopK(int* a, int n, int k)
{
for (int i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, k, i);
}
for (int i = k+1; i < n; i++)
{
if (a[i] > a[0])
{
Swap(&a[i], &a[0]);
AdjustDown(a, k, 0);
}
}
}
void TestTopk()
{
int array[] = { 27,15,19,18,28,34,65,49,25,37 };
int k = 5;
int n = sizeof(array) / sizeof(int);
PrintTopK(array, n, k);
for (int i = 0; i < k; i++)
printf("%d ", array[i]);
}
堆排序即利用堆的思想进行排序,总共分为两个步骤:
1.建堆
升序:建大堆
降序: 建小堆
2.利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序
排序过程:建立大堆,大堆能够相对较大的元素在堆顶,首先让堆顶元素和最后一个元素交换,然后数组大小减1,再对堆顶元素应用向下调整(此时第二大的数就在堆顶了),第一轮就可以将最大的树放到数组最后面,依次进行,第二轮则可以将第二大的数放到数组的倒数第二个位置,直到数组的大小变为1即排序完成

代码:
void HeapSort(int* a, int n)
{
//时间复杂度O(n)
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
int end = n - 1;
while (end>0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
end--;
}
}
topk问题即在n个数中最大或者最小的K个数,通常n数量很大
比如专业前几名,王者荣耀国服排名等
对于这种问题最容易想到的就是排序,但是当数据量非常大的时候排序就不可取了,最佳的方式就是用堆来解决,以下由两种思路,其中第一个有一定的局限性,故采用第二种思路最佳
思路1:建一个大小为n的堆,pop K次,依次取堆顶,这K次取到的元素便是最大或者最小的K个数
局限性:当N非常大的时候,比如100亿个整数,占用的空间就高达40G,这是加载不进内存的,只能放到磁盘
时间复杂度分析:O(n)
由于向下调整建堆算法为O(n),故建大小为n的堆的时间为n,然后pop K次,每次最快情况下需要移动堆的高度次,堆的高度近似为logn,故Pop过程时间为k*logn,总时间为n+k*logn
思路2:建立一个大小为K的堆,找最大的K个数则建小堆,反之则建大堆,让后面的N-K个数与堆顶元素,比堆顶的元素大则将堆顶的元素替换,再对堆顶进行向下调整,这样后面的数全部与堆顶元素比较完之后,最大的K个数便在堆中
时间复杂度分析:O(N)
建大小为K的堆时间为K,然后后面N-K个元素依次与堆顶,最坏的情况便是后面N-K个数升序排序,即每个数都会与堆顶元素替换并进行向下调整,比较过程时间为(N-K)*logK,总时间为K+(N-K)*logK
思路2代码:
void PrintTopK(int* a, int n, int k)
{
for (int i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, k, i);//建立大小为k的堆
}
for (int i = k+1; i < n; i++)
{
if (a[i] > a[0])//后面的元素与堆顶比较,大于对顶元素则替换
{
Swap(&a[i], &a[0]);
AdjustDown(a, k, 0);//替换之后再对堆顶元素进行向下调整
}
}
}
好啦,关于堆的学习就先到这里,如果对您有所帮助,欢迎一键三连~
我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h
我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i
有时我需要处理键/值数据。我不喜欢使用数组,因为它们在大小上没有限制(很容易不小心添加超过2个项目,而且您最终需要稍后验证大小)。此外,0和1的索引变成了魔数(MagicNumber),并且在传达含义方面做得很差(“当我说0时,我的意思是head...”)。散列也不合适,因为可能会不小心添加额外的条目。我写了下面的类来解决这个问题:classPairattr_accessor:head,:taildefinitialize(h,t)@head,@tail=h,tendend它工作得很好并且解决了问题,但我很想知道:Ruby标准库是否已经带有这样一个类? 最佳
给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最
我正在尝试使用Curbgem执行以下POST以解析云curl-XPOST\-H"X-Parse-Application-Id:PARSE_APP_ID"\-H"X-Parse-REST-API-Key:PARSE_API_KEY"\-H"Content-Type:image/jpeg"\--data-binary'@myPicture.jpg'\https://api.parse.com/1/files/pic.jpg用这个:curl=Curl::Easy.new("https://api.parse.com/1/files/lion.jpg")curl.multipart_form_
无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD
本教程将在Unity3D中混合Optitrack与数据手套的数据流,在人体运动的基础上,添加双手手指部分的运动。双手手背的角度仍由Optitrack提供,数据手套提供双手手指的角度。 01 客户端软件分别安装MotiveBody与MotionVenus并校准人体与数据手套。MotiveBodyMotionVenus数据手套使用、校准流程参照:https://gitee.com/foheart_1/foheart-h1-data-summary.git02 数据转发打开MotiveBody软件的Streaming,开始向Unity3D广播数据;MotionVenus中设置->选项选择Unit
文章目录一、概述简介原理模块二、配置Mysql使用版本环境要求1.操作系统2.mysql要求三、配置canal-server离线下载在线下载上传解压修改配置单机配置集群配置分库分表配置1.修改全局配置2.实例配置垂直分库水平分库3.修改group-instance.xml4.启动监听四、配置canal-adapter1修改启动配置2配置映射文件3启动ES数据同步查询所有订阅同步数据同步开关启动4.验证五、配置canal-admin一、概述简介canal是Alibaba旗下的一款开源项目,Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。Git地址:https://github.co
我正在尝试在Rails上安装ruby,到目前为止一切都已安装,但是当我尝试使用rakedb:create创建数据库时,我收到一个奇怪的错误:dyld:lazysymbolbindingfailed:Symbolnotfound:_mysql_get_client_infoReferencedfrom:/Library/Ruby/Gems/1.8/gems/mysql2-0.3.11/lib/mysql2/mysql2.bundleExpectedin:flatnamespacedyld:Symbolnotfound:_mysql_get_client_infoReferencedf
文章目录1.开发板选择*用到的资源2.串口通信(个人理解)3.代码分析(注释比较详细)1.主函数2.串口1配置3.串口2配置以及中断函数4.注意问题5.源码链接1.开发板选择我用的是STM32F103RCT6的板子,不过代码大概在F103系列的板子上都可以运行,我试过在野火103的霸道板上也可以,主要看一下串口对应的引脚一不一样就行了,不一样的就更改一下。*用到的资源keil5软件这里用到了两个串口资源,采集数据一个,串口通信一个,板子对应引脚如下:串口1,TX:PA9,RX:PA10串口2,TX:PA2,RX:PA32.串口通信(个人理解)我就从串口采集传感器数据这个过程说一下我自己的理解,