平衡二叉树(AVL Tree)


 
不是科班出生的计算机专业,当然也没有学习过数据结构,当有一天突然意识到操作系统和算法是整个计算机的灵魂的时候,就像突然醍醐灌顶之感,
没办法,补吧,可是补起来又是何等的困难,这不这棵树,就拦在了我的面前,让我冲不过去。。
 
 
介绍

我们知道在二叉查找树中,如果插入元素的顺序接近有序,那么二叉查找树将退化为链表,从而导致二叉查找树的查找效率大为降低。如何使得二叉查找树无论在什么样情况下都能使它的形态最大限度地接近满二叉树以保证它的查找效率呢?

前苏联科学家G.M. Adelson-Velskii E.M. Landis给出了答案。他们在1962年发表的一篇名为《An algorithm for the organization of information》的文章中提出了一种自平衡二叉查找树(self-balancing binary search tree)。这种二叉查找树在插入和删除操作中,可以通过一系列的旋转操作来保持平衡,从而保证了二叉查找树的查找效率。最终这种二叉查找树以他们的名字命名为“AVL-Tree”,它也被称为平衡二叉树(Balanced Binary Tree)。这里所说的平衡使我们想到了中庸之道,但有句话说得好,“中不偏,庸不易”。学会这种平衡术是一个相当痛苦的过程。

 

什么是平衡

为了保证平衡,AVL树中的每个结点都有一个平衡因子(balance factor,以下用BF表示),它表示这个结点的左、右子树的高度差,也就是左子树的高度减去右子树的高度的结果值。AVL树上所有结点的BF值只能是-101。反之,只要二叉树上一个结点的BF的绝对值大于1,则该二叉树就不是平衡二叉树。图1演示了平衡二叉树和非平衡二叉树。

 

 
AVL树的构造

如何构造一棵平衡二叉树呢?动态地调整二叉查找树平衡的方法为:每插入一个结点后,首先检查是否破坏了树的平衡性,如果因插入结点而破坏了二叉查找树的平衡,则找出离插入点最近的不平衡结点,然后将该不平衡结点为根的子树进行旋转操作,我们称该不平衡结点为旋转根,以该旋转根为根的子树称为最小不平衡子树。

失衡状态可归纳为4种,它们对应着4种旋转类型。

1、LL型旋转

插入节点到左孩子的左子树而导致失衡的情况需要进行LL旋转(LL意为左左)。

2、LR型旋转

插入节点到左孩子的右子树而导致失衡的情况需要进行LR旋转,而这其中可能又分为LR-L、LR-R

3、RR型旋转

插入节点到右孩子的右子树而导致失衡的情况需要进行RR旋转。

4、RL型旋转

插入节点到右孩子的左子树而导致失衡的情况需要进行RL旋转,而这其中可能又分为RL-L、RL-R

 

AVL树上结点的插入

AVL算法的思想理解起来还是不太困难的,但如果真要使用代码实现就没那么简单了,它拥有超高的算法实现复杂度。我查了很多资料,大部分只给出主要算法代码,对于如何回溯修改BF值,如何处理不需要旋转的情况绝口不提,甚至对删除算法直接忽略。

下面对本算法做原理上的介绍:

1、如何回溯修改祖先结点的平衡因子

我们知道,在AVL树上插入一个新结点后,有可能导致其他结点BF值的改变,哪些结点的BF值会被改变?如何计算新的BF值呢?要解决这些问题,我们必须理解以下几个要点:

1、只有根结点到插入结(橙色结点)点路径(称为插入路径)上的结点的BF值会被改变。如图2所示,只有插入路径上结点(灰色结点)的BF值被改变,其他非插入路径上结点的BF值不变。

 

2、当一个结点插入到某个结点的左子树时,该结点的BF值加1(如图2的结点5043);当一个结点插入到某个结点的右子树时,该结点的BF值减1(如图2的结点2530)。如何在程序中判断一个结点是插入到左子树还是右子树呢?很简单,根据二叉查找树的特性可以得出结论:如果插入结点小于某个结点,则必定是插入到这个结点的左子树中;如果如果插入结点大于某个结点,则必定插入到这个结点的右子树中。

