目录
前缀树的概述
前缀树的应用场景
前缀树的java实现
节点
前缀树(增加,查询字符串数量,查询前缀数量)
测试
与DFA查询敏感词的类似与不同
类似
不同
前缀树又名字典树,单词查找树,Trie树,是一种多路树形结构,是哈希树的变种,和hash效率有一拼,是一种用于快速检索的多叉树结构。
典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。
Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
Trie树也有它的缺点,Trie树的内存消耗非常大。
性质:不同字符串的相同前缀只保存一份。
操作:查找,插入,删除。
举个栗子:给出一组单词,inn, int, at, age, adv,ant, 我们可以得到下面的Trie:
从上面可以发现一些Trie树的特性:
1)根节点不包含字符,除根节点外的每一个子节点都包含一个字符。
2)从根节点到某一节点的路径上的字符连接起来,就是该节点对应的字符串。
3)每个节点的所有子节点包含的字符都不相同。
4)每条边对应一个字母。每个节点对应一项前缀。叶节点对应最长前缀,即单词本身。
单词inn与单词int有共同的前缀“in”, 因此他们共享左边的一条分支,root->i->in。同理,ate, age, adv, 和ant共享前缀"a",所以他们共享从根节点到节点"a"的边。
查询操纵非常简单。比如要查找int,顺着路径i -> in -> int就找到了。
搭建Trie的基本算法也很简单,无非是逐一把每则单词的每个字母插入Trie。插入前先看前缀是否存在。如果存在,就共享,否则创建对应的节点和边。比如要插入单词add,就有下面几步:
考察前缀"a",发现边a已经存在。于是顺着边a走到节点a。
考察剩下的字符串"dd"的前缀"d",发现从节点a出发,已经有边d存在。于是顺着边d走到节点ad
考察最后一个字符"d",这下从节点ad出发没有边d了,于是创建节点ad的子节点add,并把边ad->add标记为d。
(1)字符串的快速检索
字典树的查询时间复杂度是O(logL),L是字符串的长度。所以效率还是比较高的。字典树的效率比hash表高。
hash表:
通过hash函数把所有的单词分别hash成key值,查询的时候直接通过hash函数即可,都知道hash表的效率是非常高的为O(1),当然这是对于如果我们hash函数选取的好,计算量少,且冲突少,那单词查询速度肯定是非常快的。那如果hash函数的计算量相对大呢,且冲突律高呢?这些都是要考虑的因素。
还有就是hash表不支持动态查询,什么叫动态查询,当我们要查询单词apple时,hash表必须等待用户把单词apple输入完毕才能hash查询。当你输入到appl时肯定不可能hash吧。
字典树(tries树):
对于单词查询这种,还是用字典树比较好,但也是有前提的,空间大小允许,字典树的空间相比较hash还是比较浪费的,毕竟hash可以用bit数组。
(2)字符串排序
从上图我们很容易看出单词是排序的,先遍历字母序在前面。
减少了没必要的公共子串。
(3)最长公共前缀
inn和int的最长公共前缀是in,遍历字典树到字母n时,此时这些单词的公共前缀是in。
(4)自动匹配前缀显示后缀
我们使用辞典或者是搜索引擎的时候,输入appl,后面会自动显示一堆前缀是appl的东东吧。
那么有可能是通过字典树实现的,前面也说了字典树可以找到公共前缀,我们只需要把剩余的后缀遍历显示出来即可。
package datastructure.tree.trietree;
import java.util.Arrays;
public class TreeNode {
//经过这个节点的字符串的个数(以这个节点为前缀的字符串的个数)
public int path;
//以这个节点结束的字符串的个数(有多少个字符串有这条路径的char组成)
public int end;
//对应着小写的a-z的26个字母(如果要更多可以使用hashmap
public TreeNode[] next;
public TreeNode(){
path=0;
end=0;
next=new TreeNode[26];
}
@Override
public String toString() {
return "TreeNode [path=" + path + ", end=" + end + ", next=" + Arrays.toString(next) + "]";
}
}
package datastructure.tree.trietree;
public class TrieTree {
public TreeNode root;
public TrieTree(){
root=new TreeNode();
}
/**在前缀树中插入字符串
* 这种++的方法,导致,一个node,有多少个end,就有多少个相同的字符串
* 一个node,有多少个path,就有多少个字符串经过(root的path代表有多少个字符串)(字符串末尾的node的path也会++)
* @param string 被插入的字符串(以前插入过的也可以插入)
*/
public void insertString(String string){
if(string==null||string.length()==0){
return;
}
int length=string.length();
TreeNode nowNode=root;
for(int i=0;i
package datastructure.tree.trietree;
public class Main {
public static void main(String[] args) {
TrieTree tree=new TrieTree();
tree.insertString("aa");
tree.insertString("aa");
tree.insertString("ab");
tree.insertString("ba");
//System.out.println(tree.root);
//System.out.println(tree.size());
//System.out.println(tree.getStringNum("aa"));
//System.out.println(tree.getStringNum("ab"));
//System.out.println(tree.getStringNum("ac"));
System.out.println(tree.getPrefixNum("a"));
System.out.println(tree.getPrefixNum("b"));
System.out.println(tree.getPrefixNum("c"));
}
}
敏感词过滤-dfa算法
https://blog.csdn.net/xushiyu1996818/article/details/89308561
思路类似,都是一个个节点,每个节点包含通往下一群节点的容器。
前缀树:
这种java实现的下一个节点的容器是new TreeNode[26],同时还有path(经过这个节点的数量),end(以这个节点结尾的数量),倾向于查找字符串的个数和前缀的个数
dfa:
java实现的容器是Map
其实两者本来是一个源头,只是为了不同的目的,从而node的构造不同