一文吃透二叉树:概念、性质、遍历与高频笔试题(Java 实现)

本文系统讲解二叉树的基本概念、性质(如n0=n2+1、完全二叉树深度公式)、两种特殊类型(满/完全二叉树),重点剖析前序/中序/后序/层序四种遍历方式的递归与非递归实现,涵盖结点计数、叶子统计、K层节点数、高度计算等基本操作,并结合典型OJ题强化算法思维。所有内容基于Java实现,紧扣面试与笔试高频考点。

这是《Java 数据结构》第 8 讲的课堂笔记整理。课件讲得很细,我把它的顺序、例子、图都保留了下来,代码补全并且都跑通了。看的时候建议对着图看——二叉树这东西,画一遍比背十遍有用。

本节目标

  1. 掌握树的基本概念
  2. 掌握二叉树概念及特性
  3. 掌握二叉树的基本操作
  4. 完成二叉树相关的面试题练习

一、树型结构(了解)

1.1 概念

树是一种非线性的数据结构,它是由 n(n >= 0)个有限结点组成一个具有层次关系的集合。

把它叫做树,是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。


▲ 一棵"树":上面是树冠,下面是树根——数据结构里的树正好倒过来

它具有以下的特点:

  • 有一个特殊的结点,称为根结点,根结点没有前驱结点;
  • 除根结点外,其余结点被分成 M(M > 0) 个互不相交的集合 T1、T2、……、Tm,其中每一个集合 Ti (1 <= i <= m) 又是一棵与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有 0 个或多个后继;
  • 树是递归定义的。


▲ 一棵树的结点图:A 是根,往下层层分叉

注意:树形结构中,子树之间不能有交集,否则就不是树形结构。课件里专门用"树与非树"做了对比:


▲ 三个图里哪些是树?答案都在打问号的地方

判断标准就三条:

  • 子树是不相交的(左边第一张图里 C 和 D 被连起来了,所以不是树);
  • 除了根结点外,每个结点有且仅有一个父结点(中间那张图 E、F 的父结点说不清,不是树);
  • 一棵 N 个结点的树有 N-1 条边(右边那张图 D 有三个父结点,不是树)。

1.2 概念(重要)

这一节是"行话"集中营,后面的性质、代码、面试题全靠它。先看图:


▲ 术语全靠这张图,下面每条都对着它说

结点的度:一个结点含有子树的个数称为该结点的度;如上图,A 的度为 6。

树的度:一棵树中,所有结点度的最大值称为树的度;如上图,树的度为 6。

叶子结点或终端结点:度为 0 的结点称为叶结点;如上图,B、C、H、I…… 等结点为叶结点。

双亲结点或父结点:若一个结点含有子结点,则这个结点称为其子结点的父结点;如上图,A 是 B 的父结点。

孩子结点或子结点:一个结点含有的子树的根结点称为该结点的子结点;如上图,B 是 A 的孩子结点。

根结点:一棵树中,没有双亲结点的结点;如上图,A。

再往下还有几个,考试爱抠字眼,但不用背,知道意思就行:

术语一句话解释
结点的层次从根开始定义,根为第 1 层,根的子结点为第 2 层,以此类推
树的高度或深度树中结点的最大层次
非终端结点 / 分支结点度不为 0 的结点(D、E、F、G……)
兄弟结点具有相同父结点的结点(B、C 是兄弟)
堂兄弟结点双亲在同一层的结点互为堂兄弟(H、I)
结点的祖先从根到该结点所经分支上的所有结点(A 是所有结点的祖先)
子孙以某结点为根的子树中任一结点(所有结点都是 A 的子孙)
森林由 m(m >= 0)棵互不相交的树组成的集合

记法:度是往下连了几个,层次是从上数第几排,别混。

1.3 树的表示形式(了解)

树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦。实际中树有很多种表示方式,如:双亲表示法、孩子表示法、孩子双亲表示法、孩子兄弟表示法等。

课件里重点讲了最常用的孩子兄弟表示法:

class Node {
    int  value;        // 树中存储的数据
    Node firstChild;   // 第一个孩子引用
    Node nextBrother;  // 下一个兄弟引用
}


▲ 孩子兄弟表示法:每个结点只管"第一个孩子"和"下一个兄弟"