3、修改BF值的操作需从插入点开始向上回溯至根结点依次进行,当路径上某个结点BF值修改后变为0,则修改停止。如图3所示,插入结点30后,首先由于30<43,将结点43BF值加1,使得结点43BF值由0变为 1;接下来由于30>25,结点25BF值由1改为0;此时结点25BF值为0,停止回溯,不需要再修改插入路径上结点50的平衡因子。道理很简单:当结点的BF值由1-1变为0,表明高度小的子树添加了新结点,树的高度没有增加,所以不必修改祖先结点的平衡因子;当结点的BF值由0变为1-1时,表明原本等高左右子树由于一边变高而导致失衡,整棵子树的高度变高,所以必须向上修改祖先结点的BF值。

 

 

2、何时进行旋转操作?如何判断作什么类型的旋转?

在回溯修改祖先结点的平衡因子时,如果碰到某个结点的平衡因子变为2-2,表明AVL树失衡,这时需要以该结点为旋转根,对最小不平衡子树进行旋转操作。由于是从插入点开始回溯,所以最先碰到的BF值变为2-2的结点必定为最小不平衡子树的根结点。如图4所示,插入39后,4350两个结点的BF值都会变为2,而必定先访问到结点43,所以43是最小不平衡子树的根,旋转操作完成后,最小不平衡子树插入结点前和旋转完成后的高度不变,所以可以得出结论:旋转操作完成后,无需再回溯修改祖先的BF值。这样,图4中的结点2550的平衡因子实际上在插入结点操作完成后的BF值不变(对比图2)。

 

可以通过旋转根及其孩子的BF值来决定作什么类型的旋转操作:

1、当旋转根的BF值为2时:

如果旋转根的左孩子的BF值为1,则进行LL型旋转;

如果旋转根的左孩子的BF值为-1,则进行LR型旋转。

2、当旋转根的BF值为-2时:

如果旋转根的右孩子的BF值为1,则进行RL型旋转;

如果旋转根的右孩子的BF值为-1,则进行RR型旋转。

 

 

AVL树上结点的删除

AVL树的删除操作与插入操作有许多相似之处,它的大体步骤如下:

1、用二叉查找树的删除算法找到并删除结点(这里简称为删除点);

2、沿删除点向上回溯,必要时,修改祖先结点的BF值;

3、回溯途中,一旦发现某个祖先的BF值失衡,如插入操作那样旋转不平衡子树使之变为平衡,跟插入操作不同的是,旋转完成后,回溯不能停止,也就是说在AVL树上删除一个结点有可能引起多次旋转。

 

AVL树上的删除和插入操作虽然大体相似,但还是有一些不同之处,大家需要注意以下几点:

1、  回溯方式的不同

在删除结点的回溯过程中,当某个结点的BF值变为1-1时,则停止回溯。这一点同插入操作正好相反,因为BF值由0变为1-1,表明原本平衡的子树由于某个结点的删除导致了不平衡,子树的总体高度不变,所以不再需要向上回溯。

2、旋转方式的不同

如图5所示:删除AVL树中的结点25导致结点50BF值由原来的-1变为-2,但旋转根50的右孩子的BF值为0,这种情况在前面所讲的旋转操作中并不存在,那么是需要对它进行RR旋转还是RL旋转呢?正确方法是使用RR旋转,所不同之处是旋转后的BF值不同,需要单独处理。需要注意,这种情况在插入操作时不可能发生,LL旋转也存在类型的情况。另外旋转完成后树的整体高度没有改变,所以大部分情况下旋转操作完成后,子树的高度降低,需要继续向上回溯修改祖先的BF值,而只有这种情况由于子树的高度未改变,所以停止回溯。

 

3、删除点的选择特例

在二叉查找树中,我们知道当删除点p既有左子树,又有右子树,此时可以令p的中序遍历直接前驱结点代替p,然后再从二叉查找树中删除它的直接前驱。如图6所示,结点5既有左子树,又有右子树,它的直接前驱结点为4。在删除结点5时,首先用结点4代替结点5,然后再删除结点4完成删除操作。这里需要注意的是此时必须将删除前的结点4作为删除点来进行向上回溯操作,而不是结点5

 

AVL树的代码实现

这里没有给出AVL树的泛型实现,它只存放整数。


先给出二叉树的代码:

# include <stdio.h >
# include <stdlib.h >
 
typedef enum bool_enum {
     false,
     true,
} bool;
 
