数据结构和算法基础(4)——树

一、概念

1. 为什么需要树这种数据结构

① 数组存储方式
优点:通过下标方式访问元素,速度快。对于有序数组可以通过二分查找提高检测速度。
缺点:如果要通过内容来查找元素的位置,或者插入删除值时,效率较低。
② 链式存储方式
优点:插入和删除操作效率较好。
缺点:查找效率较低,需要从头开始依次访问链表中的每个数据项。
③ 哈希表
优点:插入删除和查询效率都非常高
缺点:空间利用率不高,底层使用的是数组,并且某些单元没有被利用。哈希表中元素是无序的,不能按照固定的顺序来遍历哈希表中的元素。不能快速找出哈希表中的最大值和最小值。
④ 树
能提高数据存储,读取效率。比如利用二叉排序树,即可以保证数据的检索速度,同时也可以保证数据的插入,删除,修改的速度。

2.常用术语

叶子节点:没有子节点的节点
路径:从root节点找到该节点的路线
路径长度: 若规定根节点的层数为1,则从更节点到第L层节点的路径长度为L-1
:根节点所在第一层
树的高度:最大层数
二叉树:每个节点最多只能有两个子节点的一种形式称为二叉树
满二叉树:如果该二叉树所有叶子节点都在最后一层,并且节点总数为2的n此方-1,n为层数,则我们称为满二叉树。
完全二叉树:如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,我们称为完全二叉树。
:某个节点的子节点或直接后继节点的个数。1度就代表只有一个子节点。
总节点数:除了根节点其余节点都有一根线指向它,所以总节点树为(树的边数+1)。
叶子节点数: 总结点数-度数非零的节点数。对于二叉树,总结点树为 2n2 + n1 + 1,叶子节点树为 2n2 + n1 + 1 - n2 - n1为n2 + 1。
前驱节点:中序遍历下,一个节点的前一个节点
后继节点:中序遍历下,一个节点的后一个节点

3.遍历

前序遍历:先输出父节点,再遍历左子树和右子树。
中序遍历: 先遍历左子树,再输出父节点,再遍历右子树。
后序遍历:先遍历左子树,再遍历右子树,最后输出父节点。
在这里插入图片描述
如果按照前序,我们怎么遍历上图的结构,其实前序就是先遍历头结点,在遍历左子树最后遍历右子树。所以其顺序是:A B D NULL NULL NULL C E NULL NULL F NULL NULL.

中序:先访问左子树再访问头结点最后访问右子树。所以其顺序为:NULL,D,NULL,B,NULL,A,NULL,E,C,NULL,F,NULL.

后序:先访问左子树,再访问右子树,最后访问头结点,所以顺序为:

NULL,NULL,D,NULL,B,NULL,NULL,E,NULL,NULL,F,C,A.

4.顺序存储二叉树

数组存储方式和树存储方式可以相互转换,即数组可以转为树,树也可以转为数组。以下规律可以堆数组看成树进行前序,中序,后序遍历。用于堆排序。
① 顺序二叉树通常只考虑完全二叉树
② 第n个元素的左子节点为2n+1(n表示二叉树中的第几个元素,从0开始编号)
③ 第n个元素的右子节点为2
n+2
④ 第n个元素的父节点为(n-1)/2

5.线索化二叉树

(1)n个节点的二叉链表中含有n+1个空指针域(每多一个节点就会多两个指针域,少一个指针域。同时根节点并不会少一个指针域,所以为2n-n+1为n+1)。利用二叉链表中的空指针域,存放指向节点在某种遍历次序下的前驱和后继节点的指针(这种附加的指针称为“线索”)。
(2)这种加上了线索的二叉链表称为线索链表,对应的二叉树称为线索二叉树。根据线索性质的不同,线索二叉树可分为前序线索二叉树,中序线索二叉树和后序线索二叉树三种。
(3)当线索话二叉树后,node节点的属性left和righ有多种情况。left指向的可能是左子树,也可能是指向前驱节点。


二、堆排序

1.简介

