当前位置:   article > 正文

手把手带你学会二叉树(Java版)_java实现简单的二叉树

java实现简单的二叉树

1树型的了解

   1.1概念

   1.2树的表达方式

2.二叉树

   2.1二叉树的定义

   2.2两种特殊的二叉树

   2.3二叉树的性质

   2.4二叉树的基本操作以及代码实现

   2.5前中后序遍历的非递归实现

   2.6 二叉树的创建

1树型的了解

 1.1概念

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

   有一个特殊的结点,称为根结点,根结点没有前驱结点

   除根结点外,其余结点被分成M(M > 0)个互不相交的集合T1、T2、......、Tm,其中每一个集合Ti (1 <= i <= m) 又是一棵与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继

    树是递归定义的。

在讲树的重要概念前我们先画一棵树出来看一下

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

树的度:一棵树中,所有结点度的最大值称为树的度; 如上图:树的度为6

叶子结点或终端结点:度为0的结点称为叶结点; 如上图:B、C、H、I...等节点为叶结点

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

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

根结点:一棵树中,没有双亲结点的结点;如上图:A

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

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

  树以下的概念只需要做了解就欧克克了

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

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

堂兄弟结点:双亲在同一层的结点互为堂兄弟;如上图:H、I互为兄弟结点

结点的祖先:从根到该结点所经分支上的所有结点;如上图:A是所有结点的祖先

子孙:以某结点为根的子树中任一结点都称为该结点的子孙。如上图:所有结点都是A的子孙

森林:由m(m>=0)棵互不相交的树组成的集合称为森林

1.2树的表达方式

树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,实际中树有很多种表示方式,如:双亲表示法, 孩子表示法、孩子双亲表示法、孩子兄弟表示法等等。我们等一会在实现二叉树的时候主要是使用到了孩子表示法

2. 二叉树

 在数据结构初阶学习树的时候我们一般都是去学习二叉树这一种树的

 2.1二叉树的定义

      一棵二叉树是结点的一个有限集合,该集合:

     1. 或者为空

     2. 或者是由一个根节点加上两棵别称为左子树和右子树的二叉树组成。

上图就一棵普通的二叉树 

2.2两种特殊的二叉树

 1. 满二叉树: 一棵二叉树,如果每层的结点数都达到最大值,则这棵二叉树就是满二叉树。也就是说,如果一棵 二叉树的层数为K,且结点总数是 ,则它就是满二叉树。

2. 完全二叉树: 完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n 个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从0至n-1的结点一一对应时称之为完 比特就业课 全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。

2.3二叉树的性质

1. 若规定根结点的层数为1,则一棵非空二叉树的第i层上最多有 (i>0)个结点

2. 若规定只有根结点的二叉树的深度为1,则深度为K的二叉树的最大结点数是2^k-1 (k>=0)

3. 对任何一棵二叉树, 如果其叶结点个数为 n0, 度为2的非叶结点个数为 n2,则有n0=n2+1

4. 具有n个结点的完全二叉树的深度k为log2(n+1) 上取整

5. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的顺序对所有节点从0开始编号,则对于序号为i 的结点有:

   5.1 若i>0,双亲序号:(i-1)/2;i=0,i为根结点编号,无双亲结点 若2i+1

   5.2若2i+1< n,左孩子序列2i+i,否则无左孩子

   5.3若2i+2<n,右孩子序列2i+2,否则无右孩子

2.4二叉树的基本操作以及代码实现

   在开始讲二叉树的基础操作前我们先把二叉树的概念用一张图去概括一下

