C++实现哈夫曼编码

【课程设计内容】

    实现一个哈夫曼树的应用程序,按照以下每个字符的出现频率(权值)序列{空格和26个英文字母(只使用大写字母)出现频率分别为:186,64,13,22,32,103,21,15,47,57,1,5,32,20,57,63,15,1,48,51,80,23,8,18,1,16,1}创建一棵哈夫曼树(要求左孩子的权值小于等于右孩子的权值,当有2个相同权值的子树时以原序列先出现的为左孩子)。

【功能要求】

1、画出对应的哈夫曼树(手工画或用软件工具绘制图或程序打印出树的结构)

2、程序打印出所有字符及其对印哈夫曼编码结果 (及格标准)

3、程序计算并打印出“THIS PROGRAM IS MY FAVORITE”总的报文传输长度 (良好标准)

4、程序能对用户输入的01串进行解码,验证编码解码结果正确,对用户输入的01串,回答其代表的字符串(即进行报文解码),若解码失败则回复“解码失败”。(优秀标准)

效果

画出对应的哈夫曼树

 程序打印出所有字符及其对印哈夫曼编码结果

 程序计算并打印出“THIS PROGRAM IS MY FAVORITE”总的报文传输长度

程序能对用户输入的01串进行解码,验证编码解码结果正确,对用户输入的01串,回答其代表的字符串(即进行报文解码),若解码失败则回复“解码失败”。

参考

 实现一个哈夫曼树的应用程序_蓟毓的博客-CSDN博客_哈夫曼树程序https://blog.csdn.net/qiminyuan/article/details/107880693?spm=1001.2014.3001.5502

(C++)利用中序遍历、层序遍历来打印二叉树树形结构_Juheng_luo的博客-CSDN博客_c++打印二叉树https://blog.csdn.net/Juheng_luo/article/details/119746998?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165711951716781683980174%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=165711951716781683980174&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-1-119746998-null-null.142%5Ev31%5Econtrol,185%5Ev2%5Econtrol&utm_term=%E5%88%A9%E7%94%A8%E4%B8%AD%E5%BA%8F%E9%81%8D%E5%8E%86%E3%80%81%E5%B1%82%E5%BA%8F%E9%81%8D%E5%8E%86%E6%9D%A5%E6%89%93%E5%8D%B0&spm=1018.2226.3001.4187[数据结构] 使用最小堆思想实现哈夫曼编解码 - 腾讯云开发者社区-腾讯云 (tencent.com)https://cloud.tencent.com/developer/article/1666610#:~:text=%E6%9E%84%E5%BB%BA%E5%93%88%E5%A4%AB%E6%9B%BC%E6%A0%91%E7%9A%84%E7%AC%AC,%E5%85%B6%E5%8F%98%E6%88%90%E4%B8%BA%E6%9C%80%E5%B0%8F%E5%A0%86%E3%80%82

实现代码

#include<iostream>//基本输入输出 
#include<fstream>//文件操作 
#include<cstring>//字符操作 
#include<cstdlib>//分配内存 
#include<queue>//STL队列 
#define MAX_DATA 100  // 最大容量 

using namespace std;//命名空间 


// 定义哈夫曼树结构
typedef struct HTNode *HuffmanTree;
struct HTNode{
	int Weight;//权值 
	char Letter;//字符 
	int Rank;//rank用来标记后面的中序遍历所得到的x坐标 
	HuffmanTree Left; //左子树 
	HuffmanTree Right;  //右子树 
};




//一些全局变量的定义
int code[30]; // 哈夫曼编码临时空间
//bool dictLoaded = false; // 是否已经加载了字典
//编码字典Key-Val对(实际上可以用STL库的map来实现,这里简单用两个数组) 
char dictKey[30];//存储键 (即字符) 
char dictVal[30][100];//存储值(即01串) 
int total=0;//初始化为零,会根据字典的编码数量进行计数
ofstream outFile; //建立ouput file stream对象,即从代码输出(output)到文件 
HuffmanTree globalTree = NULL; //全局哈夫曼树  
int countLength=0;//报文长度计算 
int xx;//绘图x轴上的坐标 