(1) 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序;它的最坏,最好,平均时间复杂度均为O(nlogn);它是不稳定排序;
(2) 每个结点的值都大于或等于其左右子结点的值,称为大顶堆。
在这里插入图片描述
对堆中结点按顺序编号,映射到数组中就是如下图样子:
在这里插入图片描述
根据之前的顺序存储二叉树可以知道,大顶堆的特点为arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2];
(3) 每个结点的值都小于等于其左右子结点的值,称为小顶堆。

2.构造大顶堆思路

(1) 找到最后一个非叶子节点为(n-1)/2,然后比较该非叶子节点是否小于它的左右两个子节点,小于就进行交换。
(2) 进行循环,如果没交换就直接退出循环,如果交换了就用交换的子节点元素比较是否小于它的左右子节点,然后重复(2)的操作。
(3) 依次递归,找倒数第二个非叶子节点重复(1)(2)的操作,直到根节点。

3.堆排序基本思路

(1) 将待排序序列构造成一个大顶堆(根据需求如果是升序就构造为大顶堆,如果是降序就构造为小顶堆)。
(2) 此时,整个序列的最大值就是堆顶的根节点。
(3) 将其于末尾元素进行交换,此时末尾就为最大值。
(4) 然后将剩余n-1个元素重新构造称为一个大顶堆,然后交换根节点和末尾节点。如此反复执行,便得到一个有序序列了。

4.代码

    public static void main(String[] args) {
        int[] arr = new int[800];
        for (int i =0;i<800;i++){
            arr[i] = (int)(Math.random()*1000);
        }
        heapSort(arr);
        System.out.println(Arrays.toString(arr));
    }

    // 堆排序
    public static void heapSort(int arr[]) {
        // 把数组构建为大顶堆
        for (int i = arr.length/2-1; i >= 0; i--) {
            adjustHeap(arr,i,arr.length);
        }
        // 交换头尾节点值,然后将交换后的数组长度减一调整为大顶堆。
        for (int i = arr.length - 1;i >= 0;i--) {
            int temp = arr[0];
            arr[0] = arr[i];
            arr[i] = temp;
            adjustHeap(arr,0,i);
        }
    }

    /**
     * 将一个节点调整为大顶堆,前提是以该节点的左右子节点的子树必须是大顶堆
     * @param arr 数组
     * @param i 非叶子节点的索引
     * @param length 数组长度
     */
    public static void adjustHeap(int[] arr,int i,int length) {
        int temp = arr[i];
        for (int k = 2*i + 1;k < length;k = 2*k + 1) {
            if (k+1 < length && arr[k+1] > arr[k]) {
                k++;
            }
            if (temp < arr[k]) {
                arr[i] = arr[k];
                i = k;
            } else {
                break;
            }
        }
        arr[i] = temp;
    }

三、HuffmanTree(霍夫曼树)

1.介绍

节点的权: 若将树中节点赋给一个有着某种含义的数值,则这个数值称为该节点的权。
节点的带权路径长度: 从根节点到该节点之间的路径长度与该节点的权的乘积。
树的带权路径长度: 树的带权路径长度规定为所有叶子节点的带权路径长度之和,记为WPL(weighted path length),权值越大的节点离根节点越近的二叉树才是最优二叉树。
HuffmanTree: 给定N个权值作为N个叶子结点,构造一棵二叉树,WPL最小的树就是霍夫曼树。

2.数组转赫夫曼树思路

(1) 从小到大进行排序,将每一个数据看成一个节点,每个节点可以看成是一颗最简单的二叉树。每个节点都是赫夫曼树的叶子节点。
(2) 取出根节点权值最小的两颗二叉树。
(3) 组成一颗新的二叉树,该节点是前面两颗二叉树根节点权值的和。
(4) 再将 这颗新的二叉树,以根节点的权值大小再次和数组其他元素排序,不断重复1-2-3-4的步骤,直到数据都被处理,得到一颗赫夫曼树。

3.数组转赫夫曼树代码实现

4.赫夫曼编码

