题意整理
- 构造一个数据结构,用来处理字符串。
- 这个数据结构可以插入、删除、查询字符串。
- 查询分两种,一种是查询字符串是否出现过,另一种是查询前缀单词出现次数。
方法一(TrieNode实现)
1.解题思路
首先构建一个TrieNode结构,包括一个TrieNode类型的child数组,用于记录所有子节点,一个整型变量pre_number,用于表示插入单词时,当前节点被访问次数,一个boolean型变量end,用于标记当前节点是否是某个单词的结尾。
然后初始化一个根节点,根节点是空心的,即不包含任何字符。
- 添加word:将单词转为字符数组,从根节点出发,遍历输入的单词,如果子节点不包含当前字符,则新建对应子节点,如果包含,则跳到对应子节点,同时访问次数加一。单词遍历完成后,当前节点标识改为true。
- 删除word:相当于添加的反向操作,不断往子节点方向移动,同时访问次数减一。遍历完成后,如果访问次数为0,则将标识改为false。
- 查询word:将单词转为字符数组,从根节点出发,遍历输入的单词,如果子节点不包含当前字符,说明不存在该单词,返回false,如果包含,就往子节点方向移动。遍历完成后,标识为true,说明存在该单词。
- 查询以pre为前缀的单词数量:将单词转为字符数组,从根节点出发,遍历输入的单词,如果子节点不包含当前字符,说明不存在该前缀,返回0,如果包含,就往子节点方向移动。遍历完成后,pre_number的值即为所求的前缀数量(因为如果某个单词以pre为前缀,插入节点的时候,必然访问过pre结尾处节点)。
动图展示:
2.代码实现
import java.util.*; public class Solution { /** * * @param operators string字符串二维数组 the ops * @return string字符串一维数组 */ public String[] trieU (String[][] operators) { //计算结果集长度,并进行初始化 int len=0; for(String[] opera:operators){ if(opera[0].equals("3")||opera[0].equals("4")){ len++; } } String[] res=new String[len]; Trie trie=new Trie(); int id=0; for(String[] opera:operators){ if(opera[0].equals("1")){ //添加单词 trie.insert(opera[1]); } else if(opera[0].equals("2")){ //删除单词 trie.delete(opera[1]); } else if(opera[0].equals("3")){ //查询单词是否存在 res[id++]=trie.search(opera[1])?"YES":"NO"; } else if(opera[0].equals("4")){ //查找以word为前缀的单词数量 String preNumber=String.valueOf(trie.prefixNumber(opera[1])); res[id++]=preNumber; } } return res; } class Trie{ //构建字典树节点 class TrieNode{ //child数组记录所有子节点 TrieNode[] child; //pre_number表示插入单词时,当前节点被访问次数 int pre_number; //end表示当前节点是否是某个单词的末尾 boolean end; TrieNode(){ child=new TrieNode[26]; pre_number=0; end=false; } } Trie(){} //初始化根节点 TrieNode root=new TrieNode(); //添加单词 void insert(String word){ TrieNode node=root; char[] arr=word.toCharArray(); for(char c:arr){ //如果子节点不存在,则新建 if(node.child[c-'a']==null){ node.child[c-'a']=new TrieNode(); } //往子节点方向移动 node=node.child[c-'a']; node.pre_number++; } node.end=true; } void delete(String word){ TrieNode node=root; char[] arr=word.toCharArray(); for(char c:arr){ //往子节点方向移动,将访问次数减一 node=node.child[c-'a']; node.pre_number--; } //如果访问次数为0,说明不存在该单词为前缀的单词,以及该单词 if(node.pre_number==0){ node.end=false; } } boolean search(String word){ TrieNode node=root; char[] arr=word.toCharArray(); for(char c:arr){ //如果子节点不存在,说明不存在该单词 if(node.child[c-'a']==null){ return false; } node=node.child[c-'a']; } //如果前面的节点都存在,并且该节点末尾标识为true,则存在该单词 return node.end; } int prefixNumber(String pre){ TrieNode node=root; char[] arr=pre.toCharArray(); for(char c:arr){ //如果子节点不存在,说明不存在该前缀 if(node.child[c-'a']==null){ return 0; } node=node.child[c-'a']; } //返回以该单词为前缀的数量 return node.pre_number; } } }
3.复杂度分析
- 时间复杂度:取决于入参word的长度,假设word长度为M,那么每次添加、删除、查找的时间复杂度为 。
- 空间复杂度:假设Σ表示组成单词的字符总数(本题为26),N表示插入的所有单词的长度之和,最坏情况下,树中节点个数为N,所以空间复杂度为 。
方法二(数组实现)
1.解题思路
首先构建一个数组结构,包括一个二维整型trie数组,一个一维整型cnt数组,一个一维整型end数组,一个索引index。
- 添加word:将单词转为字符数组,从索引0行处出发,遍历输入的单词,如果子节点(一行相当于一个节点,行对应列的值为0,说明不包含)不包含当前字符,则新建对应子节点,如果包含,则跳到对应子节点,同时cnt[cur]加一。单词遍历完成后,end[cur]加一。
- 删除word:相当于添加的反向操作,不断往子节点方向移动,同时cnt[cur]加一。遍历完成后,end[cur]减一。
- 查询word:将单词转为字符数组,从根节点出发,遍历输入的单词,如果子节点不包含当前字符,说明不存在该单词,返回false,如果包含,就往子节点方向移动。遍历完成后,end[cur]大于0,说明存在该单词。
- 查询以pre为前缀的单词数量:将单词转为字符数组,从根节点出发,遍历输入的单词,如果子节点不包含当前字符,说明不存在该前缀,返回0,如果包含,就往子节点方向移动。遍历完成后,cnt[cur]的值即为所求的前缀数量。
关于二维数组总行数的估算,由于操作次数m<=100000,假设所有操作全为插入,考虑单词长度<=20,那么总行数至多是200万,按插入、删除、查找操作平均分配,那么总行数在200万/3左右,所以估算N=70万。
TrieNode方式实现的字典树可以最大限度地利用空间,但是插入单词时,存在着频繁new对象的开销,数据量大时,可能触发GC。数组实现方式的缺点在于限制了插入的行数,初始化空间大,但是比较稳定。
2.代码实现
import java.util.*; public class Solution { /** * * @param operators string字符串二维数组 the ops * @return string字符串一维数组 */ public String[] trieU (String[][] operators) { //计算结果集长度,并进行初始化 int len=0; for(String[] opera:operators){ if(opera[0].equals("3")||opera[0].equals("4")){ len++; } } String[] res=new String[len]; Trie trie=new Trie(); int id=0; for(String[] opera:operators){ if(opera[0].equals("1")){ //添加单词 trie.insert(opera[1]); } else if(opera[0].equals("2")){ //删除单词 trie.delete(opera[1]); } else if(opera[0].equals("3")){ //查询单词是否存在 res[id++]=trie.search(opera[1])?"YES":"NO"; } else if(opera[0].equals("4")){ //查找以word为前缀的单词数量 String preNumber=String.valueOf(trie.prefixNumber(opera[1])); res[id++]=preNumber; } } return res; } //数组结构字典树 class Trie{ //二维数组总行数 private final static int N=700000; //每一行相当于一个节点,行与行之间存在映射关系 int[][] trie; //当前行插入次数 int[] cnt; //当前行被标记为结尾的次数 int[] end; //当前行数 int index; Trie(){ trie=new int[N][26]; cnt=new int[N]; end=new int[N]; index=0; } //添加单词 void insert(String word){ int cur=0; char[] arr=word.toCharArray(); for(char c:arr){ //如果子节点不存在,则新建 if(trie[cur][c-'a']==0){ trie[cur][c-'a']=++index; } //往子节点方向移动 cur=trie[cur][c-'a']; cnt[cur]++; } end[cur]++; } void delete(String word){ int cur=0; char[] arr=word.toCharArray(); for(char c:arr){ //往子节点方向移动,将访问次数减一 cur=trie[cur][c-'a']; cnt[cur]--; } end[cur]--; } boolean search(String word){ int cur=0; char[] arr=word.toCharArray(); for(char c:arr){ //如果子节点不存在,说明不存在该单词 if(trie[cur][c-'a']==0){ return false; } cur=trie[cur][c-'a']; } //如果前面的节点都存在,并且当前行之前被标记为结尾,则存在该单词 return end[cur]>0; } int prefixNumber(String pre){ int cur=0; char[] arr=pre.toCharArray(); for(char c:arr){ //如果子节点不存在,说明不存在该前缀 if(trie[cur][c-'a']==0){ return 0; } cur=trie[cur][c-'a']; } //返回以该单词为前缀的数量 return cnt[cur]; } } }
3.复杂度分析
- 时间复杂度:取决于入参word的长度,假设word长度为M,那么每次添加、删除、查找的时间复杂度为 。
- 空间复杂度:假设Σ表示组成单词的字符总数(本题为26),二维数组总行数为N,则空间复杂度为 。