05-树与二叉树

树与二叉树

一、认识树

  • 树是 n n n个结点的有限集。
    • 如果 n = 0 n=0 n=0,称为空树
    • 如果 n > 0 n>0 n>0,则有且仅有一个特定的称之为根结点,它只有直接后继,没有直接前驱。除根以外的其他结点划分为 m m m个互不相交的有限集,其中每个集合本身又是一棵树,称为根的子树
  • 树可用嵌套集合、广义表和凹入表来表示
  • 结点的度指的是拥有子树的数目;树的度指的是树种结点的最大的度。
  • 树的ADT
    ADT Tree{
        InitTree(&T);
        ...
        Value(T,cur_e);
        Assign(T,cur_e,value)
        Parent(T,cur_e)
        LeftChild(T,cur_e);
        ...
        InsertChild(&T,&p,i,c);
        DeleteChild(&T,&p,i);
        TraverseTree(T,Visit());
    }
    

二、认识二叉树

  • 一棵二叉树是结点的一个有限集,该集合为空,或由一个根加上两棵分别成为左子树和右子树的互不相交的二叉树组成
  • 每个结点至多只有两棵子树(不存在度大于2的结点)
  • 二叉树的ADT
    ADT BinaryTree{
        InitBiTree(&T);
        ...
        Value(T,e);
        Assign(T,&e,value);
        Parent(T,e);
        LeftChild(T,e);
        ...
        InsertChild(T,p,LR,c);
        DeleteChild(T,p,LR);
        PreOrederTraverse(T,Visit());
        InOrderTraverse(T,Visit());
        PostOrderTraverse(T,Visit());
        LevelOrderTraverse(T,Visit());
    }
    
  • 完全二叉树:设二叉树的高度为 h h h,则共有 h h h层。除第 h h h层外,其它各层 ( 1   h − 1 ) (1~h-1) (1 h1)的结点数都达到最大个数,第 h h h层从右向左连续缺若干节点,这就是完全二叉树
  • 满二叉树:一棵深度为 k k k且每层结点数都达到最大个数的二叉树称为满二叉树
  • 二者特点:
    • 若对结点按从上到下,自左至右连续编号,完全二叉树每个结点和相同高度满二叉树的编号结点一一对应
    • 叶节点只可能在层次最大的两层上出现
    • 任一结点,若其右分支下的子孙的最大层次为 l l l,则其左分支下的子孙的最大层次必为 l l l l + 1 l+1 l+1
  • 二叉树的性质:
    • 二叉树的某一层的结点数量
      • 在二叉树的第 i i i层上至多有 2 i − 1 2^{i-1} 2i1个结点
    • 二叉树的所有结点数量
      • 深度为 k k k的二叉树至多有 2 k − 1 2^k-1 2k1个结点
    • 不同类型结点的数量关系
      • 对任何一棵二叉树 T T T,如果其叶结点树为 n 0 n_0 n0,度为 2 2 2的结点数为 n 2 n_2 n2,则 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1
    • 完全二叉树的结点数量和深度的关系
      • 具有 n n n个结点的完全二叉树的深度为 └ l o g 2 n ┘ + 1 \llcorner log_2n\lrcorner+1 log2n+1
    • 完全二叉树的结点间关系
      • 如将一棵有 n n n个结点的完全二叉树自顶向下,同一层从左向右连续给结点编号 1 , 2 , ⋯   , n 1,2,\cdots,n 1,2,,n,则有以下关系
        • i = 1 i=1 i=1,则 i i i无双亲
        • i > 1 i>1 i>1,则 i i i的双亲为 └ i 2 ┘ \llcorner \frac{i}{2}\lrcorner 2i
        • 2 i > n 2i>n 2i>n,则 i i i无左孩子;否则, i i i的左孩子为 2 i 2i 2i
        • 2 i + 1 > n 2i+1>n 2i+1>n,则 i i i无右孩子;否则, i i i的右孩子为 2 i + 1 2i+1 2i+1