/* 二叉树节点 */
typedef struct bitnode_s {
     int data;
     int _count;
     struct bitnode_s *lchild, *rchild;
} bst_node;
 
bst_node *bst_head = NULL; //二叉树头结点
 
/*递归查找二叉排序树中是否存在关键要查找的数据
*指针f指向指针T的双亲,并初始调用值为NULL
*若查找成功,则指针P指向该元素节点,并返回true
*否则指针p支持查找路径上访问的最后一个节点并返回false
*/

bool searchbst(bst_node *t, int key, bst_node *f, bst_node * *p)
{
     if ( !t) {
         *p = f;
         return false;
    } else if (key == t - >data) {
         *p = t;
         return true;
    } else if (key < t - >data) { //左子树
         return searchbst(t - >lchild, key, t, p);
    } else { //右子树
         return searchbst(t - >rchild, key, t, p);
    }
}
 
//里面要给指针赋值,所以用指针的指针
bool insertbst_node(bst_node * *t, int key)
{
    bst_node *p, *s;
 
     /* 已有直接返回true */
     if (searchbst( *t, key, NULL, &p)) {
         return true;
    }
 
     //查找不成功
    s = (bst_node *)malloc( sizeof( *s));
     if ( !s) {
         return false;
    }
    s - >data = key;
    s - >lchild = s - >rchild = NULL;
 
     if ( !p) {
         *t = s;
    } else if (key < p - >data) { //插入到左边
        p - >lchild = s;
    } else {
        p - >rchild = s;
    }
 
    bst_head - >_count ++;
     return true;
}
 
bool _bst_delete(bst_node * *p)
{
    bst_node *q, *s;
 
     if ( !( *p) - >rchild) { //右子树为空只需从接它的左子树
        q = *p;
         *p = ( *p) - >lchild;
        free(q);
    } else if ( !( *p) - >lchild) { //左子树为空只需从接它的右子树
        q = *p;
         *p = ( *p) - >rchild;
        free(q);
    } else { //均不为空
        q = *p;
        s = ( *p) - >lchild;
 
         while (s - >rchild) { //转左,然后向右到尽头,找到待删除结点的前驱
            q = s;
            s = s - >rchild;
        }
 
        ( *p) - >data = s - >data; //s指向被删除结点的直接前驱
         if (q != *p) {
            q - >rchild = s - >lchild; //重接q的右子树
        } else {
            q - >lchild = s - >lchild; //重接q的左子树
        }
 
        free(s);
        s = NULL;
    }
    bst_head - >_count --;
     return true;
}
 
bool delete_bstnode(bst_node * *t, int key)
{
     if ( ! *t) { //不存在关键字等于key的数据
         return false;
    } else {
         if (key == ( *t) - >data) {
             return _bst_delete(t);
        } else if (key < ( *t) - >data) {
            delete_bstnode( &( *t) - >lchild, key);
        } else {
            delete_bstnode( &( *t) - >rchild, key);
        }
    }
}
 
/* 前序遍历二叉树 */
void per_order_traverse(bst_node *head)
{
     if ( !head) {
         return;
    }
 
    printf( "%d ", head - >data);
    per_order_traverse(head - >lchild);
    per_order_traverse(head - >rchild);
}
 
/* 中序遍历二叉树 */
void mid_order_traverse(bst_node *head)
{
     if ( !head) {
         return;
    }
 
    mid_order_traverse(head - >lchild);
    printf( "%d ", head - >data);
    mid_order_traverse(head - >rchild);
}
 
/* 后序遍历二叉树 */
void post_order_traverse(bst_node *head)
{
     if ( !head) {
         return;
    }
 
    post_order_traverse(head - >lchild);
    post_order_traverse(head - >rchild);
    printf( "%d ", head - >data);
}
 
# define array_num(arr) ( sizeof(arr) / sizeof(arr[ 0]))
int main( int argc, char * *argv)
{
     int i;
 
     int a[] = { 34, 67, 89, 23, 12, 3, 5, 7, 90};
     int nr = array_num(a);
 
     for (i = 0; i < nr; i ++) {
        insertbst_node( &bst_head, a[i]);
    }
 
    printf( "\nmid count %d:\n", bst_head - >_count);
    mid_order_traverse(bst_head);
 
    delete_bstnode( &bst_head, 7);
 
    printf( "\nmid count %d:\n", bst_head - >_count);
    mid_order_traverse(bst_head);
}
 