2.4二叉树的遍历

     学习二叉树结构,最简单的方式就是遍历。所谓遍历(Traversal)是指沿着某条搜索路线,依次对树中每个结 点均做一次且仅做一次访问。访问结点所做的操作依赖于具体的应用问题(比如:打印节点内容、节点内容加 1)。 遍历是二叉树上最重要的操作之一,是二叉树上进行其它运算之基础

   1.二叉树的前中后序遍历

     前序遍历最重要的一点就是先去输出根节点,在去输出根节点的左子树,最后去访问根节点的右子树

    以下是前序遍历的访问路径图

  

    其是这里的前中后序遍历可以理解为根节点到底的在那一步才开始输出的,根的左子树元素一定是比右子树的元素先被访问或者输出的。

  那么中序遍历和后序遍历就很好理解了

   中序遍历:优先访问(输出)根节点左子树的内容,过来是根节点的内容,最后是根节点右子树的内容。

   后序遍历:优先访问(输出)根节点左子树的内容,过来是根节点右子树的内容,最后是根节点的内容。

    NLR:前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点--->根的左子树--->根的右子树。(可以记为   根左右)

    LNR:中序遍历(Inorder Traversal)——根的左子树--->根节点--->根的右子树。(可以记为     左右根)

   LRN:后序遍历(Postorder Traversal)——根的左子树--->根的右子树--->根节点(可以记为    左右根)

   下面我们先来个简单的二叉树去写出它的前中后序遍历的结构

    就来我们一开始那张图来

   前序遍历的结果:1 2 3 4 5 6

   中序遍历的结果:3 2 1 5 4 6

   后序遍历的结果:3 2 5 6 4 1

   //酱紫就欧克克了,在idea中二叉树的底层代码都是使用递归法去实现的,我们这里先使用递归法去实现上面3种遍历方法,有非递归的骚玩法等等再来玩先熟悉一下遍历先

   我们先创建好一个自定义二叉树类先

  1. public class MyBinaryTree {
  2. static class TreeNode {
  3. public char val;//假设我二叉树里面存放的是char类型的数据
  4. public TreeNode left;//节点的左孩子节点
  5. public TreeNode right;//节点的右孩子节点
  6. //构造方法
  7. public TreeNode (char val) {
  8. this.val = val;
  9. }
  10. }
  11. }

  在接着去实现我遍历的功能

  1. //1.前序遍历 每一棵树都要根据 根左右 的方式进行遍历
  2. public void preOrder (TreeNode root) {
  3. if(root == null) {
  4. return;
  5. //结束条件
  6. }
  7. System.out.print(root.val + "");//先打印根 在先往左 再往右
  8. postOrder(root.left);
  9. postOrder(root.right);
  10. }
  11. //2.中序遍历 每一棵树都要根据 左根右 的方式进行遍历
  12. public void inOrder (TreeNode root) {
  13. if (root == null) {
  14. return;
  15. }
  16. postOrder(root.left);
  17. System.out.print(root.val + " ");
  18. postOrder(root.right);
  19. }
  20. //3.后序遍历 每一棵树都要根据 左右根 的方式进行遍历
  21. public void postOrder (TreeNode root) {
  22. if (root == null) {
  23. return;
  24. }
  25. postOrder(root.left);
  26. postOrder(root.right);
  27. System.out.print(root.val + " ");
  28. }

  用递归的方法去实现还是很简单的,好玩的地方是非递归的,等等就来

   2. 层序遍历

     层序遍历这东西就很简单了,就跟它的名字一样逐层访问,先从上往下再从左往右。

     这里我们把上面的那张二叉树的图使用层序遍历去访问一次,看一下下结果是咋样的

 层序遍历的结果: 1 2 4 3 5 6。

 层序遍历这里采用非递归的写法去实现了,顺便给等一下实现前中后序遍历的非递归写法先铺垫一下。

  非递归写法的最重要的一点就是要去使用到队列去实现,

  这里先说明一下实现的思路,等等会把实现的逻辑图去画出来的

  思路:层序遍历的最大一个特点就是逐层访问,这里我先把根节点先放入队列中,再进入循环每次循环的开始我们先把对头节点先弹出来然后定义一个top遍历去接收这个弹出来的值,然后去判断该节点是否有左子树或者右子树,如果有的话就把top节点的左子树(右子树)放入队列中,一个循环结束下来我二叉树就被层序遍历访问完了。

  记下来是图文实现(博主画图画的有点不好,有没看明白的地方可以私信博主)

  先上老朋友

