LeetCode.676. 实现一个魔法字典____暴力与字典树优化(超级详细解答字典树解法~~【内附递归算法实现流程详细介绍】)

676. 实现一个魔法字典

设计一个使用单词列表进行初始化的数据结构,单词列表中的单词 互不相同 。 如果给出一个单词,请判定能否只将这个单词中一个字母换成另一个字母,使得所形成的新单词存在于你构建的字典中。

实现 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

Solution1(暴力比对):

  • 我们可以定义一个HashSet用来存储dictionary的单词,接着对于每次拿到的单词searchWord的每个位置都进行’a‘ 到 ’z‘的替换,并判断替换后的字符串是否存在于HashSet中即可。

Code1:

/**
 * Your MagicDictionary object will be instantiated and called as such:
 * MagicDictionary obj = new MagicDictionary();
 * obj.buildDict(dictionary);
 * boolean param_2 = obj.search(searchWord);
 */
class MagicDictionary {
    private Set<String> set;

    public MagicDictionary() {
        set = new HashSet<>();
    }
    
    public void buildDict(String[] dictionary) {
        for(String s : dictionary)
            set.add(s);
    }
    
    public boolean search(String searchWord) {
        for(int i=0;i<searchWord.length();i++){
            char old = searchWord.charAt(i);
            for(int j=0;j<26;j++){
                if(j == old - 'a')
                    continue;
                StringBuilder s0 = new StringBuilder(searchWord);
                String replaceS = "" + (char)(j + 'a');
                s0.replace(i, i+1 ,replaceS);
                if(set.contains(s0.toString())){
                    return true;
                }
            }
        }
        return false;
    }
}

Solution2(字典树):

借助字典树,我们可以先把dictionary字符串数组中的字符串放进字典树,接着我们对于每次要查找的searchWord,将其代入字典树进行逐个字符的查找。

  • 查找时,由于是树状结构,我们可以使用递归+ 回溯,即建立一个递归函数dfs(Node node,boolean sign,String searchWord,int index) ,其中node表示此时位于字典树的节点位置,sign表示是否可以修改字母,index为此时遍历到searchWord的位置。

  • 对于此时在字典树的某个节点位置,我们每次都使其向其能扩散的所有方向(即所有nodes['a'-'a']nodes['z'-'a']不为null的节点)进行扩散,借着每次扩散后与searchWord的index位置判断,判断是否需要修改字符,需要则改变sign为false,表示已经修改一次不能再被修改,不需要则不改变sign,接着继续扩散即可。若扩散的时候发现需要修改字母但是sign已为false,则代表此路不通,进行回溯到上一次节点即可。

  • 且注意由题意可知,必须要进行一次修改,所以当成功与searchWord比对完全后,还要验证sign是否为false,若为true则代表不成功。

并且此题可以进行贪心剪枝,即若searchWord"hello"时,若是查找发现字典树内有’h’,则直接优先走’h‘,接着若有’e’,则优先走’e’,也就是能不修改就不修改,直到走到尽头判断是否可行,不可行则进行回溯;且由于此路径已经被走完,所以下面在重新向各个方向走的时候,可以剔除掉贪心剪枝未查询到的情况。

对于返回类型为布尔类型的递归函数,像下面代码中实现即可,为什么呢?

  • 首先你是要进行递归,那么必定是要执行递归函数,且递归函数又是返回的布尔类型,那么就很显然是作为条件来出现的,因此刚好可以把它放到if的条件中,这样既执行了同时又能得到返回值,同时又能根据返回值进行自己的判断。

  • 在实现递归时,一定要做到先大处着眼,即先大体笼罩全局,即对于上述这种我们可以先将递归函数假设已经实现,那么递归函数的使用意义就变的很清楚了。

