[字典树] 676. 实现一个魔法字典

196 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第18天,点击查看活动详情

每日刷题 2022.08.16

题目

  • 设计一个使用单词列表进行初始化的数据结构,单词列表中的单词 互不相同 。 如果给出一个单词,请判定能否只将这个单词中一个字母换成另一个字母,使得所形成的新单词存在于你构建的字典中。
  • 实现 MagicDictionary 类:
    • MagicDictionary() 初始化对象
    • void buildDict(String[] dictionary) 使用字符串数组 dictionary 设定该数据结构,dictionary 中的字符串互不相同
    • bool search(String searchWord) 给定一个字符串 searchWord ,判定能否只将字符串中 一个 字母换成另一个字母,使得所形成的新字符串能够与字典中的任一字符串匹配。如果可以,返回 true ;否则,返回 false 。

示例

输入
["MagicDictionary", "buildDict", "search", "search", "search", "search"]
[[], [["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

提示

  • 1 <= dictionary.length <= 100
  • 1 <= dictionary[i].length <= 100
  • dictionary[i] 仅由小写英文字母组成
  • dictionary 中的所有字符串 互不相同
  • 1 <= searchWord.length <= 100
  • searchWord 仅由小写英文字母组成
  • buildDict 仅在 search 之前调用一次
  • 最多调用 100 次 search

解题思路

  • 初次学习Trie树,又被叫做:字典树或前缀树,是一种用于快速查找某个字符串/字符前缀是否存在的数据结构。
  • 核心:使用边代表有无字符(可以直接将其字符设置为属性名,这样就比较好查找),使用点表示是否为“单词结尾”以及“其后续字符串的字符是什么”
  • 目前使用过两种实现字典树的方法。
// 第一种:创建一个新的结构体的节点
function Trie(val, isEnd) {
  this.val = val;
  this.child = [];
  this.isEnd = isEnd || false;
}
// 最终的输出样式:
Trie: {
  val: '13',
  child: [
    { 
      val: '456',
      child: [],
      isEnd: true
    }
  ],
  isEnd: false
}

// 第二种
let root = {};
!root[属性名] => 这是对象使用括号访问属性的方式
// 最终的输出样式:
{
  '0': {
    '1': {},
    '0': {}
  }
}
  • 第一种和第二种创建的方式,比较起来,还是第二种更符合字典树的定义。将边表示字符,那么对应的就应该将对象中的属性名设置为当前的字符,表示当前的字符和上一个字符存在一条边(当对象中不存在任何属性名时,则表示其没有连接的边)。
  • 字典树一般包含:创建、插入、搜索三个方法。这道题的搜索比较麻烦,借助了dfs
  • 搜索:将每一个节点都当做是一个根节点,也就是处于字典树中的每一个字符都当作其是根节点来处理,递归调用有返回值的dfs。因为每一层的逻辑是类似的,只是需要多次遍历。(根据题目可知:只能将字符串中的一个字母换成另一个字母,替换不是增加或者删除
    • dfs需要的参数:根节点root、当前字符串cur、层数pos、标记是否被修改过的变量isModify
    • dfs的终止条件:pos = cur.length当深度等于当前的字符串的长度的时候,就说明已经查找结束,此时需要判断字典树中的当前字符是否为结束单词isEnd并且还需要满足isModify = true,也就是两个字符串的长度相等且被修改过一次即可。
    • dfs的内部实现逻辑:如果root是包含cur[pos]字符的,那么就可以直接改变参数,往下一层找。(因为root包含:表示两个字符是相同的)如果往下遍历返回的是true,那么就可以直接向上返回true
    • 反之如果返回的是false,表示往下遍历找完了字符,下面的都不符合要求(要么isEnd或者isModify不为true),需要走另一条路,换一个字符。那么就需要修改isModify = true,记录下这次修改操作,接着找到当前的字典树root的下一个字符,再从这个字符往下遍历。同样的如果从当前的一直遍历下去,返回的是true,则向上返回true;否则就跳出循环返回false(因为无论如何都没有办法成功)。

字典树应用

最近学习的常用的位运算

  • 判断数组中是存在元素let arr = []; arr.length === 0
  • 判断对象中是否存在属性:Object.keys(node).length === 0

AC代码

function Trie(val, isEnd) {
  this.val = val;
  this.child = [];
  this.isEnd = isEnd || false;
}

var MagicDictionary = function() {
  this.trie = new Trie();
};

/** 
 * @param {string[]} dictionary
 * @return {void}
 */
MagicDictionary.prototype.buildDict = function(dictionary) {
  for(let dic of dictionary) {
    this.insert(dic);
  }
};

MagicDictionary.prototype.insert = function (dic) {
  // 对于每一个字符串,进行操作
  let tr = this.trie;
  for(const one of dic) {
    if(!tr.child[one]) tr.child[one] = new Trie(one);
    tr = tr.child[one];
  }
  tr.isEnd = true;
}

/** 
 * @param {string} searchWord
 * @return {boolean}
 */
MagicDictionary.prototype.search = function(searchWord) {
  // 搜索的单词
  // dfs需要的参数,需要搜索的字符串,当前搜索的位置、是否已经有一个被替换掉
  let len = searchWord.length;
  return this.dfs(this.trie, searchWord, len, 0, false);
};

MagicDictionary.prototype.dfs = function (tree, search, n, pos, isModifity) {
  if(pos == n){
    // 需要是结尾,并且仅只修改了一个元素
    return tree.isEnd && isModifity;
  }
  // 没有找到结尾的时候,的操作
  // 需要判断当前的元素是否在tree的孩子数组中
  let cur = search[pos];
  if(tree.child[cur]) {
    // 存在的时候就继续调用, 不存在被修改的一个元素
    if(this.dfs(tree.child[cur], search, n, pos + 1, isModifity)) {
      return true;
    }
  }
  if(!isModifity) {
    // 不存在的时候, 需要将当前的modifity修改,但是要判断是否还可以修改
    // 遍历26个字母,找出哪个是其的下一个字母,然后修改modify再进行递归
    let a = 'a'.charCodeAt();
    for(let i = 0; i < 26; i++) {
      let modi = String.fromCharCode(i + a);
      if(modi != cur && tree.child[modi]) {
        // 存在的话,就找到可以使用的子节点
        if(this.dfs(tree.child[modi], search, n, pos + 1, true)) {
          return true;
        }
      }
    }
  }
  return false;
}

/**
 * Your MagicDictionary object will be instantiated and called as such:
 * var obj = new MagicDictionary()
 * obj.buildDict(dictionary)
 * var param_2 = obj.search(searchWord)
 */