赫夫曼编码是赫夫曼树在电讯通信中的经典应用之一。广泛用于数据文件压缩,压缩率通常在20%~90%之间。赫夫曼码是可变字长编码(VLC)的一种,与1952年提出,称之为最佳编码。
定长编码
字符 -> ascii码 -> 二进制
每一位字符转换成的二进制长度都是一样的,称为定长。
前缀编码
即字符的编码都不能是其他字符编码的前缀,不会造成匹配的多义性。
原理
① 将要传输的字符串各个字符对应的个数统计出来
② 按照每个字符出现的次数构建一颗赫夫曼树,次数作为权值
③ 根据赫夫曼树,从根节点开始,向左路径为0,向右路径为1。给各个字符规定编码。
④ 根据③得到的赫夫曼编码,将字符串转化为对应编码。
注意:赫夫曼树根据排序方法的不同,可能不太一样。但是wpl是一样的,都是最小的,最后生成的赫夫曼编码的长度是一样的。

字符串->字节数组->赫夫曼编码表->转为赫夫曼编码的二进制字符串->二进制字符串转字节

注意事项
① 如果文件本身就是经过压缩处理的,那么使用赫夫曼编码再压缩效率不会有明显变化,比如视频,ppt等文件。
② 赫夫曼编码是按字节来处理的,因此可以处理所有的文件(二进制文件,文本文件)。
③ 如果一个文件中的内容,重复的数据不多,压缩效果也不会很明显。


四、二叉排序树(BST二叉搜索树)

1.简介

BST(Binary Sort(Search) Tree),对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。(如果有相同的值,可以将该节点放在左子节点或右子节点)。它既有数组快速查找的优势,又有链表的快速插入与删除操作的特点。

2.创建和遍历代码

public class BinarySortTree {

    public Node root;

    public void add(Node node) {
        if (root == null) {
            root = node;
        } else {
            root.add(node);
        }
    }

    public void infixOrder() {
        if (root == null) {
            System.out.println("BST为空,不能遍历");
        } else {
            root.infixOrder();
        }
    }
    static class Node {
        public int value;
        public Node left;
        public Node right;

        public Node(int value) {
            this.value = value;
        }

        public void add(Node node) {
            if (node == null) {
                return;
            }
            if (this.value > node.value) {
                if (this.left != null) {
                    this.left.add(node);
                } else {
                    this.left = node;
                }
            } else {
                if (this.right != null) {
                    this.right.add(node);
                } else {
                    this.right = node;
                }
            }
        }

       public void infixOrder() {
            if (this.left != null) {
                this.left.infixOrder();
            }
           System.out.println(this);
            if (this.right != null) {
                this.right.infixOrder();
            }
       }

        @Override
        public String toString() {
            return "Node{" +
                    "value=" + value +
                    '}';
        }
    }
}

class BinarySortTreeDemo{
    public static void main(String[] args) {
        int[] arr = {7,3,10,12,5,1,9};
        BinarySortTree binarySortTree = new BinarySortTree();
        for (int i = 0;i < arr.length;i++) {
            binarySortTree.add(new BinarySortTree.Node(arr[i]));
        }
        binarySortTree.infixOrder();
    }
}

3. 删除节点原理

① 删除的是叶子节点
(1) 先找到要删除的结点 targetNode
(2) 找到 targetNode的父结点parent
(3) 确定targetNode是parent的左子节点,还是右子节点
(4) 根据前面的情况来对应删除,左子节点parent.left=null,右子节点parent.right=null
② 删除只有一颗子树的节点
(1) 先找到要删除的节点 targetNode
(2) 找到targetNode的父节点parent
(3) 确定targetNode的子节点是左子节点还是右子节点
(4) targetNode是parent的左子节点还是右子节点
(5) 如果targetNode有左子节点。如果targetNode是parent的左子节点,parent.left = targetNode.left;如果targetNode是parent的右子节点,parent.right = targetNode.left;
(6) 如果targetNode有右子节点。如果targetNode是parent的左子节点,parent.left = targetNode.right;如果targetNode是parent的右子节点,parent.right = targetNode.right;
③删除有两颗子树的节点
(1) 先找到要删除的节点 targetNode
(2) 找到targetNode的父节点parent
(3) 从targetNode的右子树找到最小的节点
(4) 用一个临时变量,将最小节点的值保存temp
(5) 删除该最小节点
(6) targetNode.value = temp