再画一张第二次进入循环的图,接下来的就是具体代码的实现了(层序遍历的非递归实现很简答的)

 

  代码实现

  1. //4.层序遍历 从上到下 从左到右逐层范问 --- 非递归法写
  2. //用队列去实现 先把root入进去 然后在出队列的时候找有个top去记录它 然后将top的左节点和右节点 (为null就就别理就行了)
  3. public void levelOrder (TreeNode root) {
  4. Queue<TreeNode> queue = new LinkedList<>();
  5. if(root != null) {
  6. queue.offer(root);
  7. //先放根节点
  8. }
  9. while (!queue.isEmpty()) {
  10. TreeNode top = queue.poll();
  11. System.out.print(top.val+" ");
  12. if(top.left != null) {
  13. queue.offer(top.left);
  14. }
  15. if(top.right != null) {
  16. queue.offer(top.right);
  17. }
  18. }
  19. System.out.println();
  20. }

 2.4.3 二叉树的基本操作

     在二叉树中不止有遍历这一个功能的,我们再去实现一些二叉树的基本功能就来完成我前中后序遍历的非递归实现

   首当其冲的先来个获取树中节点的个数  --- int size(Node root);

    实现获取树中节点的个数其实就相当于去遍历一次整棵二叉树,在遍历二叉树时使用递归方法毋庸置疑是最简单的。下面的实现代码都是围绕递归方法去完成的

  1. //获取树中节点的个数
  2. //法1
  3. public int size1(TreeNode root) {
  4. int ret= 0;
  5. if(root == null) {
  6. return ret;
  7. }
  8. ret++;
  9. ret += size1(root.left);
  10. ret += size1(root.right);
  11. return ret;
  12. }
  13. //法2
  14. public int usedSize = 0;
  15. public int size2 (TreeNode root){
  16. if(root == null) {
  17. return 0;
  18. }
  19. usedSize++;
  20. //向左子树找
  21. size2(root.left);
  22. //向右子树找
  23. size2(root.right);
  24. return usedSize;
  25. }
  26. //法3
  27. public int size3(TreeNode root) {
  28. if(root == null) {
  29. return 0;
  30. }
  31. int ret= 0;
  32. ret = size3(root.right)+size3(root.left)+1;
  33. return ret;
  34. }

  在实现获取树中节点个数的代码中,其实法1和法3的逻辑是差不多的,有一点点意思的就是法2,它在类中去定义了一个全局变量去记录,这样的话我只管去递归找不为空的节点就行了,找到一个就加1;

  2). 获取叶子节点的个数  ---- int getLeafNodeCount(Node root);

   这个功能其实只要明白叶子节点的定义就可以去完成了,叶子节点的定义是没有左右子树,那我们在递归遍历二叉树的时候,只要遇到一个节点的左子树和右子树都为null的时候,我们就可以给它记一下数了,遍历二叉树完成,我叶子节点的个数也就出来了,实现的逻辑思路并不难

   这边直接上代码看一下

  1. //获取叶子节点的个数
  2. public int getLeafNodeCount(TreeNode root) {
  3. int ret = 0;
  4. if(root == null) {
  5. return ret;
  6. }
  7. if(root.left==null && root.right==null) {
  8. //该节点为叶子节点
  9. return 1;
  10. }
  11. ret = getLeafNodeCount(root.left)+getLeafNodeCount(root.right);
  12. return ret;
  13. }

