数据结构——二叉树(C语言版)

前言

二叉树是一种非线性的数据结构。二叉搜索树、堆、红黑树等高阶数据结构都是依托于二叉树的基础实现的,所以我们有必要好好研究一下“二叉树”这种数据结构。本文只介绍二叉树的基础及中等用法,笔者能力有限,欠妥当之处欢迎批评指正。

二叉树是树的一种,在介绍二叉树之前,我们先认识一下什么是“树”

概念

树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
有一个特殊的结点,称为根结点,根结点没有前驱结点
除根结点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1<= i<= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继
因此,树是递归定义的(这是解决很多与树相关问题的核心思想,我们也会在后文中多次强调)

树和非树

子树一定是不相交的,除了根节点没有前驱节点(也叫父节点,这些专有名词我们在后面介绍),其他节点有且只有一个前驱节点。把握以上两点,树和非数的判别我想不是什么大问题。

树的相关专有名词

这里我们也只是简单介绍常用的专有名词,其余的有兴趣的读者可以自行查询。

结点的度:一个结点含有的子树的个数称为该结点的度
叶结点或终端结点:度为0的结点称为叶结点

非终端结点或分支结点:度不为0的结点
双亲结点或父结点:若一个结点含有子结点,则这个结点称为其子结点的父结点
孩子结点或子结点:一个结点含有的子树的根结点称为该结点的子结点
兄弟结点:具有相同父结点的结点互称为兄弟结点
树的度:一棵树中,最大的结点的度称为树的度

树的表示

树的表示相对复杂,树是非线性的,一般树的度虽然已知,但在每个前驱中预留度个位置来存储可能存在的子节点的地址,难免造成内存的浪费。此时我们一般会考虑“左孩子右兄弟表示法”。

typedef int DataType;
struct Node
{
struct Node* firstChild1; // 第一个孩子结点
struct Node* pNextBrother; // 指向其下一个兄弟结点
DataType data; // 结点中的数据域
};

左孩子节点负责找到节点的第一个孩子,再由孩子节点的右兄弟找到所有的子节点。

树的应用

 文件系统的目录树结构。这也就可以解释为啥,同一个文件夹下不允许有同名的文件,但是在不同文件夹下允许同名。

二叉树

我们介绍完了树的相关概念,接下来我们介绍一下今天的主角——二叉树

概念

二叉树是度不超过二的一颗树。

注意:

二叉树不存在度大于2的结点。

二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树

一棵二叉树是结点的一个有限集合,该集合:为空或由一个根结点加上两棵别称为左子树和右子树的二叉树组成


特殊二叉树

 1. 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。
 2. 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。

 性质

1. 若规定根结点的层数为1,则一棵非空二叉树的第i层上最多有2^{i-1}个结点.
2. 若规定根结点的层数为1,则深度为h的二叉树的最大结点数是2^{h}-1 .
3. 对任何一棵二叉树, 如果度为0其叶结点个数为 , 度为2的分支结点个数为 ,则有 n0=n2 +1
4.若规定根结点的层数为1,具有n个结点的满二叉树的深度,h=log以2为底,n+1为对数
5.对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从0开始编号,则对于序号为i的结点有:
1. 若i>0,i位置结点的双亲序号:(i-1)/2;i=0,i为根结点编号,无双亲结点
2. 若2i+1<n,左孩子序号:2i+1,2i+1>=n否则无左孩子
3. 若2i+2<n,右孩子序号:2i+2,2i+2>=n否则无右孩子

二叉树的存储

二叉树我们一般有两种存储的解决方案——顺序结构和链表结构。

1. 顺序存储
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储,关于堆我们后面再展开。二叉树顺
序存储在物理上是一个数组,在逻辑上是一颗二叉树
2. 链式存储
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链。三叉链是再左右孩子的基础上添加父节点的表示形式。

typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
{
struct BinTreeNode* left; // 指向当前结点左孩子
struct BinTreeNode* right; // 指向当前结点右孩子
BTDataType data; // 当前结点值域
}
// 三叉链
struct BinaryTreeNode
{
struct BinTreeNode* parent; // 指向当前结点的双亲
struct BinTreeNode* left; // 指向当前结点左孩子
struct BinTreeNode* right; // 指向当前结点右孩子
BTDataType data; // 当前结点值域
};


堆是一种在完全二叉树的基础上再添加约束的数据结构。堆约束堆中任意结点的值总是不大于或不小于其父结点的值。如果任意父节点大于等于左右孩子节点,我们称之为大堆;如果任意父节点小于等于左右孩子节点,我们称之为小堆。注意,堆只约束了父节点和左右孩子节点的关系,左右孩子节点之间的大小没有任何关联,堆是建立在完全二叉树之上的数据结构。

