二叉树

一、二叉树的术语

1.1 树

树的定义:树是由n(n>=0)个节点组成的有限集合。n=0的树称为空树;n>0时树有以下两个条件构成:

  1. 有一个特殊的节点称为根节点,它只有后继,没有前驱;
  2. 除根节点之外的其他节点分为m(m>=0)个互不相交的集合,其中每个集合本身是一棵树,称为根节点的子树。

树是由节点组成的,节点之间具有层次关系的非线性结构。树是递归定义的。

树的术语

1、父母、孩子和兄弟节点
节点的前驱称为父母,节点的后继称为孩子。一棵树中,根节点没有父母,其他节点只有一个父母。有同一个父母的多个节点称为兄弟节点。

2、度
节点的度是节点所拥有的子树的个数。度为0 的节点称为叶子节点。

3、节点层次和树的高度
节点的层次反应节点处于树中的层次位置。约定根节点层次为1,其他节点的层次是其父母节点的层次+1。显然兄弟节点的层次相同。
树的高度或树的深度是树中节点的最大层次数。

4、有序树和无序树
在树的定义中,如果节点的子树之间没有次序,可以交换位置,称为无序树。如果节点的子树从左到右是有次序的,不能交换位置,称为有序树。

1.2 二叉树

二叉树是特殊的树。除叶子节点外,其他节点的度为2。n=0时称为空二叉树,n>0的二叉树有一个根节点和两棵互不相交的、分别称为左子树和右子树的子二叉树构成。
二叉树是有序树,左右子树不能交换位置。

满二叉树和完全二叉树
一棵高度为h的满二叉树是具有 2h1 个节点的二叉树。满二叉树中的每一层节点数都达到最大。一棵高度为h的完全二叉树,前 h1 层每一层的节点数达到最大,最后一层从左到右没有空节点。对满二叉树的节点按层,从根节点开始,自上而下,从左到右进行编号。约定根节点的序号为0。一棵具有n个节点的完全二叉树,按照以上方法编号,每个节点的编号和高度为h的满二叉树中编号从0到n-1的节点一一对应。

1.3 二叉树的性质

1、如果根节点的层次为1,则二叉树第i层最多有 2i1
2、在高度为h的二叉树中,最多有 2h1 个节点。
3、设一棵二叉树的叶子节点数为 n0 ,2度节点数为 n2 ,则有 n0=n2+1
4、一棵具有n个节点的完全二叉树,其高度为 h=log2n+1
5、一棵具有n个节点的完全二叉树,对序号为 i(0i<n) 的节点,有

  1. i=0 ,则i为根节点,无父母节点;如果i>0,则i的父母节点为 (i1)/2
  2. 若果 2i+1<n ,则i的左孩子序号为 2i+1 ;否则,没有左孩子;
  3. 若果 2i+2<n ,则i的右孩子序号为 2i+2 ;否则,没有右孩子;

二、二叉树的表示

2.1 存储结构

二叉树主要采取链式存储,顺序存储仅适用于完全二叉树和满二叉树。将一颗完全二叉树的所有节点按节点序号进行顺序存储,根据性质5,可以访问父母节点和孩子节点。比如堆,是完全二叉树,逻辑结构是二叉树,存储和实现的时候采用顺序存储。

如果对非完全的二叉树进行顺序存储,有以下两种方法:

  1. 仅存储节点,但不能反映节点之间的层次关系
  2. 通过补充空子树将一颗非完全的二叉树变成完全二叉树,顺序存储节点和空子树。

2.2 链式存储

二叉树通常采用链式存储结构,每个节点至少有两条链分别链接左右子树,才能表达二叉树的层次关系。链式存储结构主要有二叉链表和三叉链表。

二叉链表采用两条链分别连接左右孩子,每个节点有三个属性:数据、左孩子节点链和右孩子节点链。这样每个节点到达其孩子节点花费时间是O(1)。但是这种方式从一个节点到达其父母节点花费时间较长,需要从根节点在二叉树中查找。

三叉链表是在二叉链表的基础上增加一个属性,指向父母节点,这样存储了父母节点和孩子节点的双向关系。每个节点有4个属性:数据、左孩子节点链、右孩子节点链和父母节点链。

三、二叉树的操作

这里的操作采用三叉链表实现。

3.1 二叉树的表示

