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

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

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

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

  3.初识Trie数据结构

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

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

  6.最后的补充

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

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

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

[cpp] view plaincopyprint?

  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. unordered_map< int , int > Counter ;
  9. for ( int i = 0 ; i < 50000 ; ++i )
  10. {
  11. ++Counter[ pNumbers[ i ] ] ;
  12. }

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

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

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

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

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

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

[cpp] view plaincopyprint?

  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?

  1. vector< string > words ;
  2. // 生成5万个随机单词,略。。。
  3. // 统计每个数字出现个次数
  4. unordered_map< string , int > Counter ;
  5. for ( int i = 0 ; i < 50000 ; ++i )
  6. {
  7. ++Counter[ words[ i ] ] ;
  8. }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

[cpp] view plaincopyprint?

  1. // Trie树节点
  2. struct TrieNode
  3. {
  4. // 节点的值
  5. int Val ;
  6. // 子节点
  7. Node* Children[ 256 ] ;
  8. };

要直观地用图形表示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?

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

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

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

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

测试代码如下:

[cpp] view plaincopyprint?

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

实际运行的结果是:

对于统计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的那种摇招系统。

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

时间: 2024-08-11 05:45:48

Trie实践:一种比哈希表还快的数据结构的相关文章

Redis源码研究—哈希表

Redis源码研究-哈希表 Category: NoSQL数据库 View: 10,980 Author: Dong 作者:Dong | 新浪微博:西成懂 | 可以转载, 但必须以超链接形式标明文章原始出处和作者信息及版权声明 网址:http://dongxicheng.org/nosql/redis-code-hashtable/ 本博客的文章集合:http://dongxicheng.org/recommend/ 本博客微信公共账号:hadoop123(微信号为:hadoop-123),分享

哈希(2) - 垂直打印一棵二叉树(使用哈希表实现)

垂直打印给定的一棵二叉树.下面的例子演示了垂直遍历的顺序. 1 / 2 3 / \ / 4 5 6 7 \ 8 9 对这棵树的垂直遍历结果为: 4 2 1 5 6 3 8 7 9 在二叉树系列中,已经讨论过了一种O(n2)的方案.在本篇中,将讨论一种基于哈希表的更优的方法.首先在水平方向上检测所有节点到root的距离.如果两个node拥有相同的水平距离(Horizontal Distance,简称HD), 则它们在相同的垂直线上.root自身的HD为0,右侧的node的HD递增,即+1,而左侧的

数据结构与算法分析:哈希表

以下是阅读了<算法导论>后,对哈希表的一些总结: 哈希表又叫散列表,是实现字典操作的一种有效数据结构.哈希表的查询效率极高,在没有冲突(后面会介绍)的情况下可做到一次存取便能得到所查记录,在理想情况下,查找一个元素的平均时间为O(1)(最差情况下散列表中查找一个元素的时间与链表中查找的时间相同:O(n),但实际情况中一般散列表的性能是比较好的). 哈希表就是描述key-value对的映射问题的数据结构,更详细的描述是:在记录的存储位置和它的关键字之间建立一个确定的对应关系h,使每个关键字与哈希

C#中哈希表与List的比较

简单概念 在c#中,List是顺序线性表(非链表),用一组地址连续的存储单元依次存储数据元素的线性结构. 哈希表也叫散列表,是一种通过把关键码值映射到表中一个位置来访问记录的数据结构.c#中的哈希表有Hashtable,Dictionary,Hashtable继承自Map,实现一个key-value映射的关系.Dictionary则是一种泛型哈希表,不同于Hashtable的key无序,Dictionary是按照顺序存储的.哈希表的特点是:1.查找速度快,2.不能有重复的key. 创建过程 在c

Redis源码研究:哈希表 - 蕫的博客

[http://dongxicheng.org/nosql/redis-code-hashtable/] 1. Redis中的哈希表 前面提到Redis是个key/value存储系统,学过数据结构的人都知道,key/value最简单的数据结果就是哈希表(当然,还有其他方式,如B-树,二叉平衡树等),hash表的性能取决于两个因素:hash表的大小和解决冲突的方法.这两个是矛盾的:hash表大,则冲突少,但是用内存过大:而hash表小,则内存使用少,但冲突多,性能低.一个好的hash表会权衡这两个

【STL】哈希表 uthash.h

散列表(Hash table,也叫哈希表),是根据关键字(Key value)而直接访问在内存存储位置的数据结构.线性表查找的时间复杂度为O(n)而平衡二叉树的查找的时间复杂度为O(log(n)).无论是采用线程表或是树进行存储,都面临面随着数据量的增大,查找速度将不同程度变慢的问题.而哈希表正好解决了这个问题. 给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数 函

js中哈希表的几种用法总结

本篇文章只要是对js中哈希表的几种用法进行了总结介绍,需要的朋友可以过来参考下,希望对大家有所帮助 1. <html> <head> <script type="text/javascript"> // by Go_Rush(脚本之家) from http://www.jb51.net/ var hash={ "百度" :"http://www.baidu.com/", "Google" :

if 循环的深入理解 哈希表的一种应用

哈希表的值作为一个颜色容器,值默认为标识1, 表示未曾用过,若用过标识为0: 1: 程序第一步    遍历哈希表,查找标识为1 未曾用过的颜色 我用了这个: string colorno_use=""; foreach (string key in ht.Keys) { if(Convert.ToInt32(ht[key])==1)  //这个结果导致 所有为1的主键 都被循环了. 当然导致了后边的程序错乱问题.  { colorno_use = key; ht[key] = 0; +

【搜索引擎(二)】索引、倒排索引、哈希表、跳表

索引 其实在计算机中我们早已接触过跟索引有关的东西,比如数据库里的索引(index),还有硬盘文件系统中其实也有类似的东西,简而言之,索引是一种为了方便找到自己需要的东西而设计出来的条目,你可以通过找索引找到自己想要内容的位置.索引过程是: 关键字->索引->文档.在图书馆内的书分门别类,就是一种按类别来分的索引.当然索引还有很多其他的实现. 仅仅有索引的概念是不够的.虽然分门别类是一种方法,但是我们在拥有一堆文档的时候必须要有从文档到索引的规范过程,并且索引的结构要满足能够让人(或者计算机)