Leetcode_入门_字典树(前缀树)
Trie
1、实现 Trie (前缀树)
1)题目要求
实现一个 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 构成的。
2)我的解法
class Trie {
class TrieNode{
public boolean is_end;
public TrieNode[] next;
TrieNode(){
is_end=false;
next=new TrieNode[26];
}
}
TrieNode root;
/** Initialize your data structure here. */
public Trie() {
root=new TrieNode();
}
/** Inserts a word into the trie. */
public void insert(String word) {
TrieNode cur=root;
for(int i=0;i<word.length();i++){
if(cur.next[word.charAt(i)-'a']==null)cur.next[word.charAt(i)-'a']=new TrieNode();
cur=cur.next[word.charAt(i)-'a'];
}
cur.is_end=true;//最后一个字母之后那个结点设为结束,而不是最后一个字母设为结束
}
/** Returns if the word is in the trie. */
public boolean search(String word) {
TrieNode cur=root;
for(int i=0;i<word.length();i++){
if(cur.next[word.charAt(i)-'a']!=null)cur=cur.next[word.charAt(i)-'a'];
else return false;
}
if(!cur.is_end)return false;
return true;
}
/** Returns if there is any word in the trie that starts with the given prefix. */
public boolean startsWith(String prefix) {
TrieNode cur=root;
for(int i=0;i<prefix.length();i++){
if(cur.next[prefix.charAt(i)-'a']!=null)cur=cur.next[prefix.charAt(i)-'a'];
else return false;
}
return true;
}
}
/**
* Your Trie object will be instantiated and called as such:
* Trie obj = new Trie();
* obj.insert(word);
* boolean param_2 = obj.search(word);
* boolean param_3 = obj.startsWith(prefix);
*/
3)其他解法
关于前缀树的介绍、用途点击下面链接查看
public class Trie {
private boolean is_string=false;
private Trie next[]=new Trie[26];
public Trie(){}
public void insert(String word){//插入单词
Trie root=this;
char w[]=word.toCharArray();
for(int i=0;i<w.length;++i){
if(root.next[w[i]-'a']==null)root.next[w[i]-'a']=new Trie();
root=root.next[w[i]-'a'];
}
root.is_string=true;
}
public boolean search(String word){//查找单词
Trie root=this;
char w[]=word.toCharArray();
for(int i=0;i<w.length;++i){
if(root.next[w[i]-'a']==null)return false;
root=root.next[w[i]-'a'];
}
return root.is_string;
}
public boolean startsWith(String prefix){//查找前缀
Trie root=this;
char p[]=prefix.toCharArray();
for(int i=0;i<p.length;++i){
if(root.next[p[i]-'a']==null)return false;
root=root.next[p[i]-'a'];
}
return true;
}
}
作者:LeetCode
链接:link
来源:力扣(LeetCode)
4)自己的优化代码
class Trie {
class TrieNode{
public boolean is_end;
public TrieNode[] next;
TrieNode(){
is_end=false;
next=new TrieNode[26];
}
}
TrieNode root;
/** Initialize your data structure here. */
public Trie() {
root=new TrieNode();
}
/** Inserts a word into the trie. */
public void insert(String word) {
TrieNode cur=root;
for(int i=0;i<word.length();i++){
if(cur.next[word.charAt(i)-'a']==null)cur.next[word.charAt(i)-'a']=new TrieNode();
cur=cur.next[word.charAt(i)-'a'];
}
cur.is_end=true;//最后一个字母之后那个结点设为结束,而不是最后一个字母设为结束
}
/** Returns if the word is in the trie. */
public boolean search(String word) {
TrieNode cur=root;
for(int i=0;i<word.length();i++){
if(cur.next[word.charAt(i)-'a']!=null)cur=cur.next[word.charAt(i)-'a'];
else return false;
}
if(!cur.is_end)return false;
return true;
}
/** Returns if there is any word in the trie that starts with the given prefix. */
public boolean startsWith(String prefix) {
TrieNode cur=root;
for(int i=0;i<prefix.length();i++){
if(cur.next[prefix.charAt(i)-'a']!=null)cur=cur.next[prefix.charAt(i)-'a'];
else return false;
}
return true;
}
}
/**
* Your Trie object will be instantiated and called as such:
* Trie obj = new Trie();
* obj.insert(word);
* boolean param_2 = obj.search(word);
* boolean param_3 = obj.startsWith(prefix);
*/
5)学到的东西
前缀树的概念及用法
插入前缀树时,//最后一个字母之后那个结点设为结束,而不是最后一个字母设为结束
2、 键值映射(677、Medium)
1)题目要求
实现一个 MapSum 类里的两个方法,insert 和 sum。
对于方法 insert,你将得到一对(字符串,整数)的键值对。字符串表示键,整数表示值。如果键已经存在,那么原来的键值对将被替代成新的键值对。
对于方法 sum,你将得到一个表示前缀的字符串,你需要返回所有以该前缀开头的键的值的总和。
示例 1:
输入: insert(“apple”, 3), 输出: Null
输入: sum(“ap”), 输出: 3
输入: insert(“app”, 2), 输出: Null
输入: sum(“ap”), 输出: 5
2)我的解法
class MapSum {
class Trie{
boolean is_end=false;
int val=0;
Trie[] next=new Trie[26];
public Trie(){}
}
Trie root;
/** Initialize your data structure here. */
public MapSum() {
root=new Trie();
}
public void insert(String key, int val) {
Trie cur=root;
for(int i=0;i<key.length();i++){
cur.val+=val;
if(cur.next[key.charAt(i)-'a']==null)cur.next[key.charAt(i)-'a']=new Trie();
cur=cur.next[key.charAt(i)-'a'];
}
cur.val+=val;
if(cur.is_end){//当完全重复了时,进行覆盖
cur=root;
for(int i=0;i<key.length();i++){
cur.val=val;
cur=cur.next[key.charAt(i)-'a'];
}
cur.val=val;
}
cur.is_end=true;
}
public int sum(String prefix) {
Trie cur=root;
for(int i=0;i<prefix.length();i++){
if(cur.next[prefix.charAt(i)-'a']==null)return 0;
cur=cur.next[prefix.charAt(i)-'a'];
}
return cur.val;
}
}
3)其他解法
class MapSum {
private:
bool isEnd; // 是否为最后一个字母
MapSum* next[26]; // 字母表
int value; // 若为最后一个字母,其对应的值
// 深度优先遍历算法
int dfs(MapSum* root) {
if(!root) return 0; // 递归基:如果当前访问的MapSum为空,则直接返回0
int res = 0;
if(root->isEnd) res += root->value; // 若当前节点不为空且isEnd,则加上其值
for(MapSum* cur : root->next) { // 再遍历当前节点的next数组中所有的MapSum
res += dfs(cur);
}
return res;
}
public:
/** Initialize your data structure here. */
MapSum() {
isEnd = false;
memset(next, 0, sizeof(next));
value = 0;
}
void insert(string key, int val) {
MapSum* node = this;
for(char ch : key) {
if(node->next[ch - 'a'] == NULL) {
node->next[ch - 'a'] = new MapSum();
}
node = node->next[ch - 'a'];
}
node->isEnd = true;
node->value = val; // 相比较正常的前缀树,只是新增了一个value属性
}
int sum(string prefix) {
MapSum* node = this;
for(char ch : prefix) {
if(node->next[ch - 'a'] == NULL) return 0;
node = node->next[ch - 'a'];
}
return dfs(node);
}
};
作者:xiao-xiao-he-miao
链接:link
来源:力扣(LeetCode)
4)自己的优化代码
class MapSum {
class Trie{
boolean is_end=false;
int val=0;
Trie[] next=new Trie[26];
public Trie(){}
}
Trie root;
/** Initialize your data structure here. */
public MapSum() {
root=new Trie();
}
public void insert(String key, int val) {
Trie cur=root;
for(int i=0;i<key.length();i++){
cur.val+=val;
if(cur.next[key.charAt(i)-'a']==null)cur.next[key.charAt(i)-'a']=new Trie();
cur=cur.next[key.charAt(i)-'a'];
}
cur.val+=val;
if(cur.is_end){//当完全重复了时,进行覆盖
cur=root;
for(int i=0;i<key.length();i++){
cur.val=val;
cur=cur.next[key.charAt(i)-'a'];
}
cur.val=val;
}
cur.is_end=true;
}
public int sum(String prefix) {
Trie cur=root;
for(int i=0;i<prefix.length();i++){
if(cur.next[prefix.charAt(i)-'a']==null)return 0;
cur=cur.next[prefix.charAt(i)-'a'];
}
return cur.val;
}
}
5)学到的东西
前缀树
3、单词搜索 II(212、Hard)
1)题目要求
给定一个二维网格 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(前缀树)。
2)我的解法
class TrieNode{
public boolean is_end;
public TrieNode[] next;
public boolean tag;//判断该路径是否被访问过,search到达一次后置为true
TrieNode(){
is_end=false;
next=new TrieNode[26];
tag=false;
}
}
class Trie {
TrieNode root;
/** Initialize your data structure here. */
public Trie() {
root=new TrieNode();
}
/** Inserts a word into the trie. */
public void insert(String word) {
TrieNode cur=root;
for(int i=0;i<word.length();i++){
if(cur.next[word.charAt(i)-'a']==null)cur.next[word.charAt(i)-'a']=new TrieNode();
cur=cur.next[word.charAt(i)-'a'];
}
cur.is_end=true;//最后一个字母之后那个结点设为结束,而不是最后一个字母设为结束
}
/** Returns if the word is in the trie. */
public boolean search(String word) {
TrieNode cur=root;
for(int i=0;i<word.length();i++){
if(cur.next[word.charAt(i)-'a']!=null)cur=cur.next[word.charAt(i)-'a'];
else return false;
}
if(!cur.is_end)return false;
//如果被访问过,返回false
if(cur.tag)return false;
cur.tag=true;
//标记为被访问过
return true;
}
/** Returns if there is any word in the trie that starts with the given prefix. */
public boolean startsWith(String prefix) {
TrieNode cur=root;
for(int i=0;i<prefix.length();i++){
if(cur.next[prefix.charAt(i)-'a']!=null)cur=cur.next[prefix.charAt(i)-'a'];
else return false;
}
return true;
}
}
class Solution {
Trie t;
List<String> result=new ArrayList<>();
boolean[][] vi;
StringBuilder s=new StringBuilder();
public void backpack(int i,int j,char[][] board){
if(i<0||j<0||i>=board.length||j>=board[0].length||vi[i][j])return ;
s.append(board[i][j]);
vi[i][j]=true;
if(!t.startsWith(s.toString())){
vi[i][j]=false;
s.deleteCharAt(s.length()-1);
return ;
}
if(t.search(s.toString())){
result.add(s.toString());//不要return,继续向下进行(bend、benda)
}
backpack(i-1,j,board);
backpack(i+1,j,board);
backpack(i,j-1,board);
backpack(i,j+1,board);
vi[i][j]=false;
s.deleteCharAt(s.length()-1);
}
public List<String> findWords(char[][] board, String[] words) {
t=new Trie();
for(int i=0;i<words.length;i++){
t.insert(words[i]);
}
vi=new boolean[board.length][board[0].length];
for(int i=0;i<board.length;i++){
for(int j=0;j<board[0].length;j++){
backpack(i,j,board);
}
}
result.sort((o1,o2)->o1.compareTo(o2));
return result;
}
}
3)其他解法
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
public class findWords_212 {
public List<String> findWords(char[][] board, String[] words) {
//构建字典树
wordTrie myTrie=new wordTrie();
trieNode root=myTrie.root;
for(String s:words)
myTrie.insert(s);
//使用set防止重复
Set<String> result =new HashSet<>();
int m=board.length;
int n=board[0].length;
boolean [][]visited=new boolean[m][n];
//遍历整个二维数组
for(int i=0;i<board.length; i++){
for (int j = 0; j < board [0].length; j++){
find(board,visited,i,j,m,n,result,root);
}
}
System.out.print(result);
return new LinkedList<String>(result);
}
private void find(char [] [] board, boolean [][]visited,int i,int j,int m,int n,Set<String> result,trieNode cur){
//边界以及是否已经访问判断
if(i<0||i>=m||j<0||j>=n||visited[i][j])
return;
cur=cur.child[board[i][j]-'a'];
visited[i][j]=true;
if(cur==null)
{
//如果单词不匹配,回退
visited[i][j]=false;
return;
}
//找到单词加入
if(cur.isLeaf)
{
result.add(cur.val);
//找到单词后不能回退,因为可能是“ad” “addd”这样的单词得继续回溯
// visited[i][j]=false;
// return;
}
find(board,visited,i+1,j,m,n,result,cur);
find(board,visited,i,j+1,m,n,result,cur);
find(board,visited,i,j-1,m,n,result,cur);
find(board,visited,i-1,j,m,n,result,cur);
//最后要回退,因为下一个起点可能会用到上一个起点的字符
visited[i][j]=false;
}
}
//字典树
class wordTrie{
public trieNode root=new trieNode();
public void insert(String s){
trieNode cur=root;
for(char c:s.toCharArray()){
if(cur.child[c-'a']==null){
cur.child [c-'a'] = new trieNode();
cur=cur.child[c-'a'];
}else
cur=cur.child [c-'a'];
}
cur.isLeaf=true;
cur.val=s;
}
}
//字典树结点
class trieNode{
public String val;
public trieNode[] child=new trieNode[26];
public boolean isLeaf=false;
trieNode(){
}
}
作者:iwfly
链接:link
来源:力扣(LeetCode)
4)自己的优化代码
1、每次递归带上一个TrieNode cur,避免每次都重新遍历
class TrieNode{
public boolean is_end;
public TrieNode[] next;
public boolean tag;//判断该路径是否被访问过,search到达一次后置为true
TrieNode(){
is_end=false;
next=new TrieNode[26];
tag=false;
}
}
class Trie {
TrieNode root;
/** Initialize your data structure here. */
public Trie() {
root=new TrieNode();
}
/** Inserts a word into the trie. */
public void insert(String word) {
TrieNode cur=root;
for(int i=0;i<word.length();i++){
if(cur.next[word.charAt(i)-'a']==null)cur.next[word.charAt(i)-'a']=new TrieNode();
cur=cur.next[word.charAt(i)-'a'];
}
cur.is_end=true;//最后一个字母之后那个结点设为结束,而不是最后一个字母设为结束
}
}
class Solution {
Trie t;
List<String> result=new ArrayList<>();
boolean[][] vi;
StringBuilder s=new StringBuilder();
public void backpack(int i,int j,char[][] board,TrieNode cur){
if(i<0||j<0||i>=board.length||j>=board[0].length||vi[i][j])return ;
if(cur.next[board[i][j]-'a']==null)return;//相当于startwith
cur=cur.next[board[i][j]-'a'];
s.append(board[i][j]);
vi[i][j]=true;
if(cur.is_end&&!cur.tag){//相当于search
cur.tag=true;
result.add(s.toString());//不要return,继续向下进行(bend、benda)
}
backpack(i-1,j,board,cur);
backpack(i+1,j,board,cur);
backpack(i,j-1,board,cur);
backpack(i,j+1,board,cur);
vi[i][j]=false;
s.deleteCharAt(s.length()-1);
}
public List<String> findWords(char[][] board, String[] words) {
t=new Trie();
for(int i=0;i<words.length;i++){
t.insert(words[i]);
}
vi=new boolean[board.length][board[0].length];
for(int i=0;i<board.length;i++){
for(int j=0;j<board[0].length;j++){
backpack(i,j,board,t.root);
}
}
result.sort((o1,o2)->o1.compareTo(o2));
return result;
}
}
2、直接每个结点记录字符串,省去append时间
class TrieNode{
public boolean is_end;
public TrieNode[] next;
public boolean tag;//判断该路径是否被访问过,search到达一次后置为true
public String val;
TrieNode(){
is_end=false;
next=new TrieNode[26];
tag=false;
}
}
class Trie {
TrieNode root;
/** Initialize your data structure here. */
public Trie() {
root=new TrieNode();
}
/** Inserts a word into the trie. */
public void insert(String word) {
TrieNode cur=root;
for(int i=0;i<word.length();i++){
if(cur.next[word.charAt(i)-'a']==null)cur.next[word.charAt(i)-'a']=new TrieNode();
cur=cur.next[word.charAt(i)-'a'];
}
cur.is_end=true;//最后一个字母之后那个结点设为结束,而不是最后一个字母设为结束
cur.val=word;
}
}
class Solution {
Trie t;
List<String> result=new ArrayList<>();
boolean[][] vi;
StringBuilder s=new StringBuilder();
public void backpack(int i,int j,char[][] board,TrieNode cur){
if(i<0||j<0||i>=board.length||j>=board[0].length||vi[i][j])return ;
if(cur.next[board[i][j]-'a']==null)return;//相当于startwith
cur=cur.next[board[i][j]-'a'];
vi[i][j]=true;
if(cur.is_end&&!cur.tag){//相当于search
cur.tag=true;
result.add(cur.val);//不要return,继续向下进行(bend、benda)
}
backpack(i-1,j,board,cur);
backpack(i+1,j,board,cur);
backpack(i,j-1,board,cur);
backpack(i,j+1,board,cur);
vi[i][j]=false;
}
public List<String> findWords(char[][] board, String[] words) {
t=new Trie();
for(int i=0;i<words.length;i++){
t.insert(words[i]);
}
vi=new boolean[board.length][board[0].length];
for(int i=0;i<board.length;i++){
for(int j=0;j<board[0].length;j++){
backpack(i,j,board,t.root);
}
}
result.sort((o1,o2)->o1.compareTo(o2));
return result;
}
}
5)学到的东西
前缀树
回溯、剪枝
用标记tag(或HashSet)去重
每次递归带上一个TrieNode cur,避免每次都重新遍历
直接每个结点记录字符串,省去append时间