Trie

Trie树,又叫字典树、前缀树(Prefix Tree)、单词查找树 或 键树,是一种多叉树结构。

1115_1.gif

此图是一棵Trie树,表示了关键字集合{“hers”, “his”, “she”, “i”, “he”} 。从图中可以看出: 从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应的字符串。 从根节点到标记呈橙色节点路径上字符组成的字符串是一个关键字。图中橙色节点就是关键字节点。 有共同前缀关键字有共同路径。 构建字典树 如果我们以英文小写字母作为字符集,那么每个节点都有26个分支子节点。由于有些节点是标志着一个插入字符串的结尾,所以需要有一个标记位判断是否是一个关键字。 我们定义根节点的结构如下:

struct Node
{
    bool isKey;   // 标记该节点是否代表一个关键字
    Node * children[26]; // 各个子节点
};

刚开始字典树是只有一个根节点的。

Node* root = new Node();

我们需要通过一条一条加入字符串构建字典树。 插入一条字符串的过程:

void insert(string word) {
    Node* t = root;
    for (int i=0; i<word.size(); i++) {
        int s = word[i]-'a';//得到数组下标
        if (t->child[s] == NULL) {
            t->child[s] = new Node();
        }
        t = t->child[s];
    }
    t->isKey = true;
}

Trie树的应用

  1. 字符串检索 检索/查询功能是Trie树最原始的功能。思路就是从根节点开始一个一个字符进行比较: 如果沿路比较,发现不存在的字符,则表示该字符串在集合中不存在。 如果所有的字符全部比较完并且全部相同,还需判断最后一个节点的标志位

    struct Node
    {
        bool isKey;   // 标记该节点是否代表一个关键字
        Node * children[26]; // 各个子节点
    };
    
    
  2. 词频统计 Trie树常被搜索引擎系统用于文本词频统计 。

    struct Node
    {
        bool isKey;   // 标记该节点是否代表一个关键字
        Node * children[26]; // 各个子节点
    };
    

    思路:为了实现词频统计,我们修改了节点结构,用一个整型变量count来计数。对每一个关键字执行插入操作,若已存在,计数加1,若不存在,插入后count置1。

  3. 字符串排序 Trie树可以对大量字符串按字典序进行排序,思路也很简单:遍历一次所有关键字,将它们全部插入trie树,树的每个结点的所有儿子很显然地按照字母表排序,然后先序遍历输出Trie树中所有关键字即可。

  4. 前缀匹配 例如:找出一个字符串集合中所有以ab开头的字符串。我们只需要用所有字符串构造一个trie树,然后输出以a->b->开头的路径上的关键字即可。 trie树前缀匹配常用于搜索提示。如当输入一个网址,可以自动搜索出可能的选择。当没有完全匹配的搜索结果,可以返回前缀最相似的可能。

  5. 作为其他数据结构和算法的辅助结构 如后缀树,AC自动机等。

字符串检索的Trie树代码

class Trie {
public:
    struct Node {
        bool isKey;//是否当前节点作为一个单词的结尾
        Node* child[26];//a-z
    } *head;
    /** Initialize your data structure here. */
    Trie() {
        head = new Node();
    }

    /** Inserts a word into the trie. */
    void insert(string word) {
        Node* t = head;
        for (int i=0; i<word.size(); i++) {
            int s = word[i]-'a';
            if (t->child[s] == NULL) {
                t->child[s] = new Node();
            }
            t = t->child[s];
        }
        t->isKey = true;
    }

    /** Returns if the word is in the trie. */
    bool search(string word) {
        Node* t = head;
        for (int i=0; i<word.size(); i++) {
            int s = word[i] - 'a';
            if (t->child[s] == NULL) return false;
            t = t->child[s];
        }
        return t->isKey;
    }

    /** Returns if there is any word in the trie that starts with the given prefix. */
    bool startsWith(string prefix) {
        Node* t = head;
        for (int i=0; i<prefix.size(); i++) {
            int s = prefix[i] - 'a';
            if (t->child[s] == NULL) return false;
            t = t->child[s];
        }
        return true;
    }
};

用数组作为trie的储存结构。 数组下表起到指针的作用,new 一个节点得到的指针就是将当前树的大小作为指针, 因为sz是树中节点的个数(包括根节点),而节点的编号是从0开始,所以树中存在编号0到sz-1刚好sz个,此时若分配新节点刚好是sz作为编号。

// Trie
#include <iostream>
#include <cstdio>
#include <cstring>
#define MAXN 1000005
#define SIGMA 26
//仅含小写英文字母
using namespace std;
namespace Trie {
	int tr[MAXN][SIGMA];//数组存储Trie树节点,下标起到指针作用
	int val[MAXN];//若当前节点不是一个模式串的结尾则为0
	int sz;//Trie中当前节点个数
	Trie() {
		memset(tr, 0, sizeof(tr));
		memset(val, 0, sizeof(val));
		sz = 1;
	}
	//插入
	void insert(char* s) {
		int len = strlen(s), u = 0;
		for (int i=0; i<len; i++) {
			if (tr[u][s[i]-'a'] == 0) {//不存在新开就一个节点
				tr[u][s[i]-'a'] = sz;
				sz++;
			}
			u = tr[u][s[i]-'a'];
		}
		val[u] = 1;
	}
	//查询s是否存在
	bool query(char* s) {
		int len = strlen(s), u = 0;
		for (int i=0; i<len; i++) {
			if (tr[u][s[i]-'a'] == 0) return false;
			u = tr[u][s[i]-'a'];
		}
		return val[u];
	}
};

char txt[100];
char pat[5][100] = {"hers", "he", "his", "she", "i"};

int main() {
	Trie::Trie();
	for (int i=0; i<5; i++) {
		Trie::insert(pat[i]);
	}
	while (1) {
		cin >> txt;
		cout << Trie::query(txt) << endl;
	}
	return 0;
}