// 声明函数
int getCode(char ch);//获取字符所对应的01串 
int decode(HuffmanTree tree,char* code,char* result);//对传入的01串解码返回字符串 
int encode(char chs[],char* encodeResult);//对传入的字符串进行编码返回01串 
void printMenu();//打印菜单 
void printDict();//输出编码字典 
void PrintTree(HuffmanTree tree);//先序遍历哈夫曼树  
void CreateDict(HuffmanTree tree,int depth);//由哈夫曼树创建字典 
void LoadDict(ifstream &inFile);//从inFile所关联的文件中输出字典到键值对 
void loadTreeFile();//从文件中生成哈夫曼树(来源上一次的用户输入创建) 
void loadTreeByInput();//从用户输入生成哈夫曼树 
void doInit();//进行初始化(选择如何生成哈夫曼树和对应字典)
void saveCode();//保存字典键值对 
void mark(HuffmanTree t);//求树结点x轴上的x坐标
void print(HuffmanTree t);//打印哈夫曼树 
HuffmanTree CreateHTNode(int weight, char ch);//创建哈夫曼树单个结点 
HuffmanTree CreateHuffmanTree(int n,int weight[],char letter[]);//把n个哈夫曼树结点合并成一颗哈夫曼树 





int main(){
	doInit();//进行初始化(选择如何生成哈夫曼树和对应字典) 
	int choice;//存储菜单选择的数 
	
	char inCode[200];//存储用户输入的、要解码的01串(包括01串之间的空格) 
	char result[200];//存储解码后的结果 
	
	char inputStr[50];//存储用户输入的、要编码的字符串 
	char outCode[200];//存储编码后获得的01串 
	
	bool flag = true;//循环条件变量 
	while(flag){
		printMenu();
		cin >> choice;
		switch(choice){
			case 1: 
				cout <<"打印哈夫曼树 :"<<endl; 
				print(globalTree);
				break;
			case 2:
				printDict();//查看编码字典 
				break;
			case 3:
				loadTreeByInput();//重新建立哈夫曼树
				break;
			case 4:
				getchar();
				gets(inputStr);
				if(encode(inputStr,outCode)){
					cout <<"编码后的文本为: "<<endl<<outCode<<endl;
					cout <<"报文传输长度为(不包含编码之间的空格):"<<countLength<<endl;
				}else{
					cout<<"编码错误,只能输入空格和26个大写字母"<<endl;
				}
				break;
			case 5:
				getchar();
				gets(inCode);
				if(decode(globalTree,inCode,result)){
					cout<<"解码后的文本为: "<<endl<<result<<endl;
				}else{
					cout<<"解码失败"<<endl;
					cout<<"请按照编码字典正确输入编码"<<endl;
				}
				break;
			case 6:
				flag = false;
				break;//退出程序
		}
	}
	cout << "欢迎下次使用!" << endl;
	return 0;
}

// 打印菜单
void printMenu(){
	cout << endl;
	cout << "--------- Menu ----------" << endl;
	cout << "-- 1. 查看哈夫曼树结构 --" << endl;
	cout << "-- 2. 查看编码字典 ------" << endl;
	cout << "-- 3. 重新建立哈夫曼树 --" << endl;
	cout << "-- 4. 进行编码 ----------" << endl;
	cout << "-- 5. 进行解码 ----------" << endl;
	cout << "-- 6. 退出程序 ----------" << endl;
	cout << "请输入您的选择: ";
}

// 输出编码字典
void printDict(){
	if(total==0){
		cout<<"字典为空。"<<endl;
		return;
	} 
	for(int i=0;i<total;i++){
		cout << "字符: " << dictKey[i] << ", 编码值: " << dictVal[i] << endl; 
	}
} 

// 构造哈夫曼树结点 
HuffmanTree CreateHTNode(int weight, char ch){
	HuffmanTree treeNode=(HuffmanTree)malloc(sizeof(struct HTNode));
	treeNode->Weight=weight;
	treeNode->Letter=ch;
	treeNode->Left=treeNode->Right=NULL;
} 