首先用三叉链表声明节点类

public class BinaryNode <T> {
    public T data;
    public BinaryNode <T> left;
    public BinaryNode <T> right;
    public BinaryNode <T> parent;

    public BinaryNode(T data, BinaryNode<T> left, BinaryNode<T> right, BinaryNode<T> parent) {
        super();
        this.data = data;
        this.left = left;
        this.right = right;
        this.parent = parent;
    }

    public BinaryNode (T data){
        this(data,null,null,null);
    }

    public BinaryNode(){
        this(null,null,null,null);
    }

}

二叉树类声明如下

public class BinaryTree<T> {
    public BinaryNode<T> root;

    //构造空二叉树
    public BinaryTree(){
        this.root=null;
    }

    //判断二叉树是否为空
    public boolean isEmpty(){
        return this.root==null;
    }
}

3.2 二叉树的遍历

二叉树的遍历是按照一定规则和次序访问二叉树中的所有节点,并且每个节点访问一次。二叉树的遍历主要有先根、中根、后根遍历和层次遍历,遍历方法有递归和非递归。先根、中根、后根是根据访问根节点的顺序命名的。

  1. 先根:访问根节点,遍历左子树,遍历右子树。
  2. 中根:遍历左子树,访问根节点,遍历右子树。
  3. 后根:遍历左子树,遍历右子树,访问根节点。
  4. 层次遍历:按层遍历每个节点
    二叉树
    如图,先根遍历的结果是:ABDEGJKCFHLI
    中根遍历的结果是:DBJGKEACHLFI
    后根遍历的结果是:DJKGEBLHIFCA
    层次遍历的结果是:ABCDEFGHIJKL

二叉树的先根、中根和后根遍历的实现有递归和非递归两种方式

3.2.1 递归

递归方法必须有参数,通过不同的实际参数区别递归调用执行中的多个方法。一棵二叉树由多个子树组成,每个节点都是一颗子树的根。因此,二叉树实现遍历的递归方法以节点为参数,当输入的节点不同时,遍历不同的子树。当节点为空时,递归结束。
同时二叉树需要实现以根节点开始的遍历。每种遍历由两个重载方法实现。

//先根遍历,递归
    public void perOrder(){
        System.out.print("先根遍历: ");
        perOrder(root);
        System.out.println();

    }

    public void perOrder(BinaryNode<T> node ){
        if(node!=null){
            System.out.print(node.data);
            perOrder(node.left);
            perOrder(node.right);
        }
    }


    //中根遍历,递归
    public void inOrder(){
        System.out.print("中根遍历: ");
        inOrder(root);
        System.out.println();
    }

    public void inOrder(BinaryNode<T> node){
        if(node!=null){
            inOrder(node.left);
            System.out.print(node.data);
            inOrder(node.right);

        }
    }
    //后跟遍历,递归
    public void postOrder(){
        System.out.print("后跟遍历: ");
        postOrder(root);
        System.out.println();
    }

    public void postOrder(BinaryNode<T> node){
        if(node!=null){
            postOrder(node.left);
            postOrder(node.right);
            System.out.print(node.data);
        }
    }

3.2.2 非递归

二叉树的非递归遍历需要设立一个栈辅助。
1、先根遍历:优先访问根结点,然后再分别访问左孩子和右孩子。即对于任一结点,其可看做是根结点,因此可以直接访问,访问完之后,若其左孩子不为空,按相同规则访问它的左子树;当访问完其左子树时,再访问它的右子树。过程如下:
对任意节点node:
step1: 访问节点node,并将节点入栈;
step2:判断节点node的左孩子是否为空,如果空,栈顶元素出栈,并将该节点的右孩子作为当前节点,转到step1;如果不空,node的左孩子作为当前节点,转到step1;
step3: 直到node为null或栈为空,算法停止。

//先根遍历,非递归
public void perOrderLoop(){
        System.out.print("先根遍历(非递归): ");
        Stack<BinaryNode<T>> s=new Stack<BinaryNode<T>>();
        BinaryNode<T> node=this.root;
        while(node!=null || !s.isEmpty()){
            while(node!=null){
                System.out.print(node.data);
                s.push(node);
                node=node.left;
            }
            if(!s.isEmpty()){
                node=s.pop();
                node=node.right;
            }
        }
        System.out.println();
    }

