红黑树的简单学习

承接上文:B树的简单学习
结合视频:红黑树学习视频
因为文件系统和数据库一般都是存在电脑硬盘上的,如果数据量太大的话不一定能一次性加载到内存中,但是B树可以多路存储,也正因为B树的这一个优点,可以在文件查找的时候每次只加载一个节点的内容存入内存来查找,而红黑树在内存中查找非常快,但是如果在数据库和文件系统中,显然B树更优。
##一.四阶B树对应红黑树
个人认为转换红黑树的前提:B树的所有节点内的关键字数组中,其中一个关键字形成的节点一定要标成黑色,其他都是红色,也就是说红黑树中,每个黑色节点在B树中一定是处于不同的节点的,而上黑下红的这种颜色限制就表明了当前黑色节点和下面与之相连的红色节点共同构成了B树中的一个节点,也就有了下面的定义:

节点的对应关系
B树的叶子节点下面的空节点,即红黑树的叶子节点下面的空也是黑色
B树中只有一个关键字的节点,那就一定是黑色节点;
B树中有两个关键字的节点,会有两种情况,小的关键字成为红色节点并作为左子树,或者大的关键字成为红色节点并作为右子树;
B树中有三个关键字的节点,那就选择中间的关键字作为黑色节点,左右两个关键字作为红色节点,这样能形成一棵平衡二叉树。

操作的对应关系
裂变——对于四阶B树来说,一个节点内的关键字数量最大是3,当新关键字要插入已经有三个关键字的节点时,这个节点就会发生裂变,将中间的关键字传到上一层,然后左右两边的关键字形成新的节点,在这个过程中,相当于B树形成了两个新的节点,并且都是只有一个关键字的节点,那根据上面的定义,B树的节点内一定要有一个关键字形成黑色节点,因此那两个新的节点在红黑树里就成黑色,而在红黑树里中间的那个关键字就成为红色(先默认上面是有节点的,那返回到上层的节点如果没有超出限制,)最后在插入那个新关键字。

红黑树参考代码
节点存储的数据key-value视为两个相同的int整数,即1-1这种是可以的

public class RBTree<K extends Comparable<K>, V> {
    private static final boolean RED = false;
    private static final boolean BLACK = true;

    private RBNode root;
    public RBNode getRoot() {
        return root;
    }

    private void setRoot(RBNode root) {
        this.root = root;
    }

    private RBNode getParent(RBNode node) {
        return node == null ? null : node.parentNode;
    }

    private RBNode getLeft(RBNode node) {
        return node == null ? null : node.leftNode;
    }
    private RBNode getRight(RBNode node) {
        return node == null ? null : node.rightNode;
    }

    private boolean getColor(RBNode node) {
        return node == null ? BLACK : node.color;
    }
    private void setColor(RBNode node, boolean color) {
        if(node == null) return;
        node.color = color;
    }
    private RBNode getBro(RBNode node) {
        return node == null ? null : node.parentNode;
    }

