Trie树的C++实现

来源:互联网 发布:数字油画 知乎 编辑:程序博客网 时间:2024/06/12 13:53

Trie—单词查找树

Trie,又称单词查找树、前缀树,是一种哈希树的变种。应用于字符串的统计与排序,经常被搜索引擎系统用于文本词频统计。

性质:
1.根节点不包含字符,除根节点外的每一个节点都只包含一个字符。
2.从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
3.每个节点的所有子节点包含的字符都不相同。

优点:
1.查询快。对于长度为m的键值,最坏情况下只需花费O(m)的时间;而BST需要O(m log n)的时间。
2.当存储大量字符串时,Trie耗费的空间较少。因为键值并非显式存储的,而是与其他键值共享子串。


2 结构示意图




操作:
1.初始化或清空:遍历Trie,删除所有节点,只保留根节点。

2.插入字符串
1).设置当前节点为根节点,设置当前字符为插入字符串中的首个字符;
2).在当前节点的子节点上搜索当前字符,若存在,则将当前节点设为值为当前字符的子节点;否则新建一个值为当前字符的子节点,并将当前结点设置为新创建的节点。
3).将当前字符设置为串中的下个字符,若当前字符为0,则结束;否则转2.

3.查找字符串
搜索过程与插入操作类似,当字符找不到匹配时返回假;若全部字符都存在匹配,判断最终停留的节点是否为树叶,若是,则返回真,否则返回假。

4.删除字符串
首先查找该字符串,边查询边将经过的节点压栈,若找不到,则返回假;否则依次判断栈顶节点是否为树叶,若是则删除该节点,否则返回真。


 C++ Code 
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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
#include <iostream>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
using namespace std;

/* trie的节点类型 */
template <int Size> //Size为字符表的大小
struct trie_node
{
    bool terminable; //当前节点是否可以作为字符串的结尾
    int node; //子节点的个数
    trie_node *child[Size]; //指向子节点指针

    /* 构造函数 */
    trie_node() : terminable(false), node(0)
    {
        memset(child, 0sizeof(child));
    }
};

/* trie */
template <int Size, typename Index> //Size为字符表的大小,Index为字符表的哈希函数
class trie
{
public:
    /* 定义类型别名 */
    typedef trie_node<Size> node_type;
    typedef trie_node<Size> *link_type;

    /* 构造函数 */
    trie(Index i = Index()) : index(i) { }

    /* 析构函数 */
    ~trie()
    {
        clear();
    }

    /* 清空 */
    void clear()
    {
        clear_node(root);
        for (int i = 0; i < Size; ++i)
            root.child[i] = 0;
    }

    /* 插入字符串 */
    template <typename Iterator>
    void insert(Iterator begin, Iterator end)
    {
        link_type cur = &root; //当前节点设置为根节点
        for (; begin != end; ++begin)
        {
            if (!cur->child[index[*begin]]) //若当前字符找不到匹配,则新建节点
            {
                cur->child[index[*begin]] = new node_type;
                ++cur->node; //当前节点的子节点数加一
            }
            cur = cur->child[index[*begin]]; //将当前节点设置为当前字符对应的子节点
        }
        cur->terminable = true//设置存放最后一个字符的节点的可终止标志为真
    }

    /* 插入字符串,针对C风格字符串的重载版本 */
    void insert(const char *str)
    {
        insert(str, str + strlen(str));
    }

    /* 查找字符串,算法和插入类似 */
    template <typename Iterator>
    bool find(Iterator begin, Iterator end)
    {
        link_type cur = &root;
        for (; begin != end; ++begin)
        {
            if (!cur->child[index[*begin]])
                return false;
            cur = cur->child[index[*begin]];
        }
        return cur->terminable;
    }

    /* 查找字符串,针对C风格字符串的重载版本 */
    bool find(const char *str)
    {
        return find(str, str + strlen(str));
    }

    /* 删除字符串 */
    template <typename Iterator>
    bool erase(Iterator begin, Iterator end)
    {
        bool result; //用于存放搜索结果
        erase_node(begin, end, root, result);
        return result;
    }

    /* 删除字符串,针对C风格字符串的重载版本 */
    bool erase(char *str)
    {
        return erase(str, str + strlen(str));
    }

