前缀树
前缀树:又称单词查找树或键树,是一种哈希树的变种。
典型应用是用于统计和排序大量的字符串(但不仅限于字符串)
利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较。
将一组字符串数组放入前缀树中的演示
String[] str = {"abc", "bck", "abd", "ace"};
从root头节点开始,将每个字符串放入树中。
在放入第一个字符串第一个字符‘a’的时候,看头节点中有没有a的路径,如果没有,就创建;如果有,就沿着a的路径走
因此在放入字符‘b’、‘c’的时候,都创建新的路径
在放入第二个字符串的时候,依旧是从头节点开始。此时头节点没有b的路径,创建新的路径
因此在放入字符‘c’、‘k’的时候,都创建新的路径
在放入第三个字符串的时候,头节点存在a的路径,沿着a的路径向下走
在a之后的节点,存在b的路径,沿着b的路径向下走
在b之后的节点,不存在d的节点,创建新的路径
... ...
前缀树的实现解析
前缀树的节点
这里使用的是经典的用数组表示路径,因为在前缀树的相关题目中,一般会限制字符串的范围
比如这道题目限制了字符串的范围仅在小写字母的范围之中
但当字符串的返回过大,创建数组十分浪费空间
此时可以用哈希表、有序表等表示路径
key表示当前是哪一条路,value表示下一个node节点
package trietree;
public class TrieNode {
int pass;//记录这个节点被经过多少次
int end;//记录这个节点是多少个字符串的结尾
public TrieNode[] nexts;//每个节点的之后的路径
public TrieNode() {
pass = 0;
end = 0;
nexts = new TrieNode[26];//先预设每个节点后面有26条路径
//我们先设定字符串的范围仅在26个小写字母之内
//a对应0、b对应1、c对应2 .....
//nexts[0] == null; 表示没有a的路径
//nexts[0] != null; 表示有a的路径
}
}
insert()方法
如何生成前缀树
pass和end在节点上,记录字符串的记录情况
经过一个节点,就给当前节点的pass++
当字符串遍历完成,给最后一个节点的end++
根节点的pass表示加入了多少个字符串
根节点的end表示加入了多少个空字符串
如果加入一个空字符串,那么根节点的pass+1,end+1
insert部分代码
package trietree;
public class TrieTree {
private TrieNode root;
public TrieTree(){
root = new TrieNode();
}
public void insert(String str) {
if (str == null) {//加入空字符串时,头节点的pass++、end++
root.pass++;
root.end++;
return;
}
char[] chs = str.toCharArray();//把字符串切分为char型数组
TrieNode node = root;
node.pass++;
int index = 0;
for (int i = 0; i < chs.length; i++) {
index = chs[i] - 'a';//a对应0、b对应1、c对应2 ...
if (node.nexts[index] == null) {//不存在对应的路径
node.nexts[index] = new TrieNode();//创建一个路径 == 为nexts数组赋值 == 创建下一个新的节点
}
//如果存在对应的路径,复用节点,下一个节点的pass++
node = node.nexts[index];//node向下
node.pass++;//此时是下一个节点,下一个节点的pass++
}
node.end++;//遍历完成一个字符串之后,end++
}
}
search()方法
查询一个字符串str加入过几次
沿着字符串str从头节点向下查找
查找到str的最后一个字符的时候,当时的node节点的end值就是str加入的次数
如果查到一半其中一个节点没有后续节点,那么说明没有加入过,直接返回0
//查询一个字符串str加入过几次
public int search(String str) {
TrieNode node = root;//头节点
char[] chs = str.toCharArray();
int index = 0;
for (int i = 0; i < chs.length; i++) {
index = chs[i] - 'a';
if (node.nexts[index] == null) {//如果查到一半其中一个节点没有后续节点,那么说明没有加入过,直接返回0
return 0;
}
node = node.nexts[index];//node向下查找
}
return node.end;//查找到str的最后一个字符的时候,当时的node节点的end值就是str加入的次数
}
prefixNumber()方法
查询所有加入的字符串中,有几个是以pre为前缀的
沿着字符串pre从头节点向下查找
查找到pre的最后一个字符的时候,当时的node节点的pass值就是str加入的次数
如果查到一半其中一个节点没有后续节点,那么说明没有以pre为前缀的,直接返回0
//查询所有加入的字符串中,有几个是以pre为前缀的
public int prefixNumber(String pre) {
TrieNode node = root;//头节点
char[] chs = pre.toCharArray();
int index = 0;
for (int i = 0; i < chs.length; i++) {
index = chs[i] - 'a';
if (node.nexts[index] == null) {//如果查到一半其中一个节点没有后续节点,那么说明没有以pre为前缀的,直接返回0
return 0;
}
node = node.nexts[index];//node向下查找
}
return node.end;//查找到pre的最后一个字符的时候,当时的node节点的pass值就是str加入的次数
}
delete()方法
删除在前缀树中的字符串(怎么加的怎么删)
经过一个节点,就给当前节点的pass--
当字符串遍历完成,给最后一个节点的end--
注意当节点的pass值为0的时候,这个节点不存在,把整个节点及其后续节点全部标空
public void delete(String str) {
if(search(str) == 0){//先确认前缀树中是否加入过str,如果没有加入过,直接返回
return;
}
if (str == null) {//删除空字符串时,头节点的pass--、end--
root.pass--;
root.end--;
return;
}
char[] chs = str.toCharArray();
TrieNode node = root;
node.pass--;
int index = 0;
for (int i = 0; i < chs.length; i++) {
index = chs[i] - 'a';
//当节点的pass值为0的时候,这个节点不存在,把整个节点及其后续节点全部标空
if (node.pass == 0) {
node = null;//Java的JVM在标空为null之后,会自动释放内存
return;
}
node = node.nexts[index];//node向下
node.pass--;//下一个节点的pass--
}
node.end--;//遍历完成一个字符串之后,end--
}