【数据结构】:二叉树——顺序结构,链式结构的实现及相关操作
🌈个人主页:@ꪔ小林Y
✨个人专栏:《C++小白闯关日记》,《C语言小白闯关日记》,《数据结构入门——从原理到实战》
🍀代码信条:每一行代码都是成长的脚印👣,每一次调试成功都是对坚持的回应
目录
- 【树】
- 一.树的概念及特点
- 二.树的表示:
- 【二叉树】
- 一.二叉树的概念,结构与性质
- 二.特殊的二叉树
- 满二叉树
- 完全二叉树
- 三.二叉树的性质
- 四.二叉树的存储结构
- (一)顺序结构
- 1.堆的实现
- 2.堆排序
- (二)链式存储
- 链式存储——二叉树的定义
- 前中后序遍历
- 二叉树的相关操作
- 1.求二叉树结点个数
- 2.求二叉树叶子结点个数
- 3.求二叉树第k层结点个数
- 4.求二叉树的深度/高度
- 5.求二叉树查找值为x的结点
- 6.求二叉树销毁
- 层序遍历
- 判断是否为完全二叉树
【树】
学习二叉树之前,我们先来学习一个概念——树
一.树的概念及特点
- 概念:树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。
- 特点:
- 有一个特殊的结点,称为根节点,根节点没有前驱结点
- 除根结点外,其余结点被分成M(M>0)个互不相交的集合,其中每一个集合又是一颗结构与树类似的子树。每颗子树的根节点有且只有一个前驱,可以有0个或多个后继。因此,树是递归定义的。
- 树的结构特点:
- 子树是不相交的(如果存在相交就是图了)
- 除了根节点外,每个结点有且仅有一个父结点
- 一颗N个结点的树有N-1条边

4.非树形结构示例:

5.树的相关用语

- 父结点/双亲结点:若一个结点含有子结点,则称这个结点为其子结点的父结点;如:A是B的父结点
- 子节点/孩子结点:一个结点含有的子树的根结点称为该结点的子结点
- 结点的度:一个结点有几个孩子,他的度就是多少。比如A的度为6,F的度为2
- 树的度:一棵树中,最大的结点的度称为树的度。如上图树的度为6
- 叶子节点/终端结点:度为0的结点称为叶子结点
- 分支结点/非终端结点:度不为0的结点
- 兄弟结点:具有相同父结点的结点互称为兄弟结点
- 结点的层次:从根开始定义起,根为第1层,以此类推
- 树的高度或深度:树中结点的最大层次
- 结点的祖先:从根到该结点所经分支上的所有结点
- 路径:一条从树中任意结点出发,沿父结点-子结点连接,达到任意结点的序列
- 子孙:以某结点为根的子树中任一结点都称为该结点的子孙
- 森林:由m(m>0)棵互不相交的树的集合称为森林
二.树的表示:
孩子兄弟表示法:树结构相对线性表复杂,存储麻烦,既要保存值域,也要保存结点和结点之间的关系,实际中树有很多种表示方法,下面为其中一种——孩子兄弟表示法
struct TreeNode
{struct Node* child;//左边开始的第一个孩子结点struct Node* brother;//指向其右边的下一个兄弟节点int data;//结点中的数据域
};
【二叉树】
一.二叉树的概念,结构与性质
- 二叉树属于树
- 二叉树的特点:
- 二叉树不存在度大于2的结点
- 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
-
任意的二叉树都由以下情况复合而成:

-
二叉树性质:
对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从0开始编号,则对于序号为i的结点有:
- 若i>0,i位置节点的双亲序号:(i-1)/2;i,i为根结点编号,无双亲结点
- 若2i+1<n,左孩子序号:2i+1,2i+1>=n,否则无左孩子
- 若2i+2<n,右孩子序号:2i+2,2i+2>=n,否则无右孩子
二.特殊的二叉树
满二叉树
- 满二叉树
一个二叉树,如果每一层的结点数都达到最大值,即为满二叉树。如果一个二叉树的层数为k,且结点总数是(2^k)-1,则它就是满二叉树。 - 满二叉树图示:

完全二叉树
- 完全二叉树是由满二叉树引出来的,完全二叉树除了最后一层,其他每层结点个数都达到最大,最后一层结点个数不一定达到最大。完全二叉树的结点从左到右依次排列。(满二叉树就是完全二叉树的一种)
- 完全二叉树图示:

三.二叉树的性质
- 若规定根结点的层数为1,则一颗非空二叉树的第i层上最多有 2^(i-1) 个结点
- 若规定根结点的层数为1,则深度为h的二叉树的最大结点数是2^(h)-1
- 若规定根结点的层数为1,具有n个结点的满二叉树的深度h=log以2为底,n+1为对数
- 对任何一颗二叉树,如果度为0其叶节点个数为n1,度为2的分支节点个数为n2,则有n1=n2+1
四.二叉树的存储结构
(一)顺序结构
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费,完全二叉树更适合使用顺序存储结构,
顺序结构二叉树的实现会牵扯到堆:
1.堆的实现
- 堆是一种完全二叉树。将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。如图:

- 堆有以下性质:
- 堆中某个结点的值总是不大于或不小于其父结点的值;
- 堆总是一颗完全二叉树
- 堆顶是最(大/小)值
- 堆的初始化
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//定义堆结构
typedef int HPDataType;
typedef struct Heap {int* arr;int size;//表示有效数据个数int capacity;//空间大小
}HP;
void HPInit(HP* php)
{assert(php);php->arr = NULL;php->size = php->capacity = 0;
}
- 堆的销毁
//堆的销毁
void HPDesTroy(HP* php)
{assert(php);if (php->arr)free(php->arr);php->arr = NULL;php->size = php->capacity = 0;
}
- 向堆中插入数据(入堆)
再实现插入数据之前,首先要实现一个交换算法和向上调整算法
//交换算法
void Swap(int* x, int* y)
{int tmp = *x;*x = *y;*y = tmp;
}
//向上调整算法
void AdjustUp(HPDataType* arr, int child)
{int parent = (child - 1) / 2;while (child > 0){//建大堆:>//建小堆:<if (arr[child] > arr[parent]){Swap(&arr[child], &arr[parent]);child = parent;parent = (child - 1) / 2;}else {break;}}
}
//向堆插入数据
void HPPush(HP* php, HPDataType x)
{assert(php);//空间不够时需要增容if (php->size == php->capacity){//增容int newCapacity = php->capacity == 0 ? 4 : 2 * php->capacity;HPDataType* tmp = (HPDataType*)realloc(php->arr, newCapacity * sizeof(HPDataType));if (tmp == NULL){perror("realloc fail!");exit(1);}php->arr = tmp;php->capacity = newCapacity;}//空间足够php->arr[php->size] = x;//向上调整AdjustUp(php->arr, php->size);++php->size;
}
- 打印数据
//打印数据
void HPPrint(HP* php)
{for (int i = 0;i < php->size;i++){printf("%d ", php->arr[i]);}printf("\n");
}
写好代码来测试运行一下:
#include"Heap.h"
void test01()
{//堆的初始化HP hp;HPInit(&hp);//入堆HPPush(&hp, 80);HPPush(&hp, 15);HPPush(&hp, 10);HPPush(&hp, 25);HPPrint(&hp);//堆的销毁//HPDesTroy(&hp);
}
int main()
{test01();return 0;
}

- 出堆(删除堆顶的数据)
如果直接删除数据可能会导致堆中的数据乱套。
所以我们先将堆顶和堆中最后一个数据交换,交换后进行向下调整操作
//向下调整算法
void AdjustDown(HPDataType* arr, int parent,int n)
{int child = parent * 2 + 1;while (child < n){//建大堆:<//建小堆:>if (child+1<n&&arr[child] < arr[child + 1]){child++;}//孩子和父亲比较//建大堆:>//建小堆:<if (arr[child] > arr[parent]){Swap(&arr[child], &arr[parent]);parent = child;child = parent * 2 + 1;}}
}
//判断堆是否为空
bool HPEmpty(HP* php)
{assert(php);return php->size == 0;
}
//出堆
void HPPop(HP* php)
{assert(!HPEmpty(php));Swap(&php->arr[0], &php->arr[php -> size - 1]);--php->size;//堆顶数据需要向下调整AdjustDown(php->arr, 0, php->size);
}
测试一下
#define _CRT_SECURE_NO_WARNINGS 1
#include"Heap.h"
void test01()
{//堆的初始化HP hp;HPInit(&hp);//入堆HPPush(&hp, 80);HPPush(&hp, 25);HPPush(&hp, 10);HPPush(&hp, 15);HPPrint(&hp);// 出堆HPPop(&hp);HPPrint(&hp);HPPop(&hp);HPPrint(&hp);HPPop(&hp);HPPrint(&hp);HPPop(&hp);HPPrint(&hp);//堆的销毁//HPDesTroy(&hp);
}
int main()
{test01();return 0;
}

2.堆排序
- 如果我们以上为基础在测试里面循环取堆顶再出堆会得到什么呢
while (!HPEmpty(&hp))
{int top = HPTop(&hp);printf("%d ", top);HPPop(&hp);
}

循环取堆顶出堆,我们会发现大堆出来的是一个降序结果,小堆出来的是一个升序结果
那么我们就可以写一个堆排序
//堆排序
void HeapSort(int* arr, int n)
{HP hp;HPInit(&hp);//调用push将数组中的数据放入到堆中for (int i = 0;i < n;i++){HPPush(&hp, arr[i]);}int i = 0;while (!HPEmpty(&hp)){int top = HPTop(&hp);arr[i++] = top;HPPop(&hp);}HPDesTroy(&hp);
}
int main()
{int arr[6] = { 25,15,10,56,70,30 };printf("排序之前:\n");for (int i = 0;i < 6;i++){printf("%d ", arr[i]);}printf("\n");HeapSort(arr, 6);printf("排序之后:\n");for (int i = 0;i < 6;i++){printf("%d ", arr[i]);}printf("\n");return 0;
}

实际上这不是真正的的堆排序
这只是使用了一个数据结构——堆,而后进行排序
真正的堆排序是使用堆结构的思想
- 堆排序代码实现:
(1)使用向下调整算法建堆堆
如图是使用向下调整算法建堆:
//向下调整算法
void AdjustDown(HPDataType* arr, int parent,int n)
{
//时间复杂度:O(logn)int child = parent * 2 + 1;while (child < n){//建大堆:<//建小堆:>if (child+1<n && arr[child] > arr[child + 1]){child++;}//孩子和父亲比较//建大堆:>//建小堆:<if (arr[child] < arr[parent]){Swap(&arr[child], &arr[parent]);parent = child;child = parent * 2 + 1;}else {break;}}
}
//出堆
void HPPop(HP* php)
{assert(!HPEmpty(php));Swap(&php->arr[0], &php->arr[php -> size - 1]);--php->size;//堆顶数据需要向下调整AdjustDown(php->arr, 0, php->size);
}
需要移动结点总的移动步数:每层节点个数*向下调整次数(第一层调整次数为0)
T(h)=(2^h)-1-h
根据二叉树性质:n=(2^h)-1 和 h=log以2为底(n+1)的对数
则T(n)=n-log以2为底(n+1)的对数=n
向下调整算法建堆时间复杂度为:O(n)
//堆排序——使用的是堆结构的思想
void HeapSort(int* arr, int n)
{//乱序数组——建堆for (int i = (n - 1 - 1) / 2;i >= 0;i--){AdjustDown(arr, i, n);}//排升序——建大堆//排降序——建小堆int end = n - 1;while (end > 0){Swap(&arr[0], &arr[end]);AdjustDown(arr, 0, end );end--;}
}
测试运行一下:
int main()
{int arr[6] = { 30,56,25,15,70,10 };printf("排序之前:\n");for (int i = 0;i < 6;i++){printf("%d ", arr[i]);}printf("\n");HeapSort(arr, 6);printf("排序之后:\n");for (int i = 0;i < 6;i++){printf("%d ", arr[i]);}printf("\n");return 0;
}

(2)使用向上调整算法建堆
//向上调整算法
void AdjustUp(HPDataType* arr, int child)
{
//时间复杂度:O(logn)int parent = (child - 1) / 2;while (child > 0){//建大堆:>//建小堆:<if (arr[child] < arr[parent]){Swap(&arr[child], &arr[parent]);child = parent;parent = (child - 1) / 2;}else {break;}}
}
//向堆插入数据
void HPPush(HP* php, HPDataType x)
{assert(php);//空间不够时需要增容if (php->size == php->capacity){//增容int newCapacity = php->capacity == 0 ? 4 : 2 * php->capacity;HPDataType* tmp = (HPDataType*)realloc(php->arr, newCapacity * sizeof(HPDataType));if (tmp == NULL){perror("realloc fail!");exit(1);}php->arr = tmp;php->capacity = newCapacity;}//空间足够php->arr[php->size] = x;//向上调整AdjustUp(php->arr, php->size);++php->size;
}
需要移动结点总的移动步数:每层节点个数向上调整次数(第一层调整次数为0)
F(h)=(2^h)(h-2)+2
根据二叉树的性质:n=(2^h)-1 和 h=log以2为底(n+1)的对数
则F(n)=(n+1)(log以2为底(n+1)的对数-2)+2=nlogn
向上调整算法建堆的时间复杂度为:O(n*logn)
//堆排序
void HeapSort(int* arr, int n)
{//向上调整算法——建堆for (int i = 0;i < n;i++){AdjustUp(arr, i);}//排升序——建大堆//排降序——建小堆int end = n - 1;while (end > 0){Swap(&arr[0], &arr[end]);AdjustDown(arr, 0, end );end--;}
}
测试一下:

堆排序的算法复杂度:O(n*logn)
(二)链式存储
二叉树的链式存储结构是指,用链表来表示一颗二叉树,即用链来指示元素的逻辑关系。通常的方法是链表中的每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在链结点的存储地址。链式结构又分为二叉链和三叉链。
链式存储——二叉树的定义
//头文件
#include<stdio.h>
#include<stdlib.h>
//定义二叉树结点结构
typedef int BTDataType;
typedef struct BinaryTreeNode {BTDataType data;struct BinaryTreeNode* left;struct BinaryTreeNode* right;
}BTNode;
前中后序遍历
- 前序遍历:先根遍历,先根遍历根节点,再遍历左子树,最后遍历右子树——根左右
- 中序遍历:先遍历左子树,再遍历根节点,最后遍历右子树——左根右
- 后序遍历:先遍历左子树,再遍历右子树,最后遍历根节点——左右根
- 层序遍历:按照层次依次遍历(从上到下,从左到右)
例:以下二叉树遍历结果:
前序:ABDCEF
中序:DBAECF
后序:DBEFCA
现在我们来代码实现一下:
- 前序遍历
void PreOrder(BTNode* root)
{if (root == NULL){printf("NULL ");return;}printf("%c ", root->data);PreOrder(root->left);PreOrder(root->right);
}
- 中序遍历
void InOrder(BTNode* root)
{if (root == NULL){printf("NULL ");return;}InOrder(root->left);printf("%c ", root->data);InOrder(root->right);}
- 后序遍历
void PostOrder(BTNode* root)
{if (root == NULL){printf("NULL ");return;}PostOrder(root->left);PostOrder(root->right);printf("%c ", root->data);
}
- 测试运行一下:
BTNode* buyNode(char x)
{BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));newnode->data = x;newnode->left = newnode->right = NULL;return newnode;
}
void test01()
{BTNode* nodeA = buyNode('A');BTNode* nodeB = buyNode('B');BTNode* nodeC = buyNode('C');BTNode* nodeD = buyNode('D');BTNode* nodeE = buyNode('E');BTNode* nodeF = buyNode('F');nodeA->left = nodeB;nodeA->right = nodeC;nodeB->left= nodeD;nodeC->left = nodeE;nodeC->right = nodeF;//直接将根节点传过去PreOrder(nodeA);printf("\n");InOrder(nodeA);printf("\n");PostOrder(nodeA);
}
int main()
{test01();return 0;
}