这张图很值得多看两眼——它把一棵多叉树拍平成了二叉的形状:左指针存第一个孩子,右指针存下一个兄弟。所以后面"树转二叉树"的口诀就是"左孩子右兄弟"。

1.4 树的应用

文件系统管理(目录和文件),就是树最典型的落地场景:


▲ Linux 的文件系统结构:一个根 / 底下挂着所有目录

除了文件系统,组织架构图、HTML 的 DOM 树、路由表、决策树,本质上都是树。


二、二叉树(重点)

2.1 概念

一棵二叉树是结点的一个有限集合,该集合:

  1. 或者为空;
  2. 或者是由一个根节点加上两棵别称为左子树和右子树的二叉树组成。


▲ 左边是整棵二叉树,右边把它拆成"根结点 + 左子树 + 右子树"

从上图可以看出:

  1. 二叉树不存在度大于 2 的结点;
  2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树。

注意:对于任意的二叉树,都是由以下几种情况复合而成的:


▲ 二叉树的 5 种基本形态:空树、只有根、只有左子树、只有右子树、左右都在

这 5 种形态看着简单,其实是二叉树所有递归代码的"分类讨论表"——写递归时你只要想清楚"空"和"根 + 左 + 右"两种情况,剩下的交给递归就行。

另外,课件里有两张"大自然的奇观",挺有意思——大自然自己长出来的就是二叉树:


▲ 分枝树


▲ 这棵更夸张,主干上直接摊开成一整个伞状树冠

2.2 两种特殊的二叉树

1. 满二叉树:一棵二叉树,如果每层的结点数都达到最大值,则这棵二叉树就是满二叉树。也就是说,如果一棵二叉树的层数为 K,且结点总数是 2^K - 1,则它就是满二叉树。

2. 完全二叉树:完全二叉树是效率很高的数据结构,它是由满二叉树引出来的。对于深度为 K 的、有 n 个结点的二叉树,当且仅当其每一个结点都与深度为 K 的满二叉树中编号从 0 至 n-1 的结点一一对应时,称之为完全二叉树。

要注意的是:满二叉树是一种特殊的完全二叉树。


▲ 左边满二叉树:每层都塞满;右边完全二叉树:只允许最后一层右边缺,中间不许空

为什么完全二叉树"效率很高"?因为它的结点可以用数组连续存,一个空位都不浪费,PriorityQueue(堆)就是靠这个性质活着的。

2.3 二叉树的性质

下面 5 条,前 4 条笔试必考,第 5 条写堆的时候天天用。

性质 1:若规定根结点的层数为 1,则一棵非空二叉树的第 i 层上最多有 2^(i-1) 个结点(i > 0)。

性质 2:若规定只有根结点的二叉树深度为 1,则深度为 K 的二叉树的最大结点数是 2^K - 1(K >= 1)。

性质 3:对任何一棵二叉树,如果其叶结点个数为 n0,度为 2 的非叶结点个数为 n2,则 n0 = n2 + 1。

性质 4:具有 n 个结点的完全二叉树的深度 k 为 ⌊log2 n⌋ + 1(等价于 ⌈log2(n+1)⌉)。

性质 5:对于具有 n 个结点的完全二叉树,如果按照从上至下、从左至右的顺序对所有结点从 0 开始编号,则对于序号为 i 的结点有:

  • 若 i > 0,双亲序号:(i-1)/2;i = 0 时 i 为根结点编号,无双亲结点;
  • 若 2i+1 < n,左孩子序号:2i+1,否则无左孩子;
  • 若 2i+2 < n,右孩子序号:2i+2,否则无右孩子。

性质 3 的证明,课上推过一次,自己再推一遍就忘不掉了:

设结点数 n、边数 e、度为 1 的结点数 n1。结点数 n = n0 + n1 + n2;边数一方面 e = n - 1,另一方面每个度为 1 的结点贡献 1 条边、度为 2 的贡献 2 条边,e = n1 + 2n2。
联立:n0 + n1 + n2 - 1 = n1 + 2n2,得 n0 = n2 + 1。

来做 4 道题(答案是 1.B 2.A 3.B 4.B,先自己算再看解析):

