暴雪公司关于字符串匹配的hash算法

转载自:http://blog.csdn.net/shanzhizi/article/details/7736526

暴雪公司有个经典的字符串的hash公式

      先提一个简单的问题,假如有一个庞大的字符串数组,然后给你一个单独的字符串,让你从这个数组中查找是否有这个字符串并找到它,你会怎么做? 有一个方法最简单,老老实实从头查到尾,一个一个比较,直到找到为止,我想只要学过程序设计的人都能把这样一个程序作出来,但要是有程序员把这样的程序交给用户,我只能用无语来评价,或许它真的能工作,但...也只能如此了。 最合适的算法自然是使用HashTable(哈希表),先介绍介绍其中的基本知识,所谓Hash,一般是一个整数,通过某种算法,可以把一个字符串"压缩" 成一个整数,这个数称为Hash,当然,无论如何,一个32位整数是无法对应回一个字符串的,但在程序中,两个字符串计算出的Hash值相等的可能非常小,下面看看在MPQ中的Hash算法:

  1. unsigned long HashString(char*lpszFileName, unsigned long dwHashType)   
  2. {   
  3.     unsigned char*key = (unsigned char*)lpszFileName;   
  4.     unsigned long seed1 =0x7FED7FED, seed2 =0xEEEEEEEE;   
  5.     int ch;   
  6.   
  7.     while(*key !=0)   
  8.     {   
  9.         ch = toupper(*key );   
  10.   
  11.         seed1 = cryptTable[(dwHashType <<8) ch] ^ (seed1 seed2);   
  12.         seed2 = ch+ seed1+ seed2 +(seed2 <<5) 3;   
  13.     }   
  14.     return seed1;   
  15. }  

