什么是前缀树
Trie树,即前缀树,又称单词查找树,字典树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。
Trie树的核心思想是空间换时间,利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。 它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。
它有3个基本性质:
- 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
- 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
- 每个节点的所有子节点包含的字符都不相同。
构造前缀树
假设有abc,abd,bcd,efg,hii,那么我们构建如下一个树结构用于表示这些单词:
但是构造一个前缀树就是为了使遍历那些有相同的前缀的单词时更快速,目前这个多叉树上从根节点到叶节点每一条路径都是一个单词,如果我们增加了单词b,abcd两个单词呢,还是这样一条路走到黑,我们无法区分字母短的单词是否已经存储过,为了解决这个问题,我们可以在节点上记录信息,比如如果到达该字母后单词结尾,我们就在时候的节点上记录end+1,这样就可以知道有几个以该前缀结尾的单词,修改之后结构如下:
这时我们可以很明显看出来如最左边一条路径上,有一个单词是abc,一个单词是abcd。根据新添加的信息,我们可以知道所有字符串中有多少个该字符串,如果现在有另外一个问题,我想知道所有字符串中有多少个以该字符串作为前缀,那是不是得遍历该条路径后面的节点,将所有的end加起来。为了简单,我们仍然可以在节点中多存入一个信息,每个节点被划过了多少次,也就是在建立多叉树的时候就记录了以所有字符串中有多少个以某字符串作为前缀,划过的次数就是这个值。调整后结构如下:
代码实现
树节点节点
public static class TrieNode {
public int path;//记录有几个字符串经过了这个节点,删除用
public int end;//以该节点结束的字符串的数量,查询用
public TrieNode[] nexts;//子路径,这里只考虑26个字母
public TrieNode() {
path = 0;
end = 0;
nexts = new TrieNode[26];
}
}
前缀树结构
持有一个根节点,具备增删改查的功能
public static class Trie {
private TrieNode root;
public Trie() {
root = new TrieNode();
}
// 添加字符串
public void insert(String word) {
}
//删除
public void delete(String word) {
}
//查询
public int search(String word) {
}
//查询以某字符串为前缀的字符串的数量
public int prefixNumber(String pre) {
}
}
插入数据
将字符串按字母分割,从根节点依次追加进前缀树。需要注意如果树中已经存在该节点路径,则复用。
public void insert(String word) {
if (word == null) {
return;
}
char[] chs = word.toCharArray();
TrieNode node = root;
int index = 0;
for (int i = 0; i < chs.length; i++) {
index = chs[i] - 'a';
if (node.nexts[index] == null) {//没有可复用路径,新建节点
node.nexts[index] = new TrieNode();
}
node = node.nexts[index];
node.path++;//当前节点划过次数加1
}
node.end++;//以该节点结束的字符串数量加1
}
查找数据
查找与插入类似,返回最后一个节点的end值
public int search(String word) {
if (word == null) {
return 0;
}
char[] chs = word.toCharArray();
TrieNode node = root;
int index = 0;
for (int i = 0; i < chs.length; i++) {
index = chs[i] - 'a';
if (node.nexts[index] == null) {
return 0;
}
node = node.nexts[index];
}
return node.end;
}
删除数据
先类似查找的方式确定是否存在该数据,然后尝试删除。每次删除,其实是将节点的path属性-1。当path属性==0时,从上级节点的nexts列表中remove掉该节点即可return结束。
public void delete(String word) {
if (search(word) != 0) {
char[] chs = word.toCharArray();
TrieNode node = root;
int index = 0;
for (int i = 0; i < chs.length; i++) {
index = chs[i] - 'a';
if (--node.nexts[index].path == 0) {
node.nexts[index] = null;
return;
}
node = node.nexts[index];
}
node.end--;
}
}
查询以某字符串为前缀的字符串的数量
public int prefixNumber(String pre) {
if (pre == null) {
return 0;
}
char[] chs = pre.toCharArray();
TrieNode node = root;
int index = 0;
for (int i = 0; i < chs.length; i++) {
index = chs[i] - 'a';
if (node.nexts[index] == null) {
return 0;
}
node = node.nexts[index];
}
return node.path;
}