下面给出实现的avl树:

# include <stdio.h >
# include <stdlib.h >
 
# define OK       1
# define ERROR    0
 
# define TRUE     1
# define FALSE    0
 
/* 平衡因子 = 左子树高度 - 右子树高度 */
# define LH       + 1           /*  左高 */
# define EH       0            /*  等高 */
# define RH       - 1           /*  右高 */

 
typedef int Status;             /* Status是函数的类型,其值是函数结果状态代码,如OK等 */
 
/* 二叉树的二叉链表结点结构定义 */
typedef   struct BiTNode {
     int data;                 /* 结点数据 */
     int bf;                  /* 结点的平衡因子 */
     struct BiTNode *_left;   /* 左孩子指针 */
     struct BiTNode *_right; /* 右孩子指针 */
} BiTNode, *BiTree;
 
 
/* 对以p为根的二叉排序树作右旋处理,
* 处理之后p指向新的树根结点,即旋转处理之前的左子树的根结点
*/

void R_Rotate(BiTree *P)
{
    BiTree L;
 
    L = ( *P) - >_left;            /*  L指向P的左子树根结点 */
 
    ( *P) - >_left = L - >_right;    /*  L的右子树挂接为P的左子树 */
 
    L - >_right = ( *P);
 
     *P = L;                      /*  P指向新的根结点 */
}
 
/* 对以P为根的二叉排序树作左旋处理,
* 处理之后P指向新的树根结点,即旋转处理之前的右子树的根结点0
*/

void L_Rotate(BiTree *P)
{
    BiTree R;
 
    R = ( *P) - >_right;            /* R指向P的右子树根结点 */
 
    ( *P) - >_right = R - >_left;    /* R的左子树挂接为P的右子树 */
 
    R - >_left = ( *P);
 
 
     *P = R;                      /* P指向新的根结点 */
}
 
/*  对以指针T所指结点为根的二叉树作左平衡旋转处理
 *  本算法结束时,指针T指向新的根结点
*/

void LeftBalance(BiTree *T)
{
    BiTree L, Lr;
 
    L = ( *T) - >_left;            /*  L指向T的左子树根结点 */
 
     switch (L - >bf) {              /*  检查T的左子树的平衡度,并作相应平衡处理 */
     case LH : {                  /*  新结点插入在T的左孩子的左子树上,要作单右旋处理 LL */
        ( *T) - >bf = L - >bf = EH;
        R_Rotate(T);
         break;
    }
 
     case RH : {                  /*  新结点插入在T的左孩子的右子树上,要作双旋处理 LR */
        Lr = L - >_right;          /*  Lr指向T的左孩子的右子树根 */
         switch (Lr - >bf) {        /*  修改T及其左孩子的平衡因子 */
         case LH :
            ( *T) - >bf = RH;
            L - >bf = EH;
             break;
         case EH :
            ( *T) - >bf = L - >bf = EH;
             break;
         case RH :
            ( *T) - >bf = EH;
            L - >bf = LH;
             break;
        }
 
        Lr - >bf = EH;
        L_Rotate( &( *T) - >_left);     /*  对T的左子树作左旋平衡处理 */
        R_Rotate(T);                 /*  对T作右旋平衡处理 */
    }
 
     case EH :                     //deleteAVL需要,insertAVL用不着
        ( *T) - >bf = LH;
        L - >bf = RH;
        R_Rotate(T);
         break;
 
     default :
         break;
    }
}
 
/*  对以指针T所指结点为根的二叉树作右平衡旋转处理,
 *  本算法结束时,指针T指向新的根结点
*/

