Trie實踐:一種比雜湊表還快的資料結構,trie雜湊資料結構
本文乃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範圍的數字出現的次數?
可以用雜湊表來進行統計。如下:
// 產生5萬個0-99範圍的隨機數int * pNumbers = new int[ 50000 ] ;for ( int i = 0 ; i < 50000 ; ++i ){pNumbers[ i ] = rand( ) % 100 ;}// 統計每個數字出現個次數unordered_map< int , int > Counter ;for ( int i = 0 ; i < 50000 ; ++i ){++Counter[ pNumbers[ i ] ] ;}
普通的桶狀雜湊表可能是有衝突的,這取決於雜湊函數的設計。
如果有衝突,那麼就會退化成線性尋找。
對於這個問題,有一種更好的做法,就是“直接定址表”
“直接定址表”的概念第一次我是在王爽著的《組合語言》看到
使用“直接定址表”需要滿足一些條件,比如:值剛好就是key
上面那題用直接定址表來統計的話,實現是這樣:
// 統計每個數字出現個次數int Counter[ 100 ] = { 0 } ; for ( int i = 0 ; i < 50000 ; ++i ){++Counter[ pNumbers[ i ] ] ;}
以上代碼只是把雜湊表容器換成了一個數組。數組的0-99的下標範圍就是表示0-99個數字,
下標對應的元素值就是該下標表示的數位出現次數。
2.為什麼直接定址表會比桶狀雜湊表快
直接定址表也是雜湊的一種,只是比較特殊。
直接定址表不需要計算雜湊散列值,既然沒有雜湊散列值自然就不存在雜湊衝突處理了。
這就是直接定址表比桶狀雜湊表快的原因
3.初識Trie資料結構
再考慮這樣一個問題:如何統計5萬個單詞出現的次數?
哈,這個有點難度了吧?只能用雜湊表來做了吧?
實現是不是像這樣:
vector< string > words ;// 產生5萬個隨機單詞,略。。。// 統計每個數字出現個次數unordered_map< string , int > Counter ;for ( int i = 0 ; i < 50000 ; ++i ){++Counter[ words[ i ] ] ;}
還有沒有更快的統計方法呢?
首先我們來看下桶狀雜湊表慢在哪裡,有2點
1.對每個字串key都要執行一次雜湊散列函數
2.如果雜湊散列有衝突的話,就要做衝突處理
要提速,就要把這2點給幹掉,不計算雜湊散列,不做衝突處理。
咦!這不就是之前說的“直接定址表”嗎?
那用“直接定址表”怎樣做字串的統計?
如果,你自認為自己是一個天才的話,看到這裡,就先別往下看。
先自己想想:怎樣用直接定址表的思想來做字串的統計、尋找。
答案那就是Trie資料結構。Trie是啥?
簡單地說,Trie就是直接定址表和樹的結合的產物。
Trie其實是一種樹結構,既然是樹,那就會有樹節點,
Trie樹節點的特殊在於:一個節點的子節點就是一個直接定址表
Trie樹節點的定義類似如下:
// Trie樹節點struct TrieNode{// 節點的值int Val ; // 子節點Node* Children[ 256 ] ;};
要直觀地用圖形表示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寫的一個模板類
代碼如下:
#pragma once#include <string>#include <queue>#include <stack>#include <list>using namespace std ;template< typename Value_t >class TireMap{public:TireMap( );~TireMap( ) ;private:typedef pair< string , Value_t > Kv_t ;struct Node{Kv_t * pKv ;Node* Children[ 256 ] ;Node( ) :pKv( 0 ){memset( Children , 0 , sizeof( Children ) ) ;}~Node( ){if ( pKv != 0 ){//delete pKv ;}}};public : /*重載[ ] 運算子。和 map , unorder_map 容器介面一樣。*/Value_t& operator[ ]( const string& strKey ) ;// 清除儲存的資料void clear( ) ;public : const list< Kv_t >& GetKeyValueList( ) const { return m_Kvs ; }protected:// 刪除一棵樹static void DeleteTree( Node *pNode ) ; protected:// 樹根節點Node * m_pRoot ; // 映射的索引值列表list< Kv_t > m_Kvs ;};template< typename Value_t >TireMap<Value_t>::TireMap( ){m_pRoot = new Node( ) ;}template< typename Value_t >TireMap<Value_t>::~TireMap( ){clear( ) ;delete m_pRoot ;}template< typename Value_t >void TireMap<Value_t>::clear( ){for ( int i = 0 ; i < 256 ; ++i ){if ( m_pRoot->Children[ i ] != 0 ){DeleteTree( m_pRoot->Children[ i ] ) ;m_pRoot->Children[ i ] = 0 ;}}m_Kvs.clear( ) ; }template< typename Value_t >void TireMap<Value_t>::DeleteTree( Node * pRoot ){// BFS 刪除樹stack< Node* > stk ; stk.push( pRoot ) ; for ( ; stk.size( ) > 0 ; ){Node * p = stk.top( ) ; stk.pop( ) ;// 擴充for ( int i = 0 ; i < 256 ; ++i ){Node* p2 = p->Children[ i ] ;if ( p2 == 0 ){continue; }stk.push( p2 ) ;}delete p ; }}template< typename Value_t >Value_t& TireMap<Value_t>::operator[]( const string& strKey ){Node * pNode = m_pRoot ;// 建立或者尋找樹路徑for ( size_t i = 0 , size = strKey.size( ) ; i < size ; ++i ){const char& ch = strKey[ i ] ;Node*& Child = pNode->Children[ ch ] ;if ( Child == 0 ){pNode = Child = new Node( ) ;}else{pNode = Child ;}}// end for// 如果沒有資料欄位的話,就產生一個。if ( pNode->pKv == 0 ){m_Kvs.push_back( Kv_t( strKey , Value_t() ) ) ;pNode->pKv = &*( --m_Kvs.end( ) ) ;}return pNode->pKv->second ; }
有沒有std的感覺?哈哈
核心代碼就是[]運算子多載的實現。
為什麼要我搞一個list< Kv_t > m_Kvs欄位?
這個欄位主要是用來方便查看結果。
OK。下面我們來寫測試代碼
看看 Trie , 與 std::map , std::unordered_map之間的差別
測試代碼如下:
#include <string>#include <vector>#include <unordered_map>#include <map>#include <time.h>#include "TireMap.h"using namespace std ;// 隨機產生 Count 個隨機字元組合的“單詞”template< typename StringList_t >int CreateStirngs( StringList_t& strings , int Count ){int nTimeStart , nElapsed ;nTimeStart = clock( ) ;strings.clear( ) ;for ( int i = 0 ; i < Count ; ++i ){int stringLen = 5 ;string str ;for ( int i = 0 ; i < stringLen ; ++i ){char ch = 'a' + rand( ) % ( 'z' - 'a' + 1 ) ;str.push_back( ch ) ;if ( ch == 'z' ){int a = 1 ; }}strings.push_back( str ) ;}nElapsed = clock( ) - nTimeStart ;return nElapsed ; }// 建立 Count 個整型資料。同樣建立這些整型對應的字串template< typename StringList_t , typename IntList_t >int CreateNumbers( StringList_t& strings , IntList_t& Ints , int Count ){strings.clear( ) ; Ints.clear( ) ; for ( int i = 0 ; i < Count ; ++i ){int n =rand( ) % 0x00FFFFFF ; char sz[ 256 ] = { 0 } ;_itoa_s( n , sz , 10 ) ; strings.push_back( sz ) ;Ints.push_back( n ) ;}return 0 ;}// Tire 正確性檢查string Check( const unordered_map< string , int >& Right , const TireMap< int >& Tire ){string strInfo = "Tire 統計正確" ;const auto& TireRet = Tire.GetKeyValueList( ) ;unordered_map< string , int > ttt ;for ( auto& kv : TireRet ){ttt[ kv.first ] = kv.second ;}if ( ttt.size( ) != Right.size( ) ){strInfo = "Tire統計有錯" ;}else{for ( auto& kv : ttt ){auto it = Right.find( kv.first ) ;if ( it == Right.end( ) ){strInfo = "Tire統計有錯" ;break ;}else if ( kv.second != it->second ){strInfo = "Tire統計有錯" ;break ;}}}return strInfo ; }// 統計模板函數。可以用map , unordered_map , TrieMap 做統計template< typename StringList_t , typename Counter_t >int Count( const StringList_t& strings , Counter_t& Counter ){int nTimeStart , nElapsed ;nTimeStart = clock( ) ;map< string , int > Counter1 ;for ( const auto& str : strings ){++Counter[ str ] ;}nElapsed = clock( ) - nTimeStart ;return nElapsed ;}int _tmain( int argc , _TCHAR* argv[ ] ){map< string , int > ElapsedInfo ;int nTimeStart , nElapsed ;// 產生50000個隨機單詞list< string > strings ;nElapsed = CreateStirngs( strings , 50000 ) ;//ElapsedInfo[ "產生單詞 耗時" ] = nElapsed ;// 用 map 做統計map< string , int > Counter1 ;nElapsed = Count( strings , Counter1 ) ;ElapsedInfo[ "統計單詞 用map 耗時" ] = nElapsed ;// 用 unordered_map 做統計unordered_map< string , int > Counter2 ;nElapsed = Count( strings , Counter2 ) ;ElapsedInfo[ "統計單詞 用unordered_map 耗時" ] = nElapsed ;// 用 Tire 做統計TireMap< int > Counter3 ;nElapsed = Count( strings , Counter3 ) ;ElapsedInfo[ "統計單詞 用Tire 耗時" ] = nElapsed ;// Tire 統計的結果。正確性檢查string CheckRet = Check( Counter2 , Counter3 ) ; // 用雜湊表統計5萬個整形數字出現的次數// 與 用Tire統計同樣的5萬個整形數字出現的次數的 對比// 當然,用Tire統計的話,先要把那5萬個整形資料,轉換成對應的字串的表示。list< int > Ints ; CreateNumbers( strings , Ints , 50000 ) ; unordered_map< int , int > kivi ;nTimeStart = clock( ) ;for ( const auto& num : Ints ){++kivi[ num ] ;}nElapsed = clock( ) - nTimeStart ;ElapsedInfo[ "統計數字 unordered_map 耗時" ] = nElapsed ; //Counter3.clear( ) ; 這句話非常耗時。因為要遍曆樹逐個delete樹節點。樹有可能會非常大。所以我注釋掉nElapsed = Count( strings , Counter3 ) ;ElapsedInfo[ "統計數字 用Tire 耗時" ] = nElapsed ;return 0;}
實際啟動並執行結果是:
對於統計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的那種搖招系統。
有關資料結構雜湊表的問題?
舉個簡單的例子:
有一百個數字1-100,隨機產生20個,求20個不重複的數的和。
例如:1,1,1,1,1,1,1,1,1,1,2,2,3,6,3,2,3,2,3,2
則20個不重複的數的和=1+2+3+6=12
main()
{
int num;
while(迴圈20次)
{
num = GetNumber();//得到一個隨機數字
掃面鏈表;
if(鏈表裡面沒有這個數字)
{
把得到的數字加到鏈表裡;
result+= num;
}
}
}
上面的思想是,每次得到一個數字,讓它和鏈表裡的數字依依比較,如果連表裡面沒有,就把它直接加到連表裡。如果連表裡的東西多了的話,那麼就要比較很多次,很浪費時間。
如果用哈西表的話,就可以通過尋找表,一次就確定數字是否重複:
main()
{
int num;
int hash[101];//初始化都等於0
while(迴圈20次)
{
num = GetNumber();//得到一個隨機數字
if(hash[num]==0)
{
hash[num]=1;
result+= num;
}
}
}
資料結構 雜湊表建立
有些圖打不上去。如果想要完整的資料告訴我郵箱,我發給你 。
雜湊表及其應用
一、定義
二、基本原理
雜湊表的基本原理是:使用一個下標範圍比較大的數組A來儲存元素,設計一個函數h,對於要儲存的線性表的每個元素node,取一個關鍵字key,算出一個函數值h(key),把h(key)作為數組下標,用A[h(key)]這個數組單元來儲存node。也可以簡單的理解為,按照關鍵字為每一個元素“分類”,然後將這個元素儲存在相應“類”所對應的地方(這一過程稱為“直接定址”)。
但是,不能夠保證每個元素的關鍵字與函數值是一一對應的,因此極有可能出現對於不同的元素,卻計算出了相同的函數值,這樣就產生了“衝突”,換句話說,就是把不同的元素分在了相同的“類”之中。例如,假設一個結點的關鍵碼值為key,把它存入雜湊表的過程是:根據確定的函數h計算出h(key)的值,如果以該值為地址的儲存空間還沒有被佔用,那麼就把結點存入該單元;如果此值所指單元裡已存了別的結點(即發生了衝突),那麼就再用另一個函數I進行映象算出I(h(key)),再看用這個值作為地址的單元是否已被佔用了,若已被佔用,則再用I映象,……,直到找到一個空位置將結點存入為止。當然這隻是解決“衝突”的一種簡單方法,如何避免、減少和處理“衝突”是使用雜湊表的一個難題。
在雜湊表中尋找的過程與建立雜湊表的過程相似,首先計算h(key)的值,以該值為地址到基本地區中去尋找。如果該地址對應的空間未被佔用,則說明尋找失敗,否則用該結點的關鍵碼值與要找的key比較,如果相等則檢索成功,否則要繼續用函數I計算I(h(key))的值,……。如此反覆到某步或者求出的某地址空間未被佔用(尋找失敗)或者比較相等(尋找成功)為止。
三、基本概念和簡單實現
1、兩個集合:U是所有可能出現的關鍵字集合;K是實際儲存的關鍵字集合。
2、函數h將U映射到表T[0..m-1]的下標上,可以表示成 h:U→{0,1,2,...,m-1},通常稱h為“雜湊函數(Hash Function)”,其作用是壓縮待處理的下標範圍,使待處理的|U|個值減少到m個值,從而降低空間開銷(註:|U|表示U中關鍵字的個數,下同)。
3、將結點按其關鍵字的散列地址儲存到雜湊表(散列表)中的過程稱為“散列(Hashing)”。方法稱為“散列法”。
4、h(Ki)(Ki∈U)是關鍵字為Ki的結點的“儲存地址”,亦稱散列值、散列地址、雜湊地址。
5、用散列法儲存的線性表稱為“雜湊表(Hash Table)”,又稱散列表。圖中T即為雜湊表。在散列表裡可以對結點進行快速檢索(尋找)。
6、對於關鍵字為key的結點,按照雜湊函數h計算出地址h(key),若發現此地址已被別的結點佔用,也就是說有兩個不同的關鍵碼值key1和key2對應到同一個地址,即h(key1)=h(key2),這個現象叫做“衝突(碰撞)”。碰撞的兩個(或多個)關鍵碼稱為“同義字”(相對於函數h而言)。1中的關鍵字k2和k5,h(k2)=h(k5),即發生了“衝突”,所以k2和k5稱為“同義字”。假如先存了k2,則對於k5,我們可以儲存在h(k2)+1中,當然h(k2)+1要為空白,否則可以逐個往後找一個空位存放。這是另外一種簡單的解決衝突的方法。
發生了碰撞就要想辦法解決,必須想辦法找到另外一個新地址,這當然要降低處理效率,因此我們希望盡量減少碰撞的發生。這就需要分析關鍵碼集合的特性,找適當的雜湊函數h使得計算出的地址儘可能“均勻分布”在地址空間中。同時,為了提高關鍵碼到地址轉換的速度,也希望雜湊函數“盡......餘下全文>>