Hash学习整理篇

      hash表的出现主要是为了对内存中数据的快速、随机的访问。它主要有三个关键点:Hash表的大小、Hash函数、冲突的解决

(1)Hash表的大小

            Hash表的大小一般是定长的,如果太大,则浪费空间,如果太小,冲突发生的概率变大,体现不出效率。所以,选择合适的Hash表的大小是Hash表性能的关键。

           对于Hash表大小的选择通常会考虑两点:

            第一,确保Hash表的大小是一个素数。常识告诉我们,当除以一个素数时,会产生最分散的余数,可能最糟糕的除法是除以2的倍数,因为这只会屏蔽被除数中的位。由于我们通常使用表的大小对hash函数的结果进行模运算,如果表的大小是一个素数,就可以获得最佳的结果。

           第二,创建大小合理的hash表。这就涉及到hash表的一个概念:装填因子。设装填因子为a,则:

a=表中记录数/hash表表长

通常,我们关注的是使hash表的平均查找长度最小,而平均查找长度是装填因子的函数,而不是表长n的函数。a的取值越小,产生冲突的机会就越小,但如果a取值过小,则会造成较大的空间浪费,通常,只要a的取值合适,hash表的平均查找长度就是一个常数,即hash表的平均查找长度为O(1)。

           当然,根据不同的数据量,会有不同的哈希表的大小。对于数据量时多时少的应用,最好的设计是使用动态可变尺寸的哈希表,那么如果你发现哈希表尺寸太小了,比如其中的元素是哈希表尺寸的2倍时,我们就需要扩大哈希表尺寸,一般是扩大一倍。
         下面是哈希表尺寸大小的可能取值:
     17,            37,          79,        163,          331,  
    673,           1361,        2729,       5471,         10949,        
   21911,          43853,      87719,      175447,      350899,
  701819,         1403641,    2807303,     5614657,     11229331,   
 22458671,       44917381,    89834777,    179669557,   359339171,  
718678369,      1437356741,  2147483647

 

(2)Hash函数

一个好的hash函数一般具有以下两个特点:第一,速度快,第二,能够将散列键均匀的分布在整个表中,保证不会产生聚集。通常,hash函数具有如下形式:

hash-key = calculated-key % tablesize

上一节主要讨论了一下tablesize,为了提高散列键的离散程度,tablesize通常取素数。一般而言,没有绝对好的hash函数,hash函数的好坏很大程度上依赖于输入键的结构,人们讨论的最多的一般都是输入键为普通字符串的情况。这里也以字符串为例讨论如何一步步优化hash函数。

         当键是字符串时,一种选择策略是简单的将字符串中每个字符的ASCII码加起来,代码如下:   

  1. unsigned int hash(const char *key, unsigned int tableSize)  
  2. {  
  3.     unsigned int hashVal;  
  4.   
  5.     while(*key != '\0')  
  6.         hashVal += *key++;  
  7.   
  8.     return (hashVal % tableSize);  
  9. }  
unsigned int hash(const char *key, unsigned int tableSize)
{
	unsigned int hashVal;

	while(*key != '\0')
		hashVal += *key++;

	return (hashVal % tableSize);
}

         上面的hash函数实现简单而且能够很快的算出答案,不过,如果表很大,则函数就不能很好的分配键,例如,设tableSize=10949,并设所有的键至多8个字符长,由于ASCII字符的值最多是127,因此hash函数只能在0~1016之间取值,其中1016为127*8,显然这不是一种均匀的分配。

         下面的hash函数对针对上面的缺点进行了改进。

  1. unsigned int hash(const char *key, unsigned int tableSize)  
  2. {  
  3.     return (key[0] + 27*key[1] + 729*key[2]) % tableSize;  
  4. }  
