数据结构与算法学习笔记7:红黑树

红黑树

  • AVL 绝对平衡(高度差)
  • 红黑树 相对平衡(颜色)
  • 红黑树是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。它虽然是复杂的,但它的最坏情况运行时间也是非常良好的,并且在实践中是高效的: 可以 O ( l o g 2 n ) O(log_2n) O(log2n)时间内做查找,插入和删除,这里的 n n n 是树中元素的数目。

特点

  • 1、每个节点的颜色,要么是黑色的,要么是红色的。
  • 2、根节点必须是黑色的。
  • 3、认为终端节点是黑色的,称之为黑哨兵
  • 4、树中不允许两个红节点为父子关系。
  • 5、从任意节点向下出发,所能到达的各个终端节点的各条路径上,黑节点的数目必须是完全相同的。
  • 6、由于上述1-5的限制,导致红黑树上不会有一条路径长度超过其它路径的2倍。

红黑树的创建

结构:值value、颜色color、左left、右right、父亲father

查找(节点添加的位置)

参考BST即可

节点添加(新来节点是红色的)

image-20220505224406883

  • 1、 **NULL树,**新的操作节点必须是红,而根节点必须是黑色,因此该节点由红色变为黑色成为根节点。
  • 2、 **非NULL,**新的操作节点如何安置,首先看父亲的颜色
    • 1)父亲为黑色,直接放入就可以了
    • 2)父亲为红色,需要判断叔叔的情况
      • 如果叔叔在(在就一定是红色),变色,父亲和叔叔变黑,爷爷变红,然后爷爷作为新的操作节点进行调整
      • 如果叔叔不在(不在为NULL就认为是黑色),旋转。
        • 父亲在爷爷左侧
          • 操作节点在父亲的右侧,父亲为新的操作节点,以操作节点为旋转点进行左旋
          • 操作节点在父亲的左侧,将父亲变黑,爷爷变红,然后以爷爷作为旋转点进行右旋,结束。
        • 父亲在爷爷右侧
          • 操作节点在父亲的左侧,父亲为新的操作节点,以操作节点为旋转点进行右旋
          • 操作节点在父亲的右侧,将父亲变黑,爷爷变红,然后以爷爷为旋转点进行左旋,结束。
代码
#include <cstdlib>
#include <stdio.h>
#include <stdlib.h>

enum COLOR{RED,BLACK};  //枚举
//等同于:
//    #define RED    0
//    #define BLACK  1

typedef struct Node{
    int nValue;
    int nColor;
    struct Node *pLeft;
    struct Node *pRight;
    struct Node *pFather;
}RBT;

RBT *pRBT = NULL;

//右旋代码
void RightRotate(RBT *pNode){
    if (pNode == NULL || pNode->pLeft == NULL) return;
    RBT *pMark = pNode->pLeft;
    //三个孩子的关系
    pNode->pLeft = pMark->pRight;
    pMark->pRight = pNode;
    if (pNode->pFather == NULL) {
        pRBT = pMark;
    }else{
        if (pNode == pNode->pFather->pLeft) {
            pNode->pFather->pLeft = pMark;
        }
        else{
            pNode->pFather->pRight = pMark;
        }
    }
    //三个父亲的关系
    if (pNode->pLeft != NULL) {
        pNode->pLeft->pFather = pNode;
    }
    pMark->pFather = pNode->pFather;
    pNode->pFather = pMark;
}

//左旋代码
void LeftRotate(RBT *pNode){
    if (pNode == NULL || pNode->pRight == NULL) return;
    RBT *pMark = pNode->pRight;
    //三个孩子的关系
    pNode->pRight = pMark->pLeft;
    pMark->pLeft = pNode;
    if (pNode->pFather == NULL) {
        pRBT = pMark;
    }else{
        if (pNode == pNode->pFather->pLeft) {
            pNode->pFather->pLeft = pMark;
        }
        else{
            pNode->pFather->pRight = pMark;
        }
    }
    //三个父亲的关系
    if (pNode->pRight != NULL) {
        pNode->pRight->pFather = pNode;
    }
    pMark->pFather = pNode->pFather;
    pNode->pFather = pMark;
}

