POCO C++库学习和分析 -- 哈希

POCO C++库学习和分析 -- 哈希


1. Hash概论

        在理解Poco中的Hash代码之前,首先需要了解一下Hash的基本理论。下面的这些内容和教课书上的内容并没有太大的差别。

1.1 定义

        下面这几段来自于百度百科:
         Hash:一般翻译做"散列",也有直接音译为"哈希"的,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
        Hash table:散列表,也叫哈希表,是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
                 若结构中存在关键字和K相等的记录,则必定存储在f(K)的位置上。由此,不需比较便可直接取得所查记录。这个对应关系f称为散列函数(Hash function),按这个思想建立的表为散列表。
                 * 对不同的关键字可能得到同一散列地址,即key1≠key2,而f(key1)=f(key2),这种现象称冲突。具有相同函数值的关键字对该散列函数来说称做同义词。
                 * 综上所述,根据散列函数H(key)和处理冲突的方法将一组关键字映象到一个有限的连续的地址集(区间)上,并以关键字在地址集中的“象”, 作为这条记录在表中的存储位置,这种表便称为散列表,这一映象过程称为散列造表或散列,所得的存储位置称散列地址。这个现象也叫散列桶,在散列桶中,只能通过顺序的方式来查找,一般只需要查找三次就可以找到。科学家计算过,当重载因子不超过75%,查找效率最高。
         * 若对于关键字集合中的任一个关键字,经散列函数映象到地址集合中任何一个地址的概率是相等的,则称此类散列函数为均匀散列函数(Uniform Hash function),这就是使关键字经过散列函数得到一个“随机的地址”,从而减少冲突。

1.2 Hash table查找效率

         对于Hash table来言,理论上查找效率为O(1)。但在现实世界中,查找的过程存在冲突现象。产生的冲突少,查找效率就高,产生的冲突多,查找效率就低。因此,影响产生冲突多少的因素,也就是影响查找效率的因素。影响产生冲突多少有以下三个因素:
         1. 散列函数是否均匀;
         2. 处理冲突的方法;
         3. 散列表的装填因子。
         散列表的装填因子定义为:α= 填入表中的元素个数 / 散列表的长度
         实际上,散列表的平均查找长度是装填因子α的函数,只是不同处理冲突的方法有不同的函数。

1.3 Poco中的Hash内容

         Poco中的hash内容主要关注于Hash表的应用。下面是Poco中相关于Hash的类图:


         我们看到Poco的Hash内容主要被分成3部分:
         1. Hash函数。Poco提供了一组Hash函数用于,生成hash值。同时提供了模板类HashFunction,通过仿函式提供对任意数据结构生成hash值的功能。
         2. Hash table(哈希表)。Poco中实现了3种哈希表,分别是SimpleHashTable, HashTable,LinearHashTable。
         3. 在哈希表上的应用,封装出hash map和hash set。


