这篇文文章给大家讲解前缀树的知识,前缀树是非常重要的,也是AC自动机的一个基础.希望能给大家带来新的收获.提升codeing能力
目录
前缀树
Tire树又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。
前缀树与哈希表
对于哈希表来说我们都知道它的时间复杂度是O(1),但是如果是超长的字符串,也要算上样本长度,它的时间复杂度就是不是O(1),因为他要遍历一边字符串才能算出哈希地址.所以时间复杂度是O(K),k是字符串长度. 对于哈希表还有一个缺点就是无法查找前缀的数量.
而对于今天的主角前缀树来说,虽然在查询字符串上时间复杂度也是与哈希表一样,但是前缀树也能查找某一个前缀的数量.
时间复杂度 : 建树O(字符串长度)
查询 : O(K)字符串长度
前缀查询 : O(K) 前缀长度
前缀树优势 : 前缀树查询速度和哈希表一样,前缀树还能实现出哈希表所不能查询的前缀数量
前缀树性质
根节点不包含字符,除根节点外每一个节点都只包含一个字符; 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串; 每个节点的所有子节点包含的字符都不相同。
也就是在建立一颗前缀树的时候.经典的前缀树是将字符放在路上的,根节点root节点没有字符.所以对于这个前缀树而言,每一个节点要准备26个节点分别对应26个英文字母,这里用ACII码表来进行映射.
时间复杂度 : 建树O(字符串长度)
查询 : O(K)字符串长度
前缀查询 : O(K) 前缀长度
前缀树优势 : 前缀树查询速度和哈希表一样,前缀树还能实现出哈希表所不能查询的前缀数量
思路:
前缀树分为几个步骤分别是insert方法也就是建出一颗前缀树来.search方法查询这个字符串的出现次数.startsWith查询某个前缀数量.delete删除某一个字符串.
我们以下一层节点是否为空,来标记这条路有还是没有
建树-insert方法
如何建立一颗前缀树呢?我们下面以插入 字符串 "abc","abd","bce","abcd","bcf" 为例.
树的节点Node
我们先要写一个结点类,来表示树的每一个节点
class Node{
public int pass;//经过字符的次数
public int end;//以某个字符为结尾的次数
public Node[] nexts;//每一个节点的孩子.
public Node(){
nexts = new Node[26];//代表有26条路(a....z)
}
}
结点类包含三个成员 pass,end,nexts
pass: 代表在遍历字符串的时候,也就是在建立一颗前缀树的时候,经过某一个字符才能建出这条路来,每通过这个字符来建路pass就++.也就是经过这个节点的次数.标志着经过这个字符几次
end : 代表当遍历字符串到结尾的时候给end+1,标志以这个字符结尾的有几个
nexts[] : 这个是孩子节点,因为是要查询字符串包含26个英文字母,所以就要准备26条路.所以初始化的时候就new Node[26];
手把手带你演示如何建树
建树原则 :
所有的建树都要经过头结点所以根节点每次都要pass++;
- 如果遍历的字符没有这条路了,就将这条路建立出来,又经过这个字符所以对应的节点要pass++,如果到这个字符结尾end++;
- 如果遍历的字符有这条路了,就接着复用这条路,对应的pass要++因为经过这个字符了.如果到这个字符结尾end++;
对于字符串 "abc"
对于字符串 abd
接下来也就可以建立出一颗前缀树了.就长下面这样
建树代码实现--insert方法
public Node root;
public Trie(){
root = new Node();//刚开始有一个头结点
}
//向树中插入节点--->建好一颗前缀树
public void insert(String words){
if(words==null){
return;//如果字符串为空,则不能建树
}
//先转化成一个字符数组
char[] str = words.toCharArray();
int path = 0;
Node node = root;
node.pass++;
for(int i =0;i<str.length;++i){//遍历这个字符串,建好前缀树
path = str[i]-'a';//到底是哪一条路线呢
if(node.nexts[path]==null){
//如果发现没有这条路,就建出一条路
node.nexts[path] = new Node();
}
//走到下一个节点
node = node.nexts[path];
//经过这个节点就pass++,代表路过这个字符多少次
node.pass++;
}
//遍历完整个字符串之后,也就是到达这个结尾了
node.end++;
}
查询--search()
因为我们利用end来标记以某个字符为结尾,所以如果这个字符串存在,遍历这个字符串,前缀树一直走到字符串结尾,节点的end值就是这个字符串的出现次数
//查询这个字符串出现了几次
public int search(String words){
if(words==null){
return 0;
}
char[] str = words.toCharArray();
Node node = root;
int path = 0;
for(int i =0;i<str.length;++i){
path = str[i] - 'a';
if(node.nexts[path]==null) {
return 0;
}
node = node.nexts[path];
}
return node.end;
}
startsWith()-->查询字符串前缀数量
我们利用pass来标记经过某一个字符出现的次数,所以遍历完整个前缀,这个节点的pass值就是这个前缀的数量
//查询以pre前缀的有多少个字符串
public int startsWith(String prefix){
if(prefix==null){
return 0;
}
char[] str = prefix.toCharArray();
int path = 0;
Node node = root;
for(int i =0;i<str.length;++i){
path = str[i] - 'a';
if(node.nexts[path]==null){
return 0;
}
node = node.nexts[path];
}
return node.pass;
}
delete()-->字符串删除
字符串删除还是要遍历这个字符串,没经过一个字符对应前缀树的节点pass值就-1,如果在到下一条路之前pass值就已经为0代表这个字符已经不存在了,就给节点置空,到最后end值--.
//从这个前缀树上删除这个字符串
public void delete(String words){
if(search(words)!=0) {
char[] str = words.toCharArray();
int path = 0;
Node node = root;
node.pass--;
for (int i = 0; i < str.length; ++i) {
path = str[i] - 'a';
//如果下一条路pass为0,就立马回收节点设置为null
if (--node.nexts[path].pass == 0) {
node.nexts[path] = null;
}
node = node.nexts[path];
node.pass--;
}
node.end--;
}
}
顺便带走力扣一道题
Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。
请你实现 Trie 类:
Trie() 初始化前缀树对象。
void insert(String word) 向前缀树中插入字符串 word 。
boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。
boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。
示例:
输入
["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
[[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
输出
[null, null, true, false, true, null, true]解释
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple"); // 返回 True
trie.search("app"); // 返回 False
trie.startsWith("app"); // 返回 True
trie.insert("app");
trie.search("app"); // 返回 True
class Node{
public int pass;
public int end;
public Node[] nexts;
public Node(){
nexts = new Node[26];//代表有26条路(a....z)
}
}
//实现一颗前缀树
public class Trie {
public Node root;
public Trie(){
root = new Node();//刚开始有一个头结点
}
//向树中插入节点--->建好一颗前缀树
public void insert(String words){
if(words==null){
return;//如果字符串为空,则不能建树
}
//先转化成一个字符数组
char[] str = words.toCharArray();
int path = 0;
Node node = root;
node.pass++;
for(int i =0;i<str.length;++i){//遍历这个字符串,建好前缀树
path = str[i]-'a';//到底是哪一条路线呢
if(node.nexts[path]==null){
//如果发现没有这条路,就建出一条路
node.nexts[path] = new Node();
}
//经过这个节点就pass++,代表路过这个字符多少次
node = node.nexts[path];
node.pass++;
}
//遍历完整个字符串之后,也就是到达这个结尾了
node.end++;
}
//查询这个字符串出现了几次
public boolean search(String words){
if(words==null){
return false;
}
char[] str = words.toCharArray();
Node node = root;
int path = 0;
for(int i =0;i<str.length;++i){
path = str[i] - 'a';
if(node.nexts[path]==null) {
return false;
}
node = node.nexts[path];
}
return node.end!=0;
}
//查询以pre前缀的有多少个字符串
public boolean startsWith(String prefix){
if(prefix==null){
return false;
}
char[] str = prefix.toCharArray();
int path = 0;
Node node = root;
for(int i =0;i<str.length;++i){
path = str[i] - 'a';
if(node.nexts[path]==null){
return false;
}
node = node.nexts[path];
}
return node.pass!=0;
}
}
/**
* Your Trie object will be instantiated and called as such:
* Trie obj = new Trie();
* obj.insert(word);
* boolean param_2 = obj.search(word);
* boolean param_3 = obj.startsWith(prefix);
*/
前缀树应用
串的快速检索
给出N个单词组成的熟词表,以及一篇全用小写英文书写的文章,请你按最早出现的顺序写出所有不在熟词表中的生词。
在这道题中,我们可以用数组枚举,用哈希,用字典树,先把熟词建一棵树,然后读入文章进行比较,这种方法效率是比较高的。
“串”排序
给定N个互不相同的仅由一个单词构成的英文名,让你将他们按字典序从小到大输出
用字典树进行排序,采用数组的方式创建字典树,这棵树的每个结点的所有儿子很显然地按照其字母大小排序。对这棵树进行先序遍历即可。
最长公共前缀
对所有串建立字典树,对于两个串的最长公共前缀的长度即他们所在的结点的公共祖先个数,于是,问题就转化为当时公共祖先问题。
参考 : 百度百科 字典树_百度百科