数据结构-树(一)

阅读目录
数据结构-树(一)
一、树
二、二叉树
三、二叉搜索树
四、平衡二叉树
数据结构-树(二)
五、红黑树
六、B树

一、树
定义:n(n>=0)个节点构成的有限集合。当n=0时,称为空树。

*树与非树?
子树是不相交的;
除了根节点外,每个节点有且仅有一个父节点;
一棵N个结点的树有N-1条边

数的基本术语
1)结点的度(Degree):结点的子树个数
2)树的度:树的所有节点中最大的度数
3)叶结点(Leaf):度为0的结点
4)父结点(Parent):有子树的节点是其子树的根节点的父节点
5)子结点(Child):若A结点是B结点的父结点,则称B结点是A结点的子结点,子结点也称孩子结点
6)兄弟结点(Sibling):具有同一父结点的各结点彼此是兄弟结点
7)路径和路径长度:路径所包含边的个数称为路径的长度
8)祖先结点(Ancestor)/子孙结点(Descendant):某一结点的子树中所有结点是这个结点的子孙
9)结点的层次:规定根节点在1层,其它任一节点的层数是其父结点的层数加1
10)树的深度:树中所有结点中最大层次是这棵树的深度

树的表示方法
儿子-兄弟表示法
Element——>NextSibling
|
v
FirstChild
二、二叉树
定义:一个有穷的结点集合,这个集合可以为空,若不为空,则它是由根结点和称为其左子树和右子树的两个不相交的二叉树组成。(左子树和右子树是有顺序的,次序不能颠倒)
性质
1 、在非空二叉树中,第k层的结点总数不超过2^(k-1), k>=1
2、 深度为h的二叉树最多有2^h-1个结点(h>=1),最少有h个结点
3、对于任何一棵非空的二叉树,如果叶节点个数为n0,度数为2的节点个数为n2,则有: n0 = n2 + 1
证明:在一棵二叉树中,除了叶子结点(度为0)之外,就剩下度为2(n2)和1(n1)的结点了。则树的结点总数为T = n0+n1+n2;在二叉树中结点总数为T,而连线数为T-1.所以有:n0+n1+n2-1 = 2n2 +n1;最后得到n0 = n2+1;
4、具有n个结点的完全二叉树的深度为log2(n+1);
5、如果有一颗有n个节点的完全二叉树的节点按层次序编号,对任一层的节点i(1<=i<=n)有
如果i=1,则节点是二叉树的根,无双亲,如果i>1,则其双亲节点为[i/2],向下取整
如果2i>n那么节点i没有左孩子,否则其左孩子为2i
如果2i+1>n那么节点没有右孩子,否则右孩子为2i+1
6、给定N个节点,能构成h(N)种不同的二叉树,其中h(N)为卡特兰数的第N项,h(n)=C(2
n, n)/(n+1)。
7、设有i个枝点,I为所有枝点的道路长度总和,J为叶的道路长度总和J=I+2i。

特殊二叉树
1、斜二叉树:所有的结点都只有左子树(左斜树),或者只有右子树(右斜树)。

2、完美二叉树(满二叉树):所有的分支结点都存在左子树和右子树,并且所有的叶子结点都在同一层上。
在这里插入图片描述

完美二叉树的性质:
  1) 一颗树深度为h,最大层数为k,深度与最大层数相同,k=h;
  2) 叶子数为2h;
  3) 第k层的结点数是:2^(k-1);
  4) 总结点数是:2^k-1,且总节点数一定是奇数。

3、完全二叉树:若设二叉树的深度为h,除第 h 层外,其它各层 (1~(h-1)层) 的结点数都达到最大个数,第h层所有的结点都连续集中在最左边,这就是完全二叉树。

二叉树的存储结构
1、顺序存储结构
完全二叉树:按从上至下、从左到右顺序存储,n各结点的完全二叉树的节点父子关系(一般二叉树会造成空间浪费)

2、链表存储

typedef struct TreeNode *BinTree;
typedef BinTree Position;
struct TreeNode{
    ElementType Data;
    BinTree    Left;
    BinTree    Right;
}

二叉树的遍历
从树的根节点出发,按照某种次序依次访问二叉树中所有的结点,使得每个结点被访问仅且一次。
1、先序遍历(根—左—右)