2、中根遍历:先访问左孩子,再访问根节点,最后访问右孩子。即对于任一结点,可看做是根结点,先访问左孩子,若其左孩子不为空,按相同规则访问它的左子树;当访问完其左子树时,访问根节点,最后访问它的右子树。过程如下:
对任意节点node:
step1: 将当前节点node入栈;
step2:判断节点node的左孩子是否为空,如果空,栈顶元素出栈,访问,并将该节点的右孩子作为当前节点,转到step1;如果不空,node的左孩子作为当前节点,转到step1;
step3: 直到node为null或栈为空,算法停止。

//中根遍历,非递归
    public void inOrderLoop(){
        System.out.print("中根遍历(非递归): ");
        Stack<BinaryNode<T>> s=new Stack<BinaryNode<T>>();
        BinaryNode<T> node=this.root;
        while(node!=null || !s.isEmpty()){
            while(node!=null){
                s.push(node);
                node=node.left;
            }
            if(!s.isEmpty()){
                node=s.pop();
                System.out.print(node.data);
                node=node.right;
            }
        }
        System.out.println();
    }

3、后根遍历:后根遍历的实现比较麻烦。遍历根节点前需要先访问左子树和右子树。这里需要为入栈元素做一个标记。每个元素入栈时标记为0,访问完其左子树后,将其标记改为1,节点不出栈,再访问右子树。当右子树访问完之后,再将标记好的节点出栈。过程如下:
对任意基点node:
step0:构建两个栈,一个存放节点,另一个存放和节点对应的标记;
step1:将节点node和其标记入栈;
step2:判断节点的左子树是否为空。如果不空,node的左孩子当做当前节点,转到1;如果为空,返回栈顶元素,但栈顶元素不出栈,转到step3;
step3:如果栈顶元素的标记为1,访问,并出栈,相应的标记也出栈;如果标记为0,将标记改为1,并将该节点的右孩子当做当前节点,转到step1;
step4: 直到node为null或栈为空,算法停止。

//后根遍历,非递归
    public void postOrderLoop(){
        System.out.print("后根遍历(非递归): ");
        Stack<BinaryNode<T>> s=new Stack<BinaryNode<T>>();
        Stack<Integer> s2=new Stack<Integer>();
        Integer mark=new Integer(1);
        BinaryNode<T> node=this.root;
        while(node!=null || !s.isEmpty()){
            while(node!=null){
                s.push(node);
                s2.push(new Integer(0));
                node=node.left;
            }

            while(!s.isEmpty() && s2.peek()==mark){
                System.out.print(s.pop().data);
                s2.pop();
            }

            if(!s.isEmpty()){
                s2.pop();
                s2.push(mark);
                node=s.peek().right;
            }
        }
    }   

3.2.3 层次遍历

二叉树的层次遍历是兄弟优先。两个兄弟节点访问次序是先左后右,连续访问。他们的子节点的访问次序是,左兄弟的所有孩子在右兄弟的所有孩子之前访问。实现层次遍历需要“先进先出”的队列辅助。

//层次遍历
    public void levelOrder(){
        System.out.print("层次遍历: ");
        levelOrder(root);
    }

    public void levelOrder(BinaryNode<T> node){
        Queue<BinaryNode<T>> q=new LinkedList<BinaryNode<T>>();
        while(node!=null || !q.isEmpty()){
            System.out.print(node.data);
            if(node.left!=null) q.offer(node.left);
            if(node.right!=null) q.offer(node.right);
            node=q.poll();
        }
    }

3.3 基于遍历的操作

1、求节点个数,采用先根遍历

//求节点个数
    public int count(){
        return count(root);
    }

    //递归
    public int count(BinaryNode<T> node){
        if(node==null) return 0;
        return 1+count(node.left)+count(node.right);
    }

    public int countLoop(){
        return countLoop(root);
    }

    //非递归,采用先根遍历
    public int countLoop(BinaryNode<T> node){
        Stack<BinaryNode<T>> s=new Stack<BinaryNode<T>>();
        int count=0;
        while(node!=null || !s.isEmpty()){
            while(node!=null){
                count++;
                s.push(node);
                node=node.left;
            }
            if(!s.isEmpty()){
                node=s.pop();
                node=node.right;
            }
        }
        return count;
    }