RBT *GetFather(RBT *pTree,int nNum){
    while (pTree) {
        if (pTree->nValue > nNum) {
            if (pTree->pLeft == NULL) return pTree;
            pTree = pTree->pLeft;   //左侧
        }else if(pTree->nValue < nNum){
            if (pTree->pRight == NULL) return pTree;
            pTree = pTree->pRight;  //右侧
        }else{
            printf("date error!\n");
            exit(1);
        }
    }
    return NULL;
}

RBT *GetUncle(RBT * pFather){
    if (pFather == pFather->pFather->pLeft) {
        return pFather->pFather->pRight;
    }else{
        return pFather->pFather->pLeft;
    }
}

void AddNode(int nNum){
    RBT *pTemp = NULL;
    pTemp = (RBT*)malloc(sizeof(RBT));
    pTemp->nValue = nNum;
    pTemp->nColor = RED;
    pTemp->pFather = NULL;
    pTemp->pLeft = NULL;
    pTemp->pRight = NULL;

    //找到父亲节点
    RBT *pNode = NULL;
    pNode = GetFather(pRBT, nNum);
    pTemp->pFather = pNode;

    //空树情况
    if (pNode == NULL) {
        pRBT = pTemp;
        pRBT->nColor = BLACK;
        return;
    }

    //非空树情况
    //连接
    if (nNum < pNode->nValue) {
        pNode->pLeft = pTemp;
    }else{
        pNode->pRight = pTemp;
    }
    //调整
        //父亲为黑色
    if(pNode->nColor == BLACK){
        return;
    }
    //父亲为红色
    RBT * pGrandFather = NULL;
    RBT * pUncle = NULL;
    while (pNode->nColor == RED) {
        pGrandFather = pNode->pFather;
        pUncle = GetUncle(pNode);
        //叔叔为红色
        if (pUncle != NULL && pUncle->nColor == RED) {
            //不加pUncle != NULL &&情况下如果pUncle为NULL,pUncle->nColor将直接引发程序崩溃
            pNode->nColor = BLACK;
            pUncle->nColor = BLACK;
            pGrandFather->nColor = RED;
            pTemp = pGrandFather;
            pNode = pTemp->pFather;
            //需要判断其是否为根,如果是根的话需要将红色变回黑色,红黑树要求根节点必须是黑色
            if (pNode == NULL) {
                pTemp->nColor = BLACK;
                break;
            }
            continue;
        }
        //叔叔为黑色
        if (pUncle == NULL || pUncle->nColor == BLACK) {
            //不加pUncle != NULL ||情况下如果pUncle为NULL,pUncle->nColor将直接引发程序崩溃
            //父亲是爷爷的左侧
            if (pNode == pGrandFather->pLeft) {
                //当前节点是父亲的右侧
                if (pTemp == pNode->pRight) {
                    pTemp = pNode;
                    LeftRotate(pTemp);  //左旋
                    pNode = pTemp->pFather; //更新父亲
                }
                //当前节点是父亲的左侧
                if (pTemp == pNode->pLeft) {
                    pNode->nColor = BLACK;
                    pGrandFather->nColor = RED;
                    RightRotate(pGrandFather);  //右旋
                    break;
                }
            }
            //父亲是爷爷的右侧
            if (pNode == pGrandFather->pRight) {
                //当前节点是父亲的左侧
                if (pTemp == pNode->pLeft) {
                    pTemp = pNode;
                    RightRotate(pTemp);  //右旋
                    pNode = pTemp->pFather; //更新父亲
                }
                //当前节点是父亲的右侧
                if (pTemp == pNode->pRight) {
                    pNode->nColor = BLACK;
                    pGrandFather->nColor = RED;
                    LeftRotate(pGrandFather);  //左旋
                    break;
                }
            }
        }
    }
}

void CreateRBT(){
    int nNum;
    int nLenth;
    printf("请输入节点个数:\n");
    scanf("%d",&nLenth);
    printf("请输入节点:\n");
    for (int i = 0; i < nLenth; i++) {
        scanf("%d",&nNum);
        AddNode(nNum);
    }
}

void PreorderTraversal(RBT *pTree){
    if (pTree == NULL)  return;
    printf("value = %d\tcolor = %d\n",pTree->nValue,pTree->nColor);
    PreorderTraversal(pTree->pLeft);
    PreorderTraversal(pTree->pRight);
}//前序遍历