堆的实现

我们这里介绍堆在C语言上的实现

Heap.h

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <math.h>
#include <stdbool.h>

typedef int dataType;

typedef struct Tree
{
	dataType* arr;
	int sz;
	int len;
}Tree;


//初始化
Tree* chushihua();

//销毁
void xiaohui(Tree* t);

//插入元素
void push(Tree* t, dataType x);

//删除元素
void pop(Tree* t);

//获得根节点元素
dataType top(Tree* t);

//判断是否为空堆
bool isEmpty(Tree* t);

Heap.c

#include "Heap.h"

//初始化
Tree* chushihua(int x)
{
	Tree* ret = (Tree*)malloc(sizeof(Tree));
	ret->arr = NULL;
	ret->len = ret->sz = 0;
	return ret;
}

//销毁
void xiaohui(Tree* t)
{
	assert(t);
	free(t->arr);
	free(t);
}


//交换
void swap(dataType* e1, dataType* e2)
{
	dataType  tmp = *e1;
	*e1 = *e2;
	*e2 = tmp;
}

//向上调整
void xiangshangtiaozheng(dataType* arr, int i) {
	while (i > 0 && arr[i] < arr[(i - 1) / 2]) {
		swap(&(arr[i]), &(arr[(i - 1) / 2]));
		i = (i - 1) / 2;
	}
}

//插入元素
void push(Tree* t, dataType x) {
	assert(t);
	if (t->sz == t->len) {
		int newlen = t->arr == NULL ? 4 : t->len * 2;
		t->arr = (dataType*)realloc(t->arr, sizeof(dataType) * newlen);
		t->len = newlen;
	}
	(t->arr)[t->sz++] = x;
	xiangshangtiaozheng(t->arr, t->sz - 1);
}

//向下调整
void xiangxiatiaozheng(dataType* arr, int i, int sz) {
	while (i * 2 + 1 < sz) {
		int chile = i * 2 + 1;
		if (chile + 1 < sz && arr[chile + 1] < arr[chile]) {
			chile++;
		}
		if (arr[i] > arr[chile]) {
			swap(&arr[i], &arr[chile]);
			i = chile;
		}
		else return;
	}
}

//删除根元素
void pop(Tree* t)
{
	assert(t && t->sz > 0);
	swap(&t->arr[0], &t->arr[t->sz - 1]);
	t->sz--;
	xiangxiatiaozheng(t->arr, 0, t->sz);
}

//获得根节点元素
dataType top(Tree* t)
{
	assert(t && t->sz);
	return t->arr[0];
}

//判断是否为空堆
bool isEmpty(Tree* t)
{
	assert(t);
	return 0 == t->sz;
}

堆的实现最重要的是掌握向上调整算法和向下调整算法。这两种算法在逻辑上是相似的,只需要重点了解从哪里开始调整,往哪调整和什么时候调整结束这三点即可。

堆的应用

堆的应用我们在这里介绍两个——堆排序和TopK问题

堆排序

堆排序的实现可以归为两步

  1. 建堆。升序建大堆,降序建小堆
  2. 排序,依次删除堆元素,当堆删空就会得到有序的数组
//堆排序
void sort(int* arr, int sz);
//向下调整
void xiangxiatiaozheng(dataType* arr, int i, int sz) {
	while (i * 2 + 1 < sz) {
		int chile = i * 2 + 1;
		if (chile + 1 < sz && arr[chile + 1] < arr[chile]) {
			chile++;
		}
		if (arr[i] > arr[chile]) {
			swap(&arr[i], &arr[chile]);
			i = chile;
		}
		else return;
	}
}

//交换
void swap(dataType* e1, dataType* e2)
{
	dataType  tmp = *e1;
	*e1 = *e2;
	*e2 = tmp;
}


//堆排序(降序,建小堆)
void sort(int* arr, int sz)
{
	//向上调整建堆 时间复杂度 N*longN
	/*for (int i = 1; i < sz; i++) {
		xiangshangtiaozheng(arr, i);
	}*/
	//向下调整建堆 时间复杂度 N
	for (int i = (sz - 2) / 2; i >= 0; i--) {
		xiangxiatiaozheng(arr, i, sz);
	}
	int end = sz - 1;
	while (end > 0) {
		swap(&arr[0], &arr[end]);
		xiangxiatiaozheng(arr, 0, end);
		end--;
	}
}
TopK

 TOP-K问题,即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。