题 1:某二叉树共有 399 个结点,其中有 199 个度为 2 的结点,则该二叉树中的叶子结点数为( )

A 不存在这样的二叉树 B 200 C 198 D 199

由性质 3:n0 = n2 + 1 = 200,选 B。
再看它存不存在:n = n0 + n1 + n2 = 200 + n1 + 199 = 399,得 n1 = 0。结点总数是奇数时 n1 恰好为 0,合法。

题 2:在具有 2n 个结点的完全二叉树中,叶子结点个数为( )

A n B n+1 C n-1 D n/2

完全二叉树里 n1 只能是 0 或 1。总结点数是 2n(偶数),所以 n1 = 1。
2n = n0 + 1 + (n0 - 1),得 n0 = n,选 A。

题 3:一个具有 767 个结点的完全二叉树,其叶子结点个数为( )

A 383 B 384 C 385 D 386

767 是奇数,n1 = 0。由 n0 = n2 + 1 和 n0 + n2 = 767,解得 n2 = 383、n0 = 384,选 B。

题 4:一棵完全二叉树的结点数为 531 个,那么这棵树的高度为( )

A 11 B 10 C 8 D 12

性质 4 代入:⌊log2 531⌋ + 1 = 9 + 1 = 10,选 B。

2.4 二叉树的存储

二叉树的存储结构分为:顺序存储和类似于链表的链式存储。顺序存储下一节(堆)再讲,这里重点看链式存储。

链式存储是通过一个一个的结点引用起来的,常见的表示方式有二叉和三叉两种:

// 孩子表示法
class Node {
    int  val;      // 数据域
    Node left;     // 左孩子的引用,常常代表左孩子为根的整棵左子树
    Node right;    // 右孩子的引用,常常代表右孩子为根的整棵右子树
}

// 孩子双亲表示法
class Node {
    int  val;      // 数据域
    Node left;     // 左孩子的引用
    Node right;    // 右孩子的引用
    Node parent;   // 当前节点的根节点
}

孩子双亲表示法后面在平衡树的位置会用到,本文统一采用孩子表示法来构建二叉树。

2.5 二叉树的基本操作

2.5.1 前置说明

在学习基本操作前,得先有一棵二叉树。为了降低学习成本,这里先手动快速搭一棵简单的树,等结构熟悉了,再回头看真正的建树方式。

public class BinaryTree {

    static class BTNode {
        BTNode left;
        BTNode right;
        int    value;

        BTNode(int value) {
            this.value = value;
        }
    }

    private BTNode root;

    public void createBinaryTree() {
        BTNode node1 = new BTNode(1);
        BTNode node2 = new BTNode(2);
        BTNode node3 = new BTNode(3);
        BTNode node4 = new BTNode(4);
        BTNode node5 = new BTNode(5);
        BTNode node6 = new BTNode(6);

        root = node1;
        node1.left = node2;
        node2.left = node3;
        node1.right = node4;
        node4.left = node5;
        node4.right = node6;
    }
}

搭出来就是 2.1 里那棵树:

            1
           / \
          2   4
         /   / \
        3   5   6

注意:上述代码并不是创建二叉树的"正规"方式,真正的建树后序会重点讲。另外课件里这段代码 6 行都写成了 BTNode node1 = ...,是排版笔误,变量名重复编译不过,我这里改成了 node1~node6。

再看基本操作前,回顾一下二叉树的概念,二叉树是:

  1. 空树;
  2. 非空:根节点、根节点的左子树、根节点的右子树组成的。

看出来了吧——二叉树的定义是递归式的,因此后续基本操作基本都是按照这个概念实现的。

2.5.2 二叉树的遍历

学习二叉树结构,最简单的方式就是遍历。

所谓遍历(Traversal),是指沿着某条搜索路线,依次对树中每个结点均做一次且仅做一次访问。访问结点所做的操作依赖于具体的应用问题(比如打印结点内容、结点内容加 1)。遍历是二叉树上最重要的操作之一,是二叉树上进行其它运算之基础。


▲ 遍历就是沿着红色虚线把每个结点"走一遍",Φ 表示空

在遍历二叉树时,如果没有进行某种约定,每个人都按照自己的方式遍历,结果就比较混乱;如果按照某种规则进行约定,则每个人对于同一棵树的遍历结果肯定是相同的。

