在 C/C++ 中是否有任何速度和缓存高效的 trie 实现?我知道 trie 是什么,但我不想重新发明轮子,自己实现它。
12 回答
如果您正在寻找 ANSI C 实现,您可以从 FreeBSD 中“窃取”它。您要查找的文件名为radix.c。它用于管理内核中的路由数据。
我意识到问题是关于现成的实现,但仅供参考......
在你开始讨论 Judy 之前,你应该阅读“ A Performance Comparison of Judy to Hash Tables ”。然后谷歌搜索标题可能会给你一生的讨论和反驳阅读。
我所知道的一个明确的缓存意识 trie 是HAT-trie。
HAT-trie 在正确实施时非常酷。但是,对于前缀搜索,您需要对哈希桶进行排序,这与前缀结构的想法有些冲突。
一个更简单的 trie 是Burst-trie,它本质上为您提供了某种标准树(如 BST)和 trie 之间的插值。我在概念上喜欢它,而且它更容易实现。
我在libTrie上运气不错。它可能没有经过专门的缓存优化,但对于我的应用程序来说性能一直不错。
GCC 附带了一些数据结构作为其“基于策略的数据结构”的一部分。这包括一些 trie 实现。
http://gcc.gnu.org/onlinedocs/libstdc++/ext/pb_ds/trie_based_containers.html
参考,
- Double-Array Trie 实现文章(包括
C
实现) - TRASH - A dynamic LC-trie and hash data structure --(2006 PDF 参考,描述了在 Linux 内核中用于实现 IP 路由表中地址查找的动态 LC-trie
Judy 数组:非常快速且内存高效的有序稀疏动态数组,用于位、整数和字符串。Judy 数组比任何二叉搜索树(包括 avl 和红黑树)都更快且内存效率更高。
您也可以在http://tommyds.sourceforge.net/尝试 TommyDS
有关与 nedtries 和 judy 的速度比较,请参阅网站上的基准页面。
缓存优化是您可能必须要做的事情,因为您必须将数据放入单个缓存行中,该缓存行通常约为 64 字节(如果您开始组合数据,例如指针,这可能会起作用) . 但这很棘手:-)
Burst Trie似乎更节省空间。我不确定您可以从任何索引中获得多少缓存效率,因为 CPU 缓存非常小。但是,这种 trie 足够紧凑,可以将大型数据集保存在 RAM 中(常规 Trie 不会)。
我编写了一个 Burst trie 的 Scala 实现,它还结合了我在 GWT 的 type-ahead 实现中发现的一些节省空间的技术。
与我的数据集提到的几个 TRIE 实现相比,我使用 marisa-trie 获得了非常好的结果(性能和大小之间的非常好的平衡)。
分享我对 Trie 的“快速”实现,仅在基本场景中进行了测试:
#define ENG_LET 26
class Trie {
class TrieNode {
public:
TrieNode* sons[ENG_LET];
int strsInBranch;
bool isEndOfStr;
void print() {
cout << "----------------" << endl;
cout << "sons..";
for(int i=0; i<ENG_LET; ++i) {
if(sons[i] != NULL)
cout << " " << (char)('a'+i);
}
cout << endl;
cout << "strsInBranch = " << strsInBranch << endl;
cout << "isEndOfStr = " << isEndOfStr << endl;
for(int i=0; i<ENG_LET; ++i) {
if(sons[i] != NULL)
sons[i]->print();
}
}
TrieNode(bool _isEndOfStr = false):isEndOfStr(_isEndOfStr), strsInBranch(0) {
for(int i=0; i<ENG_LET; ++i) {
sons[i] = NULL;
}
}
~TrieNode() {
for(int i=0; i<ENG_LET; ++i) {
delete sons[i];
sons[i] = NULL;
}
}
};
TrieNode* head;
public:
Trie() { head = new TrieNode();}
~Trie() { delete head; }
void print() {
cout << "Preorder Print : " << endl;
head->print();
}
bool isExists(const string s) {
TrieNode* curr = head;
for(int i=0; i<s.size(); ++i) {
int letIdx = s[i]-'a';
if(curr->sons[letIdx] == NULL) {
return false;
}
curr = curr->sons[letIdx];
}
return curr->isEndOfStr;
}
void addString(const string& s) {
if(isExists(s))
return;
TrieNode* curr = head;
for(int i=0; i<s.size(); ++i) {
int letIdx = s[i]-'a';
if(curr->sons[letIdx] == NULL) {
curr->sons[letIdx] = new TrieNode();
}
++curr->strsInBranch;
curr = curr->sons[letIdx];
}
++curr->strsInBranch;
curr->isEndOfStr = true;
}
void removeString(const string& s) {
if(!isExists(s))
return;
TrieNode* curr = head;
for(int i=0; i<s.size(); ++i) {
int letIdx = s[i]-'a';
if(curr->sons[letIdx] == NULL) {
assert(false);
return; //string not exists, will not reach here
}
if(curr->strsInBranch==1) { //just 1 str that we want remove, remove the whole branch
delete curr;
return;
}
//more than 1 son
--curr->strsInBranch;
curr = curr->sons[letIdx];
}
curr->isEndOfStr = false;
}
void clear() {
for(int i=0; i<ENG_LET; ++i) {
delete head->sons[i];
head->sons[i] = NULL;
}
}
};