3).获取第K层节点的个数  --- int getKLevelNodeCount(Node root,int k);

   这里我们先观察一下这个方法给的参数有哪些,一是树的根节点,二是获取点的层树,这边我们要转变一下思路嗷,这里的第k层是对于我root节点的位置来说的,假设这里的k=3,那么是对于根节点来说是在第3层,但是对于root的左右子树来说 是相对于自己是第二层,那么在对于root的左右子树的左右子树来说是不是就相当于对于自己来说是第一层----> 就是自己所在的那一行。

   那么我们实现的思路就疏通好了,就每次递归的时候判断一下我k的大小 如果 k == 1就直接放回1就行了(这边还有一个前提就是我找到的节点不能为null,如果为null就没意义了)

  逻辑疏通了我们就来实现了

  1. //求第k层节点的个数
  2. //先找到树的左树的k-1层(root.right) 在找到右树的k-1层(root.left) 再将左右树的第一次加起来
  3. public int getKLeveNodeCont (TreeNode root , int k) {
  4. if(root == null) {
  5. return 0;
  6. }
  7. if(k == 1) {
  8. return 1;
  9. }
  10. //这里就是在往第k-1层找
  11. return getKLeveNodeCont(root.right,k-1)+getKLeveNodeCont(root.left,k-1);
  12. }

这段代码实现是很简单的,关键是在实现代码的逻辑

 4). 获取二叉树的高度  ---- int getHeight(Node root);

  实现获取二叉树的高度这个功能很简单,就是左子树的高度和右子树的高度的最大值加上自己本身(加1)完事了,也是一个递归就可以完成的功能

  这就直接上代码了

  1. //左树的高度和右树的高度的最大值加自己本身(加一)
  2. //递归
  3. //时间复杂度都为O(n)
  4. public int getHeight (TreeNode root ) {
  5. //超出树的范围了
  6. if(root == null) {
  7. return 0;
  8. }
  9. int leftH = getHeight(root.left);
  10. int rightH = getHeight(root.right);
  11. return (leftH>rightH?leftH:rightH)+1;
  12. }

5).检测值为value的元素是否存在  ---- Node find(Node root, int val);

   在查找这里也是遍历一次二叉树就完事了,我们这边统一一下就是先遍历左树如果没有找到再去遍历右树

  这边就直接上实现查找功能的代码了(上面给的注释很完全了)

  1. //查找
  2. //先看根节点是不是 再左最后右
  3. public TreeNode find(TreeNode root , int val) {
  4. //以前序遍历的方式遍历
  5. TreeNode cur;
  6. //先判断节点是否为空
  7. if(root == null) {
  8. return null;
  9. }
  10. //再判断节点的val是否为要找的val
  11. if(root.val == val) {
  12. return root;
  13. }
  14. //根节点找不到先往左找
  15. cur = find(root.left,val);
  16. if(cur == null) {
  17. //左还是没找到往右找
  18. cur = find(root.right,val);
  19. }
  20. //管你有没有找到就直接返回
  21. return cur;
  22. }

  6).判断一棵树是不是完全二叉树 --- boolean isCompleteTree(Node root);

   这里来了个好玩的,要判断一棵树是不是完全二叉树的前提是搞明白完全二叉树的定义,我们先来回顾一下定义  --- 对于深度为K的,有n 个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从0至n-1的结点一一对应时称之为完 比特就业课 全二叉树。

   我们从定义中不难看出完全二叉树有一个特性就是数据要输出都是连续输出的,在输出数据的时候是不会出现某个节点为null的情况的,那么我们只要在遍历二叉树的时候当遇到null的时候(遍历完以后)观察一下前面遍历的数据里面是否有出现null就欧克克了。

  这里要使用到的实现方法其实根上面的非递归实现层序遍历的写法是差不多的,就是在cur(临时定义的指针)去遍历二叉树的时候遇到null的时候就立马跳出循环然后去判断队列剩余元素的状况就欧克克了

  上代码!!!

  1. //判断二叉树是不是完全二叉树
  2. //要用到null值 看null是不是连续的是不是穿插在(null可以放入队列中)
  3. //遇到null就停下来 --- 看队列当中的元素 是不是全部都是null 就是完全二叉树
  4. public boolean isCompleteTree (TreeNode root) {
  5. Queue<TreeNode> queue = new LinkedList<>();
  6. if(root != null) {
  7. queue.offer(root);
  8. //先放根节点
  9. }
  10. while (!queue.isEmpty()) {
  11. TreeNode cur = queue.poll();
  12. if(cur != null) {
  13. queue.offer(cur.left);
  14. queue.offer(cur.right);
  15. } else {
  16. //队列取出的元素为null 了要看队列里面的放的东西了
  17. break;
  18. }
  19. }
  20. while (!queue.isEmpty()) {
  21. TreeNode cur1 = queue.poll();
  22. if(cur1 != null) {
  23. return false;
  24. //每次都会从队列里面那一个值
  25. //不为空就直接润回false
  26. }
  27. }
  28. return true;
  29. }

  这里面的isEmpty()方法是判断队列是否为空的方法