int main()
{
    CreateRBT();
    PreorderTraversal(pRBT);
    return 0;
}

删除

先进行被删除节点的搜索,然后进行孩子个数的分析,如果有两个孩子,那么用左的最右or右的最左与该节点进行替换,然后再开始分析替换后真正要删除的那个节点。

分析

Z为真正被删除节点,也是操作节点。

  • Z为根且无子,直接删除,变空树,结束

  • Z为根且有一子,子变为黑色,删Z,子成为新根,结束

    (只有一子时该子必为红色,因为和红黑树性质中的“从任意节点向下出发,所能到达的各个终端节点的各条路径上,黑节点的数目必须是完全相同的”)

  • Z为红色,直接删Z,结束

  • Z非根,Z为黑色且有一个,爷孙相连,子变黑色,删Z,结束

  • Z非根,Z为黑色且无子,删,然后进行如下分析和调整

    • 兄弟为红,将父亲变红,兄弟变黑,以父亲为旋转点进行旋转,以Z继续调整
    • 兄弟为黑
      • 兄弟的两孩子(Z的侄子)全黑(或者NULL)
        • 父亲为红,父亲变黑,兄弟变红,结束
        • **父亲为黑,**兄弟变红,父亲为新Z,以Z继续调整
      • 左侄子红,右侄子黑
        • 兄弟在父亲的右侧,兄弟变红,左侄子变黑色,然后以兄弟为旋转点进行右旋
        • 兄弟在父亲的左侧,把父亲的颜色给兄弟,父亲变黑,左侄子变黑,以父亲为旋转点右旋,结束
      • 右侄子红
        • 兄弟在父亲的右侧,把父亲的颜色给兄弟,父亲变黑,右侄子变黑,以父亲为旋转点左旋,结束
        • 兄弟是父亲的左侧,兄弟变红,右侄子变黑色,然后以兄弟为旋转点进行左旋
代码
#include <cstddef>
#include <cstdlib>
#include <stdio.h>
#include <stdlib.h>

enum COLOR{RED,BLACK}; 

typedef struct Node{
    int nValue;
    int nColor;
    struct Node *pLeft;
    struct Node *pRight;
    struct Node *pFather;
}RBT;

RBT *pRBT = NULL;

//右旋代码
void RightRotate(RBT *pNode){
    if (pNode == NULL || pNode->pLeft == NULL) return;
    RBT *pMark = pNode->pLeft;
    //三个孩子的关系
    pNode->pLeft = pMark->pRight;
    pMark->pRight = pNode;
    if (pNode->pFather == NULL) {
        pRBT = pMark;
    }else{
        if (pNode == pNode->pFather->pLeft) {
            pNode->pFather->pLeft = pMark;
        }
        else{
            pNode->pFather->pRight = pMark;
        }
    }
    //三个父亲的关系
    if (pNode->pLeft != NULL) {
        pNode->pLeft->pFather = pNode;
    }
    pMark->pFather = pNode->pFather;
    pNode->pFather = pMark;
}

//左旋代码
void LeftRotate(RBT *pNode){
    if (pNode == NULL || pNode->pRight == NULL) return;
    RBT *pMark = pNode->pRight;
    //三个孩子的关系
    pNode->pRight = pMark->pLeft;
    pMark->pLeft = pNode;
    if (pNode->pFather == NULL) {
        pRBT = pMark;
    }else{
        if (pNode == pNode->pFather->pLeft) {
            pNode->pFather->pLeft = pMark;
        }
        else{
            pNode->pFather->pRight = pMark;
        }
    }
    //三个父亲的关系
    if (pNode->pRight != NULL) {
        pNode->pRight->pFather = pNode;
    }
    pMark->pFather = pNode->pFather;
    pNode->pFather = pMark;
}

RBT *GetFather(RBT *pTree,int nNum){
    while (pTree) {
        if (pTree->nValue > nNum) {
            if (pTree->pLeft == NULL) return pTree;
            pTree = pTree->pLeft;   //左侧
        }else if(pTree->nValue < nNum){
            if (pTree->pRight == NULL) return pTree;
            pTree = pTree->pRight;  //右侧
        }else{
            printf("date error!\n");
            exit(1);
        }
    }
    return NULL;
}