Blizzard的这个算法是非常高效的,被称为"One-Way Hash",举个例子,字符串"unitneutralacritter.grp"通过这个算法得到的结果是0xA26067F3。 是不是把第一个算法改进一下,改成逐个比较字符串的Hash值就可以了呢,答案是,远远不够,要想得到最快的算法,就不能进行逐个的比较,通常是构造一个 哈希表(Hash Table, http://blog.csdn.net/shanzhizi)来解决问题,哈希表是一个大数组,这个数组的容量根据程序的要求来定义,例如1024,每一个Hash值通过取模运算 (mod)对应到数组中的一个位置,这样,只要比较这个字符串的哈希值对的位置又没有被占用,就可以得到最后的结果了,想想这是什么速度?是的,是最快的O(1),现在仔细看看这个算法吧


  1. int GetHashTablePos(char*lpszString, SOMESTRUCTURE *lpTable, int nTableSize)   
  2. {   
  3.     int nHash = HashString(lpszString), nHashPos = nHash % nTableSize;   
  4.   
  5.     if (lpTable[nHashPos].bExists &&!strcmp(lpTable[nHashPos].pString, lpszString))   
  6.         return nHashPos;   
  7.     else   
  8.         return-1; //Error value   
  9. }  

看到此,我想大家都在想一个很严重的问题:"假如两个字符串在哈希表中对应的位置相同怎么办?",究竟一个数组容量是有限的,这种可能性很大。解决该问题的方法很多,我首先想到的就是用"链表",感谢大学里学的数据结构教会了这个百试百灵的法宝,我碰到的很多算法都可以转化成链表来解决,只要在哈希表的每个入口挂一个链表,保存所有对应的字符串就OK了。 事情到此似乎有了完美的结局,假如是把问题独自交给我解决,此时我可能就要开始定义数据结构然后写代码了。然而Blizzard的程序员使用的方法则是更精妙的方法。基本原理就是:他们在哈希表中不是用一个哈希值而是用三个哈希值来校验字符串。 中国有句古话"再一再二不能再三再四",看来Blizzard也深得此话的精髓,假如说两个不同的字符串经过一个哈希算法得到的入口点一致有可能,但用三个不同的哈希算法算出的入口点都一致,那几乎可以肯定是不可能的事了,这个几率是1:18889465931478580854784,大概是10的 22.3次方分之一,对一个游戏程序来说足够安全了。 现在再回到数据结构上,Blizzard使用的哈希表没有使用链表,而采用"顺延"的方式来解决问题,看看这个算法:
 
  1. int GetHashTablePos(char*lpszString, MPQHASHTABLE *lpTable, int nTableSize)   
  2. {   
  3.     constint HASH_OFFSET =0, HASH_A =1, HASH_B =2;   
  4.     int nHash = HashString(lpszString, HASH_OFFSET);   
  5.     int nHashA = HashString(lpszString, HASH_A);   
  6.     int nHashB = HashString(lpszString, HASH_B);   
  7.     int nHashStart = nHash % nTableSize, nHashPos = nHashStart;   
  8.       
  9.     while (lpTable[nHashPos].bExists)   
  10.     {   
  11.         if (lpTable[nHashPos].nHashA == nHashA && lpTable[nHashPos].nHashB == nHashB)   
  12.             return nHashPos;   
  13.         else   
  14.             nHashPos = (nHashPos 1) % nTableSize;   
  15.           
  16.         if (nHashPos == nHashStart)   
  17.             break;   
  18.     }   
  19.   
  20.     return-1; //Error value   
  21. }   

1. 计算出字符串的三个哈希值(一个用来确定位置,另外两个用来校验)
2. 察看哈希表中的这个位置
3. 哈希表中这个位置为空吗?假如为空,则肯定该字符串不存在,返回
4. 假如存在,则检查其他两个哈希值是否也匹配,假如匹配,则表示找到了该字符串,返回
5. 移到下一个位置,假如已经越界,则表示没有找到,返回
6. 看看是不是又回到了原来的位置,假如是,则返回没找到 7. 回到3
 

下面用一个静态数组做一个简单模拟(没有处理hash冲突):

  1. #include <stdio.h>   
  2. #define HASH_TABLE_SIZE 13 // 哈希表的大小应是个质数   
  3. struct mapping   
  4. {   
  5.   void *key;   
  6.   void *data;   
  7. } hash_table[HASH_TABLE_SIZE];   
  8.   
  9. unsigned int   
  10. RSHash (char *str)   
  11. {   
  12.   unsigned int b = 378551;   
  13.   unsigned int a = 63689;   
  14.   unsigned int hash =      0  ;   
  15.   
  16.   while (*str)   
  17.     {   
  18.       hash = hash * a + (*str++);   
  19.       a *= b;   
  20.     }   
  21.   return (hash & 0x7FFFFFFF);   
  22. }   
  23.   
  24. int main ()   
  25. {   
  26.   char *str = "we are the world!";   
  27.   char *filename = "myfile.txt";   
  28.   unsigned int hash_offset;   
  29.   // 初始化哈希表   
  30.   memset (hash_table, 0x0, sizeof (hash_table));   
  31.   
  32.   // 将字符串插入哈希表 .   
  33.   hash_offset = RSHash (str) % HASH_TABLE_SIZE;   
  34.   hash_table[hash_offset].key = str;   
  35.   hash_table[hash_offset].data = filename;   
  36.   
  37.   // 查找 str 是否存在于 hash_table.   
  38.   hash_offset = RSHash (str) % HASH_TABLE_SIZE;   
  39.   if (hash_table[hash_offset].key)   
  40.         printf ("string '%s' exists in the file %s./n", str, hash_table[hash_offset].data);   
  41.   else   
  42.         printf ("string '%s' does not exist./n", str);  
  43.   
  44. return 0;  
  45. }   

下面是一个类的封装:
  1. 代码  
  2.   
  3. 一、类声明头文件  
  4. /  
  5. // Name:        HashAlgo.h  
  6. // Purpose:     使用魔兽Hash算法,实现索引表的填充和查找功能。  
  7. // Author:      陈相礼  
  8. // Modified by:  
  9. // Created:     07/30/09  
  10. // RCS-ID:      $Id: treetest.h 43021 2009-07-30 16:36:51Z VZ $  
  11. // Copyright:   (C) Copyright 2009, TSong Corporation, All Rights Reserved.  
  12. // Licence:       
  13. /  
  14. #define MAXFILENAME 255     // 最大文件名长度  
  15. #define MAXTABLELEN 1024    // 默认哈希索引表大小  
  16. //  
  17. // 测试宏定义,正式使用时关闭  
  18. #define DEBUGTEST 1  
  19. //  
  20. // 哈希索引表定义  
  21. typedef struct  
  22. {  
  23.     long nHashA;  
  24.     long nHashB;  
  25.     bool bExists;  
  26.     char test_filename[MAXFILENAME];  
  27.     // ......  
  28. } MPQHASHTABLE;  
  29. //  
  30. // 对哈希索引表的算法进行封装  
  31. class CHashAlgo  
  32. {  
  33. public:  
  34. #if DEBUGTEST  
  35.     long  testid;   // 测试之用  
  36. #endif  
  37.     CHashAlgo( constlong nTableLength = MAXTABLELEN )// 创建指定大小的哈希索引表,不带参数的构造函数创建默认大小的哈希索引表  
  38.     {  
  39.         prepareCryptTable();  
  40.         m_tablelength = nTableLength;  
  41.           
  42.         m_HashIndexTable =new MPQHASHTABLE[nTableLength];  
  43.         for ( int i =0; i < nTableLength; i++ )  
  44.         {  
  45.             m_HashIndexTable[i].nHashA =-1;  
  46.             m_HashIndexTable[i].nHashB =-1;  
  47.             m_HashIndexTable[i].bExists =false;  
  48.             m_HashIndexTable[i].test_filename[0] ='\0';  
  49.         }          
  50.     }  
  51.     void prepareCryptTable();                                               // 对哈希索引表预处理  
  52.     unsigned long HashString(char*lpszFileName, unsigned long dwHashType); // 求取哈希值      
  53. long GetHashTablePos( char*lpszString );                               // 得到在定长表中的位置  
  54. bool SetHashTable( char*lpszString );                                  // 将字符串散列到哈希表中  
  55.     unsigned long GetTableLength(void);  
  56.     void SetTableLength( const unsigned long nLength );  
  57.     ~CHashAlgo()  
  58.     {  
  59.         if ( NULL != m_HashIndexTable )  
  60.         {  
  61.             delete []m_HashIndexTable;  
  62.             m_HashIndexTable = NULL;  
  63.             m_tablelength =0;  
  64.         }  
  65.     }  
  66. protected:  
  67. private:  
  68.     unsigned long cryptTable[0x500];  
  69.     unsigned long m_tablelength;    // 哈希索引表长度  
  70.     MPQHASHTABLE *m_HashIndexTable;  
  71. };   
  72. 二、类实现文件  
  73. view plaincopy to clipboardprint?  
  74. /     
  75. // Name:        HashAlgo.cpp     
  76. // Purpose:     使用魔兽Hash算法,实现索引表的填充和查找功能。     
  77. // Author:      陈相礼     
  78. // Modified by:     
  79. // Created:     07/30/09     
  80. // RCS-ID:      $Id: treetest.h 43021 2009-07-30 16:36:51Z VZ $     
  81. // Copyright:   (C) Copyright 2009, TSong Corporation, All Rights Reserved.     
  82. // Licence:          
  83. /     
  84.     
  85. #include "windows.h"     
  86. #include "HashAlgo.h"     
  87.     
  88. //     
  89. // 预处理     
  90. void CHashAlgo::prepareCryptTable()     
  91. {      
  92.     unsigned long seed =0x00100001, index1 =0, index2 =0, i;     
  93.     
  94.     for( index1 =0; index1 <0x100; index1++ )     
  95.     {      
  96.         for( index2 = index1, i =0; i <5; i++, index2 +=0x100 )     
  97.         {      
  98.             unsigned long temp1, temp2;     
  99.             seed = (seed *125+3) %0x2AAAAB;     
  100.             temp1 = (seed &0xFFFF) <<0x10;     
  101.             seed = (seed *125+3) %0x2AAAAB;     
  102.             temp2 = (seed &0xFFFF);     
  103.             cryptTable[index2] = ( temp1 | temp2 );      
  104.         }      
  105.     }      
  106. }     
  107.     
  108. //     
  109. // 求取哈希值     
  110. unsigned long CHashAlgo::HashString(char*lpszFileName, unsigned long dwHashType)     
  111. {      
  112.     unsigned char*key = (unsigned char*)lpszFileName;     
  113.     unsigned long seed1 =0x7FED7FED, seed2 =0xEEEEEEEE;     
  114.     int ch;     
  115.     
  116.     while(*key !=0)     
  117.     {      
  118.         ch = toupper(*key++);     
  119.     
  120.         seed1 = cryptTable[(dwHashType <<8) + ch] ^ (seed1 + seed2);     
  121.         seed2 = ch + seed1 + seed2 + (seed2 <<5) +3;      
  122.     }     
  123.     return seed1;      
  124. }     
  125.     
  126. //     
  127. // 得到在定长表中的位置     
  128. long CHashAlgo::GetHashTablePos(char*lpszString)     
  129.     
  130. {      
  131.     const unsigned long HASH_OFFSET =0, HASH_A =1, HASH_B =2;     
  132.     unsigned long nHash = HashString(lpszString, HASH_OFFSET);     
  133.     unsigned long nHashA = HashString(lpszString, HASH_A);     
  134.     unsigned long nHashB = HashString(lpszString, HASH_B);     
  135.     unsigned long nHashStart = nHash % m_tablelength,     
  136.         nHashPos = nHashStart;     
  137.     
  138.     while ( m_HashIndexTable[nHashPos].bExists)     
  139.     {      
  140.         if (m_HashIndexTable[nHashPos].nHashA == nHashA && m_HashIndexTable[nHashPos].nHashB == nHash)      
  141.             return nHashPos;      
  142.         else      
  143.             nHashPos = (nHashPos +1) % m_tablelength;     
  144.     
  145.         if (nHashPos == nHashStart)      
  146.             break;      
  147.     }     
  148.     
  149.     return-1; //没有找到     
  150. }     
  151. //     
  152. // 通过传入字符串,将相应的表项散列到索引表相应位置中去     
  153. bool CHashAlgo::SetHashTable( char*lpszString )     
  154. {     
  155.     const unsigned long HASH_OFFSET =0, HASH_A =1, HASH_B =2;     
  156.     unsigned long nHash = HashString(lpszString, HASH_OFFSET);     
  157.     unsigned long nHashA = HashString(lpszString, HASH_A);     
  158.     unsigned long nHashB = HashString(lpszString, HASH_B);     
  159.     unsigned long nHashStart = nHash % m_tablelength,     
  160.         nHashPos = nHashStart;     
  161.     
  162.     while ( m_HashIndexTable[nHashPos].bExists)     
  163.     {      
  164.         nHashPos = (nHashPos +1) % m_tablelength;     
  165.         if (nHashPos == nHashStart)      
  166.         {     
  167.     
  168. #if DEBUGTEST     
  169.             testid =-1;     
  170. #endif  
  171.     
  172.             returnfalse;      
  173.         }     
  174.     }     
  175.     m_HashIndexTable[nHashPos].bExists =true;     
  176.     m_HashIndexTable[nHashPos].nHashA = nHashA;     
  177.     m_HashIndexTable[nHashPos].nHashB = nHash;     
  178.     strcpy( m_HashIndexTable[nHashPos].test_filename, lpszString );     
  179.     
  180. #if DEBUGTEST     
  181.     testid = nHashPos;     
  182. #endif  
  183.     
  184.     returntrue;     
  185. }     
  186.     
  187. //     
  188. // 取得哈希索引表长     
  189. unsigned long CHashAlgo::GetTableLength(void)     
  190. {     
  191.     return m_tablelength;     
  192. }     
  193.     
  194. //     
  195. // 设置哈希索引表长     
  196. void CHashAlgo::SetTableLength( const unsigned long nLength )     
  197. {     
  198.     m_tablelength = nLength;     
  199.     return;     
  200. }    
  201.   
  202. 三、测试主文件  
  203. view plaincopy to clipboardprint?  
  204. /     
  205. // Name:        DebugMain.cpp     
  206. // Purpose:     测试Hash算法封装的类,完成索引表的填充和查找功能的测试。     
  207. // Author:      陈相礼     
  208. // Modified by:     
  209. // Created:     07/30/09     
  210. // RCS-ID:      $Id: treetest.h 43021 2009-07-30 16:36:51Z VZ $     
  211. // Copyright:   (C) Copyright 2009, TSong Corporation, All Rights Reserved.     
  212. // Licence:          
  213. /     
  214.     
  215. //     
  216. // 测试参数设定宏     
  217. #define TESTNUM 32     
  218.     
  219. #include <iostream>     
  220. #include <fstream>     
  221. #include "HashAlgo.h"     
  222.     
  223. usingnamespace std;     
  224.     
  225. //     
  226. // 测试主函数开始     
  227. int main( int argc, char**argv )     
  228. {     
  229.     CHashAlgo hash_test( TESTNUM );     
  230.     
  231.     cout <<"取得初始化散列索引表长为:"<< hash_test.GetTableLength() << endl;     
  232.     
  233.     bool is_success = hash_test.SetHashTable( "test" );     
  234.     if ( is_success )     
  235.     {     
  236.         cout <<"散列结果一:成功!"<< endl;     
  237.     }     
  238.     else    
  239.     {     
  240.         cout <<"散列结果一:失败!"<< endl;     
  241.     }     
  242.          
  243.     is_success = hash_test.SetHashTable( "测试" );     
  244.     if ( is_success )     
  245.     {     
  246.         cout <<"散列结果二:成功!"<< endl;     
  247.     }     
  248.     else    
  249.     {     
  250.         cout <<"散列结果二:失败!"<< endl;     
  251.     }     
  252.     
  253.     long pos = hash_test.GetHashTablePos( "test" );     
  254.     cout <<"查找测试字符串:\"test\" 的散列位置:"<< pos << endl;     
  255.     pos = hash_test.GetHashTablePos( "测试" );     
  256.     cout <<"查找测试字符串:“测试” 的散列位置:"<< pos << endl;     
  257.     
  258.     //     
  259. // 散列测试     
  260. for ( int i =0; i < TESTNUM; i++ )     
  261.     {     
  262.         char buff[32];     
  263.         sprintf(buff, "abcdefg%d.", i);     
  264.         is_success = hash_test.SetHashTable(buff);     
  265.         is_success ? cout << buff <<"散列结果:成功!位置:"<< hash_test.testid << endl : cout << buff <<"散列结果:失败!"<< endl;           
  266.     }     
  267.     system( "pause" );     
  268.     //     
  269. // 查找测试     
  270. for ( int i =0; i < TESTNUM; i++ )     
  271.     {     
  272.         char buff[32];     
  273.         sprintf(buff, "abcdefg%d.", i);     
  274.         pos = hash_test.GetHashTablePos( buff );     
  275.         pos !=-1?  cout <<"查找测试字符串:"<< buff <<" 的散列位置:"<< pos << endl : cout << buff <<"存在冲突!"<< endl;        
  276.     }     
  277.     
  278.     system( "pause" );     
  279.     return0;     

转载自:http://www.cnblogs.com/ylan2009/archive/2012/04/13/2445101.html

中国有句古话"再一再二不能再三再四",看来Blizzard也深得此话的精髓,如果说两个不同的字符串经过一个哈希算法得到的入口点一致有可能,但用三个不同的哈希算法算出的入口点都一致,那几乎可以肯定是不可能的事了,这个几率是1:18889465931478580854784,大概是10的 22.3次方分之一,对一个游戏程序来说足够安全了。

/*********************************StringHash.h*********************************/

#pragma once

#define MAXTABLELEN 1024 // 默认哈希索引表大小 
// 
// 哈希索引表定义 
typedef struct _HASHTABLE
{ 
  long nHashA; 
  long nHashB; 
  bool bExists; 
}HASHTABLE, *PHASHTABLE ;

class StringHash
{
public:
  StringHash(const long nTableLength = MAXTABLELEN);
  ~StringHash(void);
private: 
  unsigned long cryptTable[0x500]; 
  unsigned long m_tablelength; // 哈希索引表长度 
  HASHTABLE *m_HashIndexTable; 
private:
  void InitCryptTable(); // 对哈希索引表预处理 
  unsigned long HashString(const string& lpszString, unsigned long dwHashType); // 求取哈希值 
public:
  bool Hash(string url);
  unsigned long Hashed(string url); // 检测url是否被hash过
};

 

/*********************************StringHash.cpp*********************************/

#include "StdAfx.h"
#include "StringHash.h"

StringHash::StringHash(const long nTableLength /*= MAXTABLELEN*/)
{
  InitCryptTable(); 
  m_tablelength = nTableLength; 
  //初始化hash表
  m_HashIndexTable = new HASHTABLE[nTableLength]; 
  for ( int i = 0; i < nTableLength; i++ ) 
  { 
    m_HashIndexTable[i].nHashA = -1; 
    m_HashIndexTable[i].nHashB = -1; 
    m_HashIndexTable[i].bExists = false; 
  } 
}

StringHash::~StringHash(void)
{
  //清理内存
  if ( NULL != m_HashIndexTable ) 
  { 
    delete []m_HashIndexTable; 
    m_HashIndexTable = NULL; 
    m_tablelength = 0; 
  } 
}

/************************************************************************/
/*函数名:InitCryptTable
/*功 能:对哈希索引表预处理 
/*返回值:无
/************************************************************************/
void StringHash::InitCryptTable() 
{ 
  unsigned long seed = 0x00100001, index1 = 0, index2 = 0, i;

  for( index1 = 0; index1 < 0x100; index1++ ) 
  { 
    for( index2 = index1, i = 0; i < 5; i++, index2 += 0x100 ) 
    { 
      unsigned long temp1, temp2; 
      seed = (seed * 125 + 3) % 0x2AAAAB; 
      temp1 = (seed & 0xFFFF) << 0x10; 
      seed = (seed * 125 + 3) % 0x2AAAAB; 
      temp2 = (seed & 0xFFFF); 
      cryptTable[index2] = ( temp1 | temp2 ); 
    } 
  } 
}

/************************************************************************/
/*函数名:HashString
/*功 能:求取哈希值 
/*返回值:返回hash值
/************************************************************************/
unsigned long StringHash::HashString(const string& lpszString, unsigned long dwHashType) 
{ 
  unsigned char *key = (unsigned char *)(const_cast<char*>(lpszString.c_str())); 
  unsigned long seed1 = 0x7FED7FED, seed2 = 0xEEEEEEEE; 
  int ch;

  while(*key != 0) 
  { 
    ch = toupper(*key++);

    seed1 = cryptTable[(dwHashType << 8) + ch] ^ (seed1 + seed2); 
    seed2 = ch + seed1 + seed2 + (seed2 << 5) + 3; 
  } 
  return seed1; 
}

/************************************************************************/
/*函数名:Hashed
/*功 能:检测一个字符串是否被hash过
/*返回值:如果存在,返回位置;否则,返回-1
/************************************************************************/
unsigned long StringHash::Hashed(string lpszString)

{ 
  const unsigned long HASH_OFFSET = 0, HASH_A = 1, HASH_B = 2; 
  //不同的字符串三次hash还会碰撞的几率无限接近于不可能
  unsigned long nHash = HashString(lpszString, HASH_OFFSET); 
  unsigned long nHashA = HashString(lpszString, HASH_A); 
  unsigned long nHashB = HashString(lpszString, HASH_B); 
  unsigned long nHashStart = nHash % m_tablelength, 
  nHashPos = nHashStart;

  while ( m_HashIndexTable[nHashPos].bExists) 
  { 
  if (m_HashIndexTable[nHashPos].nHashA == nHashA && m_HashIndexTable[nHashPos].nHashB == nHashB)
    return nHashPos; 
  else 
  nHashPos = (nHashPos + 1) % m_tablelength;

  if (nHashPos == nHashStart) 
  break; 
  }

  return -1; //没有找到 
}

/************************************************************************/
/*函数名:Hash
/*功 能:hash一个字符串 
/*返回值:成功,返回true;失败,返回false
/************************************************************************/
bool StringHash::Hash(string lpszString)
{ 
  const unsigned long HASH_OFFSET = 0, HASH_A = 1, HASH_B = 2; 
  unsigned long nHash = HashString(lpszString, HASH_OFFSET); 
  unsigned long nHashA = HashString(lpszString, HASH_A); 
  unsigned long nHashB = HashString(lpszString, HASH_B); 
  unsigned long nHashStart = nHash % m_tablelength, 
  nHashPos = nHashStart;

  while ( m_HashIndexTable[nHashPos].bExists) 
  { 
    nHashPos = (nHashPos + 1) % m_tablelength; 
    if (nHashPos == nHashStart) //一个轮回 
    { 
      //hash表中没有空余的位置了,无法完成hash
      return false; 
    } 
  } 
  m_HashIndexTable[nHashPos].bExists = true; 
  m_HashIndexTable[nHashPos].nHashA = nHashA; 
  m_HashIndexTable[nHashPos].nHashB = nHashB;

  return true; 
}



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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值