高级数据结构 - 前缀树
重要性质
每个节点至少包含两个基本属性
- children:数组或者集合,罗列出每个分支当中保安的所有字符
- isEnd:布尔值,表示该节点是否为某字符串的结尾
根节点是空的
除了根节点,其他所有节点都有可能是单词的结尾,叶子节点一定都是单词的结尾
案例1:LeetCode 208 实现Trie(前缀树)
实现一个 Trie (前缀树),包含 insert, search, 和 startsWith 这三个操作。
示例:
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple"); // 返回 true
trie.search("app"); // 返回 false
trie.startsWith("app"); // 返回 true
trie.insert("app");
trie.search("app"); // 返回 true
说明:
你可以假设所有的输入都是由小写字母 a-z 构成的。
保证所有输入均为非空字符串。
class Trie {
private Trie[] children = new Trie[26];
//是否为最后一个节点
private boolean isEnd = false;
/** Initialize your data structure here. */
public Trie() {
}
//添加字符串进入前缀树
public void insert(String word) {
Trie root = this;
char[] c = word.toCharArray();
for(int i = 0; i < c.length; i++){
//如果当前root节点没有这个子节点,则将当前字符加入子节点中
if(root.children[c[i] - 'a'] == null){
root.children[c[i] - 'a'] = new Trie();
}
//更新root节点
root = root.children[c[i] - 'a'];
}
root.isEnd = true;
}
//搜索
public boolean search(String word) {
Trie root = this;
char[] c = word.toCharArray();
for(int i = 0; i < c.length; i++){
//如果不存在子节点,意味着已经不匹配了,直接返回false
if(root.children[c[i] - 'a'] -= null){
return false;
}
root = root.children[c[i] - 'a'];
//最后还要记得判断是不是最后一个节点
return root.isEnd ? true : false;
}
//前缀匹配
public boolean startsWith(String prefix) {
Trie root = this;
char[] c = prefix.toCharArray();
for(int i = 0; i < c.length; i++){
if(root.children[c[i] - 'a'] == null){
return false;
}
root = root.children[c[i] - 'a'];
}
return true;
}
}
案例2:LeetCode 212 单词搜索
给定一个二维网格 board 和一个字典中的单词列表 words,找出所有同时在二维网格和字典中出现的单词。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不允许被重复使用。
示例:
输入:
words = ["oath","pea","eat","rain"] and board =
[
['o','a','a','n'],
['e','t','a','e'],
['i','h','k','r'],
['i','f','l','v']
]
输出: ["eat","oath"]
说明:
你可以假设所有输入都由小写字母 a-z 组成。
提示:
你需要优化回溯算法以通过更大数据量的测试。你能否早点停止回溯?
如果当前单词不存在于所有单词的前缀中,则可以立即停止回溯。什么样的数据结构可以有效地执行这样的操作?散列表是否可行?为什么? 前缀树如何?如果你想学习如何实现一个基本的前缀树,请先查看这个问题: 实现Trie(前缀树)。
解题:
这里的前缀树实现和上面的前缀树道理相同,唯一不同的是上面的前缀树使用布尔值记录是否是最终节点,而本题用的是String类型,为的是记录整个字典的值,方便添加在返回结果集中。
class Solution {
private int[][] directions = {{1,0},{-1,0},{0,1},{0,-1}};
private boolean[][] isChooes;
private List<String> res;
public List<String> findWords(char[][] board, String[] words) {
// 1、将字典添加到前缀树
Trie root = new Trie();
for(String word : words){
Trie cur = root;
char[] c = word.toCharArray();
for(int i = 0; i < c.length; i++){
cur = cur.append(c[i]);
}
//单词已经结束,加入前缀树节点中
cur.word = word;
}
// 2、初始化结果集
res = new ArrayList<String>();
isChooes = new boolean[board.length][board[0].length];
// 3、深度优先搜素
for(int i = 0; i < board.length; i++){
//对board每一个点都进行检索
for(int j = 0; j < board[i].length; j++){
dfs(root,i,j,board);
}
}
return res;
}
public void dfs (Trie cur,int x,int y,char[][] board){
//边界返回
if(x < 0 || y < 0 || x >= board.length || y >= board[0].length || isChooes[x][y]){
return;
}
isChooes[x][y] = true;
cur = cur.children[board[x][y] - 'a'];
if(cur != null){
if(cur.word != null){
res.add(cur.word);
// 清除节点值,防止后面重复添加该节点
cur.word = null;
}
for(int[] direction : directions){
dfs(cur, x + direction[0], y + direction[1], board);
}
}
// 回溯
isChooes[x][y] = false;
}
//前缀树实现
class Trie {
private Trie[] children = new Trie[26];
//最后一个节点,记录之前整个单词
private String word;
public Trie append(char ch){ //拼接字符
if(children[ch-'a'] != null){
return children[ch-'a']; //有则直接返回节点
}
children[ch-'a'] = new Trie(); //没有就新建节点
return children[ch-'a'];
}
}
}