前缀树
一、何为前缀树
前缀树是一个利用字符串的公共前缀从而存储字符串列表的高级数据结构。
下图是一个前缀树示例图,首先可以看出前缀树除头结点以外的每个结点都包含两个属性:
- 字符值
- 以该字符为终点
那么前缀树是如何存储字符串的呢,假如我们要搜"AEP"这个字符串,我们对这棵前缀树进行深度优先搜索,在搜索中间这条路径的"P"结点时,我们得到一条路径AEP,检查P结点的end值,值为1表示存在一个"AEP"字符串。
从而应该明白,这棵前缀树存储的字符串数组为:{“B”,“BQ”,“AE”,“AE”,“AEP”,“AED”,“D”}。
二、前缀树相关操作
1.添加字符串
假设我们需要添加"ABC","ABE"两个字符串
- 遍历字符串,并从头结点出发,若当前的子节点中不包含这个字符结点,新建一个结点并使其成为当前结点的子节点。
- 将当前结点指向这个子结点。
- 若这个子结点是字符串的结尾,其end加一。
- 循环123步骤直到遍历完字符串。
建树的过程如下:
2.查找字符串
查找步骤如下:
- 遍历字符串,并从头结点出发,若当前结点的子结点不存在这个字符结点,返回false;
- 使当前指针指向下一个子结点。
- 若已经遍历到字符串的末尾但该子结点end值为0,返回false、
- 循环123操作直到遍历完字符串。
- 返回true。
实际上查找过程就是一个剪枝的深度优先搜索。
三、数组模拟前缀树
假设字符串中仅存在小写字符。
1.定义数据结构
int[] son = new int[N][26];//模拟树结点
int[] cnt= new int[N];//模拟end
int idx = 0;//模拟指向可使用的内存的指针
- 首先我们使用一个son数组来模拟结点,son有26列,表示可能有26个分支,假如son[i][1] = k:若k = 0,表示 i 这个结点不存在值为b的后代;若k!=0,不仅表示存在值为b的后代,而且这个后代结点为k。i = 0为头结点。
- 用cnt数组模拟end的效果,cnt[i]的值即为以结点i为终点的字符串的个数。
- idx指向当前可用的最后一格内存,每次新建一个结点时++idx,并为son赋值,即:son[i][j] = ++idx。
以上说明建议结合下面的具体操作代码理解。
2.数组模拟前缀树添加字符串
public void insert(String str){
//插入一个字符串
int h = 0;
for(int i = 0;i<str.length();i++){
int u = str.charAt(i) - 'a';
if(son[h][u]==0) son[h][u] = ++idx;
h = son[h][u];
}
cnt[h]++;
}
3.数组模拟前缀树查找字符串
public int query(String str){
//查询str出现的次数
int h = 0;
for(int i = 0;i<str.length();i++){
int u = str.charAt(i)-'a';
if(son[h][u]==0) return 0;
h = son[h][u];
}
return cnt[h];
}
当然,也可以用递归的方式来查找
public int queryByDfs(String str){
return dfs(str,0,0);
}
public int dfs(String str,int h,int i){
//dfs前缀树的第h个结点,字符串的第i个字符
if(i==str.length()){
return cnt[h];
}
int u = str.charAt(i) - 'a';
if(son[h][u]==0) return 0;
return dfs(str,son[h][u],i+1);
}
4.完整代码
public class TrieTree {
public int[][] son;
public int[] cnt;
public int idx;
public TrieTree(){
son = new int[1001][26];
cnt = new int[1001];
idx = 0;
}
public void insert(String str){
//插入一个字符串
int h = 0;
for(int i = 0;i<str.length();i++){
int u = str.charAt(i) - 'a';
if(son[h][u]==0) son[h][u] = ++idx;
h = son[h][u];
}
cnt[h]++;
}
public int query(String str){
//查询str出现的次数
int h = 0;
for(int i = 0;i<str.length();i++){
int u = str.charAt(i)-'a';
if(son[h][u]==0) return 0;
h = son[h][u];
}
return cnt[h];
}
public int queryByDfs(String str){
return dfs(str,0,0);
}
public int dfs(String str,int h,int i){
//dfs前缀树的第h个结点,字符串的第i个字符
if(i==str.length()){
return cnt[h];
}
int u = str.charAt(i) - 'a';
if(son[h][u]==0) return 0;
return dfs(str,son[h][u],i+1);
}
}
测试代码:
System.out.println();
System.out.println("测试前缀树");
TrieTree tree = new TrieTree();
tree.insert("abcd");
tree.insert("qwer");
tree.insert("abc");
tree.insert("abc");
tree.insert("abdct");
System.out.println("非递归查找:"+tree.query("abc"));
System.out.println("递归查找:"+tree.queryByDfs("abc"));
System.out.println("非递归查找:"+tree.query("abcde"));
System.out.println("递归查找:"+tree.queryByDfs("abcde"));
结果:
四、力扣实战
1.题目描述
2.简单分析
- 这题很容易想到可以使用前缀树解决,建树过程中字符串不存在小数点,只有在查找的时候存在小数点,因此addWord方法正常实现即可。
- 关于查找字符串,由于被查找的字符串含有小数点,故遇到小数点时前缀树该层的所有结点都应该遍历一次。
3.代码实现
class WordDictionary {
public int[][] son;
public int[] cnt;
public int idx;
public WordDictionary() {
son = new int[250004][26];
cnt = new int[250004];
idx = 0;
}
public void addWord(String word) {
int cur = 0;
for(int i = 0;i<word.length();i++){
int u = word.charAt(i) - 'a';
if(son[cur][u]==0) son[cur][u] = ++ idx;
cur = son[cur][u];
}
cnt[cur] ++ ;
}
public boolean search(String word) {
return dfs(0,0,word);
}
public boolean dfs(int cur,int i,String word){
if(i==word.length()){
return cnt[cur] != 0;
}
boolean ans = false;
if(word.charAt(i)=='.'){
for(int j = 0;j<26;j++){
if(son[cur][j]!=0) ans = ans||dfs(son[cur][j],i+1,word);
}
}else{
if(son[cur][word.charAt(i)-'a']==0) return false;
ans = ans||dfs(son[cur][word.charAt(i)-'a'],i+1,word);
}
return ans;
}
}