前一段时间写了个微型输入法,使用map进行查找,发现效率不高。正好最近也在针对一个查找功能做优化,针对我的应用场景使用了Trie字典树。

特性

Trie树属于树形结构,查询效率比红黑树和哈希表都要快。假设有这么一种应用场景:有若干个英文单词,需要快速查找某个单词是否存在于字典中。使用Trie时先从根节点开始查找,直至匹配到给出字符串的最后一个节点。在建立字典树结构时,预先把带有相同前缀的单词合并在同一节点,直至两个单词的某一个字母不同,则再从发生差异的节点中分叉一个子节点。

节点结构:
每个节点对应一个最大可储存字符数组。假设字典只存26个小写英文字母,那么每个节点下应该有一个长度为26的数组。换言说,可存的元素类型越多,单个节点占用内存越大。如果用字典树储存汉字,那么每个节点必须为数千个常用汉字开辟一个数组作为储存空间,占用的内存实在不是一个数量级。不过Trie树就是一种用空间换时间的数据结构,鱼和熊掌往往不可兼得。

建树细节:

  • 取要插入字符串的首个字符,从根节点的孩子节点开始,匹配当前字符是否已有节点,有则把指针指向该节点。无则为该字符创建节点,并把指针指向该新建节点。
  • 迭代。
  • 遇到要插入字符串末尾结束符时停止迭代,并把最后一个非’\0’字符对应的节点设为末端节点。
    查找细节:
    循环取要插入字符串的首个字符,从根节点的孩子节点开始,匹配当前字符是否已有节点,有则继续循环,无则返回False. 直至匹配到最后一个字符则完成查找。

树结构图:
我们用apps, apply, apple, append, back, basic, backen几英文单词创建树形结构:
trie

上图很容易看出,有相同前缀的英文单词,会合并在同一个节点,Trie树顺着一个个节点进行检索,直至找到最后一个节点。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#include <stdio.h>;

struct trie_node
{
static const int letter_count = 26;

int count;
bool is_terminal;
char letter;
trie_node* childs[letter_count];

trie_node()
: letter(0), count(1), is_terminal(false)
{
for (int i = 0; i &lt; letter_count; ++i)
childs[i] = NULL;
}
};

class trie
{
public:
trie()
: root_node_(NULL)
{
}

~trie()
{
delete_trie(root_node_);
}

public:
trie_node* create()
{
trie_node* n = new trie_node();
return n;
}

void insert(const char* str)
{
if (!root_node_ || !str)
root_node_ = create();

trie_node* next_element_node = root_node_;
while (*str != 0)
{
char element_index = *str - 'a';
if (!next_element_node-&gt;childs[element_index])
{
next_element_node-&gt;childs[element_index] = create();
}
else
{
next_element_node-&gt;childs[element_index]-&gt;count++;
}

next_element_node = next_element_node-&gt;childs[element_index];
next_element_node-&gt;letter = *str;
str++;
}

next_element_node-&gt;is_terminal = true;
}

bool find_word_exists(const char* str)
{
if (!root_node_ || !str)
return NULL;

trie_node* element_node = root_node_;
do
{
element_node = element_node-&gt;childs[*str - 'a'];
if (!element_node) return false;
str++;
} while (*str != 0);

return element_node-&gt;is_terminal;
}

void delete_trie(trie_node* node)
{
if (!node) return;
for(int i = 0; i &lt; trie_node::letter_count; i++)
{
if(node-&gt;childs[i] != NULL)
delete_trie(node-&gt;childs[i]);
}

delete node;
}

private:
trie_node* root_node_;
};

Trie、红黑树和哈希表的效率对比

为了测试三者效率,我使用了一份大约有20万个英文词汇,大小约2.2MB的字典文件分别向Trie、红黑树和哈希表进行插入、查询两种操作对比,并做了时间计算。

测试环境:

操作系统

CentOS 64-bit

CPU

Intel(R) Core(TM) i7-2600K

编译环境

g++ 4.4.7



测试代码:

[cpp]

#include <string>

#include <iostream>

#include <fstream>

#include <set>

#include <unordered_map>

#include <sys/time.h>