    //测试函数:不会可视化就看先序遍历
    public void preOrder(RBNode R) {
        if(R == null) return;
        System.out.print(R.getKey() + " ");
        preOrder(R.leftNode);
        preOrder(R.rightNode);
    }
    private RBNode getPredecessor(RBNode node) {
        //获取当前输入节点的前驱节点
        if(node == null) return null;
        if(node.leftNode != null) {
            //当前node的左子不为空的情况
            RBNode p = node.leftNode;
            while(p.rightNode != null) {
                p = p.rightNode;
            }
            return p;
        } else {
            //如果当前node的左子为空,就沿着父节点的方向找
            RBNode p = node.parentNode;
            RBNode cur = node;
            //     a
            //      \
            //       b
            //      /
            //     c
            //    /
            //   d
            //  /
            // d(找到a)
            //停止的条件是cur是父亲的右孩子
            while(p != null && cur == p.leftNode) {
                cur = p;
                p = p.parentNode;
            }
            return p;
        }
    }
    private RBNode getSuccessor(RBNode node) {
        //获取当前输入节点的后继节点
        if(node == null) return null;
        if(node.rightNode != null) {
            //当前node的右子不为空的情况
            RBNode p = node.rightNode;
            while(p.leftNode != null) {
                p = p.leftNode;
            }
            return p;
        } else {
            //如果当前node的右子为空,就沿着父节点的方向找
            RBNode p = node.parentNode;
            RBNode cur = node;
            //         a
            //        /
            //       b
            //        \
            //         c
            //          \
            //           d
            //            \
            //           d(找到a)
            //停止的条件是cur是父亲的右孩子
            while(p != null && cur == p.rightNode) {
                cur = p;
                p = p.parentNode;
            }
            return p;
        }
    }
    private void leftRotate(RBNode node) {
        //对node执行左旋操作
//          node                     r
//         /    \                  /   \
//        l      r    --->       node  rr
//              / \             /   \
//            rl   rr          l    rl
        if(node == null) return;
        RBNode r = node.rightNode;
        //1.先处理node的右孩子部分
        node.rightNode = r.leftNode;//将rl放到node的右子
        if(r.leftNode != null) r.leftNode.parentNode = node;//如果rl不是空节点,那就要设置它的父节点

        //2.然后处理node父节点的部分
        r.parentNode = node.parentNode;//r的父节点设置成node的父节点
        if(node.parentNode == null) {
            //如果np是空节点,说明原来node是根节点,那么r就要成为新的根节点
            root = r;
        } else if(node == node.parentNode.leftNode) {
            //如果np不为空,且node是np的左孩子,那r就要成为新的左孩子
            node.parentNode.leftNode = r;
        } else {
            //如果np不为空,且node是np的右孩子,那r就要成为新的右孩子
            node.parentNode.rightNode = r;
        }

        //3.最后处理r的左孩子部分
        r.leftNode = node;
        node.parentNode = r;
    }
    private void rightRotate(RBNode node) {
        //对node执行右旋操作
//          node                     l
//         /    \                  /   \
//        l      r    --->       ll   node
//       / \                          /   \
//     ll   lr                       lr    r
        if(node == null) return;
        RBNode l = node.leftNode;
        //1.先处理node的左孩子部分
        node.leftNode = l.rightNode;//将lr放到node的左子
        if(l.rightNode != null) l.rightNode.parentNode = node;//如果lr不是空节点,那就要设置它的父节点

        //2.然后处理node父节点的部分
        l.parentNode = node.parentNode;//r的父节点设置成node的父节点
        if(node.parentNode == null) {
            //如果np是空节点,说明原来node是根节点,那么r就要成为新的根节点
            root = l;
        } else if(node == node.parentNode.leftNode) {
            //如果np不为空,且node是np的左孩子,那r就要成为新的左孩子
            node.parentNode.leftNode = l;
        } else {
            //如果np不为空,且node是np的右孩子,那r就要成为新的右孩子
            node.parentNode.rightNode = l;
        }

        //3.最后处理l的右孩子部分
        l.rightNode = node;
        node.parentNode = l;
    }

    public void put(K key, V value) {
        RBNode tmpRoot = this.root;//获取当前红黑树的根节点
        if(tmpRoot == null) {
            root = new RBNode<>(key, value == null ? key : value, null);
            return;
        }
        //如果有根节点,就要找到待插入的位置,对应B树就是一直找到叶子节点
        RBNode parent = tmpRoot;//定义一个双亲节点
        int cmp = 0;
        while(tmpRoot != null) {
            parent = tmpRoot;//记录当前的双亲,这个是为了给后续产生的新节点提供父节点
            cmp = key.compareTo((K) tmpRoot.key);
            if(cmp < 0) {
                tmpRoot = tmpRoot.leftNode;
            } else if(cmp > 0){
                tmpRoot = tmpRoot.rightNode;
            } else {
                tmpRoot.setValue(value == null ? key : value);
                return;
            }
        }
        //到这里就找到位置了,设置节点接上就行
        RBNode<K, Object> e = new RBNode<>(key, value == null ? key : value, parent);
        if(cmp < 0) parent.leftNode = e;
        else parent.rightNode = e;

        fixAfterPut(e);//插入后进入红黑树的结构调整
    }