RBT *GetUncle(RBT *pFather){
	if(pFather == pFather->pFather->pLeft){
		return pFather->pFather->pRight;
	}else{
		return pFather->pFather->pLeft;
	}
}

RBT *GetBrother(RBT * pNode){
    if (pNode == pNode->pFather->pLeft) {
        return pNode->pFather->pRight;
    }else{
        return pNode->pFather->pLeft;
    }
}

void AddNode(int nNum){
    RBT *pTemp = NULL;
    pTemp = (RBT*)malloc(sizeof(RBT));
    pTemp->nValue = nNum;
    pTemp->nColor = RED;
    pTemp->pFather = NULL;
    pTemp->pLeft = NULL;
    pTemp->pRight = NULL;

    //找到父亲节点
    RBT *pNode = NULL;
    pNode = GetFather(pRBT, nNum);
    pTemp->pFather = pNode;

    //空树情况
    if (pNode == NULL) {
        pRBT = pTemp;
        pRBT->nColor = BLACK;
        return;
    }

    //非空树情况
    //连接
    if (nNum < pNode->nValue) {
        pNode->pLeft = pTemp;
    }else{
        pNode->pRight = pTemp;
    }
    //调整
        //父亲为黑色
    if(pNode->nColor == BLACK){
        return;
    }
    //父亲为红色
    RBT * pGrandFather = NULL;
    RBT * pUncle = NULL;
    while (pNode->nColor == RED) {
        pGrandFather = pNode->pFather;
        pUncle = GetUncle(pNode);
        //叔叔为红色
        if (pUncle != NULL && pUncle->nColor == RED) {
            //不加pUncle != NULL &&情况下如果pUncle为NULL,pUncle->nColor将直接引发程序崩溃
            pNode->nColor = BLACK;
            pUncle->nColor = BLACK;
            pGrandFather->nColor = RED;
            pTemp = pGrandFather;
            pNode = pTemp->pFather;
            //需要判断其是否为根,如果是根的话需要将红色变回黑色,红黑树要求根节点必须是黑色
            if (pNode == NULL) {
                pTemp->nColor = BLACK;
                break;
            }
            continue;
        }
        //叔叔为黑色
        if (pUncle == NULL || pUncle->nColor == BLACK) {
            //不加pUncle != NULL ||情况下如果pUncle为NULL,pUncle->nColor将直接引发程序崩溃
            //父亲是爷爷的左侧
            if (pNode == pGrandFather->pLeft) {
                //当前节点是父亲的右侧
                if (pTemp == pNode->pRight) {
                    pTemp = pNode;
                    LeftRotate(pTemp);  //左旋
                    pNode = pTemp->pFather; //更新父亲
                }
                //当前节点是父亲的左侧
                if (pTemp == pNode->pLeft) {
                    pNode->nColor = BLACK;
                    pGrandFather->nColor = RED;
                    RightRotate(pGrandFather);  //右旋
                    break;
                }
            }
            //父亲是爷爷的右侧
            if (pNode == pGrandFather->pRight) {
                //当前节点是父亲的左侧
                if (pTemp == pNode->pLeft) {
                    pTemp = pNode;
                    RightRotate(pTemp);  //右旋
                    pNode = pTemp->pFather; //更新父亲
                }
                //当前节点是父亲的右侧
                if (pTemp == pNode->pRight) {
                    pNode->nColor = BLACK;
                    pGrandFather->nColor = RED;
                    LeftRotate(pGrandFather);  //左旋
                    break;
                }
            }
        }
    }
}

RBT *FindNode(RBT * pTree,int nNum){
    while (pTree) {
        if(pTree->nValue == nNum){
            return pTree;
        }else if(pTree->nValue > nNum){
            pTree = pTree->pLeft;
        }else {
            pTree = pTree->pRight;
        }
    }
    return NULL;
}