二叉树的相关操作
依旧以此树为例:

BTNode* buyNode(char x)
{BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));newnode->data = x;newnode->left = newnode->right = NULL;return newnode;
}
BTNode* createTree()
{BTNode* nodeA = buyNode('A');BTNode* nodeB = buyNode('B');BTNode* nodeC = buyNode('C');BTNode* nodeD = buyNode('D');BTNode* nodeE = buyNode('E');BTNode* nodeF = buyNode('F');nodeA->left = nodeB;nodeA->right = nodeC;nodeB->left = nodeD;nodeC->left = nodeE;nodeC->right = nodeF;return nodeA;
}
int main()
{test01();return 0;
}
1.求二叉树结点个数
//×,再一次调用时个数会累加
int size = 0;//全局变量
int BinaryTreeSize(BTNode* root)
{//利用前/中/后序进行遍历if (root == NULL){return 0;}size++;BinaryTreeSize(root->left);BinaryTreeSize(root->right);return size;
}
//测试
void test01()
{printf("size:%d\n",BinaryTreeSize(root));
}
这里如果将size定义为局部变量时,在递归时会出现问题;
若定义为全局变量,若调用两次计算结点个数,size会累加,不行
- 方法一:可以在函数声明中多定义一个参数,但在多次调用时需手动置0,这会非常的麻烦
- 方法二:不用size,使用递归:二叉树总节点个数=左子树总节点个数+右子树总节点个数
//方法一:
int BinaryTreeSize(BTNode* root, int *psize)//形参
{//利用前/中/后序进行遍历if (root == NULL){return 0;}(*psize)++;BinaryTreeSize(root->left,psize);BinaryTreeSize(root->right,psize);
}
//测试
void test01()
{BTNode* root = createTree();int Treesize = 0;BinaryTreeSize(root,&Treesize);//实参//形参的改变要影响实参,这里传的是地址Treesize = 0;//手动置0BinaryTreeSize(root, &Treesize);printf("size:%d\n ", Treesize);
}
//方法二:
int BinaryTreeSize(BTNode* root)
{if (root == NULL){return 0;}return 1+ BinaryTreeSize(root->left)+ BinaryTreeSize(root->right);
}
//测试一下:
void test01()
{BTNode* root = createTree();printf("size:%d\n", BinaryTreeSize(root));printf("size:%d\n", BinaryTreeSize(root));
}
2.求二叉树叶子结点个数
叶子节点个数=左子树叶子节点个数+右子树叶子节点个数
//二叉树叶子结点个数
int BinaryTreeLeafSize(BTNode* root)
{if (root == NULL){return 0;}//判断是否为叶子节点if (root->left == NULL && root->right == NULL){return 1;}
return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
//测试一下:
void test01()
{BTNode* root = createTree();printf("leaf size:%d\n", BinaryTreeLeafSize(root));
}

3.求二叉树第k层结点个数
第k层结点个数=左子树第k层结点个数+右子树第k层结点个数
//二叉树第k层结点个数
int BinaryTreeLevelKSize(BTNode* root,int k)
{if (root == NULL){return 0;}//判断是否为第k层if (k == 1){return 1;}return BinaryTreeLevelKSize(root->left,k-1) + BinaryTreeLevelKSize(root->right,k-1);
}
//测试一下
void test01()
{BTNode* root = createTree();//求第k层结点个数printf("k level size:%d\n", BinaryTreeLevelKSize(root,2));printf("k level size:%d\n", BinaryTreeLevelKSize(root, 3));
}

4.求二叉树的深度/高度
二叉树的高度=根节点+max(左子树的高度,右子树的高度)
//二叉树的深度
int BinaryTreeDepth(BTNode* root)
{if (root == NULL){return 0;}int leftDep=BinaryTreeDepth(root->left);int rightDep = BinaryTreeDepth(root->right);//根节点+最大子树的高度return 1 + (leftDep > rightDep ? leftDep : rightDep);
}
//测试一下:
void test01()
{BTNode* root = createTree();//求二叉树的高度printf("depth :%d\n", BinaryTreeDepth(root));
}

5.求二叉树查找值为x的结点
//二叉树查找值为x的结点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{if (root == NULL){return NULL;}if (root->data == x){return root;}BTNode* leftFind = BinaryTreeFind(root->left, x);if (leftFind){return leftFind;}BTNode* rightFind = BinaryTreeFind(root->right, x);if (rightFind){return rightFind;}return NULL;
}
//测试一下:
void test01()
{BTNode* root = createTree();//二叉树查找值为x的结点BTNode* find = BinaryTreeFind(root, 'E');if (find){printf("找到了!\n");}else {printf("未找到!\n");
}
}

