基础知识
- 多叉树
- 若两单词前缀相同,那么它们在前缀树中对应路径前面的节点是重合的。
- 路径不一定终止于叶节点
- 字符串最后一个字符对应的节点有特殊的标识
Q62:实现前缀树
题目(中等):Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。
请你实现 Trie 类:
Trie()初始化前缀树对象。void insert(String word)向前缀树中插入字符串 word 。boolean search(String word)如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。boolean startsWith(String prefix)如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。
/** Inserts a word into the trie. */
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;
}
/** Returns if the word is in the trie. */
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;
}
/** Returns if there is any word in the trie that starts with the given prefix. */
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;
}
前缀树的应用
主要用来解决与字符串查找相关的问题
Q63:替换单词
题目(中等):在英语中,有一个叫做 词根(root) 的概念,它可以跟着其他一些词组成另一个较长的单词——我们称这个词为 继承词(successor)。例如,词根an,跟随着单词 other(其他),可以形成新的单词 another(另一个)。现在,给定一个由许多词根组成的词典和一个句子,需要将句子中的所有继承词用词根替换掉。如果继承词有许多可以形成它的词根,则用最短的词根替换它。需要输出替换之后的句子。
示例 1:
输入:dictionary = ["cat","bat","rat"], sentence = "the cattle was rattled by the battery"
输出:"the cat was rat by the bat"
示例 2:
输入:dictionary = ["a","b","c"], sentence = "aadsfasf absbs bbab cadsfafs"
输出:"a a b c"
示例 3:
输入:dictionary = ["a", "aa", "aaa", "aaaa"], sentence = "a aa a aaaa aaa aaa aaa aaaaaa bbb baba ababa"
输出:"a a a a a a a a bbb baba a"
示例 4:
输入:dictionary = ["catt","cat","bat","rat"], sentence = "the cattle was rattled by the battery"
输出:"the cat was rat by the bat"
示例 5:
输入:dictionary = ["ac","ab"], sentence = "it is abnormal that this solution is accepted"
输出:"it is ab that this solution is ac"
class Solution {
static class TrieNode{
public TrieNode children[];
public boolean isWord;
public TrieNode(){
children = new TrieNode[26];
}
}
private TrieNode buildTrie(List<String> dictionary){
TrieNode root = new TrieNode();
for (String word : dictionary){
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;
}
return root;
}
private String findPrefix(TrieNode root,String word){
TrieNode node = root;
StringBuilder builder = new StringBuilder();
for(char ch : word.toCharArray()){
if(node.isWord || node.children[ch - 'a'] == null){//已经找到词了或者出现不一致时退出
break;
}
builder.append(ch);
node = node.children[ch - 'a'];
}
return node.isWord ? builder.toString() : "";//有对应前缀就取前缀,否则就返回空字符
}
public String replaceWords(List<String> dictionary, String sentence) {
TrieNode root = buildTrie(dictionary);
StringBuilder builder = new StringBuilder();
String [] words = sentence.split(" ");
for(int i = 0;i < words.length;i++){
String prefix = findPrefix(root,words[i]);
if(!prefix.isEmpty()){
words[i] = prefix;
}
}
return String.join(" ",words);
}
}
Q64:神奇的字典
题目(中等):设计一个使用单词列表进行初始化的数据结构,单词列表中的单词互不相同。 如果给出一个单词,请判定能否只将这个单词中一个字母换成另一个字母,使得所形成的新单词存在于已构建的神奇字典中。
实现 MagicDictionary 类:
MagicDictionary()初始化对象void buildDict(String[] dictionary)使用字符串数组 dictionary 设定该数据结构,dictionary 中的字符串互不相同bool search(String searchWord)给定一个字符串 searchWord ,判定能否只将字符串中 一个 字母换成另一个字母,使得所形成的新字符串能够与字典中的任一字符串匹配。如果可以,返回 true ;否则,返回 false 。
示例:
输入
inputs = ["MagicDictionary", "buildDict", "search", "search", "search", "search"]
inputs = [[], [["hello", "leetcode"]], ["hello"], ["hhllo"], ["hell"], ["leetcoded"]]
输出
[null, null, false, true, false, false]
解释
MagicDictionary magicDictionary = new MagicDictionary();
magicDictionary.buildDict(["hello", "leetcode"]);
magicDictionary.search("hello"); // 返回 False
magicDictionary.search("hhllo"); // 将第二个 'h' 替换为 'e' 可以匹配 "hello" ,所以返回 True
magicDictionary.search("hell"); // 返回 False
magicDictionary.search("leetcoded"); // 返回 False
class MagicDictionary {
static class TrieNode{
public TrieNode children[];
public boolean isWord;
public TrieNode(){
children = new TrieNode[26];
}
}
public TrieNode root;
/** Initialize your data structure here. */
public MagicDictionary() {
root = new TrieNode();
}
public void buildDict(String[] dictionary) {
for(String word : dictionary){
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 searchWord) {
return dfs(root,searchWord,0,0);
}
private boolean dfs(TrieNode root,String searchWord,int i,int edit){//i为当前正在核对字母位置
//字典为空,直接返回false
if(root == null){
return false;
}
//如果查完词且查到到词,且不同的字母数只有1个,返回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;//判断词的i位是否一致
found = dfs(root.children[j],searchWord,i+1,next);
}
return found;
}
return false;
}
}
Q65:最短的单词编码
题目(中等):单词数组 words 的有效编码由任意助记字符串 s 和下标数组 indices 组成,且满足:
- words.length == indices.length
- 助记字符串 s 以 '#' 字符结尾
- 对于每个下标 indices[i] ,s 的一个从 indices[i] 开始、到下一个 '#' 字符结束(但不包括 '#')的 子字符串 恰好与 words[i] 相等 给定一个单词数组 words ,返回成功对 words 进行编码的最小助记字符串 s 的长度。
示例 1:
输入:words = ["time", "me", "bell"]
输出:10
解释:一组有效编码为 s = "time#bell#" 和 indices = [0, 2, 5] 。
words[0] = "time" ,s 开始于 indices[0] = 0 到下一个 '#' 结束的子字符串,如加粗部分所示 "time#bell#"
words[1] = "me" ,s 开始于 indices[1] = 2 到下一个 '#' 结束的子字符串,如加粗部分所示 "time#bell#"
words[2] = "bell" ,s 开始于 indices[2] = 5 到下一个 '#' 结束的子字符串,如加粗部分所示 "time#bell#"
示例 2:
输入:words = ["t"]
输出:2
解释:一组有效编码为 s = "t#" 和 indices = [0] 。
解题思路
反向建树可以免去重合词的计算
static class TrieNode{
public TrieNode children[];
public TrieNode(){
children = new TrieNode[26];
}
}
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--){
if(node.children[word.charAt(i) - 'a'] == null){
node.children[word.charAt(i) - 'a'] = new TrieNode();
}
node = node.children[word.charAt(i) - 'a'];
}
}
return root;
}
private void dfs(TrieNode root,int length,int [] total){
boolean isLeaf = true;//是否是叶节点的标志
for(TrieNode child:root.children){
if(child != null){
isLeaf = false;
dfs(child,length + 1,total);
}
}
if (isLeaf){//只有叶节点才计算长度
total[0] += length;
}
}
public int minimumLengthEncoding(String[] words) {
TrieNode root = buildTrie(words);
int[] total = {0};
dfs(root,1,total);//length初始为1,因为每个词以“#”结尾
return total[0];
}
Q66:单词之和
题目(中等):实现一个 MapSum 类,支持两个方法,insert 和 sum:
MapSum()初始化 MapSum 对象void insert(String key, int val)插入 key-val 键值对,字符串表示键 key ,整数表示值 val 。如果键 key 已经存在,那么原来的键值对将被替代成新的键值对。int sum(string prefix)返回所有以该前缀 prefix 开头的键 key 的值的总和。 示例:
输入:
inputs = ["MapSum", "insert", "sum", "insert", "sum"]
inputs = [[], ["apple", 3], ["ap"], ["app", 2], ["ap"]]
输出:
[null, null, 3, null, 5]
解释:
MapSum mapSum = new MapSum();
mapSum.insert("apple", 3);
mapSum.sum("ap"); // return 3 (apple = 3)
mapSum.insert("app", 2);
mapSum.sum("ap"); // return 5 (apple + app = 3 + 2 = 5)
解题思路
int sum(string prefix):先找到prefix的最后一个字母的那一个节点,然后计算下面所有叶节点的键值和
class MapSum {
static class TrieNode{
public TrieNode children [];
public int val;
public TrieNode(){
children = new TrieNode[26];
}
}
private TrieNode root;
/** Initialize your data structure here. */
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.val = val;//到词的最后一个子节点时添加对应的键值
}
public int sum(String prefix) {
TrieNode node = root;
for(int i = 0;i < prefix.length();i++){
if(node.children[prefix.charAt(i) - 'a'] == null){
return 0;
}
node = node.children[prefix.charAt(i) - 'a'];
}//退出遍历时,node对应前缀的最后一个字母
return getSum(node);
}
private int getSum(TrieNode node){
if(node == null) return 0;
int result = node.val;
for(TrieNode child : node.children){
result += getSum(child);
}
return result;
}
}
Q67:最大的异或
题目(中等):给定一个整数数组 nums ,返回 nums[i] XOR nums[j] 的最大运算结果,其中 0 ≤ i ≤ j < n 。
示例 1:
输入:nums = [3,10,5,25,2,8]
输出:28
解释:最大运算结果是 5 XOR 25 = 28
示例 2:
输入:nums = [0]
输出:0
示例 3:
输入:nums = [2,4]
输出:6
示例 4:
输入:nums = [8,10,2]
输出:10
示例 5:
输入:nums = [14,70,53,83,49,91,36,80,92,51,66,70]
输出:127
解题思路
static class TrieNode{
public TrieNode[] children;
public TrieNode(){
children = new TrieNode[2];
}
}
private TrieNode root;
private TrieNode buildTrie(int[] nums){
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;
}
//优先选择相反数位的
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--){
int bit = num >> i & 1;
if(node.children[1-bit] != null){
xor = (xor << 1) + 1;//存在不同分支,异或记为1
node = node.children[1-bit];
}else{
xor = xor << 1;
node = node.children[bit];
}
}
max = Math.max(max,xor);
}
return max;
}
小结
- 前缀树结构
static class TrieNode{
TrieNode children[];
boolean isWord;
public TrieNode(){
children = new TrieNode[26];
}
}
- 与哈希表相比,既可以找出所有以某个前缀开头的所有单词,也可以找出修改了一个(或多个)字符的字符串
- 解决问题一般包括两步
- 创建前缀树
- 在前缀树中查找