4. 删除节点代码

public class BinarySortTree {

    public Node root;

    public void add(Node node) {
        if (root == null) {
            root = node;
        } else {
            root.add(node);
        }
    }

    public void remove(int value){
        if (root == null) {
            return;
        }
        Node current = search(value);
        if (current == null) {
            return;
        }
        // 根节点是叶子节点的情况
        if (root.right == null && root.left == null) {
            root = null;
            return;
        }
        Node parent = searchParent(value);
        if (current.left == null && current.right == null) {
            if (parent.left != null && parent.left.value == value) {
                parent.left = null;
            } else if (parent.right != null && parent.right.value == value){
                parent.right = null;
            }
        } else if (current.left != null && current.right != null) {
            Node minNode = findMinNode(current.right);
            minNode.left = current.left;
            minNode.right = current.right;
            if (parent != null) {
                if (parent.left != null && parent.left.value == value) {
                    parent.left = minNode;
                } else {
                    parent.right = minNode;
                }
            } else {
                root = minNode;
            }

        } else {
            if (current.left != null) {
                if (parent != null) {
                    if (parent.left != null && parent.left.value == value) {
                        parent.left = current.left;
                    } else {
                        parent.right = current.left;
                    }
                } else {
                    root = current.left;
                }
            } else {
                if (parent != null) {
                    if (parent.left != null && parent.left.value == value) {
                        parent.left = current.right;
                    } else {
                        parent.right = current.right;
                    }
                } else {
                    root = current.right;
                }
            }
        }
    }

    public Node findMinNode(Node node) {
        Node target = node;
        while(target.left != null) {
            target = target.left;
        }
        remove(target.value);
        return target;
    }
    public void infixOrder() {
        if (root == null) {
            System.out.println("BST为空,不能遍历");
        } else {
            root.infixOrder();
        }
    }

    public Node search(int value) {
        Node node = root;
        while (node != null) {
            if (node.value == value) {
                return node;
            } else if (node.value > value) {
                node = node.left;
            } else {
                node = node.right;
            }
        }
        return null;
    }

    public Node searchParent(int value) {
        Node node = root;
        Node parent = null;
        while (node != null) {
            if (node.value == value) {
                return parent;
            } else if (node.value > value) {
                parent = node;
                node = node.left;
            } else {
                parent = node;
                node = node.right;
            }
        }
        return null;
    }
    static class Node {
        public int value;
        public Node left;
        public Node right;

        public Node(int value) {
            this.value = value;
        }

        public void add(Node node) {
            if (node == null) {
                return;
            }
            if (this.value > node.value) {
                if (this.left != null) {
                    this.left.add(node);
                } else {
                    this.left = node;
                }
            } else {
                if (this.right != null) {
                    this.right.add(node);
                } else {
                    this.right = node;
                }
            }
        }

       public void infixOrder() {
            if (this.left != null) {
                this.left.infixOrder();
            }
           System.out.println(this);
            if (this.right != null) {
                this.right.infixOrder();
            }
       }

        @Override
        public String toString() {
            return "Node{" +
                    "value=" + value +
                    '}';
        }
    }
}

class BinarySortTreeDemo{
    public static void main(String[] args) {
        int[] arr = {7,3,10,12,5,1,9,2};
        BinarySortTree binarySortTree = new BinarySortTree();
        for (int i = 0;i < arr.length;i++) {
            binarySortTree.add(new BinarySortTree.Node(arr[i]));
        }
        binarySortTree.infixOrder();
        binarySortTree.remove(1);
        binarySortTree.remove(10);
        binarySortTree.remove(9);
        binarySortTree.remove(12);
        binarySortTree.remove(3);
        binarySortTree.remove(7);
        binarySortTree.remove(5);
        binarySortTree.remove(2);
        System.out.println("-----------");
        binarySortTree.infixOrder();
    }
}