// 构建哈夫曼树
HuffmanTree CreateHuffmanTree(int n,int weight[],char letter[]){
	HuffmanTree *temp,ht;
	temp=(HuffmanTree *)malloc(n*sizeof(struct HTNode));//创建元素类型为HuffmanTree大小为n的数组 
	
	for(int i=0;i<n;++i){
		temp[i]=CreateHTNode(weight[i],letter[i]);//n个独立的哈夫曼树结点建立
	}
	
	for(int i=0;i<n-1;++i){//n个结点需要n-1次合并 
		//这一步是为了权值相同时取前面那个 
		int firstsmall=-1,secondsmall;//设firstsmall为最小下标,secondsmall为次小下标 
		for(int j=0;j<n;++j){//这里小于n是因为,即使在最后一次合并时,第n个位置仍然有可能不为空 
			if(temp[j]!=NULL&&firstsmall==-1){ //多增加一个判断条件使最小下标赋值一次后就不再赋值 
				firstsmall=j;
				continue;//最小下标赋值后,次小下标才能赋值 
			}
			else if(temp[j]!=NULL){//后面会使结点为空,所以这里条件是不为空 
				secondsmall=j;//次小下标赋值后就break 
				break;
			}
		}
		//注意经过上一步,可以确定最小下标前面全都是空的 ,故j=secondsmall 
		for(int j=secondsmall;j<n;++j){//要实现firstsmall为最小权值下标,secondsmall为次小权值下标 
			if(temp[j]!=NULL){
				if(temp[j]->Weight<temp[firstsmall]->Weight){//如果存在权值比最小下标的权值小的下标 
					secondsmall=firstsmall;//则最小下标成为了次小下标 
					firstsmall=j;//最小下标更新 
				}
				else if(temp[j]->Weight<temp[secondsmall]->Weight){//如果大于最小下标但是小于次小下标 
					secondsmall=j;//则次小下标更新 
				}
			}
		}
		//经过上一步得出了最小权值下标和次小权值下标,满足合并条件 
		ht=(HuffmanTree)malloc(sizeof(struct HTNode));
		ht->Weight=temp[firstsmall]->Weight+temp[secondsmall]->Weight;
		ht->Left=temp[firstsmall];
		ht->Right=temp[secondsmall];
		
		temp[firstsmall]=ht;//把合并后的哈夫曼树放到最小权值的坑里面,相当于第一个 
		temp[secondsmall]=NULL;//次小权值的坑置为空 
	}
	free(temp);//释放临时数组内存(除了最后一个根结点的下标,已经全为空) 
	return ht;//返回最后一次合并的哈夫曼树,即根结点地址 
}

//先序遍历哈夫曼树 
void PrintTree(HuffmanTree tree){
	if(tree){//如果哈夫曼树非空 
		if((tree->Left == NULL) && (tree->Right == NULL)){//如果没有左右子树 ,即树叶 
			printf("(权重:%d, 数据:%c )\n",tree->Weight,tree->Letter);
		}else{
			printf("(权重:%d, 数据:非叶节点 )\n",tree->Weight);
		}
		PrintTree(tree->Left);//左子树 
		PrintTree(tree->Right); //右子树 
	}
}

// 哈夫曼编码字典生成 
void CreateDict(HuffmanTree tree,int depth){
	if(tree){
		if((tree->Left == NULL) && (tree->Right == NULL)){//如果为叶子结点 
			if(outFile==NULL){//要注意此时outFile所关联的文件是哪个 
				cout << "写出编码字典文件失败!" << endl; 
				exit(-1);//终止进程,返回-1 
			}
			//如果outFile已经关联,输入到所关联的文件中 
			outFile << tree->Letter <<",";//把字符和半角逗号写进txt文件
			for(int i=0;i<depth;i++){
				outFile<<code[i];
			}
			outFile<<endl;//end of line 换行并清空缓存流 
		}
		else{//如果不是叶子结点 
			code[depth]=0;
			CreateDict(tree->Left,depth+1);//递归函数,比较难解释 
			code[depth]=1;
			CreateDict(tree->Right,depth+1);//自己画个哈夫曼树理解一下 
		}
	}
}

// 哈夫曼编码字典加载
void LoadDict(ifstream &inFile){//取inFile的地址 ,即文件地址 
	if(inFile==NULL){//如果inFile没有关联文件 
		cout << "打开编码文件失败!!" << endl;
		return; 
	}
	char temp[30];//30要大于一行的字符数 
	while(!inFile.eof()){//end of file文件结尾 
		inFile.getline(temp,30);//获取一行不超过30字符到temp中,遇到换行符终止 
		if(strstr(temp,",")!=NULL){//从temp中检索","
			sscanf(temp,"%c,%s\n",&dictKey[total],&dictVal[total]);//从字符串读取格式化输入
			total++;//字典计数 
		}
	}
}