void DeleteNode(int nNum){
    //找到当前被删除节点
    RBT *pTemp = NULL;
    pTemp = FindNode(pRBT, nNum);
    if(pTemp == NULL) return;   //没有要删除的节点 直接结束即可
    
    RBT *pMark = NULL;  //标记
    //孩子情况的分析
    if (pTemp->pLeft != NULL && pTemp->pRight != NULL) {
        pMark = pTemp;
        pTemp = pTemp->pLeft;
        while (pTemp->pRight != NULL) {
            pTemp = pTemp->pRight;
        }   //找到左的最右,然后进行值的覆盖替换
        pMark->nValue = pTemp->nValue;
    }

    //颜色情况的讨论
    RBT *pNode = pTemp->pFather;
    //1. 根
    if (pNode == NULL) {
        //  1.1 无子的情况
        if (pTemp->pLeft == NULL && pTemp->pRight == NULL) {
            free(pTemp);
            pTemp = NULL;
            pRBT = NULL;
            return;
        }
        //  1.2 有一子(红)的情况
        else {
            pRBT = pTemp->pLeft ? pTemp->pLeft : pTemp->pRight;
            pRBT->nColor = BLACK;
            pRBT->pFather = NULL;
            free(pTemp);
            pTemp = NULL;
            return;
        }
    }
    //2. 非根
        //2.1 节点为红色
        if (pTemp->nColor == RED) {
             //判断这个红节点在父亲左侧还是右侧
            if (pTemp == pNode->pLeft) {
                pNode->pLeft = NULL;
            }else{
                pNode->pRight = NULL;
            }
            free(pTemp);
            pTemp = NULL;
            return;
        }
        //2.2 节点为黑色且有一个红子
        if (pTemp->nColor == BLACK && (pTemp->pLeft != NULL || pTemp->pRight != NULL)) {
            if (pTemp == pNode->pLeft) {    //判断这个红子在父亲左侧还是右侧,此处为左
                pNode->pLeft = pTemp->pLeft ? pTemp->pLeft : pTemp->pRight;    //爷孙相连,父亲的左侧换人换成删除节点的孩子
                pNode->pLeft->nColor = BLACK;
                pNode->pLeft->pFather = pNode;
            }else if (pTemp == pNode->pRight) {
                pNode->pRight = pTemp->pLeft ? pTemp->pLeft : pTemp->pRight;
                pNode->pRight->nColor = BLACK;
                pNode->pRight->pFather = pNode;
            }
            free(pTemp);
            pTemp = NULL;
            return;
        }
        //2.3 节点为黑色且无子
        else {
            RBT *pBrother = GetBrother(pTemp);    //兄弟节点
            //删除节点,注意获取兄弟节点和删除节点的顺序不可以乱,先删除节点就无法获取到其兄弟了
            if (pTemp == pNode->pLeft) {
                pNode->pLeft = NULL;
            }else {
                pNode->pRight = NULL;
            }
            free(pTemp);
            pTemp = NULL;
            //调整
            while (1) {
                //2.3.1 兄弟是红色
                if (pBrother->nColor == RED) {
                    pNode->nColor = RED;
                    pBrother->nColor = BLACK;
                    //判断方向进行旋转
                    if (pBrother == pNode->pRight) {
                        LeftRotate(pNode);
                        pBrother = pNode->pRight;   //旋转完兄弟会换人,要更新一下兄弟
                    }else {
                        RightRotate(pNode);
                        pBrother = pNode->pLeft;
                    }
                }
                //2.3.2 兄弟是黑色
                else if (pBrother->nColor == BLACK) {
                    //2.3.2.1 侄子全黑
                    if ( (pBrother->pLeft == NULL && pBrother->pRight == NULL) ||
                        ( (pBrother->pLeft != NULL && pBrother->pLeft->nColor == BLACK) && 
                            (pBrother->pRight != NULL && pBrother->pRight->nColor == BLACK) ) ) {
                        //2.3.2.1.1 父亲红
                        if (pNode->nColor == RED) {
                            pNode->nColor = BLACK;
                            pBrother->nColor = RED;
                            break;
                        }
                        //2.3.2.1.2 父亲黑
                        else if (pNode->nColor == BLACK) {
                            pBrother->nColor = RED;
                            pTemp = pNode;  //父亲作为新的操作节点
                            pNode = pTemp->pFather; //父亲这个变量也需要随之发生变化
                            //判断是否为根,为根则直接结束了
                            if (pNode == NULL) {
                                break;
                            }
                            pBrother = GetBrother(pTemp);   //因为temp改变了 更新一下Brother
                        }
                    }
                    //2.3.2.2 左侄子红,右侄子黑
                    else if ( (pBrother->pLeft != NULL && pBrother->pLeft->nColor ==RED) &&
                                (pBrother->pRight == NULL || pBrother->pRight->nColor == BLACK) ) {
                        //2.3.2.2.1 兄弟在父亲右侧
                        if (pBrother == pNode->pRight) {
                            pBrother->nColor = RED;
                            pBrother->pLeft->nColor = BLACK;
                            RightRotate(pBrother);
                            pBrother = pNode->pRight;   //旋转后兄弟换人了 记得更新
                        }
                        //2.3.2.2.2 兄弟在父亲左侧
                        else {
                            pBrother->nColor = pNode->nColor;   //把父亲颜色给兄弟
                            pNode->nColor = BLACK;
                            pBrother->pLeft->nColor = BLACK;
                            RightRotate(pNode);
                            break;
                        }
                    }
                    //2.3.2.3 右侄子红,左侄子随便
                    else {
                        //2.3.2.3.1 兄弟在父亲左侧
                        if (pBrother == pNode->pLeft) {
                            pBrother->nColor = RED;
                            pBrother->pRight->nColor = BLACK;
                            LeftRotate(pBrother);
                            pBrother = pNode->pLeft;
                        }
                        //2.3.2.3.2 兄弟在父亲右侧
                        else {
                            pBrother->nColor = pNode->nColor;   //把父亲颜色给兄弟
                            pNode->nColor = BLACK;
                            pBrother->pRight->nColor = BLACK;
                            LeftRotate(pNode);
                            break;
                        }
                    }
                }
            }
        }
}