unsigned int hash(const char *key, unsigned int tableSize)
{
	return (key[0] + 27*key[1] + 729*key[2]) % tableSize;
}

         这个hash函数假设key至少有3个字符。值27表示英文字母表的字母个数外加一个空格,而729是27的平方。虽然该函数只考察了前三个字符,但是,假如字符出现的概率是随机的,而表的大小还是10949,那么我们就会得到一个合理的均衡分布。可是,英文不是随机的。虽然3个字符有26*26*26=17567种可能的组合,但查验词汇量足够大的联机词典却揭示出:3个字母的不同组合数实际上只有2851种。即使这些组合没有冲突,也不过只有表的28%被真正散列到。因此,虽然容易计算,但是当hash表足够大的时候,这个函数还是不合适。

         针对以上缺点,进一步改进:

  1. unsigned int hash(const char *key,unsigned int tableSize)  
  2. {  
  3.     unsigned int hashVal;  
  4.   
  5.     while(*key != '\0')  
  6.         hashVal = (hashVal << 5) + *key++;  
  7.   
  8.     return (hashVal % tableSize);  
  9. }  
unsigned int hash(const char *key,unsigned int tableSize)
{
	unsigned int hashVal;

	while(*key != '\0')
		hashVal = (hashVal << 5) + *key++;

	return (hashVal % tableSize);
}

         这个hash函数涉及键中的所有字符,并且一般可以分布的很好,它计算了字符串的如下值:


在计算该值时,利用了Horner法则,例如计算 hash=a+32b+32*32c 的另一种方式是借助公式:hash=((c)*32+b)*32+a。Horner法则将其扩展到用于n次多项式。该算法通过将乘法运算转换为位运算保证了hash函数快速的特点。

       下面给出实际字符串hash应用中使用的很广的一个hash函数:ELFhash

  1. unsigned long ELfHash(const unsigned char * key)  
  2. {  
  3.     unsigned long h = 0, g;  
  4.   
  5.     while(*key)  
  6.     {  
  7.         h = (h << 4) + *key++;//把h左移4位加上该字符赋给h   
  8.         if(g = h & 0xF0000000)//取h的高四位赋给g   
  9.             h ^= g >> 24;//如果g不为0,让h和g的高八位异或再赋给h   
  10.   
  11.         h &= ~g;//对g取反并与h相与赋给h   
  12.     }  
  13.     return h;  
  14. }  
unsigned long ELfHash(const unsigned char * key)
{
	unsigned long h = 0, g;

	while(*key)
	{
		h = (h << 4) + *key++;//把h左移4位加上该字符赋给h
		if(g = h & 0xF0000000)//取h的高四位赋给g
			h ^= g >> 24;//如果g不为0,让h和g的高八位异或再赋给h

		h &= ~g;//对g取反并与h相与赋给h
	}
	return h;
}

 

(3)冲突的解决

 

为提高hash表查找性能,除了考虑选择合适的hash表表长和完美的hash函数外,还必须考虑hash表处理冲突的能力。当hash函数对两个不同的数据项产生了相同的hash值时,冲突就产生了。对于冲突的处理,通常采用的方法可以分为三类:

(1)线性再散列法,简单的按顺序遍历hash表,寻找下一个可用的槽;

(2)非线性再散列法,计算一个新的hash值;

(3)外部拉链法,将hash表中的每个槽当作具有相同hash值的数据项所组成链表的头部,hash表将发生冲突的项添加到同一个链表中。

下面对这三种方法分别介绍。

1.线性再散列法

       线性再散列法是形式最简单的处理冲突的方法。插入元素时,如果发生冲突,算法会简单的遍历hash表,直到找到表中的下一个空槽,并将该元素放入该槽中。查找元素时,首先散列值所指向的槽,如果没有找到匹配,则继续遍历hash表,直到:(1)找到相应的元素;(2)找到一个空槽(指示查找的元素不存在);(3)整个hash表遍历完毕(指示该元素不存在并且hash表是满的)。下表显示了以线性再散列法将{89,18,49,58,69}5个元素插入hash表的过程。(hash函数为:hash(X)=X mod 10;hash表长一般用素数,这里为了说明方便取表长为10)


         第一次冲突发生在插入关键字49时,它被放在下一个空闲地址,即地址0。关键字58依次和18,89,49发生冲突,试选三次之后才找到一个空单元。对69的冲突用类似的方法处理。从以上过程可以看出,只要表中有空闲单元,总可以找到,但这里选择步长为1,将会在hash表中产生聚集,即:即使hash表相对较空,还是会在某些区域形成一些区块,这些区块中的任何活动都将设计更大的步长。但如果以5或更大的值作为步长,可以迅速地从拥挤区域移开,从而减少聚集现象的发生。事实上,只要hash表长和检查槽的步长是互质的,那么表中的每个槽都会被检查到。

       线性再散列法有两个缺点:第一,不能从表中删除元素,因为相应的单元可能已经引起过冲突,元素绕过它存到了别处,例如,如果我们删除了18,那么其他的元素都会找不到。如果确实需要删除,可以采用懒惰删除的方法。第二,当表被填满时性能下降明显。