2.5前中后序遍历的非递归实现

   1).前序遍历 ---- 非递归版本

    这道题是力扣的题,它题目要求是

   所以在实现的时候要小心一点

      在实现前序遍历的非递归版本就和实现层序遍历的时候有点不一样了,在实现层序遍历的时候我们是使用到了队列(Queue)这一数据结构,当时在实现前中后序遍历的时候我们要使用到的是栈(Stack)这一数据结构

     思路:先遍历一个栈出来,由于我们这边要实现的前序遍历,我们直接在进入循环的时候在遍历一个循环去把根节点自己本身和左树先全部放入栈中并且放入顺序表中,然后当往左走走到null的时候就弹出栈顶元素,让指针指向该元素的右子树接这进入一开始的循环,这样就可以完成我们的前序遍历了

   我们可以直接上图来理解

 

   图文解释完了我们就直接来上代码了

  1. public List<Integer> preorderTraversal(TreeNode root) {
  2. List<Integer> list = new ArrayList<>();
  3. if (root == null) {
  4. return list;
  5. }
  6. Stack<TreeNode> stack = new Stack<>();
  7. TreeNode cur = root;
  8. //这里cur很容易找着找着就null了很容易就跳出来了 所以要加多一个stack栈不为空的情况
  9. while (cur != null || !stack.empty()) {
  10. while (cur != null) {
  11. TreeNode top = stack.push(cur);
  12. list.add(top.val);
  13. cur = cur.left;
  14. //先让节点的左子树进来
  15. }
  16. TreeNode top = stack.pop();
  17. cur = top.right;
  18. }
  19. return list;
  20. }

我们来看一下在力扣是否可以跑的过

完全没有任何问题

  2).中序遍历 ---- 非递归版本

     中序遍历的非递归实现根前序的差不多的,就是在把数据放入顺序表的时候要注意一下了

    这里就直接上代码了(代码中的注释已经足够去理解了)

  1. public List<Integer> inorderTraversal(TreeNode root) {
  2. List<Integer> ret = new ArrayList<>();
  3. // 空树直接返回
  4. if(null == root){
  5. return ret;
  6. }
  7. Stack<TreeNode> s = new Stack<>();
  8. TreeNode cur = root;
  9. while(cur != null || !s.empty()){
  10. // 沿这cur一直往左侧走,找到该条路径中最左侧的节点,并保存其所经路径中的所有节点
  11. while(cur != null){
  12. s.push(cur);
  13. cur = cur.left;
  14. }
  15. // 获取根节点,直接遍历,因为其左侧是空树
  16. cur = s.peek();
  17. s.pop();
  18. ret.add(cur.val);
  19. // cur的左子树已经遍历,cur已经遍历,剩余其右子树没有遍历,
  20. // 将其右子树当成一棵新的树进行遍历
  21. cur = cur.right;
  22. }
  23. return ret;
  24. }

  这里一样也是看一下在力扣是否能跑的过

  3).后序遍历 ---- 非递归版本

     在后序遍历这里就需要做出一点整了,我一开始还是跟上面的步骤一样先去走左树把左树走到空,但是先不往顺序表中存放数据先,以为我是后序遍历要保证我这个节点的左右子树要么为空要么都已经放进去了在将我的根节点存放进去,那么意味着我放完根节点的所有的left节点以后要去peek一下我栈里面的栈顶元素,看一下它是否有右子树,有的话就将我的cur指向peek节点的右子树,没有的话就说明了我这个节点是可以输出的, 但是实现这一步的时候会出现一个bug,就是会死循环,为什么呢 ----> 因为我在将我的cur指向peek节点的right节点的时候,当这个节点走完了,并且已经存入了顺序表中,但是你还是会回到我一开始那个peek的节点,我没有加任何东西去判断它有没有走过右边,这样的话就会死循环了,所以我们要额外定义一个变量(当我要走右树的时候判断一下这个右树的节点在上一步是否给走过),如果这个peek的右树在上一步没有给走过就把定义的变量指向peek的右数,如果有就直接把peek的节点存放到顺序表中,任何走人 

    这里就不上图文解释了,直接看一下代码实现

  1. public List<Integer> postorderTraversal(TreeNode root) {
  2. List<Integer> list = new ArrayList<>();
  3. if(root == null) {
  4. return list;
  5. }
  6. TreeNode cur = root;
  7. Stack<TreeNode> stack = new Stack();
  8. TreeNode prev = null;//用来标记节点在上一次是否被使用过
  9. while(cur != null || !stack.empty()) {
  10. while(cur != null) {
  11. stack.push(cur);
  12. cur = cur.left;
  13. }
  14. TreeNode top = stack.peek();
  15. if(top.right == null || top.right == prev) {
  16. //该数没有右节点或者已经遍历过了
  17. list.add(top.val);
  18. prev = top;
  19. stack.pop();
  20. } else {
  21. cur = top.right;
  22. }
  23. }
  24. return list;
  25. }

  这边如果有哪里不明白的点就直接私信博主,一对一解决