// 哈夫曼树文件加载 -- 从文件 
void loadTreeFile(){
	ifstream treeFile("hfmTree.txt");//input file stream 读取文件 
	
	if(treeFile==NULL){//文件不存在 
		cout << "加载树文件hfmTree失败!"<<endl;
		return; 
	}

	int num=0;
	char temp[20];
	int count=0;
	
	while(!treeFile.eof()){//end of file文件结尾 
		treeFile.getline(temp,20);//getline()获取一行不超过20的字符存入temp,遇到换行符结束 
		if(strstr(temp,",")!=NULL){//检索temp中的"," 
			num++;//哈夫曼树的叶子数 
		}
	}
	treeFile.close();//关闭文件
	
	if(num==0){
		cout<<"哈夫曼树为空"<<endl;
		return;
	} 
	
	treeFile.open("hfmTree.txt");//input file stream 读取文件
	
	int *weight=(int*)malloc(num*sizeof(int));//两个存储字符和权值的数组 
	char *ch=(char*)malloc(num*sizeof(char));//num是叶子数
	
	while(!treeFile.eof()){//end of file文件结尾 
		treeFile.getline(temp,20);//getline()获取一行不超过20的字符存入temp,遇到换行符结束 
		if(strstr(temp,",")!=NULL){//检索temp中的"," 
			sscanf(temp,"%c,%d\n",&ch[count],&weight[count]);//sscanf()从字符串读取格式化输入
			count++;//哈夫曼树的叶子数 
		}
	}
	treeFile.close();//关闭文件
	
	//注意globalTree是全局变量 
	globalTree = CreateHuffmanTree(count,weight,ch);//用从文件中获取的字符和权值构造哈夫曼树 
	
	free(weight);//清空数组内存 
	free(ch);//清空数组内存 
	
	cout << "成功从hfmTree数据文件中加载树的数据" << endl;
}

// 哈夫曼树文件加载 -- 从用户输入
void loadTreeByInput(){
	cout << "从输入中构建哈夫曼树..." <<endl; //不存在文件则创建,存在则覆盖 
	ofstream outTreeFile("hfmTree.txt",ios::out); //output file stream文件输出流,将数据写进文件中 
	
	int count;//数据数,哈夫曼叶子数,n-1次合并的n。
	
	cout << "请输入一共要读入的数据数: ";
	scanf("%d",&count); 
	getchar();//细节清除输入缓存区的回车 
	//按照要输入的数据数开辟内存空间 
	
	if(count==0){
		cout<<"输入数据数为零,哈夫曼树为空"<<endl;
		return;
	}
	
	int *weight=(int*)malloc(count*sizeof(int));
	char *ch=(char*)malloc(count*sizeof(char));
	
	cout << "请输入数据,格式: ( data,weight ) 以空格分隔,以换行符结尾 (可能需要Ctrl+Z结束输入): " <<endl; 
	for(int i=0;i<count;i++){
		scanf("%c,%d ",&ch[i],&weight[i]);
		outTreeFile << ch[i] <<","<<weight[i]<<endl;
	}
	//getchar(); //debug时候用,貌似某个内存出了问题 
	globalTree = CreateHuffmanTree(count,weight,ch);//创建哈夫曼树 
	cout << "成功从用户输入中加载并保存树的数据."<<endl;//就是这个endl让num要比叶子数多1 
	outTreeFile.close();
	
	free(weight);//清空数组内存 
	free(ch);//清空数组内存 
	
	total=0;//重新输入saveCode出现了状况,估计是全局变量total使数组溢出 
	//重新输出编码字典 
	saveCode();
}

// 哈夫曼编码读取
int getCode(char ch){
	int i;
	for(i=0;i<total;++i){//total是全局变量,表示字典(键值对)的数量  
		if(dictKey[i]==ch){//如果键数组等于传入字符 
			return i;//则返回坐标,待会用在值数组中,获取编码值(即01串) 
		}
	}
	return -1;
}