2.非线性再散列法

        线性再散列法是从冲突位置开始,采用一个步长以顺序方式遍历hash表,来查找一个可用的槽,从上面的讨论可以看出,它容易产生聚集现象。非线性再散列法可以避免遍历散列表,它会计算一个新的hash值,并通过它跳转到表中一个完全不同的部分。它的思想就是:通过跳转到表中不同的部分,从而避免相似值的聚集,如果再散列函数跳转到的槽已经被占用了,则继续执行新一轮的再散列和跳转。

    例如,还是上面的例子,如果再散列函数是hash(X)=R-(X mod R),其中R为小于hash表长的素数,如果我们选择R=7,则下表显示了插入与前面相同的关键字的结果。


        第一个冲突发生在49被插入的时候, hash(49)=7-0=7,故49被插入到位置6。Hash(58)=7-2=5,于是58被插入到位置3。最后69产生冲突,从而被插入到距离为hash(69)=7-6=1的地方。

        非线性再散列法也有不能从表中删除元素的缺点。

        无论是使用线性再散列法还是非线性再散列法,只有在散列表不会接近填满的情况下,才能使用再散列。当散列表的负载因子增大时,再散列所花费的时间也会显著增加。通过以上讨论可以看出,再散列方法适用于表负载较低并且不太可能执行删除操作的情况。

3.外部拉链法

        外部拉链法是将hash表看作是一个链表数组,表中的每个槽要不为空,要不指向hash到该槽的表项的链表。可以通过把元素添加到链表中来解决冲突。同样,可以通过从链表中删除元素来执行删除操作。因此,解决冲突的代价不会超过向链表中添加一个节点,不需要执行再散列。在再散列中,表项的最大数量是由表中槽的原始数量确定的,与之不同的是,外部拉链法可以容纳的元素于将在内存中存放的元素一样多。

        外部拉链法的原则是:hash表的大小一般与预料的元素个数差不多。

        假设有一个表长为10的hash表,给出10个关键字为前10个自然数的平方,hash函数为hash(X)=X mod 10,下图就是对应的外部拉链法的hash表。


          外部拉链法的平均查找时间是对链表的查找时间加上1,这个1是最初的定位hash表槽。外部拉链法的缺点是:它需要稍微多一些的空间来实现,因为添加任何元素都需要添加指向节点的指针,并且每次探查也要花费稍微多一点的时间,因为它需要间接引用指针,而不是直接访问元素。由于今天的内存成本很低并且可以使用非常快的CPU,所以这些缺点都是微不足道的。因此,实际使用hash表时,一般都是使用拉链法来解决hash冲突。


 

**********************************************************************************************************************************************************

Hash表的实现