三、实现二叉树

  • 二叉树的存储结构
    • 顺序存储:存储效率低,大量占位用的0
    • 链式存储:二叉链表(lChild,rChild)
    • 链式存储:三叉链表(lChild,rChild,parent)
  • 二叉树的基本操作
    • 核心:遍历二叉树
      • 树的遍历就是按照某种次序访问树中的结点,要求每个结点访问一次且仅访问一次
      • 树的遍历就是将树的结点转换为线性序列
    • 中序遍历
    int InOrder(BiTree T)
    {
      InOrder(T->lChild);
      print("$d", T->data);
      InOrder(T->rChild);
    }
    
    • 前序遍历
    int PreOrder(BiTree T)
    {
      print("$d", T->data);
      PreOrder(T->lChild);
      PreOrder(T->rChild);
    }
    
    • 后序遍历
    int PostOrder(BiTree T)
    {
      PostOrder(T->lChild);
      PostOrder(T->rChild);
      print("$d", T->data);
    }
    
    • 构建二叉树
    void CreateBiTree(BiTree &T)
    {
        scanf(&ch);
        if(ch==RefValue)  //递归出口
        {
            T=NULL;
        }
        else
        {
            T=(BiTNode*)malloc(sizeof(BiTNode));
            assert(T);
            T->data=ch;
            CreateBiTree(T->lChild);
            CreateBiTree(T->rChild);
        }
    }
    

四、二叉树的非递归遍历

  • 中序遍历的非递归算法
    • 非递归算法利用栈保存子树的根,出栈时访问
void InOrderTraverse(BiTree T)
{
    InitStack(S);
    Push(S,T);
    while(!STackEmpty(S))
    {
        while(GetTop(S,p)&&p)
            push(S,p->lChild);
        Pop(S,p);
        if(!StackEmpty(S))
        {
            Pop(S,p);
            printf("%d",p->data);
            Push(S, p->rChild);
        }
    }
}
  • 先序遍历的非递归算法
    • 从根节点开始,入栈;结点 p p p出栈,若 p p p非空时,访问,再将其非空右孩子入栈,然后再将其左孩子入栈,接下来出栈,并形成循环
  • 后序遍历的非递归算法
    • 有中序非递归遍历的思想,也有先序非递归遍历的思想。先将左孩子尽可能入栈,但是在左孩子入栈之前,先把非空的右孩子入栈,保证右子树先入栈,后访问。
    • 从根节点开始,若左子树非空一直入栈,当左子树为空后,出栈,若右子树不为空则入栈形成循环,当右子树为空,出栈,检查当前栈顶元素右子树,若为空出栈,否则入栈继续循环

五、线索二叉树

  • 二叉树的遍历输出一个特定的线性序列,本质上是线性化树这种非线性的结构
  • 结点结构(lChild,lTag,rTag,rChlid,data)
    • lTag=0,lChild指向结点的左孩子;lTag=1,lChild指向结点的前驱
    • rTag=0,rChild指向结点的右孩子;rTag=1,rChild指向结点的后继
  • 线索:指向前驱和后继的指针
  • 线索二叉树:包含线索的二叉树
  • 线索化:对二叉树进行某种次序的遍历,同时使其成为线索二叉树的过程

六、树的存储结构

  • 双亲表示法
    • 以一组连续空间存储树的结点,同时在结点中附设一个指针,存放双亲结点在链表中的位置
    • 便于找双亲,找孩子耗时;适用于以找双亲为主的算法
  • 孩子表示法
    • 多重链表
      • 每个结点有多个指针域分别指向子树的根结点
      • 结点的结构相同,结点含的孩子指针数目等于树的度(存储效率低);或者结点的孩子指针数目等于实际需求(操作复杂性大)
    • 孩子链表
      • 把每个结点的孩子结点组织为一个单链表, n n n个单链表的头指针组成一个线性表
  • 孩子兄弟表示法
    • 结点结构(data,firstChild,nextSibling)
    • 可将任意树转换为二叉树

七、树的遍历

  • 先根遍历和后根遍历共两类
  • 树的先根遍历
    • 当树非空时,先访问根结点,然后依次先根遍历根的各子树
    • 可借用二叉树的先序遍历非递归算法实现
  • 树的后根遍历
    • 当树非空时,先依次后根遍历根的各子树,然后访问根结点
    • 可借用二叉树的中序遍历非递归算法实现
  • 森林的遍历
    • 先序遍历:依次先根遍历各子树
    • 中序遍历;依次后根遍历各子树