// 编码过程
int encode(char chs[],char* encodeResult){
	int count=0;//计数编码出来的01串长度(包括01串之间的空格) 用于结果数组的结尾添加结束符 
	int flag=1;//检验编码是否正确 
	
	for(int i=0;(chs[i]!='\n' && chs[i]!='\0');i++){//chs[]是传入的字符串 
		int index = getCode(chs[i]);
		if(index==-1){
			flag=0;
			break;
		} 
		for(int j=0;dictVal[index][j]!='\0';j++){
			encodeResult[count]=dictVal[index][j];
			count++;
			countLength++;//报文传输长度(不计入编码之间的空格)
		}
		encodeResult[count]=' ';
		count++;
	}
	//countlength=count-1;//报文传输长度 (计入编码之间的空格) 
	encodeResult[count]='\0';
	return flag;//检验编码是否正确 
}

// 解码过程 
int decode(HuffmanTree tree,char* code,char* result){
	int flag=1,count=0;//flag是判断是否解码失败 
	HuffmanTree treeIndex = tree;//代替tree进行遍历 
	
	for(int i=0;code[i]!='\0';++i){ //code为存储01串的数组 
		if(code[i]==' ' || code[i]=='\n'){//如果遇到的00 11之间的空格或者回车 
			result[count]=treeIndex->Letter;//说明01串已经对应一个字符了 
			count++;//计算的是结果数组的长度 
			treeIndex=tree;//重置起点,所以传入的tree必须为根结点的地址 
		}else{
			if(code[i]=='1'){//如果是1,则取右子树 
				if(treeIndex->Right==NULL){如果到底了01串还没结束,则不存在此编码  
					flag=0;
					break;
				}else{
					treeIndex=treeIndex->Right;
				}
			}else if(code[i]=='0'){//如果是0,则取左子树(如果输入的不是01串的话,还可以往后面搞一个报错) 
				if(treeIndex->Left==NULL){//如果到底了01串还没结束,则不存在此编码 
					flag=0;
					break;
				}else{
					treeIndex=treeIndex->Left;
				}
			}else{//遇到了01以外的字符 
				flag=0;
				break;
			}
		}
	}
	result[count]=treeIndex->Letter;//最后一个01串是以结束符结束的 
	count++;//result数组存储的是0-count个,结尾加1,给数组一个结束符 
	//treeIndex=tree;//这行没必要了 
	result[count]='\0';//结束符 
	return flag;
}

// 程序初始化 
void doInit(){
	ifstream testHfmTree("hfmTree.txt");//读取文件 
	if(testHfmTree!=NULL){//如果存在文件
		testHfmTree.close();//关闭文件 
		loadTreeFile();// 从文件中读取哈夫曼树 
		saveCode();//生成字典文件并读取编码
	}else{//如果不存在文件
		testHfmTree.close();//关闭文件 
		loadTreeByInput();//从用户输入哈夫曼树 
	}
	
}

//生成字典文件并读取编码(键值对) 
void saveCode(){
	outFile.open("CodeFile.txt",ios::out);//以输入流打开文件 
	CreateDict(globalTree,0);//将用哈夫曼树生成的字典(键值对) 输入到文件中 
	outFile.close();//关闭文件 
	
	ifstream inFile("CodeFile.txt",ios::in);//读取文件 
	LoadDict(inFile);//读取编码存进键值对(即分别代表键和值的两个数组) 
	inFile.close();//关闭文件
}

// 中序遍历一次二叉树,被访问的节点投影在x轴上的位置记录在节点的rank成员里面,
//就是它的投影在x轴上的x坐标。
void mark(HuffmanTree t)
{
	if (!t) return;//如果树不存在则返回 
	mark(t->Left);
	t->Rank = xx;//这个坐标是字符长度加一个空格,效果类似123 12 111 . 
	xx += to_string(t->Weight).size()+1;//to_string()函数把权值转换成字符计算长度,比如186的长度为3 
	mark(t->Right);
}

