前缀树
本文最后更新于 2024年6月25日 晚上
前缀树
前缀树(Trie)是一种用于字符串检索的树形结构,利用字符串的共同前缀达到节省空间的目的。它的查找效率优于二分查找,查找时间和字符串的字符数有关。
前缀树用边来代表字符,用节点来标记字符串的结束。基本操作有插入、查找、寻找前缀。
用树节点实现前缀树
Node 节点
private static class Node {
// isEnd 用作字符串结束标记
boolean isEnd;
// 节点持有一个容量为 26 的数组,代表 26 个英文字符
Node[] next = new Node[26];
}
如果不使用 isEnd 作为结束标记,当同时出现 app 和 apple 两个单词,我们可以通过没有后续节点判断 apple 存在,但因为 app 单词有后续节点,所以无法判断 app 单词是不是存在。使用 isEnd 可以在 p 这个边所指向的节点处标记 isEnd = true 来作为存在 app 的依据。
插入字符串。
public void insert(String word) {
Node node = head;
// 遍历字符
for (int i = 0; i < word.length(); i++) {
int idx = word.charAt(i) - 'a';
// 当代表字符的边不存在时,新建节点,并形成对应的边
if (node.next[idx] == null) {
node.next[idx] = new Node();
}
node = node.next[idx];
}
// 在最后一个节点标记字符串结束
node.isEnd = true;
}
查找字符串。遍历字符串,按照字符映射的边沿着树向下查找,如果找不到对应的边,说明树中没有这个字符串。遍历完毕后,如果最后一个节点有结束标记,可以判断字符串是在树内。
public boolean search(String word) {
Node node = head;
for (int i = 0; i < word.length(); i++) {
int idx = word.charAt(i) - 'a';
// 如果字符对应的边不存在,说明树中没有这个字符串
if (node.next[idx] == null) {
return false;
} else {
node = node.next[idx];
}
}
return node.isEnd;
}
查找前缀。大体和查找完整字符串一致,区别在遍历完给定的前缀后,无须判断是不是存在结束标记,直接返回存在前缀。
public boolean startsWith(String prefix) {
Node node = head;
for (int i = 0; i < prefix.length(); i++) {
int idx = prefix.charAt(i) - 'a';
// 如果字符对应的边不存在,说明树中没有这个字符串
if (node.next[idx] == null) {
return false;
} else {
node = node.next[idx];
}
}
// 因为是查找前缀,所以不用判断结束标记
return true;
}
完整代码
class Trie {
private static class Node {
boolean isEnd;
Node[] next = new Node[26];
}
private Node head;
public Trie() {
head = new Node();
}
public boolean search(String word) {
Node node = head;
for (int i = 0; i < word.length(); i++) {
int idx = word.charAt(i) - 'a';
if (node.next[idx] == null) {
return false;
} else {
node = node.next[idx];
}
}
return node.isEnd;
}
public boolean startsWith(String prefix) {
Node node = head;
for (int i = 0; i < prefix.length(); i++) {
int idx = prefix.charAt(i) - 'a';
if (node.next[idx] == null) {
return false;
} else {
node = node.next[idx];
}
}
return true;
}
}
用二维数组实现前缀树
通过节点实现的前缀树中,每个节点都持有一个容量 26 的数组,每次需要新节点时会创建新的对象。
可以把这些数组全都提前用二维数组创建好,然后使用行号作为指向下一个节点的指针。
二维数组实现前缀树相对于树节点来说并没有任何优势,主要是解题的时候写起来飞快。
class Trie {
// 新节点的指针
private int spaceIndex = 0;
// 提前开好二维数组,每一行都是一个前缀树节点
private int[][] tr = new int[200100][26];
// 每一行的结束标记
private int[] end = new int[200100];
public Trie() {
}
public void insert(String word) {
int node = 0;
for (int i = 0; i < word.length(); i++) {
int idx = word.charAt(i) - 'a';
// 如果为 0 表示指针是空,指向新的节点
if (tr[node][idx] == 0) {
tr[node][idx] = ++spaceIndex;
}
// 指针移动到新的节点上
node = tr[node][idx];
}
end[node]++;
}
public boolean search(String word) {
int node = 0;
for (int i = 0; i < word.length(); i++) {
int idx = word.charAt(i) - 'a';
node = tr[node][idx];
if (node == 0) {
return false;
}
}
return end[node] > 0;
}
public boolean startsWith(String prefix) {
int node = 0;
for (int i = 0; i < prefix.length(); i++) {
int idx = prefix.charAt(i) - 'a';
node = tr[node][idx];
if (node == 0) {
return false;
}
}
return true;
}
}
使用数组构建前缀树唯一要注意的是,需要预估判题时加入树中的字符串数量来设定二维数组的行数,即预设的节点数量。
前缀树
https://blog.avezah.com/tech/20230815-trie/