int main(int argc, char* argv[])
{
std::set<std::string> rbtree_dict;
std::unordered_map<std::string, std::string> hash_map_dict;

trie t;
long time_sp = 0, time_sp_rbtree = 0, time_sp_hash_map = 0;
std::ifstream stream_in(&quot;./dict.txt&quot;, std::ios::in);
if (stream_in.is_open())
{
    int count = 0;
    int lineLength = 1024;
    char* buffer = new char[lineLength];
    while(stream_in.getline(buffer, lineLength))
    {
        count++;
        timeval trie_tv_start, trie_tv_end, rbtree_tv_start, rbtree_tv_end, hashmap_tv_start, hashmap_tv_end;

        gettimeofday(&amp;trie_tv_start, NULL);
        long trie_start = ((long)trie_tv_start.tv_sec) * 1000 + (long)trie_tv_start.tv_usec / 1000;
        t.insert(buffer);
        gettimeofday(&amp;trie_tv_end, NULL);
        time_sp += (((long)trie_tv_end.tv_sec) * 1000 + (long)trie_tv_end.tv_usec / 1000) - trie_start;

        gettimeofday(&amp;rbtree_tv_start, NULL);
        long rbtree_start = ((long)rbtree_tv_start.tv_sec) * 1000 + (long)rbtree_tv_start.tv_usec / 1000;
        rbtree_dict.insert(buffer);
        gettimeofday(&amp;rbtree_tv_end, NULL);
        time_sp_rbtree += (((long)rbtree_tv_end.tv_sec) * 1000 + (long)rbtree_tv_end.tv_usec / 1000) - rbtree_start;

        gettimeofday(&amp;hashmap_tv_start, NULL);
        long hashmap_start = ((long)hashmap_tv_start.tv_sec) * 1000 + (long)hashmap_tv_start.tv_usec / 1000;
        hash_map_dict.insert(std::make_pair(buffer, buffer));
        gettimeofday(&amp;hashmap_tv_end, NULL);
        time_sp_hash_map += (((long)hashmap_tv_end.tv_sec) * 1000 + (long)hashmap_tv_end.tv_usec / 1000) - hashmap_start;
    }
}

std::cout &lt;&lt; &quot;Build dictionary : \n&quot;
    &quot;   Trie      : &quot; &lt;&lt; time_sp &lt;&lt; &quot; ms.\n&quot; &lt;&lt;
    &quot;   Rbtree    : &quot; &lt;&lt; time_sp_rbtree &lt;&lt; &quot; ms.\n&quot; &lt;&lt;
    &quot;   HashTable : &quot; &lt;&lt; time_sp_hash_map &lt;&lt; &quot; ms.&quot; &lt;&lt; std::endl;

const char* keyword = &quot;zygomaticoauricularis&quot;;

timeval trie_find_tv_start, trie_find_tv_end, rbtree_find_tv_start, rbtree_find_tv_end, hashmap_find_tv_start, hashmap_find_tv_end;

gettimeofday(&amp;trie_find_tv_start, NULL);
std::string status;
long trie_start = ((long)trie_find_tv_start.tv_sec) * 1000 + (long)trie_find_tv_start.tv_usec / 1000;
for (int i = 0; i &lt; 10000; i++)
{
    status = t.find_word_exists(keyword) == true ? &quot;true&quot; : &quot;false&quot;;
}
gettimeofday(&amp;trie_find_tv_end, NULL);
long trie_end_time = (((long)trie_find_tv_end.tv_sec) * 1000 + (long)trie_find_tv_end.tv_usec / 1000) - trie_start;

gettimeofday(&amp;rbtree_find_tv_start, NULL);
long rbtree_start = ((long)rbtree_find_tv_start.tv_sec) * 1000 + (long)rbtree_find_tv_start.tv_usec / 1000;
for (int i = 0; i &lt; 10000; i++)
{
    rbtree_dict.find(keyword);
}
gettimeofday(&amp;rbtree_find_tv_end, NULL);
long rbtree_end_time = (((long)rbtree_find_tv_end.tv_sec) * 1000 + (long)rbtree_find_tv_end.tv_usec / 1000) - rbtree_start;

gettimeofday(&amp;hashmap_find_tv_start, NULL);
long hashtable_start = ((long)hashmap_find_tv_start.tv_sec) * 1000 + (long)hashmap_find_tv_start.tv_usec / 1000;
for (int i = 0; i &lt; 10000; i++)
{
    hash_map_dict.find(keyword);
}
gettimeofday(&amp;hashmap_find_tv_end, NULL);
long hashtable_end_time = (((long)hashmap_find_tv_end.tv_sec) * 1000 + (long)hashmap_find_tv_end.tv_usec / 1000) - hashtable_start;

std::cout &lt;&lt; &quot;Word &lt;'&quot; &lt;&lt; keyword &lt;&lt; &quot;'&gt; search result : &quot; &lt;&lt; status &lt;&lt; &quot;, elapsed time : \n&quot;
    &quot;   Trie : &quot; &lt;&lt; trie_end_time &lt;&lt; &quot; ms.\n&quot; &lt;&lt;
    &quot;   Rbtree : &quot; &lt;&lt; rbtree_end_time &lt;&lt; &quot; ms.\n&quot; &lt;&lt;
    &quot;   HashTable : &quot; &lt;&lt; hashtable_end_time &lt;&lt; &quot; ms.&quot; &lt;&lt; std::endl;

return 0;

}
[/cpp]

测试结果如下:

建表(耗时/ms)

查询(耗时/ms)

Trie(字典树)

143ms

0ms

std::set(红黑树)

235ms

14ms

std::unordered_map(哈希表)

188ms

8ms



从测试数据看来,结果无论是建树还是查询,Trie都仅仅是略胜红黑树和哈希表,但也可以看出Trie树这种非常简单的数据结构在这方面的效率足以和红黑树以及哈希表持平甚至略占优势。但并不能因此表明Trie是一种超越红黑树和哈希表的数据结构,它只是更加适合这种以公共前缀进行查询的场合。不过对于上面测试代码,仍然有失公平,比如说在哈希表建表时仍然会把hash key的消耗计算在内。但有趣的是,在VS2012环境下编译并一样在关闭编译器优化的情况下,Windows下的测试数据却普遍比在gcc编译的要慢好几倍甚至好几十倍,这是为什么呢?

字典文件下载:dict_linux