/先序递归遍历
void PreOrderTraverse(BiTree Bt)
{
    if(Bt )  //注意跳出条件
    {
       //注意访问语句顺序
        printf("%c ", Bt->data);
        PreOrderTraverse(Bt->lchild);
        PreOrderTraverse(Bt->rchild);
    }
}

先序非递归遍历:
对于任一结点p:
a. 访问结点p,并将结点p入栈;
b. 判断结点p的左孩子是否为空,若为空,则取栈顶结点并进行出栈操作,并将栈顶结点的右孩子置为当前的结点p,循环置a;若不为空,则将p的左孩子置为当前结点p;
c. 直到p为空,并且栈为空,则遍历结束。

//先序非递归遍历
int NoPreOrderTraverse(BiTree Bt)
{
    SqStack s;
    InitStack(&s);
 
    BiTree tmp = Bt;
    if(tmp == NULL)
    {
        fprintf(stdout, "the tree is null.\n");
        return ERROR;
    }
   //现将左子树压入栈,当到叶子结点后,出栈,获取右子树,然后在压入右子树的左子树。
  //顺序不能变
    while((tmp != NULL) || (IsEmpty(&s) != 1)) 
    {
        while(tmp != NULL)
        {
            Push(&s, tmp);
            printf("%c ", tmp->data);
            tmp = tmp->lchild;
        }
        if(IsEmpty(&s) != 1)
        {
            Pop(&s, &tmp);
            tmp = tmp->rchild;
        }
    }
     
    return OK;
}

2、中序遍历(左—根—右)

//中序递归遍历
void InOrderTraverse(BiTree Bt)
{
    if(Bt != NULL)
    {
        InOrderTraverse(Bt->lchild);
        printf("%c ", Bt->data);
        InOrderTraverse(Bt->rchild);
    }
}
//中序非递归遍历二叉树
int NoInOrderTraverse(BiTree Bt)
{
    SqStack s;
    InitStack(&s);
     
    BiTree tmp = Bt;
    if(tmp == NULL)
    {
        fprintf(stderr, "the tree is null.\n");
        return ERROR;
    }
 //和先序差不多,只是先访问左子树
    while(tmp != NULL || (IsEmpty(&s) != 1))
    {
        while(tmp != NULL)
        {
            Push(&s, tmp);
            tmp = tmp->lchild;
        }
 
        if(IsEmpty(&s) != 1)
        {
            Pop(&s, &tmp);
            printf("%c ", tmp->data);
            tmp = tmp->rchild;
        }
    }
    return OK;
}

3、后序遍历(左—右—根)

//后序递归遍历
void PostOrderTraverse(BiTree Bt)
{
    if(Bt != NULL)
    {
        PostOrderTraverse(Bt->lchild);
        PostOrderTraverse(Bt->rchild);
        printf("%c ", Bt->data);
    }
}

要保证根结点在左孩子和右孩子访问之后才能访问,因此对于任一结点p,先将其入栈。
若p不存在左孩子和右孩子,则可以直接访问它,或者p存在左孩子或右孩子,但是其左孩子和右孩子都已经被访问过了,则同样可以直接访问该结点。若非上述两种情况,则将p的右孩子和左孩子依次入栈,这样就保证了每次取栈顶元素的时候,左孩子在右孩子之前别访问,左孩子和右孩子都在根结点前面被访问。

//后序非递归遍历二叉树
int NoPostOrderTraverse(BiTree Bt)
{
    SqStack s;
    InitStack(&s);
 
    BiTree cur;     //当前结点  
    BiTree pre = NULL;      //前一次访问的结点
    BiTree tmp;
 
    if(Bt == NULL)
    {
        fprintf(stderr, "the tree is null.\n");
        return ERROR;
    }
 
    Push(&s, Bt);
    while(IsEmpty(&s) != 1)
    {
        GetTop(&s, &cur);
        if((cur->lchild == NULL && cur->rchild == NULL) || (pre != NULL && (pre == cur->lchild || pre == cur->rchild)))
        {
            printf("%c ", cur->data);    //如果当前结点没有孩子结点或者孩子结点都已被访问过
                                         //如果pre == cur->lchild说明当前结点没有右孩子
                                         //否则按照入栈顺序,当前栈顶结点应该为右孩其左子孩子不等于pre      
            Pop(&s, &tmp);
            pre = cur;
        }
        else
        {
            if(cur->rchild != NULL)
            {
                Push(&s, cur->rchild);
            }
            if(cur->lchild != NULL)
            {
                Push(&s, cur->lchild);
            }
        }
    }
    return OK;
}