    private void fixAfterPut(RBNode node) {
        //结构调整包括左右旋和颜色转换
        //情况一:插入到2节点形成三节点,那就只需要将父节点染黑,当前插入节点染红就行,上面已经完成了位置的插入,那二节点的调整就是染色就行
        //情况二:插入到3节点形成四节点,那就有四种小的情况,要么是 “/” 的形状,要么是 “\”的情况,这两种小情况是需要左旋或右旋加变色的,剩下两种就是变色
        //情况三:插入到3节点然后需要裂变,在B树里就是中间元素上升到父节点处插入,在红黑树就是直接变成红色(如果上面没有父节点就说明当前是根节点了,再染黑),剩下的两边就染黑,即在B树就是形成了新的节点

        //经过观察可知,当上面的插入操作完成后,如果刚好插入节点的父节点的颜色为红色,就是需要旋转的,父节点是黑色的话就是直接染当前插入节点为红色就行
        node.color = RED;
//   1.   BLACK             2.  BLACK            3.  BLACK              4.  BLACK
//         /                      \                  /   \                  /   \
//       RED                      RED        .     RED . RED     .        RED   RED
//       /                          \                      \              /
//     RED(新插入的)                 RED(新插入的)             RED(新插入的)  RED(新插入的)
// 5.  BLACK              6. BLACK              7. BLACK                8. BLACK
//    /                        \                   /   \                   /   \
//  RED                        RED               RED   RED     .         RED   RED
//    \                        /                       /                   \
//    RED(新插入的)            RED(新插入的)             RED(新插入的)         RED(新插入的)
        while(node != null && node != root && node.parentNode.color == RED) {
            if(getLeft(getParent(getParent(node))) == getParent(node)) {
                //对应上面的情况1、4、5和8
                RBNode uncleNode = getRight(getParent(getParent(node)));
                if(getColor(uncleNode) == RED) {
                    //对应情况4和8
                    //如果是需要裂变的情况,即插入之前已经是3节点了,对应上面的情况4,需要成为下面这种情况
                                //    4.   RED           8.   RED
                                //        /   \              /   \
                                //     BLACK BLACK        BLACK BLACK
                                //     /                     \
                                //   RED(新插入的)         RED(新插入的)
                    setColor(node.parentNode, BLACK);
                    setColor(uncleNode, BLACK);
                    setColor(getParent(getParent(node)), RED);//祖父节点设置为红

                    node = getParent(getParent(node));//将node指向祖父节点,和B树一样自底向上调整
                }
                else {
                    //对应情况1、5
                    if(node == getRight(getParent(node))) {
                        //对应情况5,转化为下面的情况
                        //       5. BLACK
                        //         /
                        //       RED(新插入的)
                        //       /
                        //     RED
                        node = getParent(node);
                        leftRotate(node);//转换成情况1
                    }
                    //空节点的颜色为黑色,对应上面的情况1,需要成为下面这种情况
                             //   1.    BLACK
                             //     /          \
                             //   RED(新插入的)  RED
                    setColor(node.parentNode, BLACK);
                    setColor(getParent(getParent(node)), RED);//祖父节点设置为红,然后转下来
                    //再右旋
                    rightRotate(getParent(getParent(node)));
                }
            } else {
                //对应上面的情况2、3、6和7
                RBNode uncleNode = getLeft(getParent(getParent(node)));
                if(getColor(uncleNode) == RED) {
                    //对应情况3和7
                    //如果是需要裂变的情况,即插入之前已经是3节点了,对应上面的情况4,需要成为下面这种情况
                    //    3.   RED           7.   RED
                    //        /   \              /   \
                    //     BLACK BLACK        BLACK BLACK
                    //              \               /
                    //           RED(新插入的)    RED(新插入的)
                    setColor(node.parentNode, BLACK);
                    setColor(uncleNode, BLACK);
                    setColor(getParent(getParent(node)), RED);//祖父节点设置为红,注意这里只是局部的,要一直往上调整
                    node = getParent(getParent(node));//将node指向祖父节点,和B树一样自底向上调整
                }
                else {
                    //对应情况2、6
                    if(node == getLeft(getParent(node))) {
                        //对应情况6,转化为下面的情况
                        //       6. BLACK
                        //             \
                        //           RED(新插入的)
                        //              \
                        //              RED
                        node = getParent(node);
                        rightRotate(node);//转换成情况2
                    }
                    //空节点的颜色为黑色,对应上面的情况2,需要成为下面这种情况
                    //   1.    BLACK
                    //     /          \
                    //   RED(新插入的)  RED
                    setColor(node.parentNode, BLACK);
                    setColor(getParent(getParent(node)), RED);//祖父节点设置为红,然后转下来,注意这里只是局部的,要一直往上调整
                    //再左旋
                    leftRotate(getParent(getParent(node)));
                }
            }
        }
        root.color = BLACK;//最后调整完下面后,将根节点染黑
    }
    public V removeByKey(K key) {
        RBNode node = SearchByKey(key);//先查找有没有这个key
        if(node == null) return null;
        V oldValue = (V) node.value;//拿到待删除key对应的value
        deleteByNode(node);
        return oldValue;
    }
    private RBNode SearchByKey(K key) {
        RBNode tmp = root;//先拿到根节点
        int cmp = 0;
        while (tmp != null) {
            cmp = key.compareTo((K) tmp.key);
            if(cmp < 0) tmp = tmp.leftNode;
            else if(cmp > 0) tmp = tmp.rightNode;
            else return tmp;
        }
        return null;//没找到返回空
    }