2、求高度
求二叉树的高度,最简单的是使用递归。空节点的高度为0,非空节点的高度是其左右子节点中高度较大的值+1.如果用非递归实现,需要使用后根遍历,先求出左右子树的高度,再求节点高度,最好在节点定义时加上高度属性。或者使用层次遍历。这里使用层次遍历。

//求高度
    public int height(){
        return height(root);
    }
    //递归
    public int height(BinaryNode<T> node){
        if(node==null) return 0;
        int lh=height(node.left);
        int rh=height(node.right);
        return (lh>rh)?lh+1:rh+1;
    }
    //非递归
    //这里采用层次遍历的方法
    public int heightLoop(BinaryNode<T> node){
        if(node==null) return 0;
        int h=0;
        Queue<BinaryNode<T>> q=new LinkedList<BinaryNode<T>>();
        q.offer(node);
        while(!q.isEmpty()){
            int begin=0;
            int last=q.size();
            while(begin<last){
                node=q.poll();
                if(node.left!=null) q.offer(node.left);
                if(node.right!=null) q.offer(node.right);
                begin++;
            }
            h++;    
        }
        return h;
    }

3、求父母节点
如果二叉树的实现使用二叉链表,节点的属性不包括父母节点,求一个节点的父母节点时需要遍历。如果使用三叉链表,实现很简单。这里使用三叉链表。、

    //求父母节点
    public BinaryNode<T> parentOf(BinaryNode<T> node){
        return node.parent;
    }

4、查找和替换
在二叉树中查找一个关键字为key的元素,返回首次出现的节点,如果没有找到,返回null。可以用各种遍历方法查找。在二叉查找树中,因为节点和其子节点的关键字有大小关系,需要按照一定的规则查找,不用全部遍历。

//查找
    public BinaryNode<T> search(T key){
        return search(root,key);
    }
    public BinaryNode<T> search(BinaryNode<T> node,T key){
        if(node==null || key==null) return null;
        if(node.data.equals(key)) return node;
        BinaryNode<T> find=search(node.left,key);
        if(find==null) find=search(node.right,key);
        return find;
    }

5、插入和删除
二叉树的插入需要指定插入规则,例如插入节点node为根节点,原根节点作为node的左孩子;或者将node作为p的左孩子插入,原来node的左孩子作为p的左孩子等。在插入过程中,需要改变节点的left,right和parent。例如在查找二叉树中,根据节点关键字的大小,寻找合适的插入位置。例如在平衡二叉树中,在插入过后,破坏了树的平衡,需要旋转来维持。
二叉树的中删除一个节点或者一棵子树,需要修改该节点父母节点的left和right。如果只是删除一个节点而非一棵子树,需要制定删除规则。换言之,删除节点node后,原先以node为根节点的子树变成了森林,需要约定一种规则,使这个森林作为一棵子树。例如查找二叉树。

4.4 用先根和中根序列构造二叉树

二叉树是数据元素之间具有层次关系的非线性结构,二叉树中每个节点的两个子树有左右之分,所以建立一棵二叉树必须要明确以下两点:节点与父母节点和孩子节点之间的层次关系,兄弟节点间左右关系。
二叉树的遍历将二叉树的非线性关系映射成线性关系。已知一棵二叉树可以得到唯一的遍历序列,反之,已知一个遍历序列,不能得到确切的二叉树。以下讨论一种能够唯一确定一棵二叉树的方法。

由于先根序列和后根序列反应父母与节点间的层次关系,中根序列反应兄弟节点间得左右关系。所以,已知先根和中根,或已知中根和后根都能唯一确定一棵二叉树。但是已知先根和后根不能唯一确定一棵二叉树。这里以已知先根(perList)和中根(inList)序列,确定一棵二叉树为例,明确其步骤。
1、由先根次序可知,二叉树的根为perList[0],该根节点必定在inList中,设根节点在inList中的位置为i,即inList[i]=perList[0];
2、由中根次序知,inList[i]之前的节点在根的左子树上,inList[i]后的节点在根的右子树上。因此,根的左子树由i个节点组成,其子序列为
左子树的先根序列: perList[1].perList[2],...perList[i]
左子树的中根序列: perList[0].perList[1],...perList[i1]
根的右子树由n-i-1个节点组成,其子序列为
右子树的先根序列: perList[i+1].perList[i+2],...perList[n1]
右子树的中根序列: perList[i+1].perList[i+2],...perList[n1]
3、以此递归,可确定一棵二叉树
例如已知一颗二叉树的先根序列是:ABDEGJKCFHLI
中根序列是:DBJGKEACHLFI
根节点为A,i=6,左子树的先根序列是BDEGJK,中根序列为DBJGKE
右子树的先根序列是CFHLI ,中根序列是CHLFI
由此在确定左子树和右子树,确定二叉树后,可得后根遍历序列DJKGEBLHIFCA
实现如下