4、层序遍历
队列实现:遍历从根结点开始,首先将根结点入队,然后开始执行循环:结点出队、访问该结点、其左右儿子入队

void LeveOrderTraversal(BinTtree Bt)
{
     Queue Q;
     BinTtree T;
     if( !Bt ) return ;
     Q = CreatQueue(MaxSize);
     AddQ(Q,Bt);
    while( !IsEmptyQ(Q) ){
           T = DeleteQ(Q);
           printf("%d\n",T->Data);
           if( T->Left )  AddQ(Q,T->Left);
            if( T->Right )  AddQ( Q,T->Right);
   } 
}

三、二叉搜索树

定义:二叉搜索树或者是一棵空树,或者是具有下列性质的二叉树:
 1) 若左子树不空,则左子树上所有结点的键值均小于它的根结点的键值;
 2) 若右子树不空,则右子树上所有结点的键值均大于它的根结点的键值;
 3) 左、右子树也分别为二叉排序树;
二叉搜索树操作的特别函数:

Position Find(ElementType X, BinTree BST):从二叉搜索树BST中查找元素X,返回其所在结点的地址;

Postion FindMin(BinTree BST):从二叉搜索树BST中查找并返回最小元素所在结点的地址;

Postion FindMax(BinTree BST):从二叉搜索树BST中查找并返回最大元素所在结点的地址;

BinTree Insert(ElementType X, BinTree BST)

BinTree Delete(ElementType X, BinTree BST)

二叉搜索树的查找操作Find
1)尾递归

Position Find(ElementType X, BinTree BST)
{
  if (!BST) return NULL; // 查找失败
  if (X > BST->Data)
    return Find(X, BST->Right); // 在右子树中继续查找 // 尾递归
  else if (X < BST->Data)
    return Find(X, BST->Left); // 在左子树中继续查找 // 尾递归
  else // X == BST->Data
    reutrn BST; // 查找成功,返回结点的找到结点的地址
}

2)迭代函数

Position IterFind(ElementType X, BinTree BST)
{
  while (BST){
    if (X > BST->Data)
      BST = BST->Right; // 向右子树中移动,继续查找
    else if (X < BST->Data)
      BST = BST->Left; // 向左子树中移动,继续查找
    else // X == BST->Data
      return BST; // 查找成功,返回结点的找到结点的地址
  }
  reuturn NULL; // 查找失败
}

查找最大和最小元素
从根节点开始,沿着右子树一直往下,直到找到最后一个右子树节点,最大元素一定是在树的最右分支的端结点上
从根节点开始,沿着左子树一直往下,直到找到最后一个左子树节点,最小元素一定是在树的最左分支的端结点上

// 查找最小元素的递归函数
Position FindMin(BinTree BST)
{
  if (!BST) return NULL; // 空的二叉搜索树,返回NULL
  else if (!BST->Left)
    reuturn BST; // 找到最左叶结点并返回
  else
    return FindMin(BST->Left); // 沿左分支继续查找
}
  
// 查找最大元素的迭代函数
Postion FindMax(BinTree BST)
{
  if (BST)
    while (BST->Right) BS = BST->Right; // 沿右分支继续查找,直到最右叶结点
  return BST;
}

二叉搜索树的插入操作Insert
关键是要找到元素应该插入的位置

BinTree Insert(ElementType X, BinTree BST)
{
  if (!BST){ // 若原树为空,生成并返回一个结点的二叉搜索树
    BST = malloc(sizeof(struct TreeNode));
    BST->Data = X;
    BST->Left = BST->Right = NULL;
  }else // 开始找要插入元素的位置
    if (X < BST->Data)
      BST->Left = Insert(X, BST->Left); // 递归插入左子树
    else if (X > BST->Data)
      BST->Right = Insert(X, BST->Right); // 递归插入右子树
        // else X已经存在,什么都不做
  return BST;
}

二叉搜索树的删除操作Delete
三种情况
1)删除叶节点:直接删除,并修改其父结点指针-置为NULL
2)删除只有一个孩子结点:将其父结点指向要删除结点的孩子结点
3)删除的结点有左右两颗子树:用另一结点代替被删除结点:右子树的最小元素或左子树的最大元素