    /* 按字典序遍历单词树 */
    template <typename Functor>
    void traverse(Functor &execute = Functor())
    {
        visit_node(root, execute);
    }

private:
    /* 访问某结点及其子结点 */
    template <typename Functor>
    void visit_node(node_type cur, Functor &execute)
    {
        execute(cur);
        for (int i = 0; i < Size; ++i)
        {
            if (cur.child[i] == 0continue;
            visit_node(*cur.child[i], execute);
        }
    }
    /* 清除某个节点的所有子节点 */
    void clear_node(node_type cur)
    {
        for (int i = 0; i < Size; ++i)
        {
            if (cur.child[i] == 0continue;
            clear_node(*cur.child[i]);
            delete cur.child[i];
            cur.child[i] = 0;
            if (--cur.node == 0break;
        }
    }

    /* 边搜索边删除冗余节点,返回值用于向其父节点声明是否该删除该节点 */
    template <typename Iterator>
    bool erase_node(Iterator begin, Iterator end, node_type &cur, bool &result)
    {
        if (begin == end) //当到达字符串结尾:递归的终止条件
        {
            result = cur.terminable; //如果当前节点可以作为终止字符,那么结果为真
            cur.terminable = false;  //设置该节点为不可作为终止字符,即删除该字符串
            return cur.node == 0;    //若该节点为树叶,那么通知其父节点删除它
        }
        //当无法匹配当前字符时,将结果设为假并返回假,即通知其父节点不要删除它
        if (cur.child[index[*begin]] == 0return result = false;
        //判断是否应该删除该子节点
        else if (erase_node((++begin)--, end, *(cur.child[index[*begin]]), result))
        {
            delete cur.child[index[*begin]]; //删除该子节点
            cur.child[index[*begin]] = 0//子节点数减一
            //若当前节点为树叶,那么通知其父节点删除它
            if (--cur.node == 0 && cur.terminable == falsereturn true;
        }
        return false//其他情况都返回假
    }

    /* 根节点 */
    node_type root;

    /* 将字符转换为索引的转换表或函数对象 */
    Index index;
};

//index function object
class IndexClass
{
public:
    int operator[](const char key)
    {
        return key % 26;
    }
};

int main()
{
    trie<26, IndexClass> t;
    t.insert("tree");
    t.insert("tea");
    t.insert("A");
    t.insert("ABC");

    if(t.find("tree"))
        cout << "find tree" << endl;
    else
        cout << "not find tree" << endl;

    if(t.find("tre"))
        cout << "find tre" << endl;
    else
        cout << "not find tre" << endl;

    if(t.erase("tree"))
        cout << "delete tree" << endl;
    else
        cout << "not find tree" << endl;

    if(t.find("tree"))
        cout << "find tree" << endl;
    else
        cout << "not find tree" << endl;

    return 0;
}










3. trie树的应用:

1. 字符串检索,词频统计,搜索引擎的热门查询

        事先将已知的一些字符串(字典)的有关信息保存到trie树里,查找另外一些未知字符串是否出现过或者出现频率。

        举例:

       1)有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。

       2)给出N 个单词组成的熟词表,以及一篇全用小写英文书写的文章,请你按最早出现的顺序写出所有不在熟词表中的生词。

       3)给出一个词典,其中的单词为不良单词。单词均为小写字母。再给出一段文本,文本的每一行也由小写字母构成。判断文本中是否含有任何不良单词。例如,若rob是不良单词,那么文本problem含有不良单词。

       4)1000万字符串,其中有些是重复的,需要把重复的全部去掉,保留没有重复的字符串

       5)寻找热门查询:搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。假设目前有一千万个记录,这些查询串的重复读比较高,虽然总数是1千万,但是如果去除重复和,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就越热门。请你统计最热门的10个查询串,要求使用的内存不能超过1G。

2. 字符串最长公共前缀

       Trie树利用多个字符串的公共前缀来节省存储空间,反之,当我们把大量字符串存储到一棵trie树上时,我们可以快速得到某些字符串的公共前缀。举例:

      1) 给出N 个小写英文字母串,以及Q 个询问,即询问某两个串的最长公共前缀的长度是多少.  解决方案:

        首先对所有的串建立其对应的字母树。此时发现,对于两个串的最长公共前缀的长度即它们所在结点的公共祖先个数,于是,问题就转化为了离线  (Offline)的最近公共祖先(Least Common Ancestor,简称LCA)问题。

       而最近公共祖先问题同样是一个经典问题,可以用下面几种方法:

        1. 利用并查集(Disjoint Set),可以采用采用经典的Tarjan 算法;

        2. 求出字母树的欧拉序列(Euler Sequence )后,就可以转为经典的最小值查询(Range Minimum Query,简称RMQ)问题了;


3.  排序

       Trie树是一棵多叉树,只要先序遍历整棵树,输出相应的字符串便是按字典序排序的结果。

        举例: 给你N 个互不相同的仅由一个单词构成的英文名,让你将它们按字典序从小到大排序输出。

4 作为其他数据结构和算法的辅助结构

       如后缀树,AC自动机等。


0 0
原创粉丝点击