五、平衡二叉树(AVL)

1.介绍

AVL树是最早被发明的自平衡二叉查找树。在AVL树中,任一节点对应的两棵子树的最大高度差为1,因此它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下的时间复杂度都是{O(log {n})}。增加和删除元素的操作则可能需要借由一次或多次树旋转,以实现树的重新平衡。
为什么要有avl
二叉搜索树一定程度上可以提高搜索效率,但是当原序列有序时,例如序列 A = {1,2,3,4,5,6},构造二叉搜索树如下图。依据此序列构造的二叉搜索树为右斜树,同时二叉树退化成单链表,搜索效率降低为 O(n)。
在这里插入图片描述
在此二叉搜索树中查找元素 6 需要查找 6 次。
二叉搜索树的查找效率取决于树的高度,因此保持树的高度最小,即可保证树的查找效率。同样的序列 A,将其改为下图的方式存储,查找元素 6 时只需比较 3 次,查找效率提升一倍。
在这里插入图片描述
可以看出当节点数目一定,保持树的左右两端保持平衡,树的查找效率最高。
这种左右子树的高度相差不超过 1 的树为平衡二叉树。

2.单旋转(左旋转)

原理

创建一个和当前根节点值相同的节点
把新节点的左子树设置为当前根节点的左子树
把新节点的右子树设置为当前当前根节点的右子树的左子树
把当前根节点的值设置为当前根节点右子节点的值
把当前根节点的右子树设置为当前根节点的右子树的右子树
把当前根节点的左子树设置为新节点

代码

        // 左旋转
        public void leftRotate() {
            AVLNode avlNode = new AVLNode(value);
            avlNode.left = left;
            avlNode.right = right.left;
            value = right.value;
            right = right.right;
            left = avlNode;
        }

3.单旋转(右旋转)

原理

创建一个和当前根节点值相同的新节点
把新节点的右子节点设置为当前节点的右子树
把新节点的左子节点设置 为当前节点的左子树的右子树
把当前根节点的值设置为当前根节点左子节点的值
把当前根节点的左子树设置为当前根节点左子树的左子树
把当前根节点的右子树设置为新节点

代码

    // 右旋转
    public void rightRotate() {
        AVLNode avlNode = new AVLNode(root.value);
        avlNode.right = root.right;
        avlNode.left = root.left.right;
        root.value = root.left.value;
        root.left = root.left.left;
        root.right = avlNode;
    }

4.双旋转

如下图树经过单旋转并不能完成平衡二叉树的转换,需要双旋转。
在这里插入图片描述

原理





代码

package com.ljf.system.tree;

public class AVLTree {

    public AVLTree.AVLNode root;

    public void add(AVLTree.AVLNode node) {
        if (root == null) {
            root = node;
        } else {
            root.add(node);
        }
        if ( root.rightHeight() - root.leftHeight() > 1) {
            if (root.right != null && root.right.leftHeight() > root.right.rightHeight()) {
                root.right.rightRotate();
                root.leftRotate();
            } else {
                root.leftRotate();
            }
            return;
        }
        if (root.leftHeight() - root.rightHeight() > 1) {
            if (root.left != null && root.left.rightHeight() > root.left.leftHeight()) {
                root.left.leftRotate();
                root.rightRotate();
            } else {
                root.rightRotate();
            }
        }
    }

    public void infixOrder() {
        if (root == null) {
            System.out.println("BST为空,不能遍历");
        } else {
            root.infixOrder();
        }
    }


    static class AVLNode {
        public int value;
        public AVLTree.AVLNode left;
        public AVLTree.AVLNode right;

        public AVLNode(int value) {
            this.value = value;
        }

        public void add(AVLTree.AVLNode node) {
            if (node == null) {
                return;
            }
            if (this.value > node.value) {
                if (this.left != null) {
                    this.left.add(node);
                } else {
                    this.left = node;
                }
            } else {
                if (this.right != null) {
                    this.right.add(node);
                } else {
                    this.right = node;
                }
            }
        }