void RightBalance(BiTree *T)
{
    BiTree R, Rl;
 
    R = ( *T) - >_right;                /*  R指向T的右子树根结点 */
 
     switch (R - >bf) {                 /*  检查T的右子树的平衡度,并作相应平衡处理 */
     case RH : {                      /*  新结点插入在T的右孩子的右子树上,要作单左旋处理 RR */
        ( *T) - >bf = R - >bf = EH;
        L_Rotate(T);
         break;
    }
 
     case LH : {                       /*  新结点插入在T的右孩子的左子树上,要作双旋处理 RL */
        Rl = R - >_left;              /*  Rl指向T的右孩子的左子树根 */
         switch(Rl - >bf) {             /*  修改T及其右孩子的平衡因子 */
                 case RH : ( *T) - >bf = LH;
                         R - >bf = EH;
                          break;
                 case EH : ( *T) - >bf = R - >bf = EH;
                          break;
                 case LH : ( *T) - >bf = EH;
                         R - >bf = RH;
                          break;
              }
        Rl - >bf = EH;
        R_Rotate( &( *T) - >_right);       /*  对T的右子树作右旋平衡处理 */
        L_Rotate(T);                   /*  对T作左旋平衡处理 */
         break;
    }
 
     case EH :                 //deleteAVL需要,insertAVL用不着
        ( *T) - >bf = RH;
        R - >bf = LH;
        L_Rotate(T);
         break;
 
     default :
         break;
    }
}
 
/*  若在平衡的二叉排序树T中不存在和e有相同关键字的结点,则插入一个
 *  数据元素为e的新结点,并返回1,否则返回0。若因插入而使二叉排序树
 *  失去平衡,则作平衡旋转处理,布尔变量taller反映T长高与否。
*/

Status InsertAVL(BiTree *T, int e, Status *taller)
{
     if( ! *T) { /*  插入新结点,树“长高”,置taller为TRUE */
          *T = (BiTree)malloc( sizeof(BiTNode));
          if ( ! *T) {
             return FALSE;
         }
         ( *T) - >data = e;
         ( *T) - >_left = ( *T) - >_right = NULL;
         ( *T) - >bf = EH;
          *taller = TRUE;
    } else {
         if (e == ( *T) - >data) {       /*  树中已存在和e有相同关键字的结点则不再插入 */
             *taller = FALSE;
             return FALSE;
        }
 
         if (e < ( *T) - >data) {         /*  应继续在T的左子树中进行搜索 */
             if ( !InsertAVL( &( *T) - >_left, e, taller))  { /*  未插入 */
                 return FALSE;
            }
 
             if( *taller) {                    /*  已插入到T的左子树中且左子树“长高” */
                 switch(( *T) - >bf) {           /*  检查T的平衡度 */
                     case LH :                /*  原本左子树比右子树高,需要作左平衡处理 */
                        LeftBalance(T);
                         *taller = FALSE;
                         break;
                     case EH :                /*  原本左、右子树等高,现因左子树增高而使树增高 deleteAVL需要,insertAVL用不着 */
                        ( *T) - >bf = LH;
                         *taller = TRUE;
                         break;
                     case RH :                /*  原本右子树比左子树高,现左、右子树等高 */
                        ( *T) - >bf = EH;
                         *taller = FALSE;
                         break;
                }
            }
        } else {                             /*  应继续在T的右子树中进行搜索 */
             if ( !InsertAVL( &( *T) - >_right, e, taller))  { /*  未插入 */
                 return FALSE;
            }
 
             if ( *taller) {                    /*  已插入到T的右子树且右子树“长高” */
                 switch(( *T) - >bf) {           /*  检查T的平衡度 */
                     case LH :                /*  原本左子树比右子树高,现左、右子树等高 */
                        ( *T) - >bf = EH;
                         *taller = FALSE;
                         break;
                     case EH :                /*  原本左、右子树等高,现因右子树增高而使树增高  deleteAVL需要,insertAVL用不着 */
                        ( *T) - >bf = RH;
                         *taller = TRUE;
                         break;
                     case RH :                 /*  原本右子树比左子树高,需要作右平衡处理 */
                        RightBalance(T);
                         *taller = FALSE;
                         break;
                }
            }
        }
    }
 
     return TRUE;
}
 
/*  
  若在平衡的二叉排序树t中存在和e有相同关键字的结点,则删除之  
  并返回TRUE,否则返回FALSE。若因删除而使二叉排序树  
  失去平衡,则作平衡旋转处理,布尔变量shorter反映t变矮与否 
*/
  