    private void deleteByNode(RBNode node) {
        //删除node,删除操作对应到B树里就是删除B树的叶子节点,一定要清楚B树的叶子节点和红黑树的叶子节点的对应关系
        //对应到红黑树的删除情况
        //情况一:待删除节点是叶子节点(有红黑两种情况)
        //情况二:待删除节点只有一个节点(B树的3节点),用那一个节点代替待删除节点的位置
        //情况三:待删除节点有两个节点(B树的4节点),找前驱或者后继节点进行替代,然后转为上述两种情况之一
        if(node == null) return;
        if(node.leftNode != null && node.rightNode != null) {
            //情况三
            //例如:     2:BLACK(删)               1:BLACK
            //        /       \     --->        /       \
            //     1:RED     3:RED           1:RED(删)   3:RED

            //例如:     3:BLACK(删)                 2:BLACK
            //        /       \     --->          /       \
            //     2:BLACK     4:BLACK        2:BLACK(删)   4:BLACK
            //      /                           /
            //    1:RED                      1:RED
            //RBNode p = getSuccessor(node);
            RBNode p = getPredecessor(node);//前驱
            node.key = p.key;
            node.value = p.value;
            node = p;//此时转换为情况一或情况二
        }
        //此时剩两种大情况,要么待删除的节点是有一个子节点,要么待删除节点是叶子结点
        RBNode replacement = node.leftNode == null ? node.rightNode : node.leftNode;//取出节点的非空子节点,如果取到null就是叶子节点
        if(replacement != null) {
            //node有一个子节点的情况,直接用子节点替代node的位置
            replacement.parentNode = node.parentNode;
            if(node.parentNode == null) {
                //例如:     2:BLACK(删)               3:BLACK
                //                \     --->
                //               3:RED
                //如果当前是根节点,那就直接将replacement设置为根节点
                root = replacement;
            } else if (node == node.parentNode.leftNode) {
                //例如:     2:BLACK
                //        /       \
                //     2:BLACK(删) 4:BLACK
                //      /
                //    1:RED(replacement)
                node.parentNode.leftNode = replacement;
            } else {
                node.parentNode.rightNode = replacement;
            }

            node.leftNode = node.rightNode = node.parentNode = null;//将node所有联系切断
            if(node.color == BLACK) {
                //若删除节点是黑色节点,需要调整颜色
                //例:(已删除后)    2:BLACK                    3:BLACK(replacement)
                //             /         \
                //     1:RED(replacement)  4:BLACK

                //其实这里包括两个点,在deleteByNode函数开始之前,就将4节点的情况转成2节点或者3节点,
                // 那么就意味 replacement要么是 只有一个节点(黑红都有) 和 有孩子但孩子部分必不会有黑色节点的情况(因为找前驱后继都是在B树的叶子节点)
                //而上述就分成两种,要么传入的replacement是红色,就是只变色,要么传入一个黑色节点,就是和距离下面数7行的情况一样,需要调整加变色
                fixAfterDelete(replacement);//
            }
        } else if(node.parentNode == null) {
            //这里就是叶子节点的情况,就是没有往下找到的替代节点,只有一个根节点,根节点等于叶子节点
            root = null;//直接清空,不用调色
        } else {
            //这里就是叶子节点的另一种情况,即有父节点的叶子节点,这里就要先调整,再删除
            if(node.color == BLACK) {
                //若删除节点是黑色节点,相当于B树的删除一整个节点,需要调整颜色
                //例如:(删除前)BLACK
                //          /    \
                //     BLACK(删)  BLACK
                //                   \
                //                   RED
                fixAfterDelete(node);//以替代节点为输入进行调色,node就是带“删”字的那个
            }
            //如果node是红色节点就是直接删除,不需要调色
            if(node.parentNode.leftNode == node) node.parentNode.leftNode = null;
            else node.parentNode.rightNode = null;
            node.parentNode = null;//将node所有联系切断
        }
    }
    private void fixAfterDelete(RBNode node) {
        //这个函数包含调整和变色的操作,如果是删除红色节点就只需要变色,如果是黑色节点就需要调整加变色
        while(node != root && getColor(node) == BLACK) {
            //这种情况自己是搞不定的,对应到B就是删除一个节点的操作,必须向兄弟节点借或者将父节点拉下来与旁边的兄弟合并,再删除节点
            //例1:(删除前)BLACK            例2:(删除前)BLACK
            //          /    \                     /    \
            //     BLACK(删)  BLACK(bro)       BLACK(删)  BLACK(bro)
            //                   \
            //                   RED
            if(node == node.parentNode.leftNode) {
                //node是左孩子的情况,例如上面的例子
                RBNode bro = getRight(getParent(node));//注意这里要找的是在B树里那种同一层的节点,这里获得的bro如果是红色
                //就说明此时的bro应该是属于上面的黑色节点的,在B树中和node不是同一层的,要进行调色旋转,这种变化在B树中是没有变化的
                //只不过是换一种红黑树的表达形式
                if(getColor(bro) == RED) {
                    setColor(bro, BLACK);
                    setColor(node.parentNode, RED);
                    leftRotate(node.parentNode);
                    bro = getRight(getParent(node));//找到真正的兄弟节点
                }
                //没得借的情况——例2
                if(getColor(bro.leftNode) == BLACK && getColor(bro.rightNode) == BLACK) {
                    //在B树中叶子节点下面的空节点也认为是一个完整的节点,所以对应到红黑树的颜色就是BLACK也可以表示节点为空
                    setColor(bro, RED);//既然借不到,那就当前node和兄弟节点一起变红,这样对于父节点来说就是平衡的,然后递归往上调整父节点
                    node = node.parentNode;
                } else {
                    //有得借的情况(右分为两种,比如例1的3节点情况 和 四节点情况),借的方式是先通过对兄弟节点进行旋转,将最接近node的
                    //父节点的值旋转上来,然后再将父节点的值旋转到node这边(这两种操作在兄弟节点只倾向一边的情况下可以通过一次旋转同时完成,
                    // 如果兄弟节点和它的子节点不是倾向一边,就要旋转两次)
                    if(getColor(bro.rightNode) == BLACK) {
                        //兄弟节点的右子为空,那就要转两次了
                        setColor(bro.leftNode, BLACK);
                        setColor(bro, RED);
                        rightRotate(bro);//转化为顺边的情况,就可以用下面的代码
                        bro = node.parentNode.rightNode;
                    }
                    setColor(bro, node.parentNode.color);
                    setColor(node.parentNode, BLACK);
                    setColor(bro.rightNode, BLACK);
                    leftRotate(node.parentNode);
                    node = root;//这里完成旋转后就要赋个循环的结束值,因为兄弟节点能借的话,就是不需要循环往上调整树的
                    //因为兄弟能借就不会影响到红黑树上面的结构,而上面的不能借的情况就会改变红黑树的结构,所以需要一直循环往上调整
                    //这就是为什么这个函数需要循环的原因,关键是上面那种情况,这里能借的情况是不需要循环的
                }
            } else {
                //node是右孩子的情况,和左孩子的相反就行
                RBNode bro = getLeft(getParent(node));//注意这里要找的是在B树里那种同一层的节点,这里获得的bro如果是红色
                //就说明此时的bro应该是属于上面的黑色节点的,在B树中和node不是同一层的,要进行调色旋转,这种变化在B树中是没有变化的
                //只不过是换一种红黑树的表达形式
                if(getColor(bro) == RED) {
                    setColor(bro, BLACK);
                    setColor(node.parentNode, RED);
                    rightRotate(node.parentNode);
                    bro = getLeft(getParent(node));//找到真正的兄弟节点
                }
                //没得借的情况——例2
                if(getColor(bro.rightNode) == BLACK && getColor(bro.leftNode) == BLACK) {
                    //在B树中叶子节点下面的空节点也认为是一个完整的节点,所以对应到红黑树的颜色就是BLACK也可以表示节点为空
                    setColor(bro, RED);//既然借不到,那就当前node和兄弟节点一起变红,这样对于父节点来说就是平衡的,然后递归往上调整父节点
                    node = node.parentNode;
                } else {
                    //有得借的情况(右分为两种,比如例1的3节点情况 和 四节点情况),借的方式是先通过对兄弟节点进行旋转,将最接近node的
                    //父节点的值旋转上来,然后再将父节点的值旋转到node这边(这两种操作在兄弟节点只倾向一边的情况下可以通过一次旋转同时完成,
                    // 如果兄弟节点和它的子节点不是倾向一边,就要旋转两次)
                    if(getColor(bro.leftNode) == BLACK) {
                        //兄弟节点的右子为空,那就要转两次了
                        setColor(bro.rightNode, BLACK);
                        setColor(bro, RED);
                        leftRotate(bro);//转化为顺边的情况,就可以用下面的代码
                        bro = node.parentNode.leftNode;
                    }
                    setColor(bro, node.parentNode.color);
                    setColor(node.parentNode, BLACK);
                    setColor(bro.leftNode, BLACK);
                    rightRotate(node.parentNode);
                    node = root;//这里完成旋转后就要赋个循环的结束值,因为兄弟节点能借的话,就是不需要循环往上调整树的
                    //因为兄弟能借就不会影响到红黑树上面的结构,而上面的不能借的情况就会改变红黑树的结构,所以需要一直循环往上调整
                    //这就是为什么这个函数需要循环的原因,关键是上面那种情况,这里能借的情况是不需要循环的
                }
            }
        }
        //替代节点是红色,直接染黑以补偿删除的黑色节点
        //例:            2:BLACK
        //             /         \
        //     1:RED(replacement)  4:BLACK
        setColor(node, BLACK);
    }
    static class RBNode<K extends Comparable<K>, V> {
        //静态内部类,该类要实现key-value的比较器
        //红黑树的节点构成
        private RBNode leftNode;
        private RBNode rightNode;
        private RBNode parentNode;
        private boolean color;
        private K key;
        private V value;

