面试题62:
问题:
实现三个函数insert、search、startWith。
解决方案:
- 实现前缀树的构造函数,只考虑英文小写字母,前缀树中节点可能有26个子节点。可以将26个子节点放到一个数组中,故前缀树需要一个数组,还需要一个布尔类型的字段表示到达节点的路径对应的字符串是否为字典中的一个完整的单词。
- 实现函数insert,首先来到根节点,确定根节点是否有一个子节点与字符串的第一个字符对应,如果有该子节点,则前往该子节点,如果没有就创建该子节点,再前往该子节点,接着判断该子节点是否有一个子节点与字符串的第二个字符对应,并以此类推,当字符串的所有字符都添加到前缀树时,将最后添加的子节点的布尔类型的字段改为true,标识路径到达该节点时已经对应一个完整的单词。
- 实现函数search、startWith与上述类似。
源代码:
class Trie {
//前缀树节点的数据结构
static class TrieNode{
TrieNode children[];
//标识路径到达该节点时已经对应一个完整的单词
boolean isWord;
public TrieNode(){
children = new TrieNode[26];
}
}
private TrieNode root;
public Trie() {
root = new TrieNode();
}
public void insert(String word) {
TrieNode node = root;
for(char ch:word.toCharArray()){
if(node.children[ch - 'a'] == null){
node.children[ch - 'a'] = new TrieNode();
}
node = node.children[ch - 'a'];
}
node.isWord = true;
}
public boolean search(String word) {
TrieNode node = root;
for(char ch:word.toCharArray()){
if(node.children[ch - 'a'] == null){
return false;
}
node = node.children[ch - 'a'];
}
return node.isWord;
}
public boolean startsWith(String prefix) {
TrieNode node = root;
for(char ch:prefix.toCharArray()){
if(node.children[ch - 'a'] == null){
return false;
}
node = node.children[ch - 'a'];
}
return true;
}
}
面试题63:
问题:
给定一个由词根组成的字典和一个英语句子,将英语句子中的单词在字典中有它的词根,则用词根替换该单词,然后输出替换后的句子。
解决方案:
- 创建前缀树,将字典中的单词逐一添加到前缀树中,在上一题的基础上加一个循环。
- 分割句子为单词,并在前缀树中查找,如果查找到了就返回词根,如果没查找到返回空字符串。
源代码:
class Solution {
//前缀树的数据结构
static class TrieNode{
TrieNode[] childNode;
boolean isWord;
public TrieNode(){
childNode = new TrieNode[26];
}
}
public String replaceWords(List<String> dictionary, String sentence) {
TrieNode root = bulidTirr(dictionary);
StringBuilder build = new StringBuilder();
String[] words = sentence.split(" ");
for(int i = 0;i < words.length;i++){
String prefix = findWord(root,words[i]);
if(!prefix.isEmpty()){
words[i] = prefix;
}
}
return String.join(" ",words);
}
//创建前缀树
private TrieNode bulidTirr(List<String> dictionary){
TrieNode root = new TrieNode();
for(String str:dictionary){
TrieNode node = root;
for(char ch:str.toCharArray()){
if(node.childNode[ch - 'a'] == null){
node.childNode[ch - 'a'] = new TrieNode();
}
node = node.childNode[ch - 'a'];
}
node.isWord = true;
}
return root;
}
//查找前缀树
private String findWord(TrieNode root,String word){
TrieNode node = root;
StringBuilder build = new StringBuilder();
for(char ch:word.toCharArray()){
if(node.isWord || node.childNode[ch - 'a'] == null){
break;
}
build.append(ch);
node = node.childNode[ch - 'a'];
}
return node.isWord?build.toString():"";
}
}
面试题64:
问题:
实现函数buildDict、search。
解决方案:
- 创建前缀树,将字典的每个单词保存到前缀树。
- 使用深度优先的顺序搜索前缀树的每条路径。如果到达的节点与字符串中的字符不匹配,则表示此时修改了字符串中的一个字符以匹配前缀树中的路径。如果到达对应字符串最后一个字符对应的节点时,该节点的isWord字段的值为true,而且此时正好修改了字符串中的一个字符,那么就找到了修改字符串中一个字符对应的路径,返回true。
源代码:
class MagicDictionary {
//前缀树节点的数据结构
static class TrieNode{
TrieNode children[];
boolean isWord;
public TrieNode(){
children = new TrieNode[26];
}
}
TrieNode root;
public MagicDictionary() {
root = new TrieNode();
}
//创建前缀树
public void buildDict(String[] dictionary) {
for(String str:dictionary){
TrieNode node = root;
for(char ch:str.toCharArray()){
if(node.children[ch - 'a'] == null){
node.children[ch - 'a'] = new TrieNode();
}
node = node.children[ch - 'a'];
}
node.isWord = true;
}
}
public boolean search(String searchWord) {
return dfs(root,searchWord,0,0);
}
private boolean dfs(TrieNode root,String searchWord,int i,int edit){
if(root == null){
return false;
}
//节点的isWord字段的值为true,并且长度一样,也只修改了一个字符
if(root.isWord && i == searchWord.length() && edit == 1){
return true;
}
if(i < searchWord.length() && edit <= 1){
boolean found = false;
for(int j = 0;j < 26 && !found;j++){
int next = j == searchWord.charAt(i) - 'a'? edit:edit+1;
found = dfs(root.children[j],searchWord,i+1,next);
}
return found;
}
return false;
}
}
面试题65:
问题:
给定一个单词数组,将它们编码成一个字符串和n个下标,用‘#’字符将单词隔开,然后单词a是单词b的后缀则不需要隔开。
解决方案:
- 使用前缀树,因为如果单词a是单词b的后缀,则不需要用‘#’字符来隔开,只需要将后缀转换为前缀,反转单词a与单词b,此时单词a就是单词b的前缀。
- 如果两个单词共享前缀,但是一个字符串不是另一个字符串的子字符串,那么公共前缀将会在编码时重复出现。例如:“at”、“bat”、“cat”,当进行单词反转后,“tab”与“tac”有共同的前缀ta,但是编码只能为:“bat#cat#”。
源代码:
class Solution {
//前缀树节点的数据结构
static class TrieNode{
private TrieNode children[];
public TrieNode(){
children = new TrieNode[26];
}
}
public int minimumLengthEncoding(String[] words) {
TrieNode root = buildTrie(words);
int result[] = {0};
dfs(root,1,result);
return result[0];
}
//将单词数组转换为前缀树
private TrieNode buildTrie(String[] words){
TrieNode root = new TrieNode();
for(String word:words){
TrieNode node = root;
//反转字符串
for(int i = word.length() - 1;i >= 0;i--){
char ch = word.charAt(i);
if(node.children[ch - 'a'] == null){
node.children[ch - 'a'] = new TrieNode();
}
node = node.children[ch - 'a'];
}
}
return root;
}
private void dfs(TrieNode root,int length,int[] result){
boolean flag = true;
for(TrieNode node:root.children){
//如果存在,则将长度加1
if(node != null){
flag = false;
dfs(node,length + 1,result);
}
}
if(flag){
result[0] += length;
}
}
}
面试题66:
问题:
实现类型MapSum的两个函数insert、sum。
解决方案:
- 因为函数sum是根据输入的字符串,以此返回所有以该字符串的前缀的字符串对应的值之和,所有使用前缀树作为数据容器。
- 定义前缀树节点的数据结构,由于每个字符串对应一个数值,因此添加一个整数字段。如果一个节点对应一个字符串的最后一个字符,那么该节点的数值设为字符串对应的值。
源代码:
class MapSum {
//前缀树节点的数据结构
static class TrieNode{
private TrieNode[] children;
//设计整数字段
private int num;
public TrieNode(){
children = new TrieNode[26];
}
}
private TrieNode root;
//创建前缀树
public MapSum() {
root = new TrieNode();
}
//添加节点到前缀树
public void insert(String key, int val) {
TrieNode node = root;
for(char ch:key.toCharArray()){
if(node.children[ch - 'a'] == null){
node.children[ch - 'a'] = new TrieNode();
}
node = node.children[ch - 'a'];
}
node.num = val;
}
public int sum(String prefix) {
TrieNode node = root;
//记录返回所有以该前缀prefix开头的字符串的值的总和
int[] sumVal = {0};
//扫描前缀prefix
for(char ch:prefix.toCharArray()){
//存在前缀为prefix的字符串则继续移动
if(node.children[ch - 'a'] != null){
node = node.children[ch - 'a'];
//不存在前缀为prefix的字符串则直接退出
}else{
node = null;
break;
}
}
if(node != null){
//注意:字符串aa也是字符串aa的前缀,故节点到达前缀prefix的最后一个字符也需要进行添加操作。
sumVal[0] += node.num;
dfs(node,sumVal);
}
return sumVal[0];
}
//前缀prefix已经扫描完,下面只要还存在节点就一定有以prefix为前缀的字符串。
private void dfs(TrieNode root,int[] sumVal){
for(TrieNode node:root.children){
if(node != null){
sumVal[0] += node.num;
dfs(node,sumVal);
}
}
}
}
面试题67:
问题:
输入一个整数数组,计算其中任意两个数字的异或的最大值。
解决方案:
使用前缀树,将每个整数转换为二进制,并将整数的每个数位保存下来,最后从高位开始扫描整数num的每个数位。如果前缀树中存在某个整数的相同位置的数位与num的数位相反,则优先选择这个相反的数位,这是因为两个相反的数位异或的结果为1,比两个相同的数位异或的结果大。按照优先选择与整数num相反的数位的规则就能找出与num异或最大的整数。
源代码:
class Solution {
//前缀树节点的数据结构
static class TrieNode{
TrieNode[] children;
public TrieNode(){
//因为二进制只有0或1
children = new TrieNode[2];
}
}
public int findMaximumXOR(int[] nums) {
TrieNode root = buildTrie(nums);
int max = 0;
for(int num:nums){
TrieNode node = root;
int xor = 0;
//从高位开始扫描
for(int i = 31;i >= 0;i--){
//先向右位移31位,然后在与1,得到位于32数位的数字。 再向右位移30位以此类推,得到每个数位的值
int bit = (num >> i) & 1;
//1-bit是取相反,因为当前数位是1,则取0节点,如果当前数位是0,则取1节点
if(node.children[1 - bit] != null){
xor = (xor << 1) + 1;
node = node.children[1 - bit];
}else{
xor = xor << 1;
node = node.children[bit];
}
}
max = Math.max(max,xor);
}
return max;
}
private TrieNode buildTrie(int[] nums){
TrieNode root = new TrieNode();
for(int num:nums){
TrieNode node = root;
//从高位开始扫描
for(int i = 31;i >= 0;i--){
//先向右位移31位,然后在与1,得到位于32数位的数字。 再向右位移30位以此类推,得到每个数位的值
int bit = (num >> i) & 1;
if(node.children[bit] == null){
node.children[bit] = new TrieNode();
}
node = node.children[bit];
}
}
return root;
}
}