由于实际应用中,拉链法实现hash表用的比较多,这里也以拉链法来实现hash表。

  1. /* 
  2.  * hashTable.h 
  3.  * 
  4.  *  Created on: Nov 30, 2011 
  5.  *      Author: Liam Q 
  6.  */  
  7. #ifndef HASHTABLE_H_   
  8. #define HASHTABLE_H_   
  9.   
  10. //定义DEBUG_MSG用于调试输出   
  11. #define DEBUG_MSG   
  12. //#define DEBUG_MSG(args)   printf args;    printf("\n");   
  13.   
  14. typedef char * ElemType;//对字符串进行hash   
  15.   
  16. typedef struct ListNode//链表中节点   
  17. {  
  18.     ElemType elem;  
  19.     struct ListNode * next;  
  20. }ListNode, *Position;  
  21.   
  22. typedef Position List;//链表   
  23.   
  24. typedef struct HashTbl//hash表   
  25. {  
  26.     int tableSize;  
  27.     List * theLists;  
  28. }HashTbl, *HashTable;  
  29.   
  30. HashTable initTable(int tableSize);  
  31. void destroyTable(HashTable hashTable);  
  32. Position find(HashTable hashtable, ElemType elem);  
  33. void insert(HashTable hashTable, ElemType elem);  
  34.   
  35. #endif /* HASHTABLE_H_ */   
  36.   
  37.   
  38. /* 
  39.  * hashTable.c 
  40.  * 
  41.  *  Created on: Nov 30, 2011 
  42.  *      Author: Liam Q 
  43.  */  
  44. #include <stdio.h>   
  45. #include <stdlib.h>   
  46. #include <math.h>   
  47. #include <string.h>   
  48. #include "hashTable.h"   
  49.   
  50. #define MIN_TABLE_SIZE 10//hash表最小大小   
  51.   
  52. static int isPrime(int a)  
  53. {  
  54.     int i;  
  55.     int b = (int)sqrt((double)a);  
  56.     for(i = 2; i <= b; i++)  
  57.         if(a % i == 0)  
  58.             return 0;  
  59.     return 1;  
  60. }  
  61.   
  62. static int nextPrime(int a)  
  63. {  
  64.     while(!isPrime(a++));  
  65.     return --a;  
  66. }  
  67.   
  68. HashTable initTable(int tableSize)  
  69. {  
  70.     HashTable hashTable;  
  71.     int i;  
  72.   
  73.     if(tableSize < MIN_TABLE_SIZE)  
  74.     {  
  75.         printf("Table size is too small\n");  
  76.         return NULL;  
  77.     }  
  78.   
  79.     hashTable = (HashTable)malloc(sizeof(HashTbl));  
  80.     if(hashTable == NULL)  
  81.         exit(-1);  
  82.     hashTable->tableSize = nextPrime(tableSize);  
  83.   
  84.     hashTable->theLists = (List *)malloc(hashTable->tableSize * sizeof(List));  
  85.     if(!hashTable->theLists)  
  86.         exit(-1);  
  87.   
  88.     for(i = 0; i < hashTable->tableSize; i++)  
  89.     {  
  90.         hashTable->theLists[i] = (List)malloc(sizeof(ListNode));  
  91.         if(!hashTable->theLists[i])  
  92.             exit(-1);  
  93.         else  
  94.             hashTable->theLists[i]->next = NULL;  
  95.     }  
  96.   
  97.     return hashTable;  
  98. }  
  99.   
  100. void destroyTable(HashTable hashTable)  
  101. {  
  102.     int i;  
  103.     List l, tmp;  
  104.   
  105.     if(hashTable == NULL)  
  106.         return;  
  107.     if(hashTable->theLists)  
  108.     {  
  109.         for(i = 0; i < hashTable->tableSize; i++)  
  110.         {  
  111.             l = hashTable->theLists[i];  
  112.             while(l != NULL)  
  113.             {  
  114.                 tmp = l;  
  115.                 l = l->next;  
  116.                 free(tmp);  
  117.             }  
  118.             hashTable->theLists[i] = NULL;  
  119.         }  
  120.         free(hashTable->theLists);  
  121.     }  
  122.   
  123.     free(hashTable);  
  124. }  
  125.   
  126. static int hash(ElemType elem, int tableSize)  
  127. {  
  128.     unsigned int hashVal = 0;  
  129.   
  130.     while(*elem != '\0')  
  131.         hashVal = (hashVal << 5) + *elem++;  
  132.   
  133.     return hashVal % tableSize;  
  134. }  
  135.   
  136. Position find(HashTable hashTable, ElemType elem)  
  137. {  
  138.     Position p;  
  139.     List l;  
  140.   
  141.     l = hashTable->theLists[hash(elem, hashTable->tableSize)];  
  142.     p = l->next;  
  143.     while(p != NULL && strcmp(p->elem, elem) != 0)  
  144.         p = p->next;  
  145.   
  146.     return p;  
  147. }  
  148.   
  149. void insert(HashTable hashTable, ElemType elem)  
  150. {  
  151.     Position p, pNewCell;  
  152.     List l;  
  153.   
  154.     p = find(hashTable, elem);  
  155.     if(p == NULL)  
  156.     {  
  157.         pNewCell = (List)malloc(sizeof(ListNode));  
  158.         if(pNewCell == NULL)  
  159.             exit(-1);  
  160.         else  
  161.         {  
  162.             l = hashTable->theLists[hash(elem, hashTable->tableSize)];  
  163.             pNewCell->elem = (char *)malloc(strlen(elem) * sizeof(char));  
  164.             strcpy(pNewCell->elem, elem);  
  165.             pNewCell->next = l->next;  
  166.             l->next = pNewCell;  
  167.             DEBUG_MSG(("%d-----%s\n",hash(elem, hashTable->tableSize),pNewCell->elem));  
  168.         }  
  169.     }  
  170. }  
  171.   
  172.   
  173. /* 
  174.  * demo.c .利用上面实现的hash表读取文本文件中单词,并将其放入hash表 
  175.  *  
  176.  *  Created on: Nov 30, 2011 
  177.  *      Author: Liam Q 
  178.  */  
  179. #include <stdio.h>   
  180. #include <stdlib.h>   
  181. #include <string.h>   
  182. #include <ctype.h>   
  183.   
  184. #include "hashTable.h"   
  185.   
  186. #define HASH_SIZE 500   
  187. #define MAXWORD 64   
  188.   
  189. void createHashTable(HashTable hashTable)  
  190. {  
  191.     char *fileName="Makefile.common";  
  192.     FILE * fp;  
  193.     char word[MAXWORD];  
  194.     char c;  
  195.     int i;  
  196.   
  197.     if((fp = fopen(fileName, "r")) == NULL)  
  198.     {  
  199.         fprintf(stderr,"can't open %s\n",fileName);  
  200.         exit(-1);  
  201.     }  
  202.   
  203.     c = ' ';  
  204.     while(!feof(fp))  
  205.     {  
  206.         while(c != EOF && isspace(c))  
  207.             c = fgetc(fp);  
  208.         i = 0;  
  209.         while(c != EOF && !isspace(c))  
  210.         {  
  211.             word[i++] = c;  
  212.             c = fgetc(fp);  
  213.         }  
  214.         if(c == EOF)  
  215.             break;  
  216.         word[i] = '\0';  
  217.         while(i >= 0 && ispunct(word[--i]))  
  218.             word[i] = '\0';  
  219.         insert(hashTable,word);  
  220.     }  
  221. }  
  222.   
  223. int main()  
  224. {  
  225.     HashTable hashTable;  
  226.     char word[MAXWORD];  
  227.   
  228.     hashTable = initTable(HASH_SIZE);  
  229.     createHashTable(hashTable);  
  230.     while(1)  
  231.     {  
  232.         printf("Search string:");  
  233.         fgets(word, sizeof(word), stdin);  
  234.         word[strlen(word) - 1] = '\0';  
  235.         DEBUG_MSG(("%s",word));  
  236.         printf("%s\n",find(hashTable,word)?"find":"not find");  
  237.     }  
  238.     destroyTable(hashTable);  
  239.     hashTable = NULL;  
  240. }  