        public RBNode() {
        }
        public RBNode(K key, V value, RBNode parentNode) {
            this.parentNode = parentNode;
            this.color = BLACK;
            this.key = key;
            this.value = value;
        }
        public RBNode(RBNode leftNode, RBNode rightNode, RBNode parentNode, boolean color, K key, V value) {
            this.leftNode = leftNode;
            this.rightNode = rightNode;
            this.parentNode = parentNode;
            this.color = color;
            this.key = key;
            this.value = value;
        }

        public RBNode getLeftNode() {
            return leftNode;
        }

        public void setLeftNode(RBNode leftNode) {
            this.leftNode = leftNode;
        }

        public RBNode getRightNode() {
            return rightNode;
        }

        public void setRightNode(RBNode rightNode) {
            this.rightNode = rightNode;
        }

        public RBNode getParentNode() {
            return parentNode;
        }

        public void setParentNode(RBNode parentNode) {
            this.parentNode = parentNode;
        }

        public boolean isColor() {
            return color;
        }

        public void setColor(boolean color) {
            this.color = color;
        }

        public K getKey() {
            return key;
        }

        public void setKey(K key) {
            this.key = key;
        }

        public V getValue() {
            return value;
        }

        public void setValue(V value) {
            this.value = value;
        }
    }
}
//测试代码,用先序遍历看就行
public class text {
    public static void main(String[] args) {
        RBTree rbTree = new RBTree();
        rbTree.put(1, 1);
        rbTree.put(2, 2);
        rbTree.put(3, 3);
        rbTree.put(4, 4);
        rbTree.put(5, 5);
        rbTree.put(6, 6);


        RBTree.RBNode tmpRoot = rbTree.getRoot();
        rbTree.preOrder(tmpRoot);
        rbTree.removeByKey(4);
        rbTree.removeByKey(2);
        tmpRoot = rbTree.getRoot();
        rbTree.preOrder(tmpRoot);
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

夜以冀北

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

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

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

打赏作者

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

抵扣说明:

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

余额充值