此时,我们的一般思路是所有数据入堆,在pop() K 次得到前K个数据。但如果考虑到数据量很大,此时这种方法会导致内存的急速消耗,并不是最优的方法。

此时我们推荐建一个K个大小的堆。用数据集合中前K个元素来建堆,求前k个最大的元素,则建小堆;求前k个最小的元素,则建大堆。用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素。所有数据比较完之后即可得到topK个数据。

#define K 3 //定义求K个数据
#define N 10 //定义元素总个数
#include <stdio.h>
#include <time.h>
#include <stdlib.h>

void xieru(); //随机写入N个数据供测试
int* topK(int k);//topK算法,从N个数据中找出topK
void jiandui(int* arr, int k); //前K个元素建堆算法
void swap(int* e1, int* e2);//交换算法
void xiangshang(int* arr, int ch);//向上调整算法
void xiangxia(int* arr, int pr, int k);//向下调整算法

int main()
{
	xieru();
	int* arr = topK(K);
	for (int i = 0; i < K; i++) {
		printf("%d ", arr[i]);
	}
	free(arr);
	arr = NULL;
}

void xieru()
{
	srand(time(NULL));
	FILE* pf = fopen("data.txt", "w");
	if (pf == NULL)
	{
		perror("fopen error");
		return;
	}
	for (int i = 0; i < N; i++)
	{
		fprintf(pf, "%d\n", rand() + i);
	}
	fclose(pf);
	pf = NULL;
}

int* topK(int k) {
	int* ret = malloc(sizeof(int) * k);
	FILE* pf = fopen("data.txt", "r");
	if (pf == NULL)
	{
		perror("fopen error");
		return;
	}
	for (int i = 0; i < k; i++)
	{
		fscanf(pf, "%d", ret + i);
	}
	jiandui(ret, k);
	int tmp;
	while (fscanf(pf, "%d\n", &tmp)!=EOF) {
		if (tmp > ret[0]) {
			ret[0] = tmp;
			xiangxia(ret, 0, k);
		}
	}
	fclose(pf);
	return ret;
}

void xiangshang(int* arr, int ch) {
	int pr = (ch - 1) / 2;
	while (ch > 0 && arr[ch] < arr[pr]) {
		swap(arr + ch, arr + pr);
		ch = pr;
		pr = (ch - 1) / 2;
	}
}

void xiangxia(int* arr, int pr, int k) {
	int ch = pr * 2 + 1;
	while (ch < k) {
		if (ch + 1 < k && arr[ch + 1] < arr[ch]) ch++;
		if (arr[ch] < arr[pr]) {
			swap(arr + ch, arr + pr);
			pr = ch;
			ch = pr * 2 + 1;
		}
		else return;
	}
}

void jiandui(int* arr, int k)
{
	/*for (int i = 1; i < k; i++) {
		xiangshang(arr, i);
	}*/
	for (int i = (k - 2) / 2; i >= 0; i--) {
		xiangxia(arr, i, k);
	}
}

void swap(int* e1, int* e2)
{
	int tmp = *e1;
	*e1 = *e2;
	*e2 = tmp;
}

二叉树的链式结构

前面我们介绍的堆利用的是二叉树的顺序结构,接下来我们来介绍二叉树的链式结构。

前置声明

二叉树问题的核心思想

笔者在这里完整展示C语言实现二叉树链式结构的源码。二叉树的很多算法都是递归解决的,这也是笔者一开始在前面强调的二叉树是递归定义的,这一点是很多二叉树问题的核心思想。

二叉树的遍历

二叉树的创建是比较复杂的算法,读者可以先手写一棵树研究遍历的算法。

二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的结点进行相应的操作,并且每个结点只操作一次
二叉树的遍历有:前序/中序/后序的递归结构遍历:
1. 前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。
2. 中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。
3. 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。

层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根结点所在层数为1,层序遍历就是从所在二叉树的根结点出发,首先访问第一层的树根结点,然后从左到右访问第2层上的结点,接着是第三层的结点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
 

其他非遍历问题

结点个数以及高度,二叉树的创建和销毁,只需要把握前面介绍的核心思想和遍历的方法即可完成。

二叉树的一些问题需要引入队列这一数据结构辅助完成。

二叉树的实现

Tree.h

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>

typedef char dataType;

typedef struct TreeNode
{
	dataType data;
	struct TreeNode* left;
	struct TreeNode* right;
}TreeNode;

//手动创建一棵树
TreeNode* CreatBinaryTree();

