前缀树
前缀树:又称单词查找树或键树,是一种哈希树的变种。
典型应用是用于统计和排序大量的字符串(但不仅限于字符串)
利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较。
将一组字符串数组放入前缀树中的演示
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) {//如果查到一半其中一个节点没有后续节点,那么说明没有加入过,直接返回0return 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为前缀的,直接返回0return 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--}