/*
 * hashTable.h
 *
 *  Created on: Nov 30, 2011
 *      Author: Liam Q
 */
#ifndef HASHTABLE_H_
#define HASHTABLE_H_

//定义DEBUG_MSG用于调试输出
#define DEBUG_MSG
//#define DEBUG_MSG(args)	printf args;	printf("\n");

typedef char * ElemType;//对字符串进行hash

typedef struct ListNode//链表中节点
{
	ElemType elem;
	struct ListNode * next;
}ListNode, *Position;

typedef Position List;//链表

typedef struct HashTbl//hash表
{
	int tableSize;
	List * theLists;
}HashTbl, *HashTable;

HashTable initTable(int tableSize);
void destroyTable(HashTable hashTable);
Position find(HashTable hashtable, ElemType elem);
void insert(HashTable hashTable, ElemType elem);

#endif /* HASHTABLE_H_ */


/*
 * hashTable.c
 *
 *  Created on: Nov 30, 2011
 *      Author: Liam Q
 */
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>
#include "hashTable.h"

#define MIN_TABLE_SIZE 10//hash表最小大小

static int isPrime(int a)
{
	int i;
	int b = (int)sqrt((double)a);
	for(i = 2; i <= b; i++)
		if(a % i == 0)
			return 0;
	return 1;
}

