10.1 前缀树基础
面试题62:实现前缀树
题目:请设计实现一颗前缀树Trie,它有如下操作。
- 函数insert,在前缀树中添加一个字符串。
- 函数search,查找字符串。如果前缀树中包含该字符串,则返回true;否则返回false。
- 函数startWith,查找字符串前缀。如果前缀树中包含以该前缀开头的字符串,则返回true,否则返回false。
public class Trie {
private TrieNode root;
public Trie(){
root = new TrieNode();
}
public void insert(String word){
TrieNode node = root;
for (char c : word.toCharArray()) {
if(root.children[c - 'a'] == null){
root.children[c - 'a'] = new TrieNode();
}
node = root.children[c - 'a'];
}
node.isEnd = true;
}
public boolean search(String word){
TrieNode node = root;
for(char c : word.toCharArray()){
if(node.children[c - 'a'] == null){
return false;
}
node = root.children[c - 'a'];
}
return node.isEnd;
}
public boolean startWith(String prefix){
TrieNode node = root;
for(char c : prefix.toCharArray()){
if(node.children[c - 'a'] == null){
return false;
}
node = root.children[c - 'a'];
}
return true;
}
}
class TrieNode{
// 前缀树的孩子是一个26长度的数组,代表26个字母
// 如果数组的某个位置不是null,则标明该前缀中有该字母
public TrieNode[] children;
// 标记是否是单词结尾
public boolean isEnd;
public TrieNode(){
children = new TrieNode[26];
}
}
10.2 前缀树的应用
面试题63:替换单词
题目:英语中有一个概念叫词根。在词根后面加上若干字符就能拼出更长的单词。例如,“an"是一个词根,在它后面加上"other"就能得到另一个单词another。现在给定一个由词根组成的字典和一个英文句子,如果句子中的单词在字典中有它的词根,则用它的词根替换该单词;如果单词没有词根,则保留该单词。请输出替换后的句子。例如,如果词根字典包含字符串[“cat”, “bat”, “rat”],英文句子为"the cattle was rattled by the battery”,则替换之后的句子是"the cat was rat by the bat"。
public String replaceWords(List<String> prefixs, String sentence){
TrieNode root = buildTrie(prefixs);
String[] words = sentence.split(" ");
for(int i = 0; i < words.length; i++){
String word = words[i];
words[i] = findPrefix(word, root);
}
return String.join(" ", words);
}
/**
* 查找某个单词的前缀,找到则返回前缀,找不到则返回单词本身
* @param word
* @param root
* @return
*/
private String findPrefix(String word, TrieNode root) {
TrieNode node = root;
StringBuilder prefix = new StringBuilder();
for(char c : word.toCharArray()){
if(node.children[c - 'a'] == null){
return word;
}
node = node.children[c - 'a'];
prefix.append(c);
if(node.isEnd){
return prefix.toString();
}
}
return word;
}
private TrieNode buildTrie(List<String> prefixs) {
TrieNode root = new TrieNode();
for(String prefix : prefixs){
TrieNode node = root;
for(char c : prefix.toCharArray()){
if(node.children[c - 'a'] == null){
node.children[c - 'a'] = new TrieNode();
}
node = node.children[c - 'a'];
}
node.isEnd = true;
}
return root;
}
面试题64:神奇的字典
题目:请实现有如下两个操作的神奇字典。
- 函数buildDict,输入单词数组来创建一个字典。
- 函数search,输入一个单词,判断能够修改单词中的一个字符,使修改后的单词是字典中的一个单词。
public class MagicDictionary {
private TrieNode root;
public MagicDictionary(){
root = new TrieNode();
}
public void buildDict(String[] words){
for (String word : words) {
TrieNode node = root;
for (char c : word.toCharArray()) {
if(node.children[c - 'a'] == null){
node.children[c - 'a'] = new TrieNode();
}
node = node.children[c - 'a'];
}
node.isEnd = true;
}
}
public boolean search(String word){
return dfs(word, root, 0, 0);
}
/**
* 深度优先搜索,要保证找到修改一次能够满足条件的单词
* @param word
* @param node
* @param depth 遍历深度
* @param edit 修改次数,超过一次就会返回false
* @return
*/
private boolean dfs(String word, TrieNode node, int depth, int edit) {
if(node == null){
return false;
}
if(node.isEnd && depth == word.length() && edit == 1){
return true;
}
// 如果修改次数大于1则会返回false。第一个条件要满足word.charAt(depth)是有效的
if(depth < word.length() && edit <= 1){
// 是否发现了满足条件的单词,发现之后就会跳出循环
boolean found = false;
for(int i = 0; i < 26 && !found; i++){
if(word.charAt(depth) - 'a' == i){
found = dfs(word, node.children[i], depth + 1, edit);
} else {
found = dfs(word, node.children[i], depth + 1, edit + 1);
}
}
return found;
}
return false;
}
}
面试题65:最短的单词编码
题目:输入一个包含n个单词的数组,可以把他们编码成一个字符串和n个下标。例如,单词数组[“time”, “me”, “bell”]可以编码成一个字符串"time#bell#",然后这些单词就可以通过下标[0, 2, 5]得到。输出最短的单词编码的长度,上个例子中输出10。
public int minimumLengthEncoding(String[] words){
// 反向构建前缀树
TrieNode root = new TrieNode();
for (String word : words) {
TrieNode node = root;
for(int i = word.length() - 1; i >= 0; i--){
if(node.children[word.charAt(i) - 'a'] == null){
node.children[word.charAt(i) - 'a'] = new TrieNode();
}
node = node.children[word.charAt(i)];
}
}
// 计算前缀树的深度之和
int[] result = {0};
getLength(root, 1, result);
return result[0];
}
/**
* 深度优先遍历前缀树,计算前缀树的深度之和
* @param root
* @param depth
* @param result
*/
private void getLength(TrieNode root, int depth, int[] result) {
boolean isLeaf = true;
for(int i = 0; i < 26; i++){
if(root.children[i] != null){
isLeaf = false;
getLength(root.children[i], depth + 1, result);
}
}
if(isLeaf){
result[0] += depth;
}
}
面试题66:单词之和
题目:请设计实现一个类型MapSum,它有如下两个操作。
- 函数insert,输入一个字符串和一个整数,在该数据集合中添加一个字符串及其对应的值。如果数据集合中已经包含该字符串,则将该字符串对应的值替换成新值。
- 函数sum,输入一个字符串,返回数据集合中所有以该字符串为前缀的字符串对应的值之和。
public class MapSum {
class TrieNode{
TrieNode[] children;
boolean isEnd;
int value;
public TrieNode(){
children = new TrieNode[26];
isEnd = false;
value = 0;
}
}
private TrieNode root;
public MapSum {
root = new TrieNode();
}
/**
* 插入时构建前缀树
* @param word
* @param value
*/
public void insert(String word, int value){
TrieNode node = root;
for (char c : word.toCharArray()) {
if (node.children[c - 'a'] == null) {
node.children[c - 'a'] = new TrieNode();
}
node = node.children[c - 'a'];
}
node.isEnd = true;
node.value = value;
}
/**
* 先找到满足条件的前缀,然后从前缀位置结点开始进行深度优先搜索
* @param prefix
* @return
*/
public int sum(String prefix){
TrieNode node = root;
for (char c : prefix.toCharArray()) {
if(node.children[c - 'a'] == null){
return 0;
}
node = node.children[c - 'a'];
}
int[] sum = {0};
getSum(node, sum);
return sum[0];
}
private void getSum(TrieNode node, int[] sum) {
if(node == null){
return;
}
if(node.isEnd){
sum[0] += node.value;
}
for(int i = 0; i < 26; i++){
if(node.children[i] != null){
getSum(node.children[i], sum);
}
}
}
}
面试题67:最大的异或
题目:输入一个整数数组(每个数字都大于等于0),请计算其中任意两个数字的异或的最大值。例如[1,3,4,7]中,3和4的异或结果最大,异或结果为7。
/**
* 因为bit位只有0或1
* 所以构造新的前缀树结点,孩子结点数量为2
*/
class TrieNode{
TrieNode[] children;
public TrieNode() {
children = new TrieNode[2];
}
}
/**
* 每个数字构造前缀树
* @param nums
* @return
*/
public TrieNode buildTrie(int[] nums){
TrieNode root = new TrieNode();
for (int num : nums) {
TrieNode node = root;
for(int i = 31; i > 0; i--){
int bit = (num >> i) & 1;
if(node.children[bit] == null){
node.children[bit] = new TrieNode();
}
node = node.children[bit];
}
}
return root;
}
/**
* 从高位开始遍历,找与该位相反的数字,相反异或得1,能够保证得到较大的数字
* @param nums
* @return
*/
public int findMaxXor(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--){
if(node.children[1 - ((num >> i) & 1)] != null){
node = node.children[1 - ((num >> i) & 1)];
xor = xor << 1 + 1;
} else {
node = node.children[(num >> i) & 1];
xor = xor << 1;
}
}
max = Math.max(max, xor);
}
return max;
}