0.【LeetCode】208. 实现 Trie (前缀树)
Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。
请你实现 Trie 类:
Trie()初始化前缀树对象。void insert(String word)向前缀树中插入字符串word。boolean search(String word)如果字符串word在前缀树中,返回true(即,在检索之前已经插入);否则,返回false。boolean startsWith(String prefix)如果之前已经插入的字符串word的前缀之一为prefix,返回true;否则,返回false。
前缀树的应用:
自动补全
拼写检查
词频统计
前缀树的实现
/**
* @author SJ
* @date 2021/4/2
*/
public class Trie {
public TrieNode root;//树包含根节点
//新建前缀树节点
public static class TrieNode {
public TrieNode[] children = new TrieNode[26]; //指向子节点
public int freq = 1;//以该节点为end的单词出现的频次
public boolean isEnd = false;//是否是终点
//无参构造,根节点
public TrieNode() {
}
//看子节点是否包含
public boolean containsKey(char ch) {
return this.children[ch - 'a'] != null;
}
//插入该字符
public void putKey(TrieNode node, char ch) {
this.children[ch - 'a'] = node;
}
//通过字符拿到该节点
public TrieNode getNode(char ch) {
return this.children[ch - 'a'];
}
}
//初始化前缀树
public Trie() {
root = new TrieNode();
}
//向前缀树中插入一个单词
public void insert(String word) {
TrieNode curNode = root;//拿到当前树的根节点
for (int i = 0; i < word.length(); i++) {
char curChar = word.charAt(i);
//如果当前树的子树里没有这个字符则插入
if (!curNode.containsKey(curChar)) {
curNode.putKey(new TrieNode(), curChar);
}
curNode = curNode.getNode(curChar);
}
curNode.isEnd = true;
cueNode.freq++;
}
//返回该前缀的最终节点,若不存在该前缀则返回空
public TrieNode searchPrefix(String word) {
TrieNode curNode = root;
for (int i = 0; i < word.length(); i++) {
char curChar = word.charAt(i);
if (curNode.containsKey(curChar)) {
curNode = curNode.getNode(curChar);
} else {
return null;
}
}
return curNode;
}
//判断一个单词是否在前缀树中
public boolean search(String word) {
TrieNode curNode = searchPrefix(word);
return curNode != null && curNode.isEnd;
}
//返回是否存在该前缀
public boolean startsWith(String prefix) {
TrieNode curNode = searchPrefix(prefix);
return curNode != null;
}
//单词出现的频率统计
public int getfreq(String word) {
TrieNode curNode = searchPrefix(word);
if (curNode == null || !curNode.isEnd)
return 0;
else
return curNode.freq;
}
public static void main(String[] args) {
Trie trie = new Trie();
trie.insert("apple");
trie.insert("apple");
System.out.println(trie.search("apple")); // 返回 True
System.out.println(trie.search("app")); // 返回 False
System.out.println(trie.startsWith("app")); // 返回 True
trie.insert("app");
System.out.println(trie.search("app")); // 返回 True
System.out.println(trie.getfreq("apple"));//返回2
}
}
总结:
前缀树节点:
属性:(没有data属性,data属性用隐含的下标表示,例如:children[0]不为空,则包含一个值为'a'的子节点)
- 子节点数组【大小为26,查找时采用下标索引】
- 节点出现的频次
- 是否终点
方法:
- 子节点数组是否包含该值为该节点的节点
- 向子节点数组中插入某一节点
- 通过字符值拿到当前节点所在的子节点
前缀树:
属性:
- 类型为前缀树节点的根节点
方法:
- 构造一个前缀树
- 向前缀树中插入一个单词
- 判断该前缀树种是否存在某一前缀,若存在,则返回该前缀最后一个节点的位置
- 判断该前缀树种是否存在某一单词
- 统计某一单词出现的次数
前缀树的应用例题
1.【LeetCode】面试题 16.02. 单词频率
设计一个方法,找出任意指定单词在一本书中的出现频率。
你的实现应该支持如下操作:
WordsFrequency(book)构造函数,参数为字符串数组构成的一本书get(word)查询指定单词在书中出现的频率
示例:
WordsFrequency wordsFrequency = new WordsFrequency({"i", "have", "an", "apple", "he", "have", "a", "pen"});
wordsFrequency.get("you"); //返回0,"you"没有出现过
wordsFrequency.get("have"); //返回2,"have"出现2次
wordsFrequency.get("an"); //返回1
wordsFrequency.get("apple"); //返回1
wordsFrequency.get("pen"); //返回1
提示:
book[i]中只包含小写字母1 <= book.length <= 1000001 <= book[i].length <= 10get函数的调用次数不会超过100000
代码:直接套用上面已经实现的前缀树
/**
* @author SJ
* @date 2021/4/2
*/
public class WordsFrequency {
public TrieNode root;//树包含根节点
//新建前缀树节点
public static class TrieNode {
public TrieNode[] children = new TrieNode[26]; //指向子节点
public int freq = 0;//节点的频次
public boolean isEnd = false;//是否是终点
//无参构造,根节点
public TrieNode() {
}
//看子节点是否包含
public boolean containsKey(char ch) {
return this.children[ch - 'a'] != null;
}
//插入该字符
public void putKey(TrieNode node, char ch) {
this.children[ch - 'a'] = node;
}
//通过字符拿到该节点
public TrieNode getNode(char ch) {
return this.children[ch - 'a'];
}
}
//向前缀树中插入一个单词
public void insert(String word) {
TrieNode curNode = root;//拿到当前树的根节点
for (int i = 0; i < word.length(); i++) {
char curChar = word.charAt(i);
//如果当前树的子树里没有这个字符则插入
//若存在且该节点为末尾节点。单词频次加1
if (!curNode.containsKey(curChar)) {
curNode.putKey(new TrieNode(), curChar);
}
curNode = curNode.getNode(curChar);
}
curNode.isEnd = true;
curNode.freq++;
}
//返回该前缀的最终节点,若不存在该前缀则返回空
public TrieNode searchPrefix(String word) {
TrieNode curNode = root;
for (int i = 0; i < word.length(); i++) {
char curChar = word.charAt(i);
if (curNode.containsKey(curChar)) {
curNode = curNode.getNode(curChar);
} else {
return null;
}
}
return curNode;
}
//单词出现的频率统计
public int getfreq(String word) {
TrieNode curNode = searchPrefix(word);
if (curNode == null || !curNode.isEnd)
return 0;
else
return curNode.freq;
}
public WordsFrequency(String[] book) {
root=new TrieNode();
for (int i = 0; i < book.length; i++) {
insert(book[i]);
}
}
public void insertBook(String[] book){
for (int i = 0; i < book.length; i++) {
insert(book[i]);
}
}
public int get(String word) {
return this.getfreq(word);
}
public static void main(String[] args) {
String[] book={"o","op","o"};
WordsFrequency wordsFrequency = new WordsFrequency(book);
int o = wordsFrequency.getfreq("op");
System.out.println(o);
}
}
为了完成单词搜索2,我们先完成单词搜索1
2.【LeetCode】79. 单词搜索
给定一个二维网格和一个单词,找出该单词是否存在于网格中。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例:
board =
[
['A','B','C','E'],
['S','F','C','S'],
['A','D','E','E']
]
给定 word = "ABCCED", 返回 true
给定 word = "SEE", 返回 true
给定 word = "ABCB", 返回 false
思路很简单,找到单词首字母所在棋盘内的位置,以该首字母为起点进行深度优先搜索,一旦找到单词了单词的最后一个字母就在下一轮直接跳出。
/**
* @author SJ
* @date 2021/4/3
*/
public class SearchWords {
public static boolean exist(char[][] board, String word) {
flag=false;
int X=board[0].length;//棋盘宽度
int Y=board.length;//棋盘高度
boolean[][] visited=new boolean[Y][X];
for (int i = 0; i <Y ; i++) {
for (int j = 0; j < X; j++) {
if (board[i][j]==word.charAt(0)){
//visited每一遍都要更新
for (int m = 0; m < Y; m++) {
for (int n = 0; n < X; n++) {
visited[m][n]=false;
}
}
visited[i][j]=true;
if (word.length()==1)
return true;
else if (search(board,i,j,word,1,visited))
return true;
}
}
}
return false;
}
static boolean flag;
//从坐标为(i,j)的位置开始搜索board,当前搜索到了work中下标为k的位置
public static boolean search(char[][] board,int i,int j,String word,int k,boolean[][] visited){
if (k==word.length()){
flag=true;
return true;
}
//向左找
if (j-1>=0&&!visited[i][j-1]&&word.charAt(k)==board[i][j-1]){
visited[i][j-1]=true;
search(board,i,j-1,word,k+1,visited);
if (flag)
return true;
else
visited[i][j-1]=false;
}
//向上找
if (i-1>=0&&!visited[i-1][j]&&word.charAt(k)==board[i-1][j]){
visited[i-1][j]=true;
search(board,i-1,j,word,k+1,visited);
if (flag)
return true;
else
visited[i-1][j]=false;
}
//向下找
if (i+1<board.length&&!visited[i+1][j]&&word.charAt(k)==board[i+1][j]){
visited[i+1][j]=true;
search(board,i+1,j,word,k+1,visited);
if (flag)
return true;
else
visited[i+1][j]=false;
}
//向右找
if (j+1<board[0].length&&!visited[i][j+1]&&word.charAt(k)==board[i][j+1]){
visited[i][j+1]=true;
search(board,i,j+1,word,k+1,visited);
if (flag)
return true;
else
visited[i][j+1]=false;
}
return false;
}
public static void main(String[] args) {
char[][] board={{'C','A','A'},{'A','A','A'},{'B','C','D'}};
String word="AAB";
boolean exist = exist(board, word);
System.out.println(exist);
}
}
3.【LeetCode】212. 单词搜索 II
给定一个 m x n 二维字符网格 board 和一个单词(字符串)列表 words,找出所有同时在二维网格和字典中出现的单词。
单词必须按照字母顺序,通过 相邻的单元格 内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不允许被重复使用。
示例 1:
输入:board = [["o","a","a","n"],["e","t","a","e"],["i","h","k","r"],["i","f","l","v"]], words = ["oath","pea","eat","rain"]
输出:["eat","oath"]
思路:
- 根据给定字典构造前缀树,先构造前缀树节点
- 记录子节点数组,便于快速定位
- 记录子节点数量,便于确定叶子节点,过程中进行剪枝
- 是否单词终点,这个不用多说
- 父节点,便于回溯到根打印单词
- 节点的值,便于回溯打印单词
- 遍历网格,如果当前网格字符存在于前缀树的第一层节点中(默认第0层位根),则将该子树拿出,对其进行深度优先搜索。
- 网格搜索与树的遍历同时进行。假设当前搜索到树的第3层。网格有四个方向,当且仅当该方向的字符存在于前缀树第三层,网格才继续向这个方向进行。
- 对于前缀树来说,每一层搜索都要判断当前节点是不是end节点,若是,则证明搜索到一个单词,将该单词放入结果集中,并对前缀树进行清理。
注:前缀树是你自己定义的,你想要什么属性就加什么属性,想要什么功能就自己写,我们只是参考这个数据结构的思路而已,不要被网上的条条框框框住了。
import java.util.ArrayList;
import java.util.List;
/**
* @author SJ
* @date 2021/4/3
*/
public class SearchWords2 {
//前缀树节点
public static class TrieNode {
//子节点
TrieNode[] children = new TrieNode[26];
int childrenNum = 0;
//是否终点
boolean isEnd = false;
//parent
TrieNode fatherNode;
//数据
Character data;
//子节点是否包含某一字母
public boolean contain(char ch) {
return children[ch - 'a'] != null;
}
//拿到该节点
public TrieNode getKey(char ch) {
return children[ch - 'a'];
}
//插入该节点
public void putKey(TrieNode node, char ch) {
node.fatherNode = this;
node.data = ch;
this.children[ch - 'a'] = node;
this.childrenNum++;
}
//设为终点
public void setEnd() {
this.isEnd = true;
}
//删除该节点
public void deleteNode() {
if (this.fatherNode != null) {
TrieNode node = this.fatherNode;
node.children[this.data - 'a'] = null;
}
}
}
//树根
public TrieNode root;
//插入一个单词
public void insert(String word) {
TrieNode node = root;
for (int i = 0; i < word.length(); i++) {
if (!node.contain(word.charAt(i)))
node.putKey(new TrieNode(), word.charAt(i));
node = node.getKey(word.charAt(i));
}
node.setEnd();
}
//插入一组单词
public void insertDic(String[] words) {
for (String word : words) {
insert(word);
}
}
//返回当前节点到根节点之间的单词,并对当前的前缀树进行请理
public String outPutWord(TrieNode node) {
StringBuilder stringBuilder = new StringBuilder();
node.isEnd = false;
while (node.data != null) {
stringBuilder.insert(0, node.data);
TrieNode temp = node;
node = node.fatherNode;
//在这里对前缀树进行请理,如果是叶子节点,就直接删除,如果非叶子节点,就将isEnd置为false
if (temp.childrenNum == 0 && !temp.isEnd) {
temp.deleteNode();
node.childrenNum--;
}
}
return stringBuilder.toString();
}
static List<String> ans;
public List<String> findWords(char[][] board, String[] words) {
//构造前缀树
root = new TrieNode();
insertDic(words);
//返回数组
ans = new ArrayList<>();
int X = board[0].length;//棋盘宽度
int Y = board.length;//棋盘高度
boolean[][] visited = new boolean[Y][X];
for (int i = 0; i < Y; i++) {
for (int j = 0; j < X; j++) {
if (root.contain(board[i][j])) {
//每次都要初始化visited数组
for (int m = 0; m < Y; m++) {
for (int n = 0; n < X; n++) {
visited[m][n] = false;
}
}
visited[i][j] = true;
TrieNode node = root.getKey(board[i][j]);
search(node, board, i, j, visited);
}
}
}
return ans;
}
//棋盘到了某一节点(i,j),先看当前节点的children里有无该节点,如果有则继续往下搜
public void search(TrieNode node, char[][] board, int i, int j, boolean[][] visited) {
//当搜索到end节点时,说明搜索完了一个单词,则输出
if (node.isEnd) {
ans.add(outPutWord(node));
}
//当搜索到叶子节点时,进行回溯
if (node.childrenNum == 0)
return;
//上下左右四个方向寻找
int[][] state = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
for (int[] ints : state) {
int cur_i = ints[0] + i;
int cur_j = ints[1] + j;
if (cur_i >= 0 && cur_i < board.length && cur_j >= 0 && cur_j < board[0].length && node.contain(board[cur_i][cur_j])) {
char c = board[cur_i][cur_j];
visited[cur_i][cur_j] = true;
search(node.getKey(c), board, cur_i, cur_j, visited);
visited[cur_i][cur_j] = false;
}
}
}
public static void main(String[] args) {
char[][] board = {{'o', 'a', 'a', 'n'}, {'e', 't', 'a', 'e'}, {'i', 'h', 'k', 'r'}, {'i', 'f', 'l', 'v'}};
String[] words = {"oath", "pea", "eat", "rain"};
SearchWords2 searchWords2 = new SearchWords2();
searchWords2.findWords(board, words);
for (String an : ans) {
System.out.println(an);
}
}
}