static int nextPrime(int a)
{
	while(!isPrime(a++));
	return --a;
}

HashTable initTable(int tableSize)
{
	HashTable hashTable;
	int i;

	if(tableSize < MIN_TABLE_SIZE)
	{
		printf("Table size is too small\n");
		return NULL;
	}

	hashTable = (HashTable)malloc(sizeof(HashTbl));
	if(hashTable == NULL)
		exit(-1);
	hashTable->tableSize = nextPrime(tableSize);

	hashTable->theLists = (List *)malloc(hashTable->tableSize * sizeof(List));
	if(!hashTable->theLists)
		exit(-1);

	for(i = 0; i < hashTable->tableSize; i++)
	{
		hashTable->theLists[i] = (List)malloc(sizeof(ListNode));
		if(!hashTable->theLists[i])
			exit(-1);
		else
			hashTable->theLists[i]->next = NULL;
	}

	return hashTable;
}

void destroyTable(HashTable hashTable)
{
	int i;
	List l, tmp;

	if(hashTable == NULL)
		return;
	if(hashTable->theLists)
	{
		for(i = 0; i < hashTable->tableSize; i++)
		{
			l = hashTable->theLists[i];
			while(l != NULL)
			{
				tmp = l;
				l = l->next;
				free(tmp);
			}
			hashTable->theLists[i] = NULL;
		}
		free(hashTable->theLists);
	}

	free(hashTable);
}

static int hash(ElemType elem, int tableSize)
{
	unsigned int hashVal = 0;

	while(*elem != '\0')
		hashVal = (hashVal << 5) + *elem++;

	return hashVal % tableSize;
}

Position find(HashTable hashTable, ElemType elem)
{
	Position p;
	List l;

	l = hashTable->theLists[hash(elem, hashTable->tableSize)];
	p = l->next;
	while(p != NULL && strcmp(p->elem, elem) != 0)
		p = p->next;

	return p;
}

void insert(HashTable hashTable, ElemType elem)
{
	Position p, pNewCell;
	List l;

	p = find(hashTable, elem);
	if(p == NULL)
	{
		pNewCell = (List)malloc(sizeof(ListNode));
		if(pNewCell == NULL)
			exit(-1);
		else
		{
			l = hashTable->theLists[hash(elem, hashTable->tableSize)];
			pNewCell->elem = (char *)malloc(strlen(elem) * sizeof(char));
			strcpy(pNewCell->elem, elem);
			pNewCell->next = l->next;
			l->next = pNewCell;
			DEBUG_MSG(("%d-----%s\n",hash(elem, hashTable->tableSize),pNewCell->elem));
		}
	}
}


/*
 * demo.c .利用上面实现的hash表读取文本文件中单词,并将其放入hash表
 * 
 *  Created on: Nov 30, 2011
 *      Author: Liam Q
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#include "hashTable.h"

#define HASH_SIZE 500
#define MAXWORD	64

void createHashTable(HashTable hashTable)
{
	char *fileName="Makefile.common";
	FILE * fp;
	char word[MAXWORD];
	char c;
	int i;

	if((fp = fopen(fileName, "r")) == NULL)
	{
		fprintf(stderr,"can't open %s\n",fileName);
		exit(-1);
	}

	c = ' ';
	while(!feof(fp))
	{
		while(c != EOF && isspace(c))
			c = fgetc(fp);
		i = 0;
		while(c != EOF && !isspace(c))
		{
			word[i++] = c;
			c = fgetc(fp);
		}
		if(c == EOF)
			break;
		word[i] = '\0';
		while(i >= 0 && ispunct(word[--i]))
			word[i] = '\0';
		insert(hashTable,word);
	}
}

int main()
{
	HashTable hashTable;
	char word[MAXWORD];

	hashTable = initTable(HASH_SIZE);
	createHashTable(hashTable);
	while(1)
	{
		printf("Search string:");
		fgets(word, sizeof(word), stdin);
		word[strlen(word) - 1] = '\0';
		DEBUG_MSG(("%s",word));
		printf("%s\n",find(hashTable,word)?"find":"not find");
	}
	destroyTable(hashTable);
	hashTable = NULL;
}





 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值