C++ Trie樹:cedar

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;
};

update

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));
  1. 插入key,value爲0

  2. 插入key的[0,len)子串

  3. 附加key的[pos,len)子串,到from對應的前綴後

關於from 表示附加到表明節點所對應的前綴後。例如,若是from==0,表示從root開始附加,即以子串做爲key。若是from=1000表示的節點是abc,則插入的key是abc+子串。
關於val update的代碼中,沒有設置val的節點value爲0,若是設置了節點則value += val。這樣會有一個很致命的細節,若是屢次更新同一個key,那麼val值不是覆蓋而是累加!這是一個很大的坑,必定要注意。

erase

int erase(const char* key);
int erase(const char* key, size_t len, size_t from = 0);
void erase(size_t from);
  1. 找到key對應的節點,並刪除(清空value)

  2. 找到節點:以from爲前綴,附加key的[0, len)子串的key對應的節點。並刪除

  3. 刪除節點

build

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值不是覆蓋而是累加

exactMatchSearch

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>(...)

commonPrefixSearch

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

commonPrefixPredict

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

traverse

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中。

相關文章
相關標籤/搜索