6.求二叉树销毁
//二叉树销毁
void BinaryTreeDestory(BTNode** root)
{if (*root == NULL){return;}//应使用后序遍历进行递归BinaryTreeDestory(&((*root)->left));//销毁左子树BinaryTreeDestory(&((*root)->right));//销毁右子树free(*root);//销毁根节点*root = NULL;
}
//测试一下:
void test01()
{BTNode* root = createTree();BinaryTreeDestory(&root);
}
层序遍历
深度优先遍历:前中后序遍历
广度优先遍历:层序遍历
我们可以借助数据结构——队列来实现层序遍历:
将根结点保存在队列中,使队列不为空;循环判断队列是否为空,不为空取队头,将队头结点不为空的孩子结点入队列;循环往复
实现一下:
我们首先拿出以前的队列代码,修改一下Queue.h文件里的声明
//定义节点结构
typedef int QDataType;
//将原本的上面一句改成下面一句:
// int更改为二叉树结点结构
//前置声明,不用标注头文件
typedef struct BinaryTreeNode* QDataType;//队列存储的数据类型
实现:
//层序遍历
void LevelOrder(BTNode* root)
{//借助队列Queue q;QueueInit(&q);//将根节点入队列QueuePush(&q, root);//循环判断队列是否为空while (!QueueEmpty(&q)){//取队头BTNode* top=QueueFront(&q);printf("%c ", top->data);//队头出队QueuePop(&q);//对头结点不为空的孩子结点入队列if (top->left)QueuePush(&q, top->left);if (top->right)QueuePush(&q, top->right);}QueueDesTroy(&q);
}
测试运行:
void test01()
{BTNode* root = createTree();//层序遍历printf("层序遍历:");LevelOrder(root);BinaryTreeDestory(&root);
}

