数据结构之Trie树
最近在做搜索的时候用到了AC双数组自动机,理解它的基础是Trie树,因此,学习了一下。
一、基本介绍
Trie树,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),
所以经常被搜索引擎系统用于文本词频统计。它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。
Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
Trie树的基本性质可以归纳为:
(1)根节点不包含字符,除根节点意外每个节点只包含一个字符。
(2)从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应的字符串。
(3)每个节点的所有子节点包含的字符串不相同。
Trie树有一些特性:
(1)根节点不包含字符,除根节点外每一个节点都只包含一个字符。
(2)从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
(3)每个节点的所有子节点包含的字符都不相同。
(4)如果字符的种数为n,则每个结点的出度为n,这也是空间换时间的体现,浪费了很多的空间。
(5)插入查找的复杂度为O(n),n为字符串长度。
下面我们有and,as,at,cn,com这些关键词,那么如何构建trie树呢?
从上面的图中,我们或多或少的可以发现一些好玩的特性。
第一:根节点不包含字符,除根节点外的每一个子节点都包含一个字符。
第二:从根节点到某一节点,路径上经过的字符连接起来,就是该节点对应的字符串。
第三:每个单词的公共前缀作为一个字符节点保存。
二、字典树的实际操作
(1)定义Trie树节点
为了方便,我也采用纯英文字母,我们知道字母有26个,那么我们构建的trie树就是一个26叉树,每个节点包含26个子节点。
public class TrieNode{
//26个字符,也就是26叉树
public TrieNode[] childNodes;
// 词频统计
public int freq;
// 记录该节点的字符
public char nodeChar;
// 插入记录时的编码id
public HashSet<Integer> hashSet = new HashSet<Integer>();
// 初始化
public TrieNode(){
childNodes = new TrieNode[26];
freq = 0;
}
}
(2)添加操作
既然是26叉树,那么当前节点的后续子节点是放在当前节点的哪一叉中,也就是放在childNodes中哪一个位置,这里我们采用
int k = word[0] - 'a'来计算位置。
public void AddTrieNode( TrieNode root, String word, int id) {
if (word.length() == 0)
return;
//求字符地址,方便将该字符放入到26叉树中的哪一叉中
int k = word.charAt(0) - 'a';
//如果该叉树为空,则初始化
if (root.childNodes[k] == null) {
root.childNodes[k] = new TrieNode();
//记录下字符
root.childNodes[k].nodeChar = word.charAt(0);
}
//该id途径的节点
root.childNodes[k].hashSet.add(id);
String nextWord = word.substring(1);
//说明是最后一个字符,统计该词出现的次数
if (nextWord.length() == 0)
root.childNodes[k].freq++;
AddTrieNode(root.childNodes[k], nextWord, id);
}
(3)删除操作
删除操作中,我们不仅要删除该节点的字符串编号,还要对词频减一操作。
public void DeleteTrieNode(TrieNode root, String word, int id) {
if (word.length() == 0)
return;
//求字符地址,方便将该字符放入到26叉树种的哪一颗树中
int k = word.charAt(0) - 'a';
//如果该叉树为空,则说明没有找到要删除的点
if (root.childNodes[k] == null)
return;
String nextWord = word.substring(1);
//如果是最后一个单词,则减去词频
if (word.length() == 0 && root.childNodes[k].freq > 0)
root.childNodes[k].freq--;
//删除途经节点
root.childNodes[k].hashSet.remove(id);
DeleteTrieNode(root.childNodes[k], nextWord, id);
}