字典树类似于二叉树,内部结构并不复杂,在前缀方面百试百灵,却没有红黑树,AVL那么多变化,赶紧码起来
一、字典树介绍
字典树并不是死的,其内部其实是多变的,在介绍中会讲解可能的变式
1、定义结点
与二叉树一样,结点需要存储其数据,以及下一个结点的,若结点中只包含字母,我们有两种方法连接到next结点
HashMap型:Node中用hashmap来连接到下一个结点,适用于字母类型少的情况
数组型:Node中用数组来连接到下一个结点,适用于字母类型多的情况
private class Node {
String word;
HashMap<Character, Node> child; // hashmap型
}
解释一下word的作用:有的结点可能只作为路径存在(上图白色结点),而有的需要存储信息(上图红色结点),而存储方式可以依据需要修改
boolean:可以用于判断是否存在某个单词
String:可以直接返回找到的单词
int:可以用于记录结点被访问的次数,或如果字典中有重复的单词,可以用以记录数量
依据需要去修改结点即可,还可以尝试压缩路径
2、创建根节点
根节点是字典树的起始点,是不存储信息的
Node root = new Node();
3、插入方法insert()
给一个String
,将其插入到字典树中。
将String
的char
一个个分离出来,利用hashmap进行插入就好,并在最后进行信息的存储
public void insert(String word) {
Node cur = root;
for (char c : word.toCharArray()) {
if (!cur.child.containsKey(c))
cur.child.put(c, new Node);
cur = cur.child.get(c);
}
// 此时cur位于word最后char所在的结点
cur.word = word;
}
4、查找方法search()
给一个String
,判断是否在字典树中
方法与insert一样,将String
的char
一个个分离出来,若hashmap中没找到,或最后的结点没有存信息,则返回false
public boolean search(String word) {
Node cur = root;
for (char c : word.toCharArray()) {
if (!cur.child.containsKey(c))
return false;
cur = cur.child.get(c);
}
return cur.word.equals(word);
}
当然insert()
和search()
方法很像,可以利用形参试着合并
二、字典树实战
1、题目一:LeetCode 211 添加与搜索单词
这道题仅有一点变化,搜索的时候.
可以代替单词,所以search()
方法需要特殊处理
当遇到.
时,需要遍历hashmap中所有的结点,所以search()
使用递归
另外由于不需要返回String
,所以Node存储boolean
即可
private boolean search(Node cur, String word, int i) {
if (i == word.length())
return cur.exist;
char c = word.charAt(i);
if (cur.children.containsKey(c)) {
return search(cur.children.get(c), word, i + 1);
} else if (c == '.') {
for (Node child : cur.children.values())
if (search(child, word, i + 1))
return true;
}
return false;
}
2、题目二:LeetCode 212 单词搜索Ⅱ
由于要返回我们查找到的所有单词,所以Node需要以String
来记录
private class Node {
String word;
HashMap<Character, Node> map;
Node() {
this.word = null;
map = new HashMap<>();
}
}
初始化时,添加所有单词到字典树中,insert()
就不提供代码了
final int[] dx = new int[]{0, 1, 0, -1};
final int[] dy = new int[]{1, 0, -1, 0}; // 右下左上
boolean[][] visited;
char[][] board;
List<String> ans;
int rows, cols;
Node root;
public List<String> findWords(char[][] board, String[] words) {
ans = new ArrayList<>();
root = new Node();
this.board = board;
rows = board.length;
cols = board[0].length;
for (String s : words)
insert(s);
}
我们采用dfs的方式展开搜索,让字典树和棋盘同步搜索
private void dfs(Node cur, int x, int y) {
if (cur.word != null) {
ans.add(cur.word);
cur.word = null; // 防止对同一单词重复添加
}
if (cur.map.isEmpty()) // 提前判断,剪枝
return;
visited[x][y] = true;
for (int dir = 0; dir < 4; dir++) {
int nx = x + dx[dir];
int ny = y + dy[dir];
if (nx < 0 || nx >= rows || ny < 0 || ny >= cols || visited[nx][ny] || !cur.map.containsKey(board[nx][ny]))
continue;
dfs(cur.map.get(board[nx][ny]), nx, ny);
}
visited[x][y] = false; // 还原
}
以上是基本的bfs + 回溯,只不过多了字典树,多一步判断
但我们发现字典树存在冗余,考虑下面的情况
当我们找到aaa
之后,最终的结点的数据我们已经得到了,但发现字典树的结构仍然存在,每次遍历都要到最底下的结点,发现没有数据后再一步步返回,产生大量冗余
所以我们试着去一边dfs一边删除一部分的字典树结构
当一个结点的map空了,并且没有数据了,我们便从他从父结点上将之删除,以此来去冗
private void dfs(Node cur, int x, int y) {
// 这里的cur是父节点,child是子节点(也就是当前结点)
Node child = cur.map.get(board[x][y]);
if (child.word != null) {
ans.add(child.word);
child.word = null;
}
if (child.map.isEmpty()) { // 删除部分字典树结构
cur.map.remove(board[x][y]);
return;
}
visited[x][y] = true;
for (int dir = 0; dir < 4; dir++) {
int nx = x + dx[dir];
int ny = y + dy[dir];
if (nx < 0 || nx >= rows || ny < 0 || ny >= cols || visited[nx][ny] || !cur.map.containsKey(board[nx][ny]))
continue;
dfs(child, nx, ny);
}
visited[x][y] = false;
}
4、实战题目:
LeetCode 208 实现Trie(前缀树)
LeetCode 211 添加与搜索单词-数据结构设计
LeetCode 14 最长公共前缀
LeetCode 421 数组中两个数的最大异或值