判断是否为完全二叉树
完全二叉树:最后一层结点个数不一定达到最大;其他每层节点个数都达到最大;结点从左到右依次排列
思路:判断每层节点个数;叶子节点是否从左到右依次排列。、
具体操作:根结点先入队列,保证队列不为空;循环判断队列是否为空,不为空取队头,出队头;将队头结点的左右孩子都入队列
- 非完全二叉树最后出现的情况:取到空的队头,跳出循环,此时队列中剩下了空结点和非空结点
- 完全二叉树最后出现的情况:取到空的队头,跳出循环,此时队列中只剩下了空结点
//判断是否为完全二叉树
bool BinaryTreeComplete(BTNode* root)
{Queue q;QueueInit(&q);//头节点入队列QueuePush(&q, root);while (!QueueEmpty(&q)){//取队头,出对头BTNode* top = QueueFront(&q);QueuePop(&q);if (top == NULL){//top取到空就直接出队列break;}//将队头结点的左右孩子入队列QueuePush(&q, top->left);QueuePush(&q, top->right);}//队列不为空,循环取队头while (!QueueEmpty(&q)){BTNode* top = QueueFront(&q);QueuePop(&q);if (top != NULL){//不是完全二叉树QueueDesTroy(&q);return false;}}QueueDesTroy(&q);return true;
}
测试运行一下:
void test01()
{BTNode* root = createTree();//判断是否为完全二叉树bool isComplete = BinaryTreeComplete(root);if (isComplete){printf("是完全二叉树!\n");}else {printf("不是完全二叉树!\n");}BinaryTreeDestory(&root);
}
🎊本期数据结构的内容就结束了。如果文中有表述不准的地方,或是你有更清晰的理解思路,强烈欢迎在评论区留言交流—— 技术路上多碰撞,才能更快进步
觉得内容对你有帮助的话,别忘了点赞❤️➕收藏🌟,方便后续回顾复习;想跟着一起系统学习数据结构的朋友,也可以点击关注,下一期我们会聚焦更进一步的学习,带你从理论走进实操。下期不见不散✌️





