Trie樹主要分爲兩類,一類是靜態的,一次性構建,構建完成後只讀,另外一類是動態的,隨時能夠加入新的key。固然,對於動態構建,其寫過程,是不必定保證線程安全的。
對於trie的詳細分析,見這篇老外的文章:http://www.tkl.iis.u-tokyo.ac.jp/~ynaga/cedar/c++
此部份內容爲上邊文章的摘要數組
由於大多數trie都是靜態的,因此做者還加入了標準庫的map等非trie的數據結構做爲橫向對比
靜態的包括:安全
libdatrie 0.2.8: double-array trie數據結構
libtrie 0.1.1: double-array trie函數
dary 0.1.1: double-array trie性能
doar 0.0.13: double-array trieui
Darts 0.32: double-array triegoogle
Darts-clone 0.32g: directed acyclic word graphspa
Darts-clone 0.32e5: Compacted double-array trie線程
DASTrie 1.0: Compacted double-array trie
tx-trie* 0.18: LOUDS (Level-Order Unary Degree Sequence) trie
ux-trie* 0.1.9: LOUDS double-trie
marisa-trie* 0.2.4: LOUDS nested patricia trie
動態的包括
libdatrie 0.2.8: double-array trie
libtrie 0.1.1: double-array trie
dary 0.1.1: double-array trie
doar 0.0.13: double-array trie
critbit: crit-bit (patricia) tree [4]
libdict: splay tree [5], treap [6], skiplist [7]
C Containers library: scapegoat tree [8]
Andersson tree library: AA tree [9]
tst_vanilla: ternary search tree [10]
Judy Array 1.0.5: Judy trie SL [11]
hat-trie 0.1.0: HAT-trie [12]
array-hash Array Hash: (cache-conscious) hash table [13]
CMPH 2.0: hash table (w/ minimal perfect hash function [14])
std::map <std::string, int> (gcc 4.9.0): red-black tree
std::unordered_map <std::string, int> (gcc 4.9.0): hash table
cpp-btree 1.0.1: B-tree
sparsehash 2.0.2: hash table (sparsetable)
Software | Data Structure | Space [MiB] | Insert [ns/key] | Lookup [ns/key] |
---|---|---|---|---|
cedar | Double-array trie | 1173.02 | 631.06 | 50.40 |
cedar ORDERED=false | Double-array prefix trie | 671.66 | 786.02 | 49.99 |
libdatrie 0.2.8 | Double-array prefix trie | n/a | n/a | n/a |
libtrie 0.1.1 | Double-array two-trie | 2756.30 | 8116.16 | 185.85 |
dary | Double-array trie | 1119.04 | 1786.93 | 79.96 |
doar 0.0.13 | Compacted double-array trie | 2285.21 | 17687.60 | 83.41 |
critbit | Crit-bit (patricia) tree | 1457.02 | 1713.69 | 752.49 |
libdict | Splay tree | 1823.12 | 1541.48 | 229.34 |
libdict | Treap | 1823.13 | 1682.26 | 902.43 |
libdict | Skip list | 1852.86 | 1907.25 | 1265.79 |
Andersson tree library | AA tree | 1457.02 | 2100.03 | 337.14 |
C Containers library | Scapegoat tree | 1891.74 | 2380.65 | 254.34 |
tst_vanilla | ternary search tree | 3318.75 | 1109.25 | 129.12 |
Judy 1.0.5 | Judy trie SL | 897.59 | 580.67 | 142.64 |
hat-trie 0.1.0 | HAT-trie | 695.49 | 916.02 | 75.51 |
std::map | Red-black tree | 2506.27 | 1617.60 | 851.33 |
std::unordered_map | Hash table | 2471.60 | 615.30 | 170.41 |
array hash | Array Hash | 1725.56 | 17273.22 | 330.76 |
CMPH 2.0 | Hash table | 2741.03 | 2744.92 | 285.11 |
cpp-btree 1.0.1 | B-tree | 1744.96 | 1749.96 | 1080.04 |
sparsetable 2.0.2 | Sparse hash table | 1685.41 | 2635.32 | 157.63 |
sparsetable 2.0.2 (dense) | Hash table | 2335.04 | 502.66 | 123.3 |
能夠看出cedar
在動態trie中有是有明顯優點的,惟一的敗像不太難看的是google的sparsetable
,不過sparsetable
是hash表,在查詢和容量上都更差一些。一樣的hash表的unordered map
由於實現臃腫,速度更慢。
Software | Data Structure | Space [MiB] | Size [MiB] | Build [ns/key] | Lookup [ns/key] |
---|---|---|---|---|---|
cedar | Double-array trie | 832.82 | 816.54 | 183.57 | 38.95 |
cedar ORDERED=false | Double-array prefix trie | 490.59 | 488.35 | 221.87 | 39.07 |
libdatrie 0.2.8 | Double-array prefix trie | 1229.12 | 644.97 | 209955.04 | 124.66 |
libtrie 0.1.1 | Double-array two-trie | 2312.11 | 654.39 | 5401.59 | 181.95 |
dary | Double-array trie | 897.75 | 895.54 | 51144.92 | 57.90 |
doar 0.0.13 | Compacted double-array trie | 1937.25 | 334.59 | 990.51 | 48.00 |
Darts 0.32 | Double-array trie | 4306.02 | 858.93 | 2387.87 | 40.89 |
Darts-clone 0.32g | Directed-acyclic word graph | 2311.39 | 409.17 | 1339.14 | 36.39 |
Darts-clone 0.32e5 | Compacted double-array trie | 2779.10 | 309.31 | 1011.92 | 59.42 |
DASTrie 1.0 | Compacted double-array trie | 2626.16 | 383.37 | 92634.88 | 85.02 |
tx-trie 0.18 | LOUDS trie | 1791.10 | 113.11 | 626.90 | 972.32 |
ux-trie 0.1.9 | LOUDS two-trie | 2223.80 | 92.39 | 1229.11 | 1975.28 |
marisa-trie 0.2.4 | LOUDS nested patricia trie | 2036.49 | 87.27 | 698.76 | 194.87 |
ceder
是動態的,若是傳入的key是有序的,會減小內部的操做,因此速度也會提升。靜態trie中比較突出的是darts
系列。可是cedar
與其相比並不遜色,二者最終內存佔用和查詢速度相差無幾,可是cedar
的構建時間不到darts
的1/5。而且,darts系的構建過程會耗費大量內存,即峯值內存是cedar
的3倍以上。
綜上,選擇cedar做爲trie是可行的。
使用cedar
十分簡單,直接包含頭文件便可。
template <typename value_type, const int NO_VALUE = nan<value_type>::N1, const int NO_PATH = nan<value_type>::N2, const bool ORDERED = true, const int MAX_TRIAL = 1, const size_t NUM_TRACKING_NODES = 0> class da;
NO_VALUE的值是-1,NO_PATH的值是-2
由於其餘的模版參數都有默認值,通常只特化value_type
便可。
cedar::da<int> trie; trie.update("hello", strlen("hello"), 1);
cedar
的接口以下,選擇一些經常使用的進行介紹。須要說明的是原始代碼中的不少參數有歧義性。這裏我對參數名稱進行了修改,更符合直觀的含義。
template <...> class da { size_t capacity() const; size_t size() const; size_t total_size() const; size_t unit_size() const; size_t nonzero_size() const; // warning: O(size) size_t num_keys() const; // warning: O(size) template <typename T> T exactMatchSearch(const char* key) const; template <typename T> T exactMatchSearch(const char* key, size_t len, size_t from=0) const; template <typename T> size_t commonPrefixSearch(const char* str, T* result, size_t result_len) const; template <typename T> size_t commonPrefixSearch(const char* str, T* result, size_t result_len, size_t len, size_t from=0) const; template <typename T> size_t commonPrefixPredict(const char* str, T* result, size_t result_len); template <typename T> size_t commonPrefixPredict(const char* str, T* result, size_t result_len, size_t len, size_t from = 0); void suffix(char* key, size_t len, size_t to) const; value_type traverse(const char* key, size_t& from, size_t& pos) const; value_type traverse(const char* key, size_t& from, size_t& pos, size_t end_pos) const; value_type& update(const char* key); value_type& update(const char* key, size_t len, value_type val=value_type(0)); value_type& update(const char* key, size_t& from, size_t& pos, size_t len, value_type val=value_type(0)); template <typename T> value_type& update(const char* key, size_t& from, size_t& pos, size_t len, value_type val, T& cf) int erase(const char* key); int erase(const char* key, size_t len, size_t from = 0); void erase(size_t from); int build(size_t num, const char** key, const size_t* len = 0, const value_type* val = 0); template <typename T> void dump(T* result, const size_t result_len); int save(const char* fn, const char* mode = "wb") const; int open(const char* fn, const char* mode = "rb", const size_t offset = 0, size_t size_ = 0); void restore() void set_array(void* p, size_t size_ = 0); const void* array() const; void clear(const bool reuse = true); int begin(size_t& from, size_t& len); int next(size_t& from, size_t& len, const size_t root=0); void test(const size_t from=0) const; };
value_type& update(const char* key); // update(key, from=0, len=strlen(key), val=0) value_type& update(const char* key, size_t len, value_type val=value_type(0)); // update(key, from=0, len, val) value_type& update(const char* key, size_t& from, size_t& pos, size_t len, value_type val=value_type(0));
插入key,value爲0
插入key的[0,len)子串
附加key的[pos,len)子串,到from對應的前綴後
關於from 表示附加到表明節點所對應的前綴後。例如,若是from==0,表示從root開始附加,即以子串做爲key。若是from=1000表示的節點是abc,則插入的key是abc+子串。
關於val update的代碼中,沒有設置val的節點value爲0,若是設置了節點則value += val
。這樣會有一個很致命的細節,若是屢次更新同一個key,那麼val值不是覆蓋而是累加!這是一個很大的坑,必定要注意。
int erase(const char* key); int erase(const char* key, size_t len, size_t from = 0); void erase(size_t from);
找到key對應的節點,並刪除(清空value)
找到節點:以from爲前綴,附加key的[0, len)子串的key對應的節點。並刪除
刪除節點
int build(size_t num, const char** key, const size_t* len=NULL, const value_type* val=NULL);
仿照darts的接口。num爲數組的大小。key是cstr的數組。len是key對應的長度列表。val是key對應的值列表
關於排序 cedar
是不須要死板的build的,這裏只是爲了兼容darts
的接口,內層實際上是循環調用update。因此key是不須要有序的。
關於val `build內層實際上是循環調用update。因而,update中關於val的細節依然適用。若是有重複的key,那麼val值不是覆蓋而是累加
template <typename T> T exactMatchSearch(const char* key) const; // exactMatchSearch(key, len=strlen(key), from=0); template <typename T> T exactMatchSearch(const char* key, size_t len, size_t from=0) const;
在內部查找中,不管是NO_PATH(N2),仍是NO_VALUE(N1),都返回NO_VALUE(N1)。
這個和darts的行爲是一致的。
須要注意的是,這個函數是模板函數,而且沒法經過參數推算模版,因此必須顯式的指定類型:exactMatchSearch<int>(...)
template <typename T> size_t commonPrefixSearch(const char* str, T* result, size_t result_len) const; // commonPrefixSearch(str, result, result_len, len=strlen(key), from=0); template <typename T> size_t commonPrefixSearch(const char* str, T* result, size_t result_len, size_t len, size_t from=0) const;
返回的是剛好爲str的前綴的key的集合。例如"helloworld" -> ["hell", "hello"]
返回的是找到的結果數,參數中的result_len是result的容量。若是有10個結果,可是result_len爲5的話,只會寫出5個結果,可是返回值是10
template <typename T> size_t commonPrefixPredict(const char* str, T* result, size_t result_len); // commonPrefixPredict(str, result, result_len, len=strlen(key), from=0); template <typename T> size_t commonPrefixPredict(const char* str, T* result, size_t result_len, size_t len, size_t from = 0);
返回的以給定的str爲前綴的key的集合。例如"he" -> ["hell", "hello", "help"]
返回的是找到的結果數,參數中的result_len是result的容量。若是有10個結果,可是result_len爲5的話,只會寫出5個結果,可是返回值是10
value_type traverse(const char* key, size_t& from, size_t& pos) const; // traverse(key, form, pos, end_pos=strlen(key)) value_type traverse(const char* key, size_t& from, size_t& pos, size_t end_pos) const;
trie中最重要的函數,能夠最靈活的查找trie重點是依據返回值來斷定traverse的結果若是返回NO_VALUE(N1),說明有key的前綴是當前[pos, end_pos)子串,但沒有精確匹配。若是返回NO_PATH(N2),說明當前子串對應的路徑在trie中不存在。若是返回其餘值,說明當前子串對應表示的key剛好在trie中。