BinTree Delete(ElementType X, BinTree BST)
{
  Position Tmp;
  if (!BST) printf("要删除的元素未找到");
  else if (X < BST->Data)
    BST->Left = Delete(X, BST->Left); // 左子树递归删除
  else if (X > BST->Data)
    BST->Right = Delete(X, BST->Right); // 右子树递归删除
  else // 找到要删除的结点
    if (BST->Left && BST->Right){ // 被删除结点有左右两个子结点
      Tmp = FindMin(BST->Right); // 在右子树中找最小的元素填充删除结点
      BST->Data = Tmp->Data;
      BST->Right = Delete(BST->Data, BST->Right); // 在删除结点的右子树中删除最小元素
    } else { // 被删除结点有一个或无子结点
      Tmp = BST;
      if (!BST->Left)
        BST = BST->Right; // 有右孩子或无子结点
      else if (!BST->Right) 
        BST = BST->Left; // 有左孩子或无子结点
      free(Tmp);
    }
  return BST;
}

性质:对二叉查找树进行中序遍历,即可得到有序的数列
二叉搜索树的时间复杂度:它和二分查找一样,插入和查找的时间复杂度均为O(logn),但是在最坏的情况下仍然会有O(n)的时间复杂度。原因在于插入和删除元素的时候,树没有保持平衡。我们追求的是在最坏的情况下仍然有较好的时间复杂度,这就是平衡查找树设计的初衷。
二叉查找树的高度决定了二叉查找树的查找效率。

四、平衡二叉树(AVL树)
定义:平衡二叉树(Balanced Binary Tree)又被称为AVL树(有别于AVL算法),且具有以下性质:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。平衡二叉树的常用算法有红黑树、AVL树等。在平衡二叉搜索树中,我们可以看到,其高度一般都良好地维持在O(log2n),大大降低了操作的时间复杂度。

最小二叉平衡树的节点的公式如下:F(n)=F(n-1)+F(n-2)+1
这个类似于一个递归的数列,可以参考Fibonacci数列,1是根节点,F(n-1)是左子树的节点数量,F(n-2)是右子树的节点数量。
平衡二叉树的调整:
1、定义平衡二叉树节点结构:

    typedef struct Node
    {
        int key;//值
        struct Node *left;//指向左儿子的地址
        struct Node *right;//指向右儿子的地址
        int height;//高度
    }BTNode;

2、旋转 AVL树最关键的也是最难的一步操作就是旋转。旋转主要是为了实现AVL树在实施了插入和删除操作以后,树重新回到平衡的方法。下面我们重点研究一下AVL树的旋转。
  对于一个平衡的节点,由于任意节点最多有两个儿子,因此高度不平衡时,此节点的两颗子树的高度差2.容易看出,这种不平衡出现在下面四种情况:
在这里插入图片描述1) 6节点的左子树3节点高度比右子树7节点大2,左子树3节点的左子树1节点高度大于右子树4节点,这种情况成为左左。
2) 6节点的左子树2节点高度比右子树7节点大2,左子树2节点的左子树1节点高度小于右子树4节点,这种情况成为左右。
3) 2节点的左子树1节点高度比右子树5节点小2,右子树5节点的左子树3节点高度大于右子树6节点,这种情况成为右左。
4) 2节点的左子树1节点高度比右子树4节点小2,右子树4节点的左子树3节点高度小于右子树6节点,这种情况成为右右。
从图2中可以可以看出,1和4两种情况是对称的,这两种情况的旋转算法是一致的,只需要经过一次旋转就可以达到目标,我们称之为单旋转。2和3两种情况也是对称的,这两种情况的旋转算法也是一致的,需要进行两次旋转,我们称之为双旋转。
单旋转
单旋转是针对于左左和右右这两种情况的解决方案,这两种情况是对称的,只要解决了左左这种情况,右右就很好办了。图3是左左情况的解决方案,节点k2不满足平衡特性,因为它的左子树k1比右子树Z深2层,而且k1子树中,更深的一层的是k1的左子树X子树,所以属于左左情况。
在这里插入图片描述
为使树恢复平衡,我们把k2变成这棵树的根节点,因为k2大于k1,把k2置于k1的右子树上,而原本在k1右子树的Y大于k1,小于k2,就把Y置于k2的左子树上,这样既满足了二叉搜索树的性质,又满足了平衡二叉树的性质。
  这样的操作只需要一部分指针改变,结果我们得到另外一颗二叉查找树,它是一棵AVL树,因为X向上一移动了一层,Y还停留在原来的层面上,Z向下移动了一层。整棵树的新高度和之前没有在左子树上插入的高度相同,插入操作使得X高度长高了。因此,由于这颗子树高度没有变化,所以通往根节点的路径就不需要继续旋转了。