//用先根和中根序列构造二叉树
    public BinaryTree(T[] perList,T[] inList){
        this.root=creat(perList,inList,0,0,perList.length);
        setParent(root,root.left);
        setParent(root,root.right);
    }

    //递归的方法构造二叉树
    //perList :先根遍历序列
    //inList:中根遍历序列
    //perStart:先根序列中构造开始的位置
    //inStart:中根序列中构造开始的位置
    //n:序列长度
    private BinaryNode<T> creat(T[] perList,T[] inList,int perStart,int inStart,int n){
        if(n<=0) return null;
        T elem=perList[perStart];
        BinaryNode<T> node=new BinaryNode<T>(elem); //创建节点
        int i=0;
        while(i<n && !elem.equals(inList[inStart+i])) 
            i++;
        node.left=creat(perList,inList,perStart+1,inStart,i);
        node.right=creat(perList,inList,perStart+i+1,inStart+i+1,n-i-1);
        return node;
    }

    //已知从node的父母节点到node的连接,设置node的parent
    private void setParent(BinaryNode<T> parent,BinaryNode<T> node){
        if(node==null) return;

        node.parent=parent;

        setParent(node,node.left);
        setParent(node,node.right);
    }

四、Huffman树和Huffman编码

Huffman编码是数据压缩技术中的一种无损压缩方法。
ASCII码是最常用的一种定长编码方案,一个字符由8位二进制数表示。ASCII码中每个字符的编码与字符的使用频率无关。
Huffman编码是一种变长的编码方案,字符的编码根据使用频率的不同而长短不一,使用频率高的字符编码较短,使用频率低的字符编码较长,从而使所有字符的编码长度为最短。
信息论中Huffman编码的实现步骤如下:
1、统计原始数据中信源符号出现的频率,按频率高低排序;
2、将两个最小的频率相加,作为新信源符号的频率;
3、重复以上两个步骤,直到和为1;在每次合并信源符号时,将合并的信源符号赋值0和1;
4、寻找从频率为1处每个信源符号的路径,记录下路径上的1和0,从而得到每个信源符号的编码。
huffman编码
huffman编码的过程,实际上是构建了一棵二叉树,将所有信源符号当做叶节点,字符的使用频率当做叶节点的权值,构造的二叉树是带权外路径最短的二叉树。(一个节点X的带权路径是X节点的权值与从根节点到X节点路径长度的乘积。所有叶节点的带权路径长度之和称为该二叉树的带权外路径长度。WPL)
这样的二叉树也称作huffman树。huffman树定义为带权外路径长度最短的二叉树,也称为最优二叉树。构造huffman树的算法思路是:使权值越大的叶子结点越靠近根节点。给定n个叶子结点和其权值集合,对应的huffman树不唯一,但是WPL唯一。

构造huffman树的算法如下:
1、由给定的n个权值构造具有n棵二叉树的森林,每一棵二叉树只有一个节点;
2、在森林中选取两个根节点权值最小的二叉树,作为左右子树构造一棵新的二叉树,设置新二叉树的根节点的权值为左右子树根节点的权值之和;
3、在森林中删除原来的两棵二叉树,并将新的二叉树加入森林。
4、重复步骤2、3,直到只剩下一棵二叉树,即为所构造的huffman树。

huffman树构造完毕之后,从根节点开始,为每个节点的左子树附上编码0,右子树附上编码1,然后从根节点开始到每个叶子结点的路径上的0和1组成该字符的huffman编码。由于每个字符都是叶子结点,而叶子结点不可能在根到其他叶子结点的路径上,所有任何一个字符的huaffman编码不是另一个字符huffman编码的前缀。