八、哈夫曼树

  • 数据压缩
    • 无损压缩:数据文件被压缩,解压后恢复原始数据
    • 有损压缩:常见,单方向,如图像、视频、语音等各种格式
    • 基本原理:数据(文件)就是一长的0-1串,统计发现数据中0-1子串出现的规律,将出现斌率高得,较长的0-1子串用较短得0-1子串表示出来
  • 无损压缩:哈夫曼编码
  • 有损压缩:Gif图像
  • 哈夫曼树(带权路径长度最小的二叉树)
  • 构造Huffman树算法
    • 输入:给定 n n n各权值
    • 输出:包括 n n n个叶结点的二叉树,其带权路径长度最小
    • 为每个权值构造一棵只有根节点的二叉树
    • 任意选择根节点权值最小的两棵树
    • 选择出的两棵树作为左右孩子,合并出一棵新的树,新树的根的权值为左右孩子权值的和
    • 用新树替换那两棵树
    • 重复上述过程,直至只有一棵树
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/*
* 1.压缩过程
* 待压缩文件
* 压缩后文件
* 1.1 分析带压缩文件	获取文件中出现过的字节和出现次数 组合成字典(索引)
* 1.2 根据字典创建哈夫曼树 获得每个字符的哈夫曼编码 并存入字典中
* 1.2.0 准备个数组,存储所有字符节点
* 1.2.1 创建出现次数最小的节点和第二小的节点
* 1.2.2 创建空的节点成为1.2.1中两个节点的父节点
* 1.2.3 数组中 1.2.2中创建的节点覆盖1.2.1中出现次数最小的节点,数组中1.2.1中第二小的节点置空
* 1.2.4 循环知道数组中所有节点都到了树中未知
* 1.3 先把字典写入压缩后的文件
* 1.4 根据字典中的编码把待压缩文件中的每个字符的哈夫曼编码写入压缩后文件中
* 1.5 关闭两个文件
* 
* 2.解压过程
* 压缩后文件
* 解压文件
* 2.1 把字典读出来
* 2.2 根据字典生成哈夫曼树
* 2.3 接着读,一个个字节读 然后一个个二进制位分析,如果是叶子节点,就把对应字符写入解压文件中
* 2.4 关闭
*/

#define HFM_CODE_LEN	20	//哈夫曼编码长度

#define TESTHUFFMANCODE 0

#define MAX 1000

struct ZiFu {
	unsigned char zf;				//字符
	unsigned int count;				//字符出现次数
	char code[HFM_CODE_LEN];		//哈夫曼编码
	int idx;						//在字典中的下标
};

struct allZiFu {
	int zf_count;		//出现过字符的个数
	ZiFu zf_arr[256];	//字符数组
};

//哈夫曼树节点类型
struct treeNode {
	ZiFu data;
	treeNode* pLeft;
	treeNode* pRight;
	treeNode* pParent;
};

//分析文件,获得字典
void analysisFile(char* fileName, allZiFu* pZd);
//创建节点的函数
treeNode* createNode(ZiFu* pZf);
//根据字典创建哈夫曼树
treeNode* createHuffmanTree(allZiFu* pZd);
//根据哈夫曼树创建哈夫曼编码并写入字典中
void createHuffmanCode(treeNode* root, allZiFu* pZd);
//写压缩后文件(字典、哈夫曼编码)
void writeCompressionFile(char* srcfileName, char* dstFilename, allZiFu* pZd);

//用来测试 analysisFile 函数
void _testZd(allZiFu* pZd);

//判断某个节点是否叶子节点
bool isLeaf(treeNode* root);
//在字典中设置哈夫曼编码
void setHuffmanCode(treeNode* root, char* pCodeStr);
//从字典中获取某个字符的哈夫曼编码
void getHuffmanCode(unsigned char zf, allZiFu* pZd, char* pCodeStr);
//解压文件
void readCompressionFile(char* dstFileName, char* srcFilename);