        public void infixOrder() {
            if (this.left != null) {
                this.left.infixOrder();
            }
            System.out.println(this);
            if (this.right != null) {
                this.right.infixOrder();
            }
        }

        // 左旋转
        public void leftRotate() {
            AVLNode avlNode = new AVLNode(value);
            avlNode.left = left;
            avlNode.right = right.left;
            value = right.value;
            right = right.right;
            left = avlNode;
        }
        // 右旋转
        public void rightRotate() {
            AVLNode avlNode = new AVLNode(value);
            avlNode.right = right;
            avlNode.left = left.right;
            value = left.value;
            left = left.left;
            right = avlNode;
        }

        /**
         * 返回以该节点为根节点的树的高度
         */
        public int height() {
            return Math.max(left == null ? 0 : left.height(),right == null ? 0 : right.height()) + 1;
        }

        public int rightHeight() {
            if (right == null) {
                return 0;
            }
            return right.height();
        }
        public int leftHeight() {
            if (left == null) {
                return 0;
            }
            return left.height();
        }
        @Override
        public String toString() {
            return "Node{" +
                    "value=" + value +
                    '}';
        }
    }
}


class AVLTreeDemo{
    public static void main(String[] args) {
        int[] arr = {10,11,7,6,8,9};
        AVLTree avlTree = new AVLTree();
        for (int i = 0;i < arr.length;i++) {
            avlTree.add(new AVLTree.AVLNode(arr[i]));
        }
        avlTree.infixOrder();
        System.out.println("-----------");
        System.out.println(avlTree.root.height());
        System.out.println(avlTree.root.leftHeight());
        System.out.println(avlTree.root.rightHeight());
    }
}

六、多叉树

1.介绍

如果允许每个节点可以有更多的数据项和更多的子节点,就是多叉树。
2-3树,2-3-4树就是多叉树,多叉树通过重新组织节点,减少树的高度,能对二叉树进行优化。

2.2-3树介绍

2-3树是最简单的b-树结构,具有一下特点
2-3树所有叶子节点都在同一层(只要是b树都满足这个条件)。
有两个子节点的节点叫做二节点,二节点要么没有子节点,那么有两个子节点。
有三个子节点的节点 叫三节点,三节点要么没有子节点,要么有三个子节点。
2-3树是由二节点和三节点构成的树。
当按照规则插入一个数到某个节点时,不能满足上面要求时,就需要拆,先向上拆,如果上层满,则拆本层,拆后仍然要满足上面条件。
对于三节点的子树的值大小仍然遵守BST的规则。

3.b树(B-tree,B-树)介绍

特点

B树的阶:节点的最多子节点个数。比如2-3的阶是3,2-3-4树的阶是4。
B树的搜索,从根节点开始,对节点内的关键字序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子节点;重复直到所对应的儿子指针为空,或已经是叶子节点。
每个中间节点包含k-1个元素和k个孩子,其中m/2 <= k < = m;每个叶子节点都包含k个元素,其中m/2 <= k < = m。m/2向上取整。
搜索可能在非叶子节点结束。
所有叶子节点再同一层,叶子节点中不包含关键字信息,实际上这些节点不存在,指向这些节点的指针都为null;
在这里插入图片描述

4.b+树

特点

B+树所有关键字都出现在叶子节点的链表中。叶子节点的链表有序。
所有的非叶子节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素
有k个子树的非叶子节点包含k个元素(b树是k-1个元素),每个元素不保存数据,只用来做索引(稀疏索引)
在这里插入图片描述
B+树的分裂:当一个结点满时,分配一个新的结点,并将原结点中1/2的数据复制到新结点,最后在父结点中增加新结点的指针;B+树的分裂只影响原结点和父结点,而不会影响兄弟结点,所以它不需要指向兄弟的指针;

5.b*树

特点

B*树定义了非叶子节点关键字个数至少为(2/3)M,即块的最低使用率为2/3,而B+树的块的最低使用率为1/2。
B
树分配新节点的概率比B+树要低,空间使用率更高。
在这里插入图片描述

B*树的分裂:当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了);如果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针;

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值