huffman编码的译码过程:给定一串二进制编码S,从S的第一位开始,逐位去匹配二叉树边上标记的0 和1,从huffman树的根节点开始,遇到0向左,遇到1向右。一旦到达一个叶子结点,便译出一个字符。接着从S的下一位开始,重复以上的步骤。

huffman树的构造实现如下:

package binarytree;

import java.util.PriorityQueue;
import java.util.Stack;
import java.util.TreeMap;

/*
 * 给定一个字符串,构造huffman树和实现huffman编码
 * 1、统计字符串中的每个字符出现的次数
 * 2、利用每个字符的频率构造huffman树
 * 3、实现huffman编码
 * */
public class HuffmanTree<T> {
    public HuffmanNode<T> root;

    public HuffmanTree(){
        this.root=null;
    }
    public HuffmanTree(T[] list){
        this.root=creatTree(list);
        setParent(root,root.left);
        setParent(root,root.right);

    }

    //节点类
    static class HuffmanNode<T> implements Comparable<HuffmanNode<T>>{
        public T data;
        public Integer frequence;
        public HuffmanNode<T> left;
        public HuffmanNode<T> right;
        public HuffmanNode<T> parent;

        public HuffmanNode(T data, Integer frequence, HuffmanNode<T> left, HuffmanNode<T> right, HuffmanNode<T> parent) {
            super();
            this.data = data;
            this.frequence = frequence;
            this.left = left;
            this.right = right;
            this.parent = parent;
        }

        public HuffmanNode(T data,Integer frequence){
            this(data,frequence,null,null,null);
        }

        public int compareTo(HuffmanNode<T> node){
            return this.frequence-node.frequence;
        }
    }

    private HuffmanNode<T> creatTree(T[] list){
        if(list==null) return null;
        //首先统计元素个数,使用treemap,然后将treemap中的元素初始化为节点,节点加入优先队列
        //统计元素
        TreeMap<T,Integer> tm=new TreeMap<T,Integer>();
        for(T elem:list){
            if(tm.containsKey(elem))
                tm.put(elem,tm.get(elem)+1);
            else tm.put(elem, 1);
        }

        //用字符和其个数构造节点,加入优先队列
        PriorityQueue<HuffmanNode<T>> p=new PriorityQueue<HuffmanNode<T>>();
        for(T elem:tm.keySet()){
            p.offer(new HuffmanNode<T>(elem,tm.get(elem),null,null,null));
        }

        //利用优先队列构造huffman树
        HuffmanNode<T> nodel,noder;
        while(!p.isEmpty()){
            nodel=p.poll();
            noder=p.poll();
            if(noder==null)
                return nodel;
            else{
                HuffmanNode<T> nodep=new HuffmanNode<T>(null,nodel.frequence+noder.frequence,nodel,noder,null);
                p.offer(nodep);
            }
        }
        return null;
    }

    private void setParent(HuffmanNode<T> parent,HuffmanNode<T> node){
        if(node==null || parent==null) return;

        node.parent=parent;

        setParent(node,node.left);
        setParent(node,node.right);
    }

    //利用Huffman树进行Huffman编码
    public String HuffmanEnCode(T[] list){

        //对list中的每一个字符编码,输出编码后的字符串
        String str="";
        if(list==null){
            return str;
        }
        TreeMap<T,String> tm=encode();
        for(T elem:list){
            str+=tm.get(elem);
        }
        return str;
    }

    //对每个字符编码
    private TreeMap<T,String> encode(){
        //中根遍历,遇到根节点时,向上寻找父母节点,如果是左孩子,编码0,如果是右孩子,编码1,直到根节点
        TreeMap<T,String> tm=new TreeMap<T,String>();
        Stack<HuffmanNode<T>> s=new Stack<HuffmanNode<T>>();
        HuffmanNode<T> node=this.root;
        while(!s.isEmpty()|| node!=null){
            while(node!=null){
                s.push(node);
                node=node.left;
            }
            if(!s.isEmpty()){
                node=s.pop();
                if(node.left==null && node.right==null)
                    tm.put(node.data, encode(node));
                node=node.right;
            }
        }
        return tm;
    }

    private String encode(HuffmanNode<T> node){
        String str="";
        while(node.parent!=null){
            str=(node==node.parent.left?"0":"1")+str;
            node=node.parent;
        }
        return str;
    }
}
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值