BTNode *ll_rotate(BTNode *K2)//左左单旋转实现,右右同理
{
    BTNode *K1 = K2->left;
    K2->left = K1->right;
    K1->right = K2;   
 
    K2->height = max(height(K2->left), height(K2->right)) + 1;
    K1->height = max(height(K1->left), height(K2->right)) + 1;
 
    return K1;
}

双旋转
对于左右和右左这两种情况,单旋转不能使它达到一个平衡状态,要经过两次旋转。双旋转是针对于这两种情况的解决方案,同样的,这样两种情况也是对称的,只要解决了左右这种情况,右左就很好办了。图4是左右情况的解决方案,节点k3不满足平衡特性,因为它的左子树k1比右子树Z深2层,而且k1子树中,更深的一层的是k1的右子树k2子树,所以属于左右情况。
在这里插入图片描述为使树恢复平衡,我们需要进行两步,第一步,把k1作为根,进行一次右右旋转,旋转之后就变成了左左情况,所以第二步再进行一次左左旋转,最后得到了一棵以k2为根的平衡二叉树。

BTNode* lr_rotate(BTNode* K3)//左右情况下双旋转的过程,右左同理
{
    BTNode* K1 = K3->left;
    K3->left = rr_rotate(K1);
    return ll_rotate(K3);
}

平衡二叉树实现的实例 平衡二叉树-月雲之霄
选取一组数据分别为2,1,0,3,4,5,6,9,8,7的10个结点来构造平衡二叉树。
1)首先数据为2的结点作为根结点插入,接着插入1,仍是平衡的,再插入0是,2的平衡因子变为2,此时出现了不平衡,因此需要进行调整,最低不平衡结点为2,属于LL型,调整过程如图1所示。
在这里插入图片描述
2)接着插入3,是平衡的,再插入4,此时出现了不平衡,结点 1 和 2 的平衡因子都为 -2,结点2为最低不平衡结点,属于RR型,调整过程如图2所示
在这里插入图片描述3)接着插入5,此时结点 1 的平衡因子为 -2,导致不平衡,结点1为最低不平衡结点,属于RR型,调整如图3所示
在这里插入图片描述4)接着插入6,此时结点4的平衡因子为 -2,导致不平衡,结点4为最低不平衡结点,属于RR型,调整如图4所示。
在这里插入图片描述5)接着插入9,是平衡的,再插入8,此时结点 3、5、6 的平衡因子都为 -2,导致不平衡,结点6为最低不平衡结点,属于RL型,调整如图5所示
在这里插入图片描述6)插入7,此时结点3、5的平衡因子为 -2,导致不平衡,最低不平衡结点为5,属于RL型,调整如图6所示。(先以8为根进行左左旋转,得到
5 -6-8-9旋转之后变成了右
|       |
4     7
右情况所以第二次再进行一次右右旋转最后得到了一颗以6为根的平衡子树 )
在这里插入图片描述在这里插入代码片完整代码

#include<stdio.h>
#include<stdlib.h>
 
typedef struct Node
{
    int key;
    struct Node *left;
    struct Node *right;
    int height;
}BTNode;
 
int max(int a, int b);
 
 
int height(struct Node *N)
{
    if (N == NULL)
        return 0;
    return N->height;
}
 
int max(int a, int b)
{
    return (a > b) ? a : b;
}
 
BTNode* newNode(int key)
{
    struct Node* node = (BTNode*)malloc(sizeof(struct Node));
    node->key = key;
    node->left = NULL;
    node->right = NULL;
    node->height = 1;
    return(node);
}
 