//打印哈夫曼树 
void print(HuffmanTree t)//t是哈夫曼树头结点 
{
	xx = 2;      //中序遍历前把坐标起始xx(左边界的坐标) 可以把它设为其他数字看看,距离左边边界的距离 
	mark(t);     //中序遍历,处理结点的Rank,使之为结点投影在x轴上的坐标 
	printf("\n\nTree:\n\n");
	if (!xx) printf("    NULL");//如果xx为零,即哈夫曼树为空 
	
	queue<HuffmanTree> q;//创建一个元素类型为HuffmanTree的队列,用来存储需要处理的哈夫曼树结点 
	if(t){
		q.push(t);//将t入队 
	} 
	
	while (!q.empty()) {//如果队列不为空,即存入了哈夫曼树的结点,层序遍历
		int k = q.size(), p = 0;//k为队列中元素的个数,即待处理的结点数 ,p是循环条件变量 
		queue<int> q1;//创建一个int队列 ,用来存储竖线的坐标 
		
		while (k--) {   //一次打印一层的结点,k表示一层的结点数(因为内循环会把子树都入队) 
			t = q.front();//获取队首元素
			q.pop();//令队首元素出队
			string str = "";//创建一个空字符串 
			
			//打印左边 
			if (t->Left) {//如果存在左子树 
				q.push(t->Left);//将左子树入队
				while (p < t->Left->Rank) {//存入左子树的rank个空格 
					str += ' ';
					++p;
				}
				q1.push(p);//将p入队,此时p就等于左子树的坐标,也是竖线的坐标 
				while (p < t->Rank) {//将从t的左子树结点到t结点的剩余长度补全为横线 
					str += '-';
					++p;
				} 
			}
			
			while (p < t->Rank) {//如果不存在左子树,则左边不需要横线延伸,只要空格就行了 
				str += ' '; 
				++p;
			}
			//此时p就等于t的坐标
			printf("%s", str.c_str());//c_str()函数返回一个指向正规C字符串的指针常量,
			 						  //内容与本string串相同。输出存储的字符串 
			
			if(t->Left==NULL&&t->Right==NULL){//如果是叶子结点则输出字符和权值 
				p+=printf("#%c%d",t->Letter,t->Weight);//为了认出空格字符,前面加个# 
			}else{
				p+=printf("%d",t->Weight);//如果非叶子结点则只输出权值 
			}
			//printf()函数返回的是与输出字符数量相等的int,例如打印了123则返回3,1234则返回4。 
			//此时p就等于t的坐标加上  要输出的内容  的长度 
			
			
			//经过上述操作,已经把结点左边包括结点信息打印出来了。例如:   ----1000(右边还没打印) 
			//打印右边 
			str.resize(0);//将字符串的长度设置为零,即置为空。 
 
			if (t->Right) {//如果存在右子树 
				q.push(t->Right);//将右子树入队 
				while (p < t->Right->Rank) {//向右子树的坐标延伸,添加横线 
					str += '-';
					++p;
				}//此时p为右子树的坐标 
				printf("%s-", str.c_str());//c_str()函数返回一个指向正规C字符串的指针常量,%s后面的横线
				q1.push(p++);//此时p为竖线坐标 
			}
		}
		printf("\n");//注意此时光标在第二行的开头 
 
 
        //打印竖线
		p = 0;
		string str = "";
		while (!q1.empty()) { //如果q1队列不为空,即上一个结点存在子树,且把坐标存进去了 
			int tmp = q1.front();//获取队列头元素,即竖线的坐标 
			q1.pop();
			while (p < tmp) {
				str += ' ';
				++p;
			}
			str += '|';
			++p;
		}
		printf("%s\n", str.c_str());//c_str()函数返回一个指向正规C字符串的指针常量
	}
}

测试数据

//建立编码字典
 ,186 A,64 B,13 C,22 D,32 E,103 F,21 G,15 H,47 I,57 J,1 K,5 L,32 M,20 N,57 O,63 P,15 Q,1 R,48 S,51 T,80 U,23 V,8 W,18 X,1 Y,16 Z,1
//编码解码测试
THIS PROGRAM IS MY FAVORITE
1101 0001 0110 0011 111 100010 0010 1001 100001 0010 1010 110010 111 0110 0011 111 110010 100011 111 110011 1010 1100000 1001 0010 0110 1101 010

写在最后

写得太烂了,全部实现都放在一个主程序,而且注释也显得很杂乱(哪有人每条都注释的...),是一个缝缝补补的屑作,如果有疑问的可以在评论区发言,看到就回复。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值