前缀树定义
分析
先来看字典树树的大致结构
可以看到, 我们使用每条边表示一个字符, 每个结点使用一个唯一的序号标识
比如
- 1->2->5 表示的字符串是 aa
- 1->3->7 表示的字符串是 ba
- …
通过图可以知道, 只要遵循上述约定, 假如有两个不相同的字符串有相同的前缀, 则它们可以共用一些结点, 但是这两个字符串的最后一个字符所在边指向的结点序号必定不同, 这个结论对n个不同字符串同样成立
题目要求
Trie()
初始化前缀树对象。void insert(String word)
向前缀树中插入字符串word
。boolean search(String word)
如果字符串word
在前缀树中,返回true
(即,在检索之前已经插入);否则,返回false
。boolean startsWith(String prefix)
如果之前已经插入的字符串word
的前缀之一为prefix
,返回true
;否则,返回false
1 <= word.length, prefix.length <= 2000
word
和prefix
仅由小写英文字母组成insert
、search
和startsWith
调用次数 总计 不超过 30000 次
题目分析
由于题目所要求的是建立小写英文字母的字典树, 故单个结点至多可以引出 26 条边
我们在字典树上插入字符串时, 比如插入字符串 bab, 但是字典树上只存在 ba 子串, 故需要在 6 号结点上新增一个指向 b 的边
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UL6IXUHY-1621752381574)(C:\Users\悠一木碧\AppData\Roaming\Typora\typora-user-images\image-20210523142212644.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u9Ft4Ra8-1621752381576)(C:\Users\悠一木碧\AppData\Roaming\Typora\typora-user-images\image-20210523142346138.png)]
故至此, 插入 bab 字符串结束
插入
为了表示在 6 号结点建立了 b 字符的边, 我们可以使用一个二维数组 nodesPath, 其中 nodesPath[i] 表示 i 号结点
其中 26 表示单个结点最多可以有26条边, 具体问题具体分析, 因为这个问题要求的是小写英文字母
int maxNodes = 100000;
int[][] nodesPath = new int[maxNodes][26];
我们可以以 nodesPath[0] 表示字典树的根节点, 根节点不表示字符, 所有指向根节点的边, 表示没有对应字符的边
// 表示根节点没有到 a 字母结点的边
nodesPath[0][0] = 0;
// 表示根节点到 b 字母结点的边指向的是索引为 1 的结点
nodesPath[0][1] = 1
故我们可以用字段来维护结点数, 这样增加一个结点的时候, 对结点数++, 同时结点获得了一个唯一的索引序号
遍历一个字符串 word 的所有字符, 当对应的边指向根节点时, 说明要增加边了
int currentNodeIndex = 0;
for (int i = 0; i < word.length(); i++) {
int c = word.charAt(i) - 'a';
if (nodesPath[currentNodeIndex][c] == 0) {
++nodeCount;
nodesPath[currentNodeIndex][c] = nodeCount;
}
currentNodeIndex = nodesPath[currentNodeIndex][c];
}
查找
当我们在字典树上新增了我们所需的全部字符后, 还需要做一个操作, 用于标记当前字符串被插入了
还记得上面我们说的: 假如有两个不相同的字符串有相同的前缀, 则它们可以共用一些结点, 但是这两个字符串的最后一个字符所在边指向的结点序号必定不同 结论吗?
思考下面一个问题
insert("abcd");
search("abc") = false ?
插入了 abcd 后, 字典树上必定存在 abc, 但是要求返回 search(“abc”) = false, startsWith(“abc”) = true;
故可以用一个一维数组 exists
int maxNodes = 100000;
int[] exists = new int[maxNodes];
当插入一个字符串后, 设置 exists[字符串最后一个字符的唯一标识] = true
这样, 即使找到了所有字符都在字典树上, 只要 exists[index] = false, 就说明这个字符串没有插入过, 也就是 search() = false;
而 startsWith()方法只需要字符存在即可, 返回 true
实现代码
class Trie {
/*
每个结点到下一个结点的路径
*/
private final int[][] nodesPath;
/*
标记每个单词是否插入过字典树
*/
private final boolean[] exist;
/*
结点数量
*/
private int nodeCount;
/*
当查询不到所需字符时, 返回的索引值
*/
private final int INDEX_WHEN_CHAR_NOT_EXISTS = -1;
/**
* Initialize your data structure here.
*/
public Trie() {
nodeCount = 0;
nodesPath = new int[100000][26];
exist = new boolean[100000];
}
public void insert(String word) {
int currentNodeIndex = 0;
for (int i = 0; i < word.length(); i++) {
int targetChar = word.charAt(i) - 'a';
if (nodesPath[currentNodeIndex][targetChar] == 0) {
++nodeCount;
nodesPath[currentNodeIndex][targetChar] = nodeCount;
}
currentNodeIndex = nodesPath[currentNodeIndex][targetChar];
}
exist[currentNodeIndex] = true;
}
public boolean search(String word) {
int index = getIndexOfLastChar(word);
/*
尽管每个字符都在字典树上出现过, 但是可能是其他字符串的子串, 故为了区分, 采用返回 exist数组中的值
*/
return INDEX_WHEN_CHAR_NOT_EXISTS != index && exist[index];
}
/**
* 查找对应的子串是否出现在字典树中, 只需要每个字符依次出现在字典树中即可
*/
public boolean startsWith(String prefix) {
int index = getIndexOfLastChar(prefix);
return INDEX_WHEN_CHAR_NOT_EXISTS != index;
}
private int getIndexOfLastChar(String word) {
int currentNodeIndex = 0;
for (int i = 0; i < word.length(); i++) {
int c = word.charAt(i) - 'a';
if (nodesPath[currentNodeIndex][c] == 0) {
return INDEX_WHEN_CHAR_NOT_EXISTS;
}
currentNodeIndex = nodesPath[currentNodeIndex][c];
}
return currentNodeIndex;
}
}
运行结果
总结
这份代码虽能实现问题所需, 但是效率有待提高