Status deleteAVL(BiTree *t, int key, Status *shorter)  
{  
     if ( *t == NULL)  {                   //不存在该元素    
         return FALSE;                    //删除失败    
    } else if (key == ( *t) - >data) {          //找到元素结点   
        BiTree q = NULL;  
         if(( *t) - >_left == NULL) {            //左子树为空    
            q = *t;  
             *t = ( *t) - >_right;  
            free(q); 
             *shorter = TRUE;  
        } else if(( *t) - >_right == NULL) {        //右子树为空    
            q = *t;  
             *t = ( *t) - >_left;  
            free(q);  
             *shorter = TRUE;  
        } else {                          //左右子树都存在,   
            q = ( *t) - >_left;  
             while(q - >_right)   {  
                q = q - >_right;  
            }  
            ( *t) - >data = q - >data;  
            deleteAVL( &( *t) - >_left, q - >data, shorter);    //在左子树中递归删除前驱结点    
        }  
    }   else if(key < ( *t) - >data) {          //左子树中继续查找    
         if( !deleteAVL( &( *t) - >_left, key, shorter)) {  
             return FALSE;  
        }  
         if(shorter) {  
             switch(( *t) - >bf) {  
                 case LH :  
                    ( *t) - >bf = EH;  
                     *shorter = TRUE;  
                     break;  
                 case EH :  
                    ( *t) - >bf = RH;  
                     *shorter = FALSE;  
                     break;  
                 case RH :  
                    RightBalance(t);     //右平衡处理   
                     if(( *t) - >_right - >bf == EH) //注意这里,画图思考一下    
                         *shorter = FALSE;  
                     else  
                         *shorter = TRUE;  
                     break;  
            }  
        }  
    }   else {                                //右子树中继续查找       
         if( !deleteAVL( &( *t) - >_right, key, shorter)) {  
             return FALSE;  
        }
 
         if (shorter) {  
             switch(( *t) - >bf) {  
                 case LH :  
                    LeftBalance(t);      //左平衡处理    
                     if(( *t) - >_left - >bf == EH) //注意这里,画图思考一下    
                         *shorter = FALSE;  
                     else  
                         *shorter = TRUE;  
                     break;  
                 case EH :  
                    ( *t) - >bf = LH;  
                     *shorter = FALSE;  
                     break;  
                 case RH :  
                    ( *t) - >bf = EH;  
                     *shorter = TRUE;  
                     break;  
            }  
        }  
    }  
 
     return TRUE;  

 
/* 递归查找二叉排序树T中是否存在key,
* 指针f指向T的双亲,其初始调用值为NULL
* 若查找成功,则指针p指向该数据元素结点,并返回TRUE
* 否则指针p指向查找路径上访问的最后一个结点并返回FALSE
*/

Status SearchBST(BiTree T, int key, BiTree f, BiTree *p)

     if ( !T)    { /*  查找不成功 */
         *p = f; 
         return FALSE;
    } else if (key == T - >data) { /*  查找成功 */
         *p = T; 
         return TRUE;
    } else if (key < T - >data) {
         return SearchBST(T - >_left, key, T, p);   /*  在左子树中继续查找 */
    } else
         return SearchBST(T - >_right, key, T, p);   /*  在右子树中继续查找 */
    }
}
 
/* 中序递归遍历T */
void InOrderTraverse(BiTree T)
{
     if(T == NULL) {
         return;
    }
 
    InOrderTraverse(T - >_left);          /* 中序遍历左子树 */
    printf( "%d ",T - >data);               /* 显示结点数据,可以更改为其它对结点操作 */
    InOrderTraverse(T - >_right);          /* 最后中序遍历右子树 */
}
 
int main( int argc, char * *argv)
{
     int i;
     int arr_len;
     int a[] = { 3, 2, 1, 4, 5, 6, 7, 10, 9, 8, 30};
 
    BiTree T = NULL;
    Status taller, shorter;
 
    arr_len = sizeof(a) / sizeof(a[ 0]);
     for(i = 0;i < arr_len; i ++) {
        InsertAVL( &T, a[i], &taller);
    }
 
    InOrderTraverse(T);
 
    BiTree    find;
     if (SearchBST(T, 3, NULL, &find)) {
        printf( "\nfind node %d\n", find - >data);
    } else {
        printf( "\n not find node 3\n");
    }
 
    deleteAVL( &T, 5, &shorter);
    InOrderTraverse(T);
     if (SearchBST(T, 5, NULL, &find)) {
        printf( "\nfind node %d\n", find - >data);
    } else {
        printf( "\n not find node 5\n");
    }
 
 
     return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值