前缀树(字典树)
- 概念
- 原理分析
- 代码
- 应用
- 例题及题解
一、什么是前缀树
这是一种多叉树,它主要解决的问题是能在一组字符串里快速的进行某个字符串的匹配。而它的这种高效正是建立在算法的以空间换时间的思想上,字符串的每一个字符都会成为一个树的节点,这些字符可以是任意一个字符集中的字符。比如对于都是小写字母的字符串,字符集就是’a’-‘z’;对于都是数字的字符串,字符集就是’0’-‘9’;对于二进制字符串,字符集就是0和1。
例如我们把这样一组单词[‘bag’, ‘and’, ‘banana’, ‘ban’, ‘am’, ‘board’, ‘ball’]进行Trie化后就会成为以下这样:
根节点为空,因为子节点都是存储单词开头的缘故。Trie树的本质就是将单词之间的公共前缀合并起来,这也就会造成单词ban和banana公用同一条路径,所以需要在单词的结尾处给一个标识符,表示该字符为一个单词的结束。
二、原理分析
前缀树通常有4个功能:
1、Trie.insert(String str) 插入字符串str
2、Trie.search(String str) 查询字符串str
3、Trie.prefixNum(String pre) 查询以pre为前缀字符串的个数
4、Trie.delete(String str) 从该Trie树删除字符串str
假设我们的前缀树用来存储单词,首先我们定义节点类,它用于描述每个节点的具体情况
class Node{
public int pass; //经过该节点的次数
public int end; //该节点作为字符串结尾的次数
public Node[] next;
public Node() {
pass=0;
end=0;
next=new Node[26];
}
}
现在我们要插入一个单词 abc,node从root开始,以a为子节点不存在创建a子节点,并使node指向该节点且pass+1;以b为子节点不存在创建b子节点,并使node指向该节点且pass+1;如此循环直到遍历完整个字符串,最后node指向最后一个节点,end+1。
此时前缀树为:
我们再插入一个单词abd,此时前缀树变为:
关于构建前缀树的总结:
三、代码
实现方式有两种:
1、数组的方式
2、哈希表的方式(更灵活,只需把next[]数组换位HashMap)
以下我们以数组的方式实现
public class TrieTree {
public static void main(String[] args) {
Trie trieTree=new Trie();
String[] arr= {"abc","abcd","ade","abcde"};
for(String s:arr) {
trieTree.insert(s);
}
// trieTree.insert("abc");
// System.out.println(trieTree.search("abc")!=0?"存在":"不存在");
// trieTree.delete("abc");
// System.out.println(trieTree.search("abc")==1?"存在":"不存在");
System.out.println(trieTree.prefixNumber("abcd"));
}
}
//实现类
class Trie{
private Node root; //前缀树的根节点
public Trie() {
root=new Node();
}
//插入新字符串
public void insert(String word) {
if(word==null) {
return;
}
char[] arr=word.toCharArray(); //将插入的字符串转化为char数组
Node node=root;
int path=0;
for(char c:arr) {
path=c-'a';
if(node.next[path]==null) { //该节点不存在
node.next[path]=new Node();
}
node=node.next[path];
node.pass++;
}
node.end++;
}
//查找该字符串是否存在
public int search(String word) {
if(word==null) {
return 0;
}
char[] arr=word.toCharArray();
int path=0;
Node node=root;
for(char c:arr) {
path=c-'a';
if(node.next[path]==null) { //查找的字符对应的节点不存在
return 0;
}
node=node.next[path];
}
return node.end;
}
public void delete(String word) {
if(search(word)!=0) {
char[] arr=word.toCharArray();
int path=0;
Node node=root; //从root开始
node.pass--; //该节点的pass减一
for(char c:arr) {
path=c-'a';
if(--node.next[path].pass==0) {
node.next[path]=null;
return;
}
node=node.next[path];
}
node.end--; //结尾节点的end减一
}
}
//查找以该pre为前缀的字符的数量
public int prefixNumber(String pre) {
if(pre==null) {
return 0;
}
char[] arr=pre.toCharArray();
Node node=root;
int path=0;
for(char c:arr) {
path=c-'a';
if(node.next[path]==null) {
return 0;
}
node=node.next[path];
}
return node.pass;
}
}
//节点类
class Node{
public int pass; //经过该节点的次数
public int end; //该节点作为字符串结尾的次数
public Node[] next;
public Node() {
pass=0;
end=0;
next=new Node[26];
}
}
四、应用
IDE的自动补全,单词搜索,IP路由(最长前缀匹配)等。
五、例题及题解
LeetCode212:单词搜索II
我们可以使用 TrieTrie 结构进行建树,对于任意一个当前位置 (i, j)(i,j) 而言,只有在 TrieTrie 中存在往从字符 aa 到 bb 的边时,我们才在棋盘上搜索从 aa 到 bb 的相邻路径。
对于本题,我们可以使用「TrieNode」的方式进行建 TrieTrie。
题解:
class Solution {
Set<String> set = new HashSet<>();
List<String> ans = new ArrayList<>();
char[][] board;
int[][] dirs = new int[][]{{1,0},{-1,0},{0,1},{0,-1}};
int n, m;
boolean[][] vis = new boolean[15][15];
public List<String> findWords(char[][] _board, String[] words) {
board = _board;
m = board.length; n = board[0].length;
for (String w : words) set.add(w);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
vis[i][j] = true;
sb.append(board[i][j]);
dfs(i, j, sb);
vis[i][j] = false;
sb.deleteCharAt(sb.length() - 1);
}
}
return ans;
}
void dfs(int i, int j, StringBuilder sb) {
if (sb.length() > 10) return ;
if (set.contains(sb.toString())) {
ans.add(sb.toString());
set.remove(sb.toString());
}
for (int[] d : dirs) {
int dx = i + d[0], dy = j + d[1];
if (dx < 0 || dx >= m || dy < 0 || dy >= n) continue;
if (vis[dx][dy]) continue;
vis[dx][dy] = true;
sb.append(board[dx][dy]);
dfs(dx, dy, sb);
vis[dx][dy] = false;
sb.deleteCharAt(sb.length() - 1);
}
}
}
参考博文:https://blog.csdn.net/weixin_43314519/article/details/106990273