Trie实践:一种比哈希表更快的数据结构

来源:互联网 发布:lua调用java 编辑:程序博客网 时间:2024/05/24 05:27

本文乃Siliphen原创。转载请注明出处:http://blog.csdn.net/stevenkylelee


本文分为5部分。从思考和认知的角度,由浅到深带你认识Trie数据结构。

  1.桶状哈希表与直接定址表的概念。

  2.为什么直接定址表会比桶状哈希表快

  3.初识Trie数据结构

  4.Trie为什么会比桶状哈希表快

  5.实际做实验感受下Trie , std::map , std::unordered_map的差距

  6.最后的补充


1.桶状哈希表与直接定址表的概念。


先考虑一下这个问题:如何统计5万个0-99范围的数字出现的次数?

可以用哈希表来进行统计。如下:

[cpp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
  1. // 生成5万个0-99范围的随机数  
  2. int * pNumbers = new int[ 50000 ] ;  
  3. for ( int i = 0 ; i < 50000 ; ++i )  
  4. {  
  5.     pNumbers[ i ] = rand( ) % 100 ;  
  6. }  
  7.   
  8. // 统计每个数字出现个次数  
  9. unordered_map< int , int > Counter ;  
  10. for ( int i = 0 ; i < 50000 ; ++i )  
  11. {  
  12.     ++Counter[ pNumbers[ i ] ] ;  
  13. }  


普通的桶状哈希表可能是有冲突的,这取决于哈希函数的设计。

如果有冲突,那么就会退化成线性查找。


对于这个问题,有一种更好的做法,就是“直接定址表”

“直接定址表”的概念第一次我是在王爽著的《汇编语言》看到

使用“直接定址表”需要满足一些条件,比如:值刚好就是key


上面那题用直接定址表来统计的话,实现是这样:

[cpp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
  1. // 统计每个数字出现个次数  
  2. int Counter[ 100 ] = { 0 } ;   
  3. for ( int i = 0 ; i < 50000 ; ++i )  
  4. {  
  5.     ++Counter[ pNumbers[ i ] ] ;  
  6. }  

以上代码只是把哈希表容器换成了一个数组。数组的0-99的下标范围就是表示0-99个数字,

下标对应的元素值就是该下标表示的数字的出现次数。


2.为什么直接定址表会比桶状哈希表快


直接定址表也是哈希的一种,只是比较特殊。

直接定址表不需要计算哈希散列值,既然没有哈希散列值自然就不存在哈希冲突处理了。

这就是直接定址表比桶状哈希表快的原因


3.初识Trie数据结构


再考虑这样一个问题:如何统计5万个单词出现的次数?

哈,这个有点难度了吧?只能用哈希表来做了吧?

实现是不是像这样:

[cpp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
  1. vector< string > words ;  
  2. // 生成5万个随机单词,略。。。  
  3.   
  4. // 统计每个数字出现个次数  
  5. unordered_map< string , int > Counter ;  
  6. for ( int i = 0 ; i < 50000 ; ++i )  
  7. {  
  8.     ++Counter[ words[ i ] ] ;  
  9. }  

还有没有更快的统计方法呢?

首先我们来看下桶状哈希表慢在哪里,有2点

1.对每个字符串key都要执行一次哈希散列函数

2.如果哈希散列有冲突的话,就要做冲突处理

要提速,就要把这2点给干掉,不计算哈希散列,不做冲突处理。

咦!这不就是之前说的“直接定址表”么?

那用“直接定址表”怎样做字符串的统计?


如果,你自认为自己是一个天才的话,看到这里,就先别往下看。

先自己想想:怎样用直接定址表的思想来做字符串的统计、查找。


答案那就是Trie数据结构。Trie是啥?

简单地说,Trie就是直接定址表和树的结合的产物

Trie其实是一种树结构,既然是树,那就会有树节点,

Trie树节点的特殊在于:一个节点的子节点就是一个直接定址表


Trie树节点的定义类似如下:

[cpp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
  1. // Trie树节点  
  2. struct TrieNode  
  3. {  
  4.     // 节点的值  
  5.     int Val ;   
  6.   
  7.     // 子节点  
  8.     Node* Children[ 256 ] ;  
  9.   
  10. };  

要直观地用图形表示Trie树,大概是这样:



4.Trie为什么会比桶状哈希表快


从代码定义和图示可以看出,每个节点,对其子节点的定位,都是一个直接定址表。

要查找"Siliphen"这个字符串对应的值,过程是怎样的呢?

从根节点开始,用S的Ascii值直接定位找到S对应的子节点,

从S对应的节点,直接定位找到i对应的子节点

从i对应的节点,直接定位找到l对应的子节点

以此类推,直到最后的

从e对应的节点,直接定位找到n对应的子节点

n对应的子节点的数据字段就是"Siliphen"的字符串对应的值


从这个过程可以看到对于字符串的键值映射查找,Trie根本没有进行哈希散列和冲突处理。

This is the reason that Trie is faster than Hashtable!

这就是Trie比哈希表快的原因!


5.实际做实验感受下Trie , std::map , std::unordered_map的差距


理论上来说,Trie要比哈希表快。

到底快多少呢?咱们就做一个实验看看吧。有一个直观的感受。

首先,我们要写一个Trie。


我自己实现了一个TrieMap,

模仿C++的std标准库的map , unordered_map写的一个模板类

代码如下:

[cpp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
  1. #pragma once  
  2. #include <string>  
  3. #include <queue>  
  4. #include <stack>  
  5. #include <list>  
  6. using namespace std ;  
  7.   
  8. templatetypename Value_t >  
  9. class TireMap  
  10. {  
  11. public:  
  12.     TireMap( );  
  13.     ~TireMap( ) ;  
  14.   
  15. private:  
  16.   
  17.     typedef pair< string , Value_t > Kv_t ;  
  18.   
  19.     struct Node  
  20.     {  
  21.         Kv_t * pKv ;  
  22.   
  23.         Node* Children[ 256 ] ;  
  24.   
  25.         Node( ) :  
  26.             pKv( 0 )  
  27.         {  
  28.             memset( Children , 0 , sizeof( Children ) ) ;  
  29.         }  
  30.   
  31.         ~Node( )  
  32.         {  
  33.             if ( pKv != 0 )  
  34.             {  
  35.                 //delete pKv ;  
  36.             }  
  37.         }  
  38.   
  39.     };  
  40.   
  41. public :   
  42.   
  43.     /* 
  44.     重载[ ]  运算符。和 map , unorder_map 容器接口一样。 
  45.     */  
  46.     Value_t& operator[ ]( const string& strKey ) ;  
  47.   
  48.     // 清除保存的数据  
  49.     void clear( ) ;  
  50.   
  51. public :   
  52.   
  53.     const list< Kv_t >& GetKeyValueList( ) const { return m_Kvs ; }  
  54.   
  55. protected:  
  56.   
  57.     // 删除一棵树  
  58.     static void DeleteTree( Node *pNode ) ;   
  59.   
  60. protected:  
  61.   
  62.     // 树根节点  
  63.     Node * m_pRoot ;   
  64.   
  65.     // 映射的键值列表  
  66.     list< Kv_t > m_Kvs ;  
  67.   
  68. };  
  69.   
  70. templatetypename Value_t >  
  71. TireMap<Value_t>::TireMap( )  
  72. {  
  73.     m_pRoot = new Node( ) ;  
  74. }  
  75.   
  76. templatetypename Value_t >  
  77. TireMap<Value_t>::~TireMap( )  
  78. {  
  79.     clear( ) ;  
  80.     delete m_pRoot ;  
  81. }  
  82.   
  83. templatetypename Value_t >  
  84. void TireMap<Value_t>::clear( )  
  85. {  
  86.     for ( int i = 0 ; i < 256 ; ++i )  
  87.     {  
  88.         if ( m_pRoot->Children[ i ] != 0 )  
  89.         {  
  90.             DeleteTree( m_pRoot->Children[ i ] ) ;  
  91.             m_pRoot->Children[ i ] = 0 ;  
  92.         }  
  93.     }  
  94.   
  95.     m_Kvs.clear( ) ;   
  96. }  
  97.   
  98. templatetypename Value_t >  
  99. void TireMap<Value_t>::DeleteTree( Node * pRoot )  
  100. {  
  101.     // BFS 删除树  
  102.     stack< Node* > stk ;   
  103.     stk.push( pRoot ) ;   
  104.     for ( ; stk.size( ) > 0 ; )  
  105.     {  
  106.         Node * p = stk.top( ) ; stk.pop( ) ;  
  107.         // 扩展  
  108.         for ( int i = 0 ; i < 256 ; ++i )  
  109.         {  
  110.             Node* p2 = p->Children[ i ] ;  
  111.             if ( p2 == 0 )  
  112.             {  
  113.                 continue;   
  114.             }  
  115.             stk.push( p2 ) ;  
  116.         }  
  117.   
  118.         delete p ;   
  119.     }  
  120. }  
  121.   
  122. templatetypename Value_t >  
  123. Value_t& TireMap<Value_t>::operator[]( const string& strKey )  
  124. {  
  125.     Node * pNode = m_pRoot ;  
  126.   
  127.     // 建立或者查找树路径  
  128.     for ( size_t i = 0 , size = strKey.size( ) ; i < size ; ++i )  
  129.     {  
  130.         const char& ch = strKey[ i ] ;  
  131.         Node*& Child = pNode->Children[ ch ] ;  
  132.   
  133.         if ( Child == 0 )  
  134.         {  
  135.             pNode = Child = new Node( ) ;  
  136.         }  
  137.         else  
  138.         {  
  139.             pNode = Child ;  
  140.         }  
  141.   
  142.     }  
  143.     // end for  
  144.   
  145.     // 如果没有数据字段的话,就生成一个。  
  146.     if ( pNode->pKv == 0 )  
  147.     {  
  148.         m_Kvs.push_back( Kv_t( strKey , Value_t() ) ) ;  
  149.         pNode->pKv = &*( --m_Kvs.end( ) ) ;  
  150.     }  
  151.   
  152.     return pNode->pKv->second ;   
  153. }  

有没有std的感觉?哈哈
核心代码就是[]运算符重载的实现。
为什么要我搞一个list< Kv_t > m_Kvs字段?
这个字段主要是用来方便查看结果。


OK。下面我们来写测试代码

看看 Trie , 与 std::map , std::unordered_map之间的差别

测试代码如下:

[cpp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
  1. #include <string>  
  2. #include <vector>  
  3. #include <unordered_map>  
  4. #include <map>  
  5. #include <time.h>  
  6. #include "TireMap.h"  
  7. using namespace std ;  
  8.   
  9. // 随机生成 Count 个随机字符组合的“单词”  
  10. templatetypename StringList_t >  
  11. int CreateStirngs( StringList_t& strings , int Count )  
  12. {  
  13.     int nTimeStart , nElapsed ;  
  14.     nTimeStart = clock( ) ;  
  15.     strings.clear( ) ;  
  16.     for ( int i = 0 ; i < Count ; ++i )  
  17.     {  
  18.         int stringLen = 5 ;  
  19.         string str ;  
  20.         for ( int i = 0 ; i < stringLen ; ++i )  
  21.         {  
  22.             char ch = 'a' + rand( ) % ( 'z' - 'a' + 1 ) ;  
  23.             str.push_back( ch ) ;  
  24.   
  25.             if ( ch == 'z' )  
  26.             {  
  27.                 int a = 1 ;   
  28.             }  
  29.         }  
  30.         strings.push_back( str ) ;  
  31.     }  
  32.   
  33.     nElapsed = clock( ) - nTimeStart ;  
  34.     return nElapsed ;   
  35. }  
  36.   
  37. // 创建 Count 个整型数据。同样创建这些整型对应的字符串  
  38. templatetypename StringList_t , typename IntList_t >  
  39. int CreateNumbers( StringList_t& strings , IntList_t& Ints , int Count )  
  40. {  
  41.     strings.clear( ) ;   
  42.     Ints.clear( ) ;   
  43.   
  44.     for ( int i = 0 ; i < Count ; ++i )  
  45.     {  
  46.         int n =rand( ) % 0x00FFFFFF ;   
  47.         char sz[ 256 ] = { 0 } ;  
  48.         _itoa_s( n , sz , 10 ) ;   
  49.   
  50.         strings.push_back( sz ) ;  
  51.         Ints.push_back( n ) ;  
  52.     }  
  53.   
  54.     return 0 ;  
  55. }  
  56.   
  57. // Tire 正确性检查  
  58. string Check( const unordered_map< string , int >& Right , const TireMap< int >& Tire )  
  59. {  
  60.     string strInfo = "Tire 统计正确" ;  
  61.   
  62.     const auto& TireRet = Tire.GetKeyValueList( ) ;  
  63.     unordered_map< string , int > ttt ;  
  64.     for ( auto& kv : TireRet )  
  65.     {  
  66.         ttt[ kv.first ] = kv.second ;  
  67.     }  
  68.   
  69.     if ( ttt.size( ) != Right.size( ) )  
  70.     {  
  71.         strInfo = "Tire统计有错" ;  
  72.     }  
  73.     else  
  74.     {  
  75.         for ( auto& kv : ttt )  
  76.         {  
  77.             auto it = Right.find( kv.first )  ;  
  78.             if ( it == Right.end( ) )  
  79.             {  
  80.                 strInfo = "Tire统计有错" ;  
  81.                 break ;  
  82.             }  
  83.             else if ( kv.second != it->second )  
  84.             {  
  85.                 strInfo = "Tire统计有错" ;  
  86.                 break ;  
  87.             }  
  88.   
  89.         }  
  90.     }  
  91.   
  92.     return strInfo ;   
  93.   
  94. }  
  95.   
  96. // 统计模板函数。可以用map , unordered_map , TrieMap 做统计  
  97. templatetypename StringList_t , typename Counter_t >  
  98. int Count( const StringList_t& strings , Counter_t& Counter )  
  99. {  
  100.     int nTimeStart , nElapsed ;  
  101.   
  102.     nTimeStart = clock( ) ;  
  103.     map< string , int > Counter1 ;  
  104.     for ( const auto& str : strings )  
  105.     {  
  106.         ++Counter[ str ] ;  
  107.     }  
  108.     nElapsed = clock( ) - nTimeStart ;  
  109.   
  110.     return nElapsed  ;  
  111.   
  112. }  
  113.   
  114. int _tmain( int argc , _TCHAR* argv[ ] )  
  115. {  
  116.     map< string , int > ElapsedInfo ;  
  117.     int nTimeStart , nElapsed ;  
  118.   
  119.     // 生成50000个随机单词  
  120.     list< string > strings ;  
  121.     nElapsed = CreateStirngs( strings , 50000 ) ;  
  122.     //ElapsedInfo[ "生成单词 耗时" ] = nElapsed  ;  
  123.   
  124.     // 用 map 做统计  
  125.     map< string , int > Counter1 ;  
  126.     nElapsed = Count( strings , Counter1 ) ;  
  127.     ElapsedInfo[ "统计单词 用map 耗时" ] = nElapsed  ;  
  128.   
  129.     // 用 unordered_map 做统计  
  130.     unordered_map< string , int > Counter2 ;  
  131.     nElapsed = Count( strings , Counter2 ) ;  
  132.     ElapsedInfo[ "统计单词 用unordered_map 耗时" ] =  nElapsed  ;  
  133.   
  134.     // 用 Tire 做统计  
  135.     TireMap< int > Counter3 ;  
  136.     nElapsed = Count( strings , Counter3 ) ;  
  137.     ElapsedInfo[ "统计单词 用Tire 耗时" ] = nElapsed  ;  
  138.   
  139.     // Tire 统计的结果。正确性检查  
  140.     string CheckRet = Check( Counter2 , Counter3 ) ;   
  141.   
  142.     // 用哈希表统计5万个整形数字出现的次数  
  143.     // 与 用Tire统计同样的5万个整形数字出现的次数的 对比  
  144.     // 当然,用Tire统计的话,先要把那5万个整形数据,转换成对应的字符串的表示。  
  145.   
  146.     list< int > Ints ;   
  147.     CreateNumbers( strings , Ints , 50000 ) ;   
  148.     unordered_map< int , int > kivi ;  
  149.     nTimeStart = clock( ) ;  
  150.     for ( const auto& num : Ints )  
  151.     {  
  152.         ++kivi[ num ] ;  
  153.     }  
  154.     nElapsed = clock( ) - nTimeStart ;  
  155.     ElapsedInfo[ "统计数字 unordered_map 耗时" ] = nElapsed  ;  
  156.   
  157.     //Counter3.clear( ) ; 这句话非常耗时。因为要遍历树逐个delete树节点。树有可能会非常大。所以我注释掉  
  158.     nElapsed = Count( strings , Counter3 ) ;  
  159.     ElapsedInfo[ "统计数字 用Tire 耗时" ] = nElapsed  ;  
  160.   
  161.     return 0;  
  162. }  

实际运行的结果是:




对于统计5万个单词出现的次数

std::map耗时:3122毫秒

std::unordered_map耗时:2421毫秒

而我们写的Trie耗时:1332毫秒


可以看到,红黑树实现的std::map比桶状哈希表实现的std::unordered_map慢了差不多一秒

std::unordered_map又比Trie慢了差不多一秒。


这里有一个有趣的实验。

哈希表的Key类型用int,会不会快?

最后,我生成了5万个随机int整型整数,同时也把这5万个int转换成对应的string。

用key为int的哈希表和key为string的Trie做测试,看哪个快。

答案是:用key为string的Trie超过了key为int的哈希表

unordered_map耗时:1269毫秒

Trie耗时:750毫秒


6.最后的补充


Trie又称为字典树,是哈希树的一个变种。

Trie有一个特点是:有字符串公共前缀的信息

比如字符串"Siliphen"和字符串"Siliphen Lee"的公共前缀是"Siliphen"

在匹配字符串"Siliphen Lee"时,一定会先发现是否存在"Siliphen",

因为走的前缀树路径都是一样的。

是否还记得KMP算法。一种带有回溯的字符串匹配算法。

如果Trie+KMP的话,就变成另一个玩意:AC自动机。

AC自动机用于编译原理。

也可以用来做格斗游戏的摇招判定。就像拳皇KOF的那种摇招系统。



转自:http://blog.csdn.net/stevenkylelee/article/details/38343985

0 0