如果 N 代表根节点,L 代表根节点的左子树,R 代表根节点的右子树,则根据遍历根节点的先后次序有以下遍历方式:

  • NLR:前序遍历(Preorder Traversal,亦称先序遍历)——访问根结点 → 根的左子树 → 根的右子树;
  • LNR:中序遍历(Inorder Traversal)——根的左子树 → 根节点 → 根的右子树;
  • LRN:后序遍历(Postorder Traversal)——根的左子树 → 根的右子树 → 根节点。
// 前序遍历
void preOrder(Node root);
// 中序遍历
void inOrder(Node root);
// 后序遍历
void postOrder(Node root);

课件下面主要分析了前序递归遍历,中序与后序图解类似。递归到底是怎么"进去又出来"的,看这张图最清楚:


▲ 红色箭头是"递归"往下走,绿色箭头是"回溯"往回返,NULL 处返回

三段代码几乎一模一样,唯一区别就是那行访问语句放在第几行:

/** 前序:根 → 左 → 右 */
public void preOrder(BTNode root) {
    if (root == null) return;
    System.out.print(root.value + " ");   // ① 访问根
    preOrder(root.left);                  // ② 左子树
    preOrder(root.right);                 // ③ 右子树
}

/** 中序:左 → 根 → 右 */
public void inOrder(BTNode root) {
    if (root == null) return;
    inOrder(root.left);
    System.out.print(root.value + " ");
    inOrder(root.right);
}

/** 后序:左 → 右 → 根 */
public void postOrder(BTNode root) {
    if (root == null) return;
    postOrder(root.left);
    postOrder(root.right);
    System.out.print(root.value + " ");
}

对上面那棵 1(2(3), 4(5,6)) 的树,结果是:

前序遍历结果:1 2 3 4 5 6
中序遍历结果:3 2 1 5 4 6
后序遍历结果:3 2 5 6 4 1

说个面试常被追问的点:递归写起来爽,但递归深度受栈限制。同一个遍历用非递归怎么写?

前序非递归:根先入栈,弹出即访问,然后先压右孩子、再压左孩子(保证左孩子先出栈)。

public void preOrderNoRec(BTNode root) {
    if (root == null) return;
    Deque<BTNode> stack = new ArrayDeque<>();
    stack.push(root);
    while (!stack.isEmpty()) {
        BTNode cur = stack.pop();
        System.out.print(cur.value + " ");
        if (cur.right != null) stack.push(cur.right);  // 先右后左
        if (cur.left != null)  stack.push(cur.left);
    }
}

中序非递归:一路向左压栈,压到底就弹出一个访问,再转向它的右子树。

public void inOrderNoRec(BTNode root) {
    Deque<BTNode> stack = new ArrayDeque<>();
    BTNode cur = root;
    while (cur != null || !stack.isEmpty()) {
        while (cur != null) {      // 一路向左
            stack.push(cur);
            cur = cur.left;
        }
        cur = stack.pop();
        System.out.print(cur.value + " ");
        cur = cur.right;           // 转向右子树
    }
}

后序非递归稍微绕一点,要多记一个"上一个访问过的结点",用来判断右子树是不是已经处理完了。

public void postOrderNoRec(BTNode root) {
    Deque<BTNode> stack = new ArrayDeque<>();
    BTNode cur = root, last = null;
    while (cur != null || !stack.isEmpty()) {
        while (cur != null) {
            stack.push(cur);
            cur = cur.left;
        }
        BTNode peek = stack.peek();
        if (peek.right == null || peek.right == last) {
            System.out.print(peek.value + " ");
            last = stack.pop();
        } else {
            cur = peek.right;
        }
    }
}

2. 层序遍历

除了前序、中序、后序,还可以对二叉树进行层序遍历。

设二叉树的根节点所在层数为 1,层序遍历就是从根节点出发,首先访问第一层的根节点,然后从左到右访问第 2 层的节点,接着是第三层…… 自上而下、自左至右逐层访问,这就是层序遍历。


▲ 层序遍历的走法:A → B → C → D → E → F → G → H → I,一层走完再走下一层

思路很直白:用队列。

