• 【数据结构】二叉树遍历的实现(超详细解析,小白必看系列)


    目录

    一、前言

    🍎为何使用链式二叉树 

     🍐何为链式二叉树

     🍉二叉树的构建

    💦创建二叉链结构

    💦手动构建一颗树 

    🍓二叉树的遍历 (重点)

    💦前序遍历

     💦中序遍历

     💦后续遍历

     🍌二叉树的经典问题(重点)

    💦二叉树节点个数

     💦二叉树叶子节点的个数

     💦二叉树第K层节点个数

     💦二叉树的深度

     💦二叉树查找值为x的节点 

     💦二叉树的销毁

    🍊总代码和演示图

    二、共勉


    一、前言

    在之前的文章中,已经详细的讲解了二叉树、堆等问题,所以本次博客将继续延续之前的知识点,讲解链式二叉树的遍历和一些经典问题

    🍎为何使用链式二叉树 

    在前几篇博文中,我们学习的都是完全二叉树或满二叉树,而这两个都是可以用数组来实现的,但是如果不是完全二叉树呢?回顾下曾经学过的知识点:

     
    由上图得知,普通二叉树也可以使用数组来存储,但是会存在大量的空间浪费,而完全二叉树就不会这样,因为其空间利用率是%100的。既然这样,那普通二叉树该如何进行存储呢?答案是使用链式结构进行存储。

     🍐何为链式二叉树

    •  链式结构分为两种:二叉链和三叉链

    💦先看下代码结构:

    1. typedef int BTDataType;
    2. // 二叉链
    3. struct BinaryTreeNode
    4. {
    5. struct BinTreeNode* _pLeft; // 指向当前节点左孩子
    6. struct BinTreeNode* _pRight; // 指向当前节点右孩子
    7. BTDataType _data; // 当前节点值域
    8. };
    9. // 三叉链
    10. struct BinaryTreeNode
    11. {
    12. struct BinTreeNode* _pParent; // 指向当前节点的双亲
    13. struct BinTreeNode* _pLeft; // 指向当前节点左孩子
    14. struct BinTreeNode* _pRight; // 指向当前节点右孩子
    15. BTDataType _data; // 当前节点值域
    16. };
    💦 画图演示:


    ⚠ 注意:链式二叉树和我们之前的学习略有差别。以前我们学习的数据结构无非就是增删查改这些东西,而链式二叉树不太关注这块的增删查改。因为普通二叉树的增删查改没有意义。如下的二叉树:


    链式二叉树是要比之前链表啥的更加复杂的,如果只是单纯的让链式二叉树存储数据的话,价值就不大了,不如使用线性表。接下来,我将通过其遍历方式,结点个数……为大家展开讨论。此节内容是为了后续学习更复杂的搜索二叉树打基础,具体是啥后面再谈。
     

    在具体讲解之前,再回顾下二叉树,二叉树是:

    1. 空树
    2. 非空:根节点,根节点的左子树、根节点的右子树组成的。


    从概念中可以看出,二叉树定义是递归式的,因此后序基本操作中基本都是按照该概念实现的。

     🍉二叉树的构建

    💦创建二叉链结构

    创建二叉链结构其实就很easy了,也就是创建一个结构体罢了,这种在先前已经写过很多,咱就是说直接上代码:

    1. // 二叉树结构体
    2. typedef struct BrinyTree
    3. {
    4. struct BrinyTree* left;
    5. struct BrinyTree* right;
    6. int data;
    7. }BT;

    💦手动构建一颗树 

    其实构建一棵树的思想还是挺简单的,按照图示创建6个节点,并根据图中的样子将节点顺次链接起来

    1. // 创建一个节点 (有返回值,返回值类型为---结构体指针)
    2. BT* BuyNode(int x)
    3. {
    4. BT* newnode = (BT*)malloc(sizeof(BT));
    5. if (newnode == NULL)
    6. {
    7. perror("malloc fail!");
    8. exit(-1);
    9. }
    10. newnode->data = x;
    11. newnode->left = NULL;
    12. newnode->right = NULL;
    13. return newnode;
    14. }
    15. // 创建一个树
    16. BT* CreatBinaryTree()
    17. {
    18. // 创建6个节点
    19. BT* node1 = BuyNode(1);
    20. BT* node2 = BuyNode(2);
    21. BT* node3 = BuyNode(3);
    22. BT* node4 = BuyNode(4);
    23. BT* node5 = BuyNode(5);
    24. BT* node6 = BuyNode(6);
    25. //将结点连接起来,构成自己想要的树
    26. node1->left = node2;
    27. node1->right = node4;
    28. node2->left = node3;
    29. node4->left = node5;
    30. node4->right = node6;
    31. return node1;
    32. }

    🍓二叉树的遍历 (重点)

    以一颗二叉树为例:

    后续的遍历均是建立在次二叉树基础上展开。 
     

    💦前序遍历

    •  遍历规则:

    前序遍历,也叫先根遍历

    遍历顺序:根 -> 左子树 -> 右子树

    • 思路:

    既然先从根走,根就是1,接下来访问1的左子树,此时又要先访问其左子树的根为2,接着再访问2的左子树->根:3,接着访问其左子树和右子树,不过均为空,递归返回,此时3作为2的左子树访问完毕,访问2的右子树为NULL,再递归返回此时1的左子树就访问完毕了,访问其右子树,同理访问左树4,再访问左树5,接着左右子树NULL,递归返回访问4的右树,……类似的

    • 图示:

    •  代码演示:

    前序遍历的代码非常简洁,短短几行即可操作,先看代码:

    1. // 前序遍历 根 左 右
    2. void PrevOrder(BT* root)
    3. {
    4. if (root == NULL)
    5. {
    6. printf("NULL "); // 如果为空,就打印
    7. return; // 当前函数结束
    8. }
    9. printf("%d ", root->data);
    10. PrevOrder(root->left);
    11. PrevOrder(root->right);
    12. }
    • 效果如下:


    跟我们先前画的图一模一样,让我们通过一副递归图来深刻理解其原理:

    • 递归图:

     💦中序遍历

    •  遍历规则:

    中序遍历,也叫中根遍历

    遍历顺序:左子树 -> 根结点 -> 右子树

    • 思路:

    根据遍历顺序,我们得知,如若想访问1,得先访问其左子树2,访问2还得先访问其左子树3,类似的,再访问其左子树为NULL,递归返回访问根结点3,再访问右子树NULL,递归返回访问根结点2,再访问右子树NULL,递归返回访问根结点1,再访问其右子树,1的右子树访问规律同1的左子树,这里不过多赘述。

    • 画图演示:


     

    •  代码演示:

    中序遍历的代码和前序遍历一样,看起来都非常简洁:

    1. // 中序遍历 左 根 右
    2. void InOrder(BT* root)
    3. {
    4. if (root == NULL)
    5. {
    6. printf("NULL "); // 如果为空,就打印
    7. return; // 当前函数结束
    8. }
    9. InOrder(root->left);
    10. printf("%d ", root->data);
    11. InOrder(root->right);
    12. }
    • 效果如下:


    单纯看代码看不出啥头绪,还得是画递归图。


     

     💦后续遍历

    •  遍历规则:

    后续遍历,也叫后根遍历

    遍历顺序:左子树 -> 右子树 -> 根结点

    • 思路:

    要访问1得先访问1的左子树2,继而得先访问2的左子树3,再先访问3的左子树NULL,右子树NULL,根3,递归返回访问2的右子树NULL,根2,再递归返回访问1的右子树……类似的

    • 画图演示:

    • 代码演示:
    1. // 后序 左 右 根
    2. void PosOrder(BT* root)
    3. {
    4. if (root == NULL)
    5. {
    6. printf("NULL "); // 如果为空,就打印
    7. return; // 当前函数结束
    8. }
    9. PosOrder(root->left);
    10. PosOrder(root->right);
    11. printf("%d ", root->data);
    12. }
    • 效果如下:

    • 画图演示递归过程:


     

     🍌二叉树的经典问题(重点)

    💦二叉树节点个数

    •  思想:

    求结点个数,这里我将提供如下几种方法,但不都是可行的,要对比着看,本质都是递归的思想:

    • 法一:遍历

    在前文中,我们已经学习了如何遍历链式二叉树,现在想求结点的个数,那么只需要随便采用一种遍历方式,并把打印换成计数++来求个数即可,听起来非常容易,先看代码:

    1. //节点个数
    2. void BTreeSize(BTNode* root)
    3. {
    4. int count = 0; //局部变量
    5. if (root == NULL) //如果为空
    6. return;
    7. ++count;
    8. BTreeSize(root->left);
    9. BTreeSize(root->right);
    10. }

    难道上述代码能够准确求出结点个数吗?其实不然,根本求不出来。


    具体解释起来需要借用栈帧的思想,因为这里用的是递归,而递归是每递归一次在栈帧里头都会开辟一块空间,每一块栈帧都会有一个count,而我希望的是只需要有一个count,然后所有的count均加在一起,可是现在每递归一次,重新开辟一个count,count即局部变量。递归完就销毁,同形参的改变不会影响实参一样,一个道理。所有此法根本就行不通,得换。
     

    • 法二:定义局部静态变量count

    在法一中,我们定义的是局部变量count,会导致每递归一次就开辟栈帧,并创建count,每次递归结束返回就销毁栈帧。那如果可以把count放在静态区里头,不久可以保留住count吗

    1. //节点个数
    2. int BTreeSize(BTNode* root)
    3. {
    4. static int count = 0; //局部静态变量
    5. if (root == NULL) //如果为空
    6. return count;
    7. ++count;
    8. BTreeSize(root->left);
    9. BTreeSize(root->right);
    10. return count;
    11. }
    • 效果如下:

    看似好像是成功了,确实结点个数为6,但真的就是成功了吗?当然不是,如果我们现在想多打印几次呢?
    什么鬼?怎么size还呈现等差数列递增呢?就是因为这里运用了static关键字,将count扣在静态区,导致多次调用没办法初始化为0,使其每次递归调用累计加加,但是当你再重新调用自己时,count不会重新置为0,会依旧保留为曾经++的结果。局部的静态变量有一个好处,它的生命周期在全局,但是只能在局部去访问。它的初始化为0只有第一次调用会访问,其余均不会。由此可见,局部的静态也是不行的,还得再优化。
     

    • 法三:定义全局变量count

    法二的局部静态变量行不通,那就把count设定为全局变量。要知道全局变量是存在静态区的。虽然也在静态区,但是其初始化为0是可以重复访问的。
     

    1. //节点个数
    2. int count = 0;
    3. void BTreeSize(BTNode* root)
    4. {
    5. if (root == NULL) //如果为空
    6. return;
    7. ++count;
    8. BTreeSize(root->left);
    9. BTreeSize(root->right);
    10. }
    • 效果如下:


    确实可以求出结点个数,并且也不会出现像法二一样的问题。但是其实定义全局变量也会存在一个小问题:线程安全的问题,这个等以后学到Linux再来讨论,我们这边考虑再换一种更优解。

    • 法四:最优解

    我们这里可以考虑多套一层,可以考虑把变量的地址传过去。这样操作不会存在任何问题,上代码:
     

    1. //节点个数
    2. void BTreeSize(BTNode* root, int* pCount)
    3. {
    4. if (root == NULL) //如果为空
    5. return;
    6. ++(*pCount);
    7. BTreeSize(root->left, pCount);
    8. BTreeSize(root->right, pCount);
    9. }

    确实可以求出结点个数,并且也不会出现像法二一样的问题。但是其实定义全局变量也会存在一个小问题:线程安全的问题,这个等以后学到Linux再来讨论,我们这边考虑再换一种更优解。

    • 法五:新思路

    直接利用子问题的思想来写,返回当root为空为0,不是就递归左树+右树+1。

    1. 空树,最小规模子问题,结点个数返回0
    2. 非空,左子树结点个数+右子树结点个数 + 1(自己)
    1. int BTreeSize(BTNode* root)
    2. {
    3. return root == NULL ? 0 :
    4. BTreeSize(root->left) +
    5. BTreeSize(root->right) + 1;
    6. }

    此法非常巧妙,很灵活的运用了递归的思想,我们通过递归图来深刻理解下:
     


     

     💦二叉树叶子节点的个数

    •  思路1:遍历+计数

    在遍历的基础上如果结点的左右子树均为空则count++。但是此题我们依旧采用分治思想。

    1. // 计算叶子节点个数
    2. // 遍历 + 计数
    3. void TreeLeveSize1(BT* root, int* count)
    4. {
    5. if(root==NULL)
    6. {
    7. return ;
    8. }
    9. if(root->left==NULL&&root->right==NULL)
    10. {
    11. (*count)++;
    12. }
    13. TreeLeveSize1(root->left,count);
    14. TreeLeveSize1(root->right,count);
    15. }
    •  思路2:遍历+递归
    1. // 叶子节点个数
    2. int TreeLeveSize2(BT*root)
    3. {
    4. if(root==NULL)
    5. {
    6. return 0;
    7. }
    8. int left = TreeLeveSize2(root->left);
    9. int right = TreeLeveSize2(root->right);
    10. if(left==0&&right==0)
    11. {
    12. return 1;
    13. }
    14. return left+right;
    15. }
    • 思路3:分治思想

    首先,如果为空,直接返回0,如若结点的左子树和右子树均为空,则为叶节点,此时返回1,其它的继续分治递归。

    • 代码演示:
    1. / 计算叶子节点个数
    2. int TreeLeveSize(BT* root)
    3. {
    4. if(root==NULL)
    5. {
    6. return 0;
    7. }
    8. if(root->left==NULL&&root->right==NULL)
    9. {
    10. return 1;
    11. }
    12. return TreeLeveSize(root->left)+TreeLeveSize(root->right);
    13. }
    • 递归图:


     

     💦二叉树第K层节点个数

    •  思路:

    假设K=3

    1. 空树,返回0
    2. 非空,且K == 1,返回1
    3. 非空,且K>1,转换成左子树K-1层节点个数 + 右子树K-1层节点个数
    • 代码演示:
    1. // 树的第K层节点个数
    2. // 1. 当前树的第K层 = 左子树的第K-1+ 右子树的第K-1
    3. int TreeKSize(BT* root, int k)
    4. {
    5. assert(k > 0);
    6. // 给出 所问 问题 能成成立的条件(所问,问题的的结束条件)
    7. if (root == NULL)
    8. {
    9. return 0;
    10. }
    11. if (k == 1)
    12. {
    13. return 1;
    14. }
    15. return TreeKSize(root->left, k - 1) + TreeKSize(root->right, k - 1);
    16. }
    • 递归图:


     

     💦二叉树的深度

    •  思路:

    此题同样是运用分治的思想来解决,要比较左子树的高度和右子树的高度,大的那个就+1,因为还有根结点也算1个高度。

    • 代码演示:
    1. // 树的深度
    2. int TreeDeep(BT* root)
    3. {
    4. if(root==NULL)
    5. {
    6. return 0;
    7. }
    8. int left = TreeDeep(root->left);
    9. int right = TreeDeep(root->right);
    10. if(left>right)
    11. {
    12. return left+1;
    13. }
    14. else
    15. {
    16. return right+1;
    17. }
    18. }
    • 递归图:


     

     💦二叉树查找值为x的节点 

    •  思路:

    还是利用分治的思想,将其递归化成子问题去解决

    1. 先去根结点寻找,是就返回此节点
    2. 此时去左子树查找有无节点x
    3. 最后再去右子数去查找有无节点x
    4. 若左右子树均找不到节点x,直接返回空
    • 代码演示:
    1. // 查询 树中的某一个节点是否存在
    2. BT* TreeNode(BT* root,TreeDatatype x)
    3. {
    4. if(root==NULL)
    5. {
    6. return NULL;
    7. }
    8. if(root->data==x)
    9. {
    10. return root;
    11. }
    12. BT* left = TreeNode(root->left,x);
    13. BT* right = TreeNode(root->right,x);
    14. if(left!=NULL)
    15. {
    16. return left;
    17. }
    18. if(right!=NULL)
    19. {
    20. return right;
    21. }
    22. return NULL;
    23. }
    • 递归图:

    假设我们寻找的是数字5

     

     💦二叉树的销毁

    •  思路:

    销毁的思想和遍历类似,如若我挨个遍历的同时,没遍历一次就销毁一次,岂不能达到效果,但是又会存在一个问题,那就是你要采用什么样的遍历方式?倘若你采用前序遍历,刚开始就把根销毁了,那么后面的子树还怎么销毁呢?因为此时根没了,子树找不到了就,所以要采用倒着销毁的规则,也就是后续的思想

    • 代码演示:
    1. // 二叉树的销毁
    2. // 从后往前销毁-------后续思想
    3. // 进行遍历然后在销毁
    4. void DestoryTree(BT*root)
    5. {
    6. if(root==NULL)
    7. {
    8. return;
    9. }
    10. DestoryTree(root->left);
    11. DestoryTree(root->right);
    12. free(root);
    13. }

    思想和后续遍历类似,不做递归图演示。
     

    🍊总代码和演示图

     💦代码

    1. #include <stdio.h>
    2. #include <stdlib.h>
    3. #include <stdbool.h>
    4. #include <assert.h>
    5. typedef int TreeDatatype;
    6. typedef struct BinaryTree
    7. {
    8. struct BinaryTree* left;
    9. struct BinaryTree* right;
    10. TreeDatatype data;
    11. }BT;
    12. BT* BuyTreenode(TreeDatatype x)
    13. {
    14. BT* tree = (BT*)malloc(sizeof(BT));
    15. if(tree==NULL)
    16. {
    17. perror(" fail");
    18. exit(-1);
    19. }
    20. tree->data = x;
    21. tree->left = tree->right = NULL;
    22. return tree;
    23. }
    24. BT* CreatTree()
    25. {
    26. BT* node1 = BuyTreenode(1);
    27. BT* node2 = BuyTreenode(2);
    28. BT* node3 = BuyTreenode(3);
    29. BT* node4 = BuyTreenode(4);
    30. BT* node5 = BuyTreenode(5);
    31. BT* node6 = BuyTreenode(6);
    32. //BT* node7 = BuyTreenode(7);
    33. node1->left = node2;
    34. node1->right = node4;
    35. node2->left = node3;
    36. //node2->right = node7;
    37. node4->left = node5;
    38. node4->right = node6;
    39. return node1;
    40. }
    41. // 前序遍历:根 左 右
    42. void PreOrder(BT* root)
    43. {
    44. if(root==NULL)
    45. {
    46. printf("NULL ");
    47. return;
    48. }
    49. printf("%d ",root->data);
    50. PreOrder(root->left);
    51. PreOrder(root->right);
    52. }
    53. // 中序遍历
    54. void InOrder(BT*root)
    55. {
    56. if(root==NULL)
    57. {
    58. printf("NULL");
    59. return;
    60. }
    61. InOrder(root->left);
    62. printf("%d ",root->data);
    63. InOrder(root->right);
    64. }
    65. // 后序遍历
    66. void PastOrder(BT* root)
    67. {
    68. if(root==NULL)
    69. {
    70. printf("NULL");
    71. return;
    72. }
    73. PastOrder(root->left);
    74. PastOrder(root->right);
    75. printf("%d ",root->data);
    76. }
    77. // 计算一颗树的节点个数
    78. int TreeSize(BT* root)
    79. {
    80. if(root==NULL)
    81. {
    82. return 0;
    83. }
    84. return TreeSize(root->left)+TreeSize(root->right)+1;
    85. }
    86. // 计算叶子节点个数
    87. int TreeLeveSize(BT* root)
    88. {
    89. if(root==NULL)
    90. {
    91. return 0;
    92. }
    93. if(root->left==NULL&&root->right==NULL)
    94. {
    95. return 1;
    96. }
    97. return TreeLeveSize(root->left)+TreeLeveSize(root->right);
    98. }
    99. // 计算叶子节点个数
    100. // 遍历 + 计数
    101. void TreeLeveSize1(BT* root, int* count)
    102. {
    103. if(root==NULL)
    104. {
    105. return ;
    106. }
    107. if(root->left==NULL&&root->right==NULL)
    108. {
    109. (*count)++;
    110. }
    111. TreeLeveSize1(root->left,count);
    112. TreeLeveSize1(root->right,count);
    113. }
    114. // 叶子节点个数
    115. int TreeLeveSize2(BT*root)
    116. {
    117. if(root==NULL)
    118. {
    119. return 0;
    120. }
    121. int left = TreeLeveSize2(root->left);
    122. int right = TreeLeveSize2(root->right);
    123. if(left==0&&right==0)
    124. {
    125. return 1;
    126. }
    127. return left+right;
    128. }
    129. // 树的第 K 层的节点
    130. int TreeKLeveSize(BT* root,int k)
    131. {
    132. if(root==NULL)
    133. {
    134. return 0;
    135. }
    136. if(k==1)
    137. {
    138. return 1;
    139. }
    140. return TreeKLeveSize(root->left,k-1)+TreeKLeveSize(root->right,k-1);
    141. }
    142. // 树的深度
    143. int TreeDeep(BT* root)
    144. {
    145. if(root==NULL)
    146. {
    147. return 0;
    148. }
    149. int left = TreeDeep(root->left);
    150. int right = TreeDeep(root->right);
    151. if(left>right)
    152. {
    153. return left+1;
    154. }
    155. else
    156. {
    157. return right+1;
    158. }
    159. }
    160. // 查询 树中的某一个节点是否存在
    161. BT* TreeNode(BT* root,TreeDatatype x)
    162. {
    163. if(root==NULL)
    164. {
    165. return NULL;
    166. }
    167. if(root->data==x)
    168. {
    169. return root;
    170. }
    171. BT* left = TreeNode(root->left,x);
    172. BT* right = TreeNode(root->right,x);
    173. if(left!=NULL)
    174. {
    175. return left;
    176. }
    177. if(right!=NULL)
    178. {
    179. return right;
    180. }
    181. return NULL;
    182. }
    183. // 二叉树的销毁
    184. // 从后往前销毁-------后续思想
    185. // 进行遍历然后在销毁
    186. void DestoryTree(BT*root)
    187. {
    188. if(root==NULL)
    189. {
    190. return;
    191. }
    192. DestoryTree(root->left);
    193. DestoryTree(root->right);
    194. free(root);
    195. }
    196. int main()
    197. {
    198. BT* tree = CreatTree();
    199. printf("树的前序遍历 >\n");
    200. PreOrder(tree);
    201. printf("\n");
    202. printf("\n");
    203. printf("树的中序遍历 >\n");
    204. InOrder(tree);
    205. printf("\n");
    206. printf("\n");
    207. printf("树的后序遍历 >\n");
    208. PastOrder(tree);
    209. printf("\n");
    210. printf("\n");
    211. int size = 0;
    212. printf("树的节点个数\n");
    213. size = TreeSize(tree);
    214. printf("%d\n",size);
    215. printf("\n");
    216. int sizeLeve = 0;
    217. printf("树的叶子节点个数\n");
    218. int i = 0;
    219. TreeLeveSize1(tree,&i);
    220. printf("%d\n",i);
    221. printf("\n");
    222. int SizeLeve = 0;
    223. printf("树的叶子节点个数1\n");
    224. SizeLeve = TreeLeveSize2(tree);
    225. printf("%d\n",SizeLeve);
    226. printf("\n");
    227. int ksize = 0;
    228. printf("树的第K层节点个数\n");
    229. ksize = TreeKLeveSize(tree,3);
    230. printf("%d \n",ksize);
    231. printf("\n");
    232. int deep = 0;
    233. printf("树的深度\n");
    234. deep = TreeDeep(tree);
    235. printf("%d \n",deep);
    236. printf("\n");
    237. printf("\n");
    238. BT* tre;
    239. int x;
    240. printf("请输入你要在树中查询的数值:>\n");
    241. scanf("%d",&x);
    242. tre = TreeNode(tree,x);
    243. if(tre==NULL)
    244. {
    245. printf("没有你要查询的数据");
    246. }
    247. else
    248. {
    249. printf("查询到了:%d\n",tre->data);
    250. }
    251. // 销毁这颗树
    252. DestoryTree(tree);
    253. tree = NULL;
    254. return 0;
    255. }

    💦视图

    二、共勉

    以下就是我对数据结构---二叉树遍历的理解,如果有不懂和发现问题的小伙伴,请在评论区说出来哦,同时我还会继续更新对数据结构-------二叉树的面试题型请持续关注我哦!!!!!  

  • 相关阅读:
    Tomcat的下载与介绍
    VME-7807RC-414001 350-93007807-414001 VMIVME-017807-411001 VMIVME-017807-414001
    YbtOJ「动态规划」第2章 区间DP
    Spring框架(四)Spring的Bean作用域和生命周期
    软件定义存储不能打?这家成立刚三年的公司问鼎全球存储性能榜
    FPGA的数字钟带校时闹钟报时功能VHDL
    持安科技孙维伯:零信任理念下的实战攻防:ISC2023数字小镇演讲
    【精通内核】CPU控制并发原理CPU中断控制内核解析
    NFTScan 正式上线 TON NFTScan 浏览器!
    通知!7月SCI/SSCI/EI目录已更新,大批中科院1-4区好刊,请查收!
  • 原文地址:https://blog.csdn.net/weixin_45031801/article/details/133747485