//创建节点
TreeNode* BuyNewNode(dataType x);

//前序便利
void PrevOrder(TreeNode* root);

//中序便利
void InOrder(TreeNode* root);

//后序便利
void HouOrder(TreeNode* root);

//求节点个数
int TreeSize(TreeNode* root);

//求叶子节点个数
int TreeYeZiSize(TreeNode* root);

//求树的高度
int TreeHeight(TreeNode* root);

//求第K层节点的个数
int TreeSizeByK(TreeNode* root, int k);

// 二叉树查找值为x的节点
TreeNode* BinaryTreeFind(TreeNode* root, dataType x);

// 二叉树销毁
void BinaryTreeDestory(TreeNode** root);

// 判断二叉树是否是完全二叉树
int BinaryTreeComplete(TreeNode* root);

// 层序遍历
void BinaryTreeLevelOrder(TreeNode* root);

// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
TreeNode* BinaryTreeCreate(dataType* a, int n, int* pi);

//根据字符串创建二叉树
//TreeNode* chuangJian(char* arr, int* pi);

Tree.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "Tree.h"
#include "duilie.h"

TreeNode* CreatBinaryTree()
{
	TreeNode* node1 = BuyNewNode(1);
	TreeNode* node2 = BuyNewNode(2);
	TreeNode* node3 = BuyNewNode(3);
	TreeNode* node4 = BuyNewNode(4);
	TreeNode* node5 = BuyNewNode(5);
	TreeNode* node6 = BuyNewNode(6);
	TreeNode* node7 = BuyNewNode(7);
	node1->left = node2;
	node1->right = node3;
	node2->left = node4;
	node2->right = node5;
	node3->left = node6;
	node3->right = node7;
	return node1;
}

TreeNode* BuyNewNode(dataType x)
{
	TreeNode* ret = (TreeNode*)malloc(sizeof(TreeNode));
	if (ret == NULL) {
		perror("malloc");
		return NULL;
	}
	ret->data = x;
	ret->left = ret->right = NULL;
	return ret;
}

void PrevOrder(TreeNode* root)
{
	if (root == NULL) return;
	printf("%d ", root->data);
	PrevOrder(root->left);
	PrevOrder(root->right);
}

void InOrder(TreeNode* root)
{
	if (root == NULL) return;
	InOrder(root->left);
	printf("%d ", root->data);
	InOrder(root->right);
}

void HouOrder(TreeNode* root)
{
	if (root == NULL) return;
	HouOrder(root->left);
	HouOrder(root->right);
	printf("%d ", root->data);
}

int TreeSize(TreeNode* root)
{
	if (root == NULL) return 0;
	return 1 + TreeSize(root->left) + TreeSize(root->right);
}

int TreeYeZiSize(TreeNode* root)
{
	if (root == NULL) return 0;
	if (root->left == NULL && root->right == NULL) return 1;
	return TreeYeZiSize(root->left) + TreeYeZiSize(root->right);
}

int TreeHeight(TreeNode* root)
{
	if (root == NULL) return 0;
	int left = TreeHeight(root->left);
	int right = TreeHeight(root->right);
	return left > right ? left + 1 : right + 1;
}

int TreeSizeByK(TreeNode* root, int k)
{
	if (root == NULL) return 0;
	if (k == 1) return 1;
	return TreeSizeByK(root->left, k - 1) + TreeSizeByK(root->right, k - 1);
}

TreeNode* BinaryTreeFind(TreeNode* root, dataType x)
{
	if (root == NULL) return NULL;
	if (root->data == x) return root;
	TreeNode* tmp = BinaryTreeFind(root->left, x);
	if (tmp != NULL)
		return tmp;
	tmp = BinaryTreeFind(root->right, x);
	if (tmp != NULL)
		return tmp;
	return NULL;

}

void BinaryTreeDestory(TreeNode** root)
{
	if ((*root) == NULL) return;
	BinaryTreeDestory(&(*root)->left);
	BinaryTreeDestory(&(*root)->right);
	free(*root);
	*root = NULL;
}

int BinaryTreeComplete(TreeNode* root)
{
	if (root == NULL) return 1;
	Queue q;
	QueueInit(&q);
	QueuePush(&q, root);
	while (!QueueEmpty(&q))
	{
		TreeNode* tmp = QueueFront(&q);
		QueuePop(&q);
		if (tmp == NULL)
			break;
		QueuePush(&q, tmp->left);
		QueuePush(&q, tmp->right);
	}
	while (!QueueEmpty(&q)) {
		TreeNode* tmp = QueueFront(&q);
		QueuePop(&q);
		if (tmp != NULL)
		{
			QueueDestroy(&q);
			return 0;
		}			
	}
	QueueDestroy(&q);
	return 1;
}