public void levelOrder(BTNode root) {
    if (root == null) return;
    Queue<BTNode> queue = new LinkedList<>();
    queue.offer(root);
    while (!queue.isEmpty()) {
        BTNode cur = queue.poll();
        System.out.print(cur.value + " ");
        if (cur.left != null)  queue.offer(cur.left);
        if (cur.right != null) queue.offer(cur.right);
    }
}

如果要"按层分行"输出,就在循环里先用 queue.size() 把当前层一次性取完:

public List<List<Integer>> levelOrderByLevel(BTNode root) {
    List<List<Integer>> res = new ArrayList<>();
    if (root == null) return res;
    Queue<BTNode> queue = new LinkedList<>();
    queue.offer(root);
    while (!queue.isEmpty()) {
        int size = queue.size();
        List<Integer> level = new ArrayList<>(size);
        for (int i = 0; i < size; i++) {
            BTNode cur = queue.poll();
            level.add(cur.value);
            if (cur.left != null)  queue.offer(cur.left);
            if (cur.right != null) queue.offer(cur.right);
        }
        res.add(level);
    }
    return res;
}

一句话记住:深度优先用栈(递归),广度优先用队列。二叉树这章的题,八成都能往这两个模子上套。

练习 1:请根据以上三种遍历方式,写出下面这棵二叉树的三种遍历结果。


▲ 练习题用的二叉树

(按图中画法,H 是 E 的右孩子)

前序:A B D E H C F G
中序:D B E H A F C G
后序:D H E B F G C A

练习 2:四道选择题(参考答案:1.A 2.A 3.D 4.A)

1. 某完全二叉树按层次输出(同一层从左到右)的序列为 ABCDEFGH。该完全二叉树的前序序列为( )

A: ABDHECFG B: ABCDEFGH C: HDBEAFCG D: HDEBFGCA

层序正好对应完全二叉树的数组编号:A 是根,B、C 是第二层,D~G 是第三层,H 是第四层且是 E 的左孩子。

        A
      /   \
     B     C
    / \   / \
   D   E F   G
  /
 H

前序 A → B → D → H → E → C → F → G,选 A。

2. 二叉树的先序遍历和中序遍历如下:先序遍历:EFHIGJK;中序遍历:HFIEJKG。则二叉树根结点为( )

A: E B: F C: G D: H

先序的第一个结点一定是根,所以根是 E,选 A。

3. 设一棵二叉树的中序遍历序列:badce,后序遍历序列:bdeca,则二叉树前序遍历序列为( )

A: adbce B: decab C: debac D: abcde

后序的最后一个结点是根 → 根是 a。中序 badce 里 a 左边是 b(左子树)、右边是 dce(右子树)。右子树后序 dec 末位是 c,所以 c 是右子树的根,d、e 分别是它的左、右孩子。
树形为 a(b, c(d, e)),前序 a b c d e,选 D。

4. 某二叉树的后序遍历序列与中序遍历序列相同,均为 ABCDEF,则按层次输出(同一层从左到右)的序列为( )

A: FEDCBA B: CBAFED C: DEFCBA D: ABCDEF

后序最后一个是根 → 根是 F;中序里 F 也在最后,说明 A~E 全在 F 的左子树里。逐层递归下来,整棵树是一条只有左孩子的单链:F → E → D → C → B → A。
层序输出 FEDCBA,选 A。

顺带记两条规律:后序 = 中序 ⟹ 每个结点都没有右孩子;前序 = 中序 ⟹ 每个结点都没有左孩子。

2.5.3 二叉树的基本操作

这几个方法的签名,课件里都列出来了,基本就是"标准模板":

// 获取树中节点的个数
int size(Node root);
// 获取叶子节点的个数
int getLeafNodeCount(Node root);
// 子问题思路-求叶子结点个数
// 获取第K层节点的个数
int getKLevelNodeCount(Node root, int k);
// 获取二叉树的高度
int getHeight(Node root);
// 检测值为value的元素是否存在
Node find(Node root, int val);
// 层序遍历
void levelOrder(Node root);
// 判断一棵树是不是完全二叉树
boolean isCompleteTree(Node root);

实现全是"大问题 = 根的处理 + 左子树的答案 + 右子树的答案":

① 结点个数

public int size(BTNode root) {
    if (root == null) return 0;
    return 1 + size(root.left) + size(root.right);
}