比如上述题目,其dfs就是在只能查找从此时的节点开始,向下是否能查找到searchWord字符串从index位置到末尾位置任意剔除一个位置的字符的其他所有字符。所以在写递归函数时,我们先让自己的函数假设已经实现,接着我们只要模拟第一次进入函数即可,即先判断能否直接从贪心的方向解决,使用递归函数的返回值来判断是否解决,解决了即return true,表示成功;没解决则使其向各个方向进行移动,然后每个都判断是否走成功,成功则return true,不成功则return false,并且如果此时searchWord的index位置的字符与查找到的字典树的此时节点字符不相同,且此时sign又为false,则不成功,即return false

因此实现递归函数时一定要先弄清楚自己的递归函数是干什么的,然后给其假设已经成立,接着从大局出发,模拟走一次递归的流程即可。

Code2:

class Node{
    Node[] nodes;
    boolean flag;

    public Node(){
        nodes = new Node[26];   
        // 看样子是直接开了26个坑,但是其实由于每一个坑都没有new,所以都没数据,所以其实只是假开,只是把26个指向开了,
        //  后面只有真正存在某个方向的元素才会真开内存。
        flag = false;
    }

    public boolean contains(char key){
        if(nodes[key - 'a'] != null)
            return true;

        return false;
    }

    public Node get(char key){
        return nodes[key - 'a'];
    }  

    public void put(char key){
        nodes[key - 'a'] = new Node();
    }

    public void setFlag(){
        flag = true;
    }

    public boolean checkFlag(){
        return flag;
    }
}

 class Trie{
     //分成两个类就是为了能多个头指针,这样能一直保持头指针不动,使得此类的实例化对象可以进行多次运算而不是只能用一次
    Node root;

    public Trie(){
        root = new Node();
    }
   

    public void insert(String word){
        Node temp = root;
        for(int i=0;i<word.length();i++){
            char c = word.charAt(i);
            if(temp.contains(c)){
                temp = temp.get(c);
            }
            else{
                temp.put(c);
                temp = temp.get(c);
            }
        }
        temp.flag = true;
    }
}

class MagicDictionary {
    private Trie trie;

    public MagicDictionary() {
        trie = new Trie();
    }
    
    public void buildDict(String[] dictionary) {
        for(String s : dictionary){
            trie.insert(s);
        }
    }
    
    public boolean search(String searchWord) {
        // 头指针先判断是否为null, 这样dfs就可以不判断此时节点的情况了,只要判断此节点所链接的节点即可
        if(trie.root != null){
            return dfs(trie.root,true,searchWord,0);
        }
        else{
            return false;
        }
    }

    public boolean dfs(Node node,boolean sign,String searchWord,int index){
        if(node.nodes == null)
            return false;
        if(index == searchWord.length()){
            if(node.checkFlag() && !sign)  // 题目要求必须修改一次
                return true;
            return false;
        }

        char c = searchWord.charAt(index);
        // 符合条件则先根据贪心剪枝
        if(node.contains(c)){
            if(dfs(node.get(c),sign,searchWord,index+1)){  // 不能放index++和++index,因为要保证回溯过来的index不变!
                return true;
            }
        }
        
        if(sign){
            sign = false;
            for(int i=0;i<26;i++){
                char temp = (char)(i + 'a');
                if(temp == c)
                    continue; // 把原先贪心剪枝未查询到的情况去除,原先不符合剪枝条件的也不会受影响,因为本来就不含有temp指向
                if(node.get(temp) != null){
                    if(dfs(node.get(temp),sign,searchWord,index+1)){
                        return true;
                    }
                }
            }
            return false;
        }
        // 这里要加return,因为虽然是if-else结尾看样子直接分别在if和else里写return即可,但是由于if里不是一定return
        // if里也有if,所以导致if不一定被完全return,所以外部要加上
        return false;
    }
        
}

/* 
 含有他一定不要换他吗?
 hello 
 hezzo    hzllo

 所以我们发现含有的也是可能会换的,比如上面的就是换e,但是trie中有e
 所以是会有回溯操作的
*/
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

向光.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值