void CreateRBT(){
    int nNum;
    int nLenth;
    printf("请输入节点个数:\n");
    scanf("%d",&nLenth);
    printf("请输入节点:\n");
    for (int i = 0; i < nLenth; i++) {
        scanf("%d",&nNum);
        AddNode(nNum);
    }
}

void PreorderTraversal(RBT *pTree){
    if (pTree == NULL)  return;
    printf("value = %d\tcolor = %d\n",pTree->nValue,pTree->nColor);
    PreorderTraversal(pTree->pLeft);
    PreorderTraversal(pTree->pRight);
}//前序遍历

int main()
{
    CreateRBT();
    PreorderTraversal(pRBT);
    printf("------------------------\n");
    DeleteNode(1);
    PreorderTraversal(pRBT);
    printf("------------------------\n");
    DeleteNode(8);
    PreorderTraversal(pRBT);
    printf("------------------------\n");
    DeleteNode(11);
    PreorderTraversal(pRBT);
    printf("------------------------\n");
    DeleteNode(14);
    PreorderTraversal(pRBT);
    printf("------------------------\n");
    DeleteNode(2);
    PreorderTraversal(pRBT);
    printf("------------------------\n");
    DeleteNode(5);
    PreorderTraversal(pRBT);
    printf("------------------------\n");
    DeleteNode(7);
    PreorderTraversal(pRBT);
    printf("------------------------\n");
    DeleteNode(15);
    PreorderTraversal(pRBT);
    printf("------------------------\n");
    return 0;
}

应用

  • map:key、value
  • set:value,不允许数值重复插入(insert_unique)
  • multiset:value,允许数值重复插入(insert_equal)
红黑树优势
  • 搜索很快,中序遍历能够得到有序数据
红黑树在Linux底层应用:
  • ① 进程管理

    • PCB 本质上就是一个结构体 task_struct

    • 图中显示的标白部分就是负责进程调度的,实际上底层就是:

      进程放到红黑树中管理,红黑树的每个节点都是一个进程,key可以理解成当前进程运行时间。

      例如:设置定时器9点,然后在红黑树中进行搜索到9点,该结点左侧部分(包括兄弟的孩子等)就是需要运行的部分

  • ② 内存管理

    • 每次申请的空间(主要就是起始地址+结束地址or偏移量)作为红黑树中的结点,可以快速找到对应内容的位置
Linux中的epoll模型
  • 通过红黑树能快速判断消息关联到哪个线程的能力(非轮询监听),所以高并发IO可以考虑epoll模型,
  • 当然,哈希表搜索速度也很快,但其很严重的缺陷是占用空间,数组一开始就需要定长,不可以动态变化,所以红黑树更实用。除此外,还可以用于索引的还有B+树。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

97Marcus

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值