2.6 二叉树的创建

    在二叉树的关键的时候,一般都是给出两个遍历序列给出来 然后 让我们去画出对应二叉树的,这里给的序列一般都是前序加中序,或者中序加后序遍历的,如果给的是前序加后序是不可能去画出一棵二叉树的,因为这样是无法确定每一棵子树的根节点的,所以必须要搭配中序遍历。

   这里我们先把如何画出来的用图文形式表达出来,然后就直接上代码(我在这边采用的是先序遍历加中序遍历的新式,中序加后序的逻辑是差不多的)

 实现代码展示

  1. public int preIndex = 0;
  2. public TreeNode buildTree(int[] preorder, int[] inorder) {
  3. TreeNode ret = buildTreeChild(preorder,inorder,0,inorder.length-1);
  4. return ret;
  5. }
  6. public TreeNode buildTreeChild(int[] preorder, int[] inorder,int inbegin, int inend) {
  7. //递归出口
  8. //当我查找根节点找着找着 inbegin比inend大的时候就说明已经找完了
  9. if(inbegin > inend) {
  10. return null;
  11. }
  12. //创建根节点
  13. TreeNode root = new TreeNode(preorder[preIndex]);
  14. //查找我节点在中序遍历中的位置
  15. int rootIndex = findIndex(inorder,inbegin,inend,preorder[preIndex]);
  16. preIndex++;//指针移动
  17. //因为题目给的是先序遍历 先创建左子树 再创建右子树
  18. root.left = buildTreeChild(preorder,inorder,inbegin,rootIndex-1);
  19. root.right = buildTreeChild(preorder,inorder,rootIndex+1,inend);
  20. return root;
  21. }
  22. public int findIndex(int[] inorder,int inbegin,int inend,int key) {
  23. for(int i = inbegin;i <= inend;i++) {
  24. if(inorder[i] == key) {
  25. return i;
  26. }
  27. }
  28. return -1;
  29. }

力扣运行结果

 实现构建二叉树的代码会有一点点难,我在这表达的可能不是很清楚,如果没有搞明白的话随时欢迎私信博主,我一对一教学(提前是问的时候学校这没课)

那么二叉树到这就差不多结束了二叉树挺难一下的,还有很多概念我这没有讲到,到时候应该还会再写一篇关于二叉树的博客去说明,这篇博客到这就结束了,如果内容有哪出了问题欢迎在评论区指出,润了(博主要去养老了)

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/457160?site
推荐阅读
相关标签
  

闽ICP备14008679号