一起刷力扣之【30. 串联所有单词的子串】

140 阅读1分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第8天,点击查看活动详情

题目描述

给定一个字符串 s 和一些 长度相同 的单词 words 。找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置。

注意子串要与 words 中的单词完全匹配,中间不能有其他字符 ,但不需要考虑 words 中单词串联的顺序。

示例

输入:s = "barfoothefoobarman", words = ["foo","bar"]
输出:[0,9]
解释:
从索引 0 和 9 开始的子串分别是 "barfoo" 和 "foobar" 。
输出的顺序不重要, [9,0] 也是有效答案。
输入: s = "wordgoodgoodgoodbestword", words = ["word","good","best","word"]
输出: []
输入: s = "barfoofoobarthefoobarman", words = ["bar","foo","the"]
输出: [6,9,12]

提示

  • 1 <= s.length <= 104^4
  • s 由小写英文字母组成
  • 1 <= words.length <= 5000
  • 1 <= words[i].length <= 30
  • words[i] 由小写英文字母组成

回溯 + 滑动窗口

通过回溯的方式,我们可以将所有的单词进行一个排列组合,得到字符串集合。 image.png

再取得字符串的拼接长度,通过滑动窗口的方式,遍历字符串s,判断其字串是否存在字符串集合中。

image.png

class Solution {
    private Set<String> set;
    public List<Integer> findSubstring(String s, String[] words) {
        // 保存拼接的字符串
        set = new HashSet<>();
        // 回溯
        back(words, 0, "");
        
        // 计算窗口长度
        int len = 0;
        for(String word : words){
            len += word.length();
        }

        List<Integer> list = new ArrayList<>();
        for(int i = 0; i + len <= s.length(); ++i){
            // 判断当前字串是否匹配
            if(set.contains(s.substring(i, i + len))){
                list.add(i);
            }
        }
        return list;
    }

    /**
     * 回溯
     * @param words 字符串数组
     * @param idx 指针
     * @param str 前置拼接的字符串
     */
    private void back(String[] words, int idx, String str){
        if(idx == words.length){
            set.add(str);
            return;
        }

        for(int i = idx; i < words.length; ++i){
            if(i != idx && words[i].equals(words[idx])){
                continue;
            }
            swap(words, idx, i);
            back(words, idx + 1, str + words[idx]);
            swap(words, idx, i);
        }
    }

    /**
     * 交换
     * @param i 指针
     * @param j 指针
     */
    private void swap(String[] words, int i, int j){
        String tmp = words[i];
        words[i] = words[j];
        words[j] = tmp;
    }
}

回溯算法的时间复杂度是: O(nn!){O(n * n!)},在数据量小的时候进行组合排列还可以得到结果,但是一旦数据量上去了,其所需的时间会增长很多,导致内存溢出或超时,因此该解法不适用于当前的题目。

哈希表

由于回溯排序会超时,那我们尝试着使用计数法来判断下。将words中出现的单词次数统计起来,再遍历字符串s,通过计算出来的区间,滑动统计子字符串中的单词出现次数是否与words中单词出现的次数匹配。如果当前子字符串中单词出现的次数超过words中统计的数量,则表示该排列不匹配。

class Solution {
    public List<Integer> findSubstring(String s, String[] words) {
        // 统计单词出现的次数
        Map<String, Integer> wordMap = new HashMap<>();
        for(String word : words){
            wordMap.put(word, wordMap.getOrDefault(word, 0) + 1);
        }
        // 窗口长度
        int n = words[0].length(), wordLen = n * words.length;

        List<Integer> res = new ArrayList<>();
        int idx = 0;
        while(idx + wordLen <= s.length()){
            // 统计子字符串中单词出现次数
            Map<String, Integer> map = new HashMap<>();
            boolean flag = true;
            
            for(int i = idx; i <= idx + wordLen - n; i += n){
                String word = s.substring(i, i + n);
                map.put(word, map.getOrDefault(word, 0) + 1);
                // 如果单词出现的次数超过字符串数组中出现的次数,则表示不匹配
                if(wordMap.getOrDefault(word, 0) < map.get(word)){
                    flag = false;
                    break;
                }
            }
            if(flag){
                res.add(idx);
            }
            ++idx;
        }

        return res;
    }
}