void BinaryTreeLevelOrder(TreeNode* root)
{
	if (root == NULL) return;
	Queue q;
	QueueInit(&q);
	QueuePush(&q, root);
	while (!QueueEmpty(&q))
	{
		TreeNode* tmp = QueueFront(&q);
		QueuePop(&q);
		printf("%d ", tmp->data);
		if (tmp->left != NULL)
			QueuePush(&q, tmp->left);
		if (tmp->right != NULL)
			QueuePush(&q, tmp->right);
	}
	QueueDestroy(&q);
}

TreeNode* BinaryTreeCreate(dataType* a, int n, int* pi)
{
	if (a == NULL) return NULL;
	if (!(*pi <= n)) return NULL;
	TreeNode* root = NULL;
	if (a[*pi] == '\0') return NULL;
	if (a[*pi] != '#') {
		root = BuyNewNode(a[(*pi)++]);
	}
	else {
		(*pi)++;
		return NULL;
	}
	root->left = BinaryTreeCreate(a, n, pi);
	root->right = BinaryTreeCreate(a, n, pi);
	return root;
}

//TreeNode* chuangJian(char* arr, int* pi)
//{
//	if (arr == NULL) return NULL;
//	TreeNode* root = NULL;
//	//if (arr[*pi] == '\0') return NULL;
//	if (arr[*pi] != '#') {
//		root = BuyNewNode(arr[(*pi)++]);
//	}
//	else {
//		(*pi)++;
//		return NULL;
//	}
//	root->left = chuangJian(arr, pi);
//	root->right = chuangJian(arr, pi);
//	return root;
//}

dui.h 

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include "Tree.h"

typedef TreeNode* QDataType;

// 链式结构:表示队列 
typedef struct QListNode
{
    struct QListNode* _next;
    QDataType _data;
}QNode;

// 队列的结构 
typedef struct Queue
{
    QNode* _head;
    QNode* _last;
}Queue;

// 初始化队列 
void QueueInit(Queue* q);
// 队尾入队列 
void QueuePush(Queue* q, QDataType data);
// 队头出队列 
void QueuePop(Queue* q);
// 获取队列头部元素 
QDataType QueueFront(Queue* q);
// 获取队列队尾元素 
QDataType QueueBack(Queue* q);
// 获取队列中有效元素个数 
int QueueSize(Queue* q);
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0 
int QueueEmpty(Queue* q);
// 销毁队列 
void QueueDestroy(Queue* q);

dui.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "duilie.h"

void QueueInit(Queue* q)
{
	assert(q);
	q->_head = NULL;
	q->_last = NULL;
}

QNode* NewNode(QDataType data)
{
	QNode* ret = (QNode*)malloc(sizeof(QNode));
	ret->_data = data;
	ret->_next = NULL;
	return ret;
}

void QueuePush(Queue* q, QDataType data)
{
	assert(q);
	QNode* newNode = NewNode(data);
	if (q->_head == NULL) {
		q->_head = newNode;
		q->_last = newNode;
	}
	else {
		q->_last->_next = newNode;
		q->_last = newNode;
	}
}

void QueuePop(Queue* q)
{
	assert(q && q->_head != NULL);
	QNode* rem = q->_head;
	q->_head = q->_head->_next;
	free(rem);
	if (q->_head == NULL) {
		q->_last = NULL;
	}
}

QDataType QueueFront(Queue* q)
{
	assert(q && q->_head);
	return q->_head->_data;
}

QDataType QueueBack(Queue* q)
{
	assert(q && q->_last);
	return q->_last->_data;
}

int QueueSize(Queue* q)
{
	assert(q);
	int ret = 0;
	QNode* cut = q->_head;
	while (cut != NULL) {
		ret++;
		cut = cut->_next;
	}
	return ret;
}

int QueueEmpty(Queue* q)
{
	assert(q);
	return q->_head == NULL;
}

void QueueDestroy(Queue* q)
{
	assert(q);
	if (q->_head == NULL) return;
	QNode* cut = q->_head;
	while (cut != NULL) {
		QNode* rem = cut->_next;
		free(cut);
		cut = rem;
	}
	q->_head = q->_last = NULL;
}

结语

以上便是今天的全部内容。如果有帮助到你,请给我一个免费的赞。

因为这对我很重要。

编程世界的小比特,希望与大家一起无限进步。

感谢阅读!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值