int main()
{
	//带压缩文件名 路径
	char srcFile[256] = "1.txt";
	//压缩后文件名 路径
	char dstFile[256] = "2.txt";
	//解压文件名
	char nsrcFile[256] = "3.txt";
	treeNode* root = NULL;
	//字典
	allZiFu zd;
	memset(&zd, 0, sizeof(struct allZiFu));
	analysisFile(srcFile, &zd);

	root = createHuffmanTree(&zd);
	createHuffmanCode(root, &zd);

	writeCompressionFile(srcFile, dstFile, &zd);
	//_testZd(&zd);

	readCompressionFile(dstFile, nsrcFile);

	system("pause");
	return 0;
}

//分析文件,获得字典
void analysisFile(char* fileName, allZiFu* pZd)
{
	//1. 打开文件
	FILE* fp = fopen(fileName, "rb");
	if (NULL == fp)
	{
		printf("analysisFile open srcfile %s error!\n", fileName);
		return;
	}
	//2. 循环读取一字节的内容,读取文件末尾停止
	char c;				//存储文件中读取出来的字节的内存段
	int r;				//记录fread的返回值
	bool is;			//用来标记字符有没有出现过
	int i;				//循环变量
	//2.1 循环控制
	while (1)
	{
		r = fread(&c, 1, 1, fp);
		if (1 != r) break;
		//2.2 统计
		is = false;	//字符没有出现过
		for (i = 0; i < pZd->zf_count; i++)
		{
			if (c == pZd->zf_arr[i].zf)
			{
				is = true;
				break;
			}
		}// end of for
		if (!is)
		{
			pZd->zf_count++;		//字典中字符数量增加
			pZd->zf_arr[i].zf = c;	//某个字符
		}

		pZd->zf_arr[i].count++;

	}// end of while
	

	//3. 关闭文件
	fclose(fp);
}
//创建节点的函数
treeNode* createNode(ZiFu* pZf)
{
	treeNode* pNew = new treeNode;
	if (pNew == NULL) return NULL;
	memset(pNew, 0, sizeof(treeNode));

	//pNew->data = *pZf;
	memcpy(pNew->data.code, pZf->code, 20);
	pNew->data.count = pZf->count;
	pNew->data.zf = pZf->zf;
	pNew->data.idx = pZf->idx;
	
	return pNew;
}
//根据字典创建哈夫曼树
treeNode* createHuffmanTree(allZiFu* pZd)
{
	//1.2.0 准备个数组,存储所有字符节点
	treeNode** pArray = new treeNode * [pZd->zf_count];
	treeNode* pTemp = NULL;

	for (int i = 0; i < pZd->zf_count; i++)
	{
		pArray[i] = createNode(&(pZd->zf_arr[i]));
		pArray[i]->data.idx = i;	//设置下标
	}

	int minIdx, secMinIdx;
	int j;
	//1.2.1 循环找最小的和第二小的 循环n-1次
	for (int i = 0; i < pZd->zf_count - 1; i++)
	{
		//找出最小的和第二小的
		minIdx = 0;
		//找出最小的
		j = 0;
		while (pArray[j] == NULL) j++;
		minIdx = j;
		for (j = 0; j < pZd->zf_count; j++)
		{
			if (pArray[j] && pArray[j]->data.count < pArray[minIdx]->data.count)
				minIdx = j;
		}
		//找出第二小的
		j = 0;
		while (pArray[j] == NULL || j == minIdx) j++;
		secMinIdx = j;
		for (j = 0; j < pZd->zf_count; j++)
		{
			if (pArray[j] && j != minIdx && pArray[j]->data.count < pArray[secMinIdx]->data.count)
				secMinIdx = j;
		}


		//1.2.1.2 创建树
		//创建新节点
		ZiFu tempZf = { 0, pArray[minIdx]->data.count + pArray[secMinIdx]->data.count };
		tempZf.idx = -1;			//做个保险
		pTemp = createNode(&tempZf);
		//连接新节点和最小节点第二小节点
		pArray[minIdx]->pParent = pTemp;
		pArray[secMinIdx]->pParent = pTemp;

		pTemp->pLeft = pArray[minIdx];
		pTemp->pRight = pArray[secMinIdx];

		//pTemp覆盖数组中最小的位置,数组中第二小的位置置空
		pArray[minIdx] = pTemp;
		pArray[secMinIdx] = NULL;

	}

	return pTemp;
}
//根据哈夫曼树创建哈夫曼编码并写入字典中
void createHuffmanCode(treeNode* root, allZiFu* pZd)
{
	int idx;
	if (root)
	{
		if (isLeaf(root))
		{
			idx = root->data.idx;
			//在字典中设置它的哈夫曼编码
			setHuffmanCode(root, pZd->zf_arr[idx].code);
		}
		else
		{
			//遍历左子树
			createHuffmanCode(root->pLeft, pZd);
			//遍历右子树
			createHuffmanCode(root->pRight, pZd);
		}
	}
}
//判断某个节点是否叶子节点
bool isLeaf(treeNode* root)
{
	if (root && root->pLeft == NULL && root->pRight == NULL)
	{
		return true;
	}
	return false;
}
//在字典中设置哈夫曼编码
void setHuffmanCode(treeNode* root, char* pCodeStr)
{
	treeNode* pTemp = root;
	//一路往上追溯,判断是其父节点的左孩子还是右孩子,记录下来
	//临时数组
	char buff[20] = { 0 };
	//buff数组下标
	int buffIdx = 0;
	while (pTemp->pParent)
	{
		if (pTemp == pTemp->pParent->pLeft)
		{
			//左孩子
			buff[buffIdx] = 1;
		}
		else
		{
			//右孩子
			buff[buffIdx] = 2;
		}
		buffIdx++;
		pTemp = pTemp->pParent;
	}
	//逆序存储到pCodeStr指向的内存段中
	//实现反转
	char tmp;
	for (int i = 0; i < buffIdx / 2; i++)
	{
		tmp = buff[i];
		buff[i] = buff[buffIdx - i - 1];
		buff[buffIdx - i - 1] = tmp;
	}
	//赋值
	strcpy(pCodeStr, buff);

	//printf("%c:%s\n", root->data.zf, pCodeStr);
}
//从字典中获取某个字符的哈夫曼编码
void getHuffmanCode(unsigned char zf, allZiFu* pZd, char* pCodeStr)
{
	for (int i = 0; i < pZd->zf_count; i++)
	{
		if (zf == pZd->zf_arr[i].zf)
			strcpy(pCodeStr, pZd->zf_arr[i].code);
	}
}
//写压缩后文件(字典、哈夫曼编码)
void writeCompressionFile(char* srcfileName, char* dstFilename, allZiFu* pZd)
{
	//1. 打开两个文件
	FILE* fpSrc = fopen(srcfileName, "rb");
	FILE* fpDst = fopen(dstFilename, "wb");
	if (fpSrc == NULL || fpDst == NULL)
	{
		printf("writeCompressionFile open file error !\n");
		return;
	}
	
	//2. 往dstFileName文件中写入字典
	fwrite(pZd, 1, sizeof(struct allZiFu), fpDst);

	//3. 循环读取scrFileName文件中的字节 找到其哈夫曼编码 凑齐八个bit写入dstFileName
	int r;		//fread函数返回值
	char c;		//从srcFileName 中读取字节存放于此

	char charForWrite;	//用来存储 写入 dstFileName 中的字节数据
	int idxForWrite;	//写入字节的索引

	int huffmanCodeIdx = 0;	//哈弗曼编码下标 code数组下标
	char huffmanCode[20] = {0};	//用来临时存储哈弗吗编码

	bool isEnd = false;
	while (1)
	{
		if (huffmanCode[huffmanCodeIdx] == 0)
		{

			r = fread(&c, 1, 1, fpSrc);
			if (r != 1) break;

			getHuffmanCode(c, pZd, huffmanCode);
			huffmanCodeIdx = 0;
		}
#if TESTHUFFMANCODE
		//测试从字典中获得的哈夫曼编码
		printf("%c--------------", c);
		for (int i = 0; i < 20; i++)
		{
			printf("%d", huffmanCode[i]);
		}
		printf("\n");
#endif
		idxForWrite = 0;
		charForWrite = 0;

		while (idxForWrite < 8)
		{
			if (huffmanCode[huffmanCodeIdx] == 2)
			{
				//设置二进制位为0
				charForWrite &= ~(1 << (7 - idxForWrite));

				idxForWrite++;
				huffmanCodeIdx++;
			}
			else if (huffmanCode[huffmanCodeIdx] == 1)
			{
				//设置二进制位为1
				charForWrite |= (1 << (7 - idxForWrite));

				idxForWrite++;
				huffmanCodeIdx++;
			}
			else
			{
				r = fread(&c, 1, 1, fpSrc);
				if (1 != r)
				{
					isEnd = true;
					break;
				}

				getHuffmanCode(c, pZd, huffmanCode);
#if TESTHUFFMANCODE
				//测试从字典中获得的哈夫曼编码
				printf("%c--------------", c);
				for (int i = 0; i < 20; i++)
				{
					printf("%d", huffmanCode[i]);
				}
				printf("\n");
#endif

				huffmanCodeIdx = 0;
			}
		}// end of while (idxForwrite < 8)

		fwrite(&charForWrite, 1, 1, fpDst);		//往文件中写入一个字节数据

#if 0
		//测试写入到目的文件中的字节是否正确
		for (int i = 0; i < 8; i++)
		{
			if ((charForWrite & 0x80) == 0x80)
				printf("%d", 1);
			else
				printf("%d", 0);
			charForWrite <<= 1;
		}
		printf("\n");
#endif
		if (isEnd)
			break;


	}//end of while(1)
	printf("压缩成功!\n");
	//关闭两个文件
	fclose(fpDst);
	fclose(fpSrc);
}
//解压文件
void readCompressionFile(char* dstFileName, char* srcFilename)
{
	//打开两个文件
	FILE* fpDst = fopen(dstFileName, "rb");
	FILE* fpSrc = fopen(srcFilename, "wb");
	if (fpSrc == NULL || fpDst == NULL)
	{
		printf("readCompressionFile open file error !\n");
		return;
	}
	//读字典
	allZiFu Zd;
	fread(&Zd, sizeof(struct allZiFu), 1, fpDst);
#if 0
	for (int i = 0; i < Zd.zf_count + 1; i++)
	{
		for(int j = 0; j < Zd.zf_arr[i].idx; j++)
			printf("%d", Zd.zf_arr[i].code[j]);
		printf("\n");
	}
#endif
	//根据字典生成哈夫曼树
	treeNode* root = createHuffmanTree(&Zd);

	//读取字节
	int r;
	char charForRead = 0;
	int k = 0;
	int code[MAX] = { 0 };
	while (1)
	{
		r = fread(&charForRead, 1, 1, fpDst);
		if (r != 1)
			break;
		for (int i = 0; i < 8; i++)
		{
			if ((charForRead & 0x80) == 0x80)
			{
				code[k++] = 1;
				//printf("%d", 1);
			}
			else
			{
				code[k++] = 2;
				//printf("%d", 0);
			}
			charForRead <<= 1;
		}
	}
	//for (int k = 0; code[k] != 0; k++)
	//{
	//	printf("%d", code[k]);
	//	if ((k+1) % 8 == 0)
	//		printf("\n");
	//}
	treeNode* temp = root;
	int num = 0;
	for (int i = 0; i < Zd.zf_count; i++)
	{
		num += Zd.zf_arr[i].count;
	}
	for (int i = 0; code[i] != 0 && num > 0; i++)
	{
		if (temp->pLeft == NULL && temp->pRight == NULL)
		{
			fwrite(&temp->data.zf, sizeof(temp->data.zf), 1, fpSrc);
			temp->data.count--;
			num--;
			temp = root;
		}
		if (code[i] == 2)
		{
			temp = temp->pRight;
		}
		else
		{
			temp = temp->pLeft;
		}
	}
	printf("解压成功!\n");
	fclose(fpDst);
	fclose(fpSrc);
}
//用来测试 analysisFile 函数
void _testZd(allZiFu* pZd)
{
	printf("带压缩文件中共有%d个字符\n", pZd->zf_count);
	int j;
	for (int i = 0; i < pZd->zf_count; i++)
	{
		printf("%c:%d", pZd->zf_arr[i].zf, pZd->zf_arr[i].count);
		j = 0;
		printf(":code:");
		while (pZd->zf_arr[i].code[j])
		{
			printf("%d", pZd->zf_arr[i].code[j]);
			j++;
		}
		printf("\n");
	}

/*
* abcdefgaaaaabbdess 共8个字符
* a:6
* b:3
* c:1
* d:2
* e:2
* f:1
* g:1
* s:2
*/
}
  • 应用:最佳判定树
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值