BTNode* ll_rotate(BTNode* y)
{
    BTNode *x = y->left;
    y->left = x->right;
    x->right = y;
 
    y->height = max(height(y->left), height(y->right)) + 1;
    x->height = max(height(x->left), height(x->right)) + 1;
 
    return x;
}
 
BTNode* rr_rotate(BTNode* y)
{
    BTNode *x = y->right;
    y->right = x->left;
    x->left = y;
 
    y->height = max(height(y->left), height(y->right)) + 1;
    x->height = max(height(x->left), height(x->right)) + 1;
 
    return x;
}
 
int getBalance(BTNode* N)
{
    if (N == NULL)
        return 0;
    return height(N->left) - height(N->right);
}
 
BTNode* insert(BTNode* node, int key)
{
 
    if (node == NULL)
        return newNode(key);
 
    if (key < node->key)
        node->left = insert(node->left, key);
    else if (key > node->key)
        node->right = insert(node->right, key);
    else
        return node;
 
    node->height = 1 + max(height(node->left), height(node->right));
 
 
    int balance = getBalance(node);
 
 
 
    if (balance > 1 && key < node->left->key) //LL型
        return ll_rotate(node);
 
 
    if (balance < -1 && key > node->right->key)     //RR型
        return rr_rotate(node);
 
 
    if (balance > 1 && key > node->left->key)     //LR型
    {
        node->left = rr_rotate(node->left);
        return ll_rotate(node);
    }
 
    if (balance < -1 && key < node->right->key)     //RL型
    {
        node->right = ll_rotate(node->right);
        return rr_rotate(node);
    }
 
    return node;
}
 
 
BTNode * minValueNode(BTNode* node)
{
    BTNode* current = node;
 
    while (current->left != NULL)
        current = current->left;
 
    return current;
}
 
BTNode* deleteNode(BTNode* root, int key)
{
 
    if (root == NULL)
        return root;
 
    if (key < root->key)
        root->left = deleteNode(root->left, key);
 
    else if (key > root->key)
        root->right = deleteNode(root->right, key);
 
    else
    {
        if ((root->left == NULL) || (root->right == NULL))
        {
            BTNode* temp = root->left ? root->left : root->right;
 
            if (temp == NULL)
            {
                temp = root;
                root = NULL;
            }
            else
                *root = *temp;
            free(temp);
        }
        else
        {
            BTNode* temp = minValueNode(root->right);
 
            root->key = temp->key;
 
            root->right = deleteNode(root->right, temp->key);
        }
    }
 
 
    if (root == NULL)
        return root;
 
    root->height = 1 + max(height(root->left), height(root->right));
 
    int balance = getBalance(root);
 
 
    if (balance > 1 && getBalance(root->left) >= 0) //LL型
        return ll_rotate(root);
 
 
    if (balance > 1 && getBalance(root->left) < 0) //LR型
    {
        root->left = rr_rotate(root->left);
        return ll_rotate(root);
    }
 
    if (balance < -1 && getBalance(root->right) <= 0) //RR型
        return rr_rotate(root);
 
    if (balance < -1 && getBalance(root->right) > 0)  //Rl型
    {
        root->right = ll_rotate(root->right);
        return rr_rotate(root);
    }
 
    return root;
}
 
 
void preOrder(struct Node *root)
{
    if (root != NULL)
    {
        printf("%d ", root->key);
        preOrder(root->left);
        preOrder(root->right);
    }
}
 
int main()
{
    BTNode *root = NULL;
 
    root = insert(root, 9);
    root = insert(root, 5);
    root = insert(root, 10);
    root = insert(root, 0);
    root = insert(root, 6);
    root = insert(root, 11);
    root = insert(root, -1);
    root = insert(root, 1);
    root = insert(root, 2);
    printf("前序遍历:\n");
    preOrder(root);
 
    /* The constructed AVL Tree would be
                     9
                    /  \
                   1    10
                 /  \     \
                0    5     11
               /    /  \
              -1   2    6
    */
    
    root = deleteNode(root, 10);
    /* The AVL Tree after deletion of 10
                       1
                     /   \
                    0     9
                  /     /  \
                -1     5     11
                     /  \
                    2    6
    */
    printf("\n");
    printf("前序遍历:\n");
    preOrder(root);
    return 0;
}
发布了15 篇原创文章 · 获赞 1 · 访问量 4143

猜你喜欢

转载自blog.csdn.net/jingli456/article/details/104455645