2. Hash函数

         Hash函数是解决hash冲突的第一个要素。
         Poco中提供了一组Hash函数,用于产生hash值。其定义如下:
  1. inline std::size_t hash(Int8 n)  
  2. {  
  3.     return static_cast<std::size_t>(n)*2654435761U;   
  4. }  
  5.   
  6. inline std::size_t hash(UInt8 n)  
  7. {  
  8.     return static_cast<std::size_t>(n)*2654435761U;   
  9. }  
  10.   
  11. inline std::size_t hash(Int16 n)  
  12. {  
  13.     return static_cast<std::size_t>(n)*2654435761U;   
  14. }  
  15.   
  16.   
  17. inline std::size_t hash(UInt16 n)  
  18. {  
  19.     return static_cast<std::size_t>(n)*2654435761U;   
  20. }  
  21.   
  22. inline std::size_t hash(Int32 n)  
  23. {  
  24.     return static_cast<std::size_t>(n)*2654435761U;   
  25. }  
  26.   
  27. inline std::size_t hash(UInt32 n)  
  28. {  
  29.     return static_cast<std::size_t>(n)*2654435761U;   
  30. }  
  31.   
  32.   
  33. inline std::size_t hash(Int64 n)  
  34. {  
  35.     return static_cast<std::size_t>(n)*2654435761U;   
  36. }  
  37.   
  38. inline std::size_t hash(UInt64 n)  
  39. {  
  40.     return static_cast<std::size_t>(n)*2654435761U;   
  41. }  
  42.   
  43. std::size_t hash(const std::string& str)  
  44. {  
  45.     std::size_t h = 0;  
  46.     std::string::const_iterator it  = str.begin();  
  47.     std::string::const_iterator end = str.end();  
  48.     while (it != end)  
  49.     {  
  50.         h = h * 0xf4243 ^ *it++;  
  51.     }  
  52.     return h;  
  53. }  
         这里就不对hash函数做过多叙述了,下面列出一些其他的常用hash函数。网上有专门的论述,并对不同的hash函数效果做了比较,有兴趣的话可以google一下。
         附:各种哈希函数的C语言程序代码
  1. unsigned int SDBMHash(char *str)  
  2. {  
  3.     unsigned int hash = 0;  
  4.     while (*str)  
  5.     {  
  6.         // equivalent to: hash = 65599*hash + (*str++);  
  7.         hash = (*str++) + (hash << 6) + (hash << 16) - hash;  
  8.     }  
  9.     return (hash & 0x7FFFFFFF);  
  10. }  
  11.   
  12.   
  13. // RS Hash   
  14. unsigned int RSHash(char *str)  
  15. {  
  16.     unsigned int b = 378551;  
  17.     unsigned int a = 63689;  
  18.     unsigned int hash = 0;  
  19.     while (*str)  
  20.     {  
  21.         hash = hash * a + (*str++);  
  22.         a *= b;  
  23.     }  
  24.     return (hash & 0x7FFFFFFF);  
  25. }  
  26.   
  27.   
  28. // JS Hash   
  29. unsigned int JSHash(char *str)  
  30. {  
  31.     unsigned int hash = 1315423911;  
  32.     while (*str)  
  33.     {  
  34.         hash ^= ((hash << 5) + (*str++) + (hash >> 2));  
  35.     }  
  36.     return (hash & 0x7FFFFFFF);  
  37. }  
  38.   
  39.   
  40. // P. J. Weinberger Hash   
  41. unsigned int PJWHash(char *str)  
  42. {  
  43.     unsigned int BitsInUnignedInt = (unsigned int)(sizeof(unsigned int) * 8);  
  44.     unsigned int ThreeQuarters  = (unsigned int)((BitsInUnignedInt  * 3) / 4);  
  45.     unsigned int OneEighth = (unsigned int)(BitsInUnignedInt / 8);  
  46.     unsigned int HighBits = (unsigned int)(0xFFFFFFFF) << (BitsInUnignedInt - OneEighth);  
  47.     unsigned int hash   = 0;  
  48.     unsigned int test   = 0;  
  49.     while (*str)  
  50.     {  
  51.         hash = (hash << OneEighth) + (*str++);  
  52.         if ((test = hash & HighBits) != 0)  
  53.         {  
  54.             hash = ((hash ^ (test >> ThreeQuarters)) & (~HighBits));  
  55.         }  
  56.     }  
  57.     return (hash & 0x7FFFFFFF);  
  58. }  
  59.   
  60.   
  61. // ELF Hash   
  62. unsigned int ELFHash(char *str)  
  63. {  
  64.     unsigned int hash = 0;  
  65.     unsigned int x  = 0;  
  66.     while (*str)  
  67.     {  
  68.         hash = (hash << 4) + (*str++);  
  69.         if ((x = hash & 0xF0000000L) != 0)  
  70.         {  
  71.             hash ^= (x >> 24);  
  72.             hash &= ~x;  
  73.         }  
  74.     }  
  75.     return (hash & 0x7FFFFFFF);  
  76. }  
  77.   
  78.   
  79. // BKDR Hash   
  80. unsigned int BKDRHash(char *str)  
  81. {  
  82.     unsigned int seed = 131; // 31 131 1313 13131 131313 etc..  
  83.     unsigned int hash = 0;  
  84.     while (*str)  
  85.     {  
  86.         hash = hash * seed + (*str++);  
  87.     }  
  88.     return (hash & 0x7FFFFFFF);  
  89. }  
  90.   
  91.   
  92. // DJB Hash   
  93. unsigned int DJBHash(char *str)  
  94. {  
  95.     unsigned int hash = 5381;  
  96.     while (*str)  
  97.     {  
  98.         hash += (hash << 5) + (*str++);  
  99.     }  
  100.     return (hash & 0x7FFFFFFF);  
  101. }  
  102.   
  103.   
  104. // AP Hash   
  105. unsigned int APHash(char *str)  
  106. {  
  107.     unsigned int hash = 0;  
  108.     int i;  
  109.     for (i=0; *str; i++)  
  110.     {  
  111.         if ((i & 1) == 0)  
  112.         {  
  113.             hash ^= ((hash << 7) ^ (*str++) ^ (hash >> 3));  
  114.         }  
  115.         else  
  116.         {  
  117.             hash ^= (~((hash << 11) ^ (*str++) ^ (hash >> 5)));  
  118.         }  
  119.     }  
  120.     return (hash & 0x7FFFFFFF);  
  121. }  
  122.   
  123.   
  124. unsigned int hash(char *str)  
  125. {  
  126.     register unsigned int h;  
  127.     register unsigned char *p;  
  128.     for(h=0, p = (unsigned char *)str; *p ; p++)  
  129.         h = 31 * h + *p;  
  130.     return h;  
  131. }  

  1. // PHP中出现的字符串Hash函数  
  2. static unsigned long hashpjw(char *arKey, unsigned int nKeyLength)  
  3. {  
  4.     unsigned long h = 0, g;  
  5.     char *arEnd=arKey+nKeyLength;  
  6.   
  7.     while (arKey < arEnd) {  
  8.         h = (h << 4) + *arKey++;  
  9.         if ((g = (h & 0xF0000000))) {  
  10.             h = h ^ (g >> 24);  
  11.             h = h ^ g;  
  12.         }  
  13.     }  
  14.     return h;  
  15. }  


  1. // OpenSSL中出现的字符串Hash函数  
  2. unsigned long lh_strhash(char *str)  
  3. {  
  4.     int i,l;  
  5.     unsigned long ret=0;  
  6.     unsigned short *s;  
  7.   
  8.     if (str == NULL) return(0);  
  9.     l=(strlen(str)+1)/2;  
  10.     s=(unsigned short *)str;  
  11.     for (i=0; i  
  12.         ret^=(s[i]<<(i&0x0f));  
  13.         return(ret);  
  14. }   
  15.   
  16. /* The following hash seems to work very well on normal text strings 
  17. * no collisions on /usr/dict/words and it distributes on %2^n quite 
  18. * well, not as good as MD5, but still good. 
  19. */  
  20. unsigned long lh_strhash(const char *c)  
  21. {  
  22.     unsigned long ret=0;  
  23.     long n;  
  24.     unsigned long v;  
  25.     int r;  
  26.   
  27.   
  28.     if ((c == NULL) || (*c == '\0'))  
  29.         return(ret);  
  30.     /* 
  31.     unsigned char b[16]; 
  32.     MD5(c,strlen(c),b); 
  33.     return(b[0]|(b[1]<<8)|(b[2]<<16)|(b[3]<<24)); 
  34.     */  
  35.   
  36.   
  37.     n=0x100;  
  38.     while (*c)  
  39.     {  
  40.         v=n|(*c);  
  41.         n+=0x100;  
  42.         r= (int)((v>>2)^v)&0x0f;  
  43.         ret=(ret(32-r));  
  44.         ret&=0xFFFFFFFFL;  
  45.         ret^=v*v;  
  46.         c++;  
  47.     }  
  48.     return((ret>>16)^ret);  
  49. }  

  1. // MySql中出现的字符串Hash函数  
  2. #ifndef NEW_HASH_FUNCTION  
  3.   
  4. /* Calc hashvalue for a key */  
  5. static uint calc_hashnr(const byte *key,uint length)  
  6. {  
  7.     register uint nr=1, nr2=4;  
  8.     while (length--)  
  9.     {  
  10.         nr^= (((nr & 63)+nr2)*((uint) (uchar) *key++))+ (nr << 8);  
  11.         nr2+=3;  
  12.     }  
  13.     return((uint) nr);  
  14. }  
  15.   
  16.   
  17. /* Calc hashvalue for a key, case indepenently */  
  18. static uint calc_hashnr_caseup(const byte *key,uint length)  
  19. {  
  20.     register uint nr=1, nr2=4;  
  21.     while (length--)  
  22.     {  
  23.         nr^= (((nr & 63)+nr2)*((uint) (uchar) toupper(*key++)))+ (nr << 8);  
  24.         nr2+=3;  
  25.     }  
  26.     return((uint) nr);  
  27. }  
  28.   
  29. #else  
  30.   
  31. /* 
  32. * Fowler/Noll/Vo hash 
  33. * 
  34. * The basis of the hash algorithm was taken from an idea sent by email to the 
  35. * IEEE Posix P1003.2 mailing list from Phong Vo (kpv@research.att.com) and 
  36. * Glenn Fowler (gsf@research.att.com). Landon Curt Noll (chongo@toad.com) 
  37. * later improved on their algorithm. 
  38. * 
  39. * The magic is in the interesting relationship between the special prime 
  40. * 16777619 (2^24 + 403) and 2^32 and 2^8. 
  41. * 
  42. * This hash produces the fewest collisions of any function that we've seen so 
  43. * far, and works well on both numbers and strings. 
  44. */  
  45.   
  46. uint calc_hashnr(const byte *key, uint len)  
  47. {  
  48.     const byte *end=key+len;  
  49.     uint hash;  
  50.     for (hash = 0; key < end; key++)  
  51.     {  
  52.         hash *= 16777619;  
  53.         hash ^= (uint) *(uchar*) key;  
  54.     }  
  55.     return (hash);  
  56. }  
  57.   
  58. uint calc_hashnr_caseup(const byte *key, uint len)  
  59. {  
  60.     const byte *end=key+len;  
  61.     uint hash;  
  62.     for (hash = 0; key < end; key++)  
  63.     {  
  64.         hash *= 16777619;  
  65.         hash ^= (uint) (uchar) toupper(*key);  
  66.     }  
  67.     return (hash);  
  68. }  
  69. #endif  

3. Hash 表

         我们接下去分析Poco中Hash表的实现。Poco中实现了3种哈希表,分别是SimpleHashTable, HashTable,LinearHashTable。它们的实现对应了当出现冲突时,解决冲突的不同方法。首先我们看一下通用的解决方法。
         1. 线性探测。当出现碰撞时,顺序依次查询后续位置,直到找到空位。 《利用线性探测法构造散列表》
         2. 双重散列法。当使用第一个散列Hash函数,出现碰撞时,用第二个散列函数去寻找空位
         3. 拉链法。出现碰撞的时候,使用list存储碰撞数据
         4. 线性哈希,linear hash。立刻分裂或者延迟分裂。通过分裂,控制桶的高度,每次分裂时,会重新散列碰撞元素。 《linear hashing》

        SimpleHashTable的实现对应了方法一;HashTable对应了方法3;LinearHashTable对应了方法4。


3.1 SimpleHashTable

         从类图里我们看到,SimpleHashTable是一个HashEntry容器, 内部定义如下:
  1. std::vector<HashEntry*> _entries  
         当插入新数据时,首先根据hash值,计算空位,然后存储;如果发现冲突,顺着计算的hash值按地址顺序依次寻找空位;如_entries容器无空位,则抛出异常。
  1. UInt32 insert(const Key& key, const Value& value)  
  2. /// Returns the hash value of the inserted item.  
  3. /// Throws an exception if the entry was already inserted  
  4. {  
  5.     UInt32 hsh = hash(key);  
  6.     insertRaw(key, hsh, value);  
  7.     return hsh;  
  8. }  
  9.   
  10. Value& insertRaw(const Key& key, UInt32 hsh, const Value& value)  
  11. /// Returns the hash value of the inserted item.  
  12. /// Throws an exception if the entry was already inserted  
  13. {  
  14.     UInt32 pos = hsh;  
  15.     if (!_entries[pos])  
  16.         _entries[pos] = new HashEntry(key, value);  
  17.     else  
  18.     {  
  19.         UInt32 origHash = hsh;  
  20.         while (_entries[hsh % _capacity])  
  21.         {  
  22.             if (_entries[hsh % _capacity]->key == key)  
  23.                 throw ExistsException();  
  24.             if (hsh - origHash > _capacity)  
  25.                 throw PoolOverflowException("SimpleHashTable full");  
  26.             hsh++;  
  27.         }  
  28.         pos = hsh % _capacity;  
  29.         _entries[pos] = new HashEntry(key, value);  
  30.     }  
  31.     _size++;  
  32.     return _entries[pos]->value;  
  33. }  

         SimpleHashTable进行搜索时,策略也一致。

  1. const Value& get(const Key& key) const  
  2. /// Throws an exception if the value does not exist  
  3. {  
  4.     UInt32 hsh = hash(key);  
  5.     return getRaw(key, hsh);  
  6. }  
  7.   
  8. const Value& getRaw(const Key& key, UInt32 hsh) const  
  9. /// Throws an exception if the value does not exist  
  10. {  
  11.     UInt32 origHash = hsh;  
  12.     while (true)  
  13.     {  
  14.         if (_entries[hsh % _capacity])  
  15.         {  
  16.             if (_entries[hsh % _capacity]->key == key)  
  17.             {  
  18.                 return _entries[hsh % _capacity]->value;  
  19.             }  
  20.         }  
  21.         else  
  22.             throw InvalidArgumentException("value not found");  
  23.         if (hsh - origHash > _capacity)  
  24.             throw InvalidArgumentException("value not found");  
  25.         hsh++;  
  26.     }  
  27. }  

         SimpleHashTable没有提供删除数据的接口,只适用于数据量不大的简单应用。

3.2 HashTable

         HashTable是拉链法的一个变种。当冲突数据发生时,存储的容器是map而不是list。其内部容器定义为:
  1. HashEntryMap** _entries;  
         同map相比,它实际上是把一个大map分成了很多个小map,通过hash方法寻找到小map,再通过map的find函数寻找具体数据。其插入和搜索数据函数如下:

  1. UInt32 insert(const Key& key, const Value& value)  
  2. /// Returns the hash value of the inserted item.  
  3. /// Throws an exception if the entry was already inserted  
  4. {  
  5.     UInt32 hsh = hash(key);  
  6.     insertRaw(key, hsh, value);  
  7.     return hsh;  
  8. }  
  9.   
  10.   
  11. Value& insertRaw(const Key& key, UInt32 hsh, const Value& value)  
  12. /// Returns the hash value of the inserted item.  
  13. /// Throws an exception if the entry was already inserted  
  14. {  
  15.     if (!_entries[hsh])  
  16.         _entries[hsh] = new HashEntryMap();  
  17.     std::pair<typename HashEntryMap::iterator, bool> res(_entries[hsh]->insert(std::make_pair(key, value)));  
  18.     if (!res.second)  
  19.         throw InvalidArgumentException("HashTable::insert, key already exists.");  
  20.     _size++;  
  21.     return res.first->second;  
  22. }  
  23.   
  24.   
  25. const Value& get(const Key& key) const  
  26. /// Throws an exception if the value does not exist  
  27. {  
  28.     UInt32 hsh = hash(key);  
  29.     return getRaw(key, hsh);  
  30. }  
  31.   
  32.   
  33. const Value& getRaw(const Key& key, UInt32 hsh) const  
  34. /// Throws an exception if the value does not exist  
  35. {  
  36.     if (!_entries[hsh])  
  37.         throw InvalidArgumentException("key not found");  
  38.   
  39.     ConstIterator it = _entries[hsh]->find(key);  
  40.     if (it == _entries[hsh]->end())  
  41.         throw InvalidArgumentException("key not found");  
  42.   
  43.     return it->second;  
  44. }  

         HashTable支持remove操作。


3.2 LinearHashTable

         LinearHashTable按照解决冲突的方法4实现。它内部的容器为vector<vector<Value>>,同时还存在两个控制量_split和_front:
  1. std::size_t _split;  
  2. std::size_t _front;  
  3. vector<vector<Value>> _buckets;  

         它的插入操作如下:
  1. std::pair<Iterator, bool> insert(const Value& value)  
  2. /// Inserts an element into the table.  
  3. ///  
  4. /// If the element already exists in the table,  
  5. /// a pair(iterator, false) with iterator pointing to the   
  6. /// existing element is returned.  
  7. /// Otherwise, the element is inserted an a   
  8. /// pair(iterator, true) with iterator  
  9. /// pointing to the new element is returned.  
  10. {  
  11.     std::size_t hash = _hash(value);  
  12.     std::size_t addr = bucketAddressForHash(hash);  
  13.     BucketVecIterator it(_buckets.begin() + addr);  
  14.     BucketIterator buckIt(std::find(it->begin(), it->end(), value));  
  15.     if (buckIt == it->end())  
  16.     {  
  17.         split();  
  18.         addr = bucketAddressForHash(hash);  
  19.         it = _buckets.begin() + addr;  
  20.         buckIt = it->insert(it->end(), value);  
  21.         ++_size;  
  22.         return std::make_pair(Iterator(it, _buckets.end(), buckIt), true);  
  23.     }  
  24.     else  
  25.     {  
  26.         return std::make_pair(Iterator(it, _buckets.end(), buckIt), false);  
  27.     }  
  28. }  

         其中split函数是所有操作的关键:
  1. void split()  
  2. {  
  3.     if (_split == _front)  
  4.     {  
  5.         _split = 0;  
  6.         _front *= 2;  
  7.         _buckets.reserve(_front*2);  
  8.     }  
  9.     Bucket tmp;  
  10.     _buckets.push_back(tmp);  
  11.     _buckets[_split].swap(tmp);  
  12.     ++_split;  
  13.     for (BucketIterator it = tmp.begin(); it != tmp.end(); ++it)  
  14.     {  
  15.         using std::swap;  
  16.         std::size_t addr = bucketAddress(*it);  
  17.         _buckets[addr].push_back(Value());  
  18.         swap(*it, _buckets[addr].back());  
  19.     }  
  20. }  

         从上面的代码中我们可以看到,在每次插入新元素的时候,都会增加一个新的桶,并对桶_buckets[_split]进行重新散列;在_split == _front时,会把_buckets的容积扩大一倍。通过动态的增加桶的数量,这种方法降低了每个桶的高度,从而保证了搜索的效率。

4. HashMap和HashSet

         HashMap和HashSet是在LinearHashTable上的封装,使接口同stl::map和stl::set相类似,使用时非常的简单。下面来看一个例子:
  1. #include "Poco/HashMap.h"  
  2. int main()  
  3. {  
  4.     typedef HashMap<intint> IntMap;  
  5.     IntMap hm;  
  6.       
  7.     for (int i = 0; i < N; ++i)  
  8.     {  
  9.         std::pair<IntMap::Iterator, bool> res = hm.insert(IntMap::ValueType(i, i*2));  
  10.         IntMap::Iterator it = hm.find(i);  
  11.     }         
  12.       
  13.     assert (!hm.empty());  
  14.       
  15.     for (int i = 0; i < N; ++i)  
  16.     {  
  17.         IntMap::Iterator it = hm.find(i);  
  18.     }  
  19.       
  20.     for (int i = 0; i < N; ++i)  
  21.     {  
  22.         std::pair<IntMap::Iterator, bool> res = hm.insert(IntMap::ValueType(i, 0));  
  23.     }     
  24.         return 0;  
  25. }     

(版权所有,转载时请注明作者和出处  http://blog.csdn.net/arau_sh/article/details/8698257

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值