② 叶子结点个数

public int getLeafNodeCount(BTNode root) {
    if (root == null) return 0;
    if (root.left == null && root.right == null) return 1;  // 左右都空 → 叶子
    return getLeafNodeCount(root.left) + getLeafNodeCount(root.right);
}

③ 第 K 层结点个数

public int getKLevelNodeCount(BTNode root, int k) {
    if (root == null || k < 1) return 0;
    if (k == 1) return 1;                    // 第 1 层只有根
    return getKLevelNodeCount(root.left,  k - 1)
         + getKLevelNodeCount(root.right, k - 1);
}

④ 树的高度

public int getHeight(BTNode root) {
    if (root == null) return 0;
    return Math.max(getHeight(root.left), getHeight(root.right)) + 1;
}

⑤ 查找值为 value 的结点

public BTNode find(BTNode root, int val) {
    if (root == null) return null;
    if (root.value == val) return root;
    BTNode leftRes = find(root.left, val);
    return leftRes != null ? leftRes : find(root.right, val);  // 左边找到就不找右边
}

⑥ 判断一棵树是不是完全二叉树

思路:层序遍历时把空孩子也入队。一旦弹出空结点,说明"后面必须全是空结点";若之后又冒出非空结点,就不是完全二叉树。

public boolean isCompleteTree(BTNode root) {
    if (root == null) return true;
    Queue<BTNode> queue = new LinkedList<>();
    queue.offer(root);
    boolean mustBeLeaf = false;
    while (!queue.isEmpty()) {
        BTNode cur = queue.poll();
        if (cur == null) {
            mustBeLeaf = true;
            continue;
        }
        if (mustBeLeaf) return false;   // 空结点之后又出现了非空结点
        queue.offer(cur.left);
        queue.offer(cur.right);
    }
    return true;
}

这段代码的前提是 LinkedList 允许 offer(null)。换成 ArrayDeque 会直接抛 NullPointerException——这是实际编码时最常踩的坑。

2.6 二叉树相关 OJ 题

课件最后留了 14 道题,基本覆盖了二叉树所有考点。我按"由易到难"重排了一下,每题给一句核心思路,链接可以在 优快云 题库/牛客搜同题名:

#题目核心思路
1检查两棵树是否相同同时递归,值不等或结构不等即 false
2另一棵树的子树对每个结点做一次"是否相同"的判断
3翻转二叉树交换左右孩子再递归
4判断是否是平衡二叉树求高度时顺便判断,用 -1 剪枝避免 O(n²)
5对称二叉树比较 left.left 与 right.right、left.right 与 right.left
6二叉树的构建及遍历读入含空标记的前序串递归建树,再中序输出
7二叉树的分层遍历队列 + size() 分层
8二叉树的最近公共祖先后序递归:左右各找到一个就返回根
9前序 + 中序构造二叉树前序定根,中序切区间,HashMap 定位下标
10中序 + 后序构造二叉树后序定根,其余同上
11二叉树创建字符串左空右不空时必须补 ()
12前序非递归遍历栈,先压右再压左
13中序非递归遍历一路向左压栈,弹出后转右
14后序非递归遍历记录 last,或双栈反转

三、小结

把这一讲压缩成几句话:

  1. 树是递归定义的层次结构,子树之间不能有交集;
  2. 二叉树度 <= 2 且有序,n0 = n2 + 1 必须会推;
  3. 满二叉树结点数 2^K - 1;完全二叉树深度 ⌊log2 n⌋ + 1,且 n1 只能是 0 或 1;
  4. 四种遍历:前/中/后序靠递归或栈,层序靠队列;
  5. 基本操作全是"根 + 左子树答案 + 右子树答案",写不出递归时先问自己"这棵树的子问题是什么"。

下一讲是二叉树的顺序存储和堆——完全二叉树真正发光的地方。


附:示例代码跑出来的完整输出

前序:1 2 3 4 5 6
中序:3 2 1 5 4 6
后序:3 2 5 6 4 1
层序:1 2 4 3 5 6
结点数:6
叶子数:3
第 2 层结点数:2
高度:3

如果这篇笔记帮你把二叉树串起来了,点个赞再走;有讲错的地方欢迎在评论区指出来。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值