【力扣时间】【472】【困难】连接词

用最简洁的题目出最难的题。

1、来看题

我是题

此题不愧是困难题,好在也没难到哪去。
但题目简洁是真的简洁。一共就两句话,还带了些许废话。

2、审题

题目简洁,但处处值得推敲,直接看重点:

  1. 题目明确了words[]不含重复字符串。
  2. 解释了连接词的含义:一个完全由给定数组中的至少两个较短单词组成的字符串。什么意思呢?就是该字符串必须完全words[]中出现过字符串拼接得来。而又由于本身不能包含,所以不可以用空字符串和本体组成。
  3. 含义中的较短两个词是废话,由于字符串必须由两个及以上的字符串拼接组成,则组成该字符串的其余字符串一定小于本体。不过虽然是废话,但却一定程度上给了启迪。

题目的要点基本上提炼完了。
但思路却没有成形,慢慢来吧。

3、思路

首先,考虑到目标字符串必须是两个及以上的字符串组成,则这些字符串一定比目标字符串
所以我想当然地首先考虑到对原本的words[]进行排序,仅按照字符长度排序
这样,我们就可以优先判断较短的字符,而较短的字符必然不会由较长的字符发生关联,故遍历时不需要考虑后面的字符串。

剩下的问题就在于如何匹配子串了,我们需要记录已经获取到的所有的字符串,并能够快速地判断其是否是自身的子串。由于是全词匹配,前些日子学习到的字符串匹配方式似乎不太好施展。
但是,我们还有其他招式,标签里提到的字典树

字典树,或者说前缀树,其基本概念我不做讲解,若是你没有接触过,或是已经忘记,可以另行查看解释。
而之所以使用字典树,一来是自己有相关的经验,二来是可以在节省空间的同时,精确地查询字符串。

于是,整体的想法就成形了。

4、动手

class Solution {
    public List<String> findAllConcatenatedWordsInADict(String[] words) {
        DictionaryTreeNode root = new DictionaryTreeNode();
        Arrays.sort(words, Comparator.comparingInt(String::length));

        List<String> results = new ArrayList<>();

        for (int i = 0; i < words.length; i++) {
            if (words[i].length() < 1) {
                continue;
            }

            if (search(root, words[i], 0)) {
                //当前词能匹配成功的话,添加到结果集中
                results.add(words[i]);
            } else {
                //匹配失败的话,说明有字典树不存在的结点,需将当前词添加到字典树中
                root.add(words[i]);
            }
        }

        return results;
    }

    private boolean search(DictionaryTreeNode root, String word, int cur) {
        if (cur == word.length()) {
            //所有字符匹配完毕
            return true;
        }

        DictionaryTreeNode node = root;
        for (int i = cur; i < word.length(); i++) {
            Character next = word.charAt(i);
            //不包含时,说明当前字符匹配失败,则无法匹配后续
            if (!node.childMap.containsKey(next)) {
                return false;
            }

            //移动当下一结点
            node = node.childMap.get(next);

            //如果此结点为尾结点,说明成功匹配了一个字符串
            if (node.isEnd) {
                //以当前位置分割,重头匹配子串
                if (search(root, word, i + 1)) {
                    //如果子串能匹配成功,说明此字符能够成功
                    return true;
                }

                //如果子串匹配失败,则不以当前分割,再次匹配后续
            }
        }

        //匹配失败时
        return false;
    }

    public class DictionaryTreeNode {

        private boolean isEnd;

        private Map<Character, DictionaryTreeNode> childMap;

        public DictionaryTreeNode() {
            this.childMap = new HashMap<>();
        }

        public void add(String string) {
            DictionaryTreeNode node = this;
            for (int i = 0; i < string.length(); i++) {
                Character next = string.charAt(i);

                //当不包含当前字符时
                if (!node.childMap.containsKey(next)) {
                    //新建节点,并添加到对应字符下
                    DictionaryTreeNode child = new DictionaryTreeNode();

                    node.childMap.put(next, child);
                }

                //移向子节点
                node = node.childMap.get(next);
            }

            //所有字符添加完毕后,将isEnd标识置为true
            node.isEnd = true;
        }

    }
}

5、解读

字典树的实现我不做讲解,我的写法也挺一般的。
在节点中,我使用了isEnd来判断当前节点,是否为某一字符串的末尾。即查询到当前节点时,存在有出现在数组中的较短字符串,可以作为其子串。

在比较了官解后发现,在此处使用map存储子节点,读写耗时会比数组要差些。

核心比较逻辑在search()中实现。
首先我们从根节点出发,挨个比较字符在字典树中是否存在,如若进行到某个节点时,其子节点中不包涵对应字符,则说明当前字符无法再做查询。
而如果查询到某一节点,发现其isEnd为true,说明查询到目前时,可以确定了一个子串的存在。当然,该子串并不一定就是最终的解。
在查询到子串后,我们会以当前位置截取字符串,以相同的逻辑递归查询子串。
如果当前截取位置的子串也能成功查询到,则认为该完整字符串为连接词。
否则,回到截取处。继续向后查询,直到再次匹配到一个子串。

在主函数中,你会发现:

if (search(root, words[i], 0)) {
  //当前词能匹配成功的话,添加到结果集中
    results.add(words[i]);
} else {
    //匹配失败的话,说明有字典树不存在的结点,需将当前词添加到字典树中
    root.add(words[i]);
}

只有无法作为连接词的字符串会添加到字典树中,因为其可能成为后续的子串。
而已经确认为连接词的字符串,由于其子串已在字典树中出现过,所以无需再度添加。

6、提交

在这里插入图片描述
本以为用上字典树了,耗时排名能够做到挺高的,看来还有其他黑科技。

7、咀嚼

额……分析不出来……

由于解法和官解的字典树解法挺像的,所以参考官解的吧

在这里插入图片描述

8、看看大牛们

官解在给出字典树解法后,还抛出了一个思考。
我确实也想过通过记忆化来减少截取子串后的递归操作,但结果似乎并不理想。

之后,收集了大牛芸芸的解法,比较亮眼或者说与众不同的果然还是三叶大佬的字符串哈希做法。

9、总结

总之,又是一天困难题。
今天其实是严重超时了。但好在项目已经上线,临近年末了,暂时有些空窗期出现,才有足够的时间来完成这题。
收获肯定会有,但往往不会一朝一夕就呈现。

总之勤能补拙,各位社畜今天也是共勉了。

妈呀怎么今天还是这么冷……

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值