30. 串联所有单词的子串

目录

一、问题描述

二、解题思路

三、代码

四、复杂度分析


一、问题描述

给定一个字符串 s 和一个字符串数组 words words 中所有字符串 长度相同

 s 中的 串联子串 是指一个包含  words 中所有字符串以任意顺序排列连接起来的子串。

  • 例如,如果 words = ["ab","cd","ef"], 那么 "abcdef", "abefcd""cdabef", "cdefab""efabcd", 和 "efcdab" 都是串联子串。 "acdbef" 不是串联子串,因为他不是任何 words 排列的连接。

返回所有串联子串在 s 中的开始索引。你可以以 任意顺序 返回答案。

二、解题思路

这个问题要求找到字符串 s 中的所有“串联子串”,这些子串由字符串数组 words 中的所有字符串组成,并且每个子串中的字符串都必须是 words 中的元素,且每个元素只能使用一次。具体的思路如下:

  1. 固定子串长度:每个子串的总长度是 words 中每个字符串的长度乘以 words 中的字符串数量。即,子串的长度为 len(words) * len(words[0])

  2. 滑动窗口:我们可以通过滑动窗口的方式遍历字符串 s,每次检查长度为 len(words) * len(words[0]) 的子串,判断该子串是否由 words 中的字符串拼接而成。

  3. 使用哈希表计数:为了判断一个子串是否包含 words 中的所有字符串,我们可以使用一个哈希表记录 words 中每个字符串的出现次数,并在检查每个子串时,对其进行匹配。

  4. 边界条件:如果 s 的长度小于 len(words) * len(words[0]),则直接返回空列表。

具体步骤:

  1. 创建哈希表 word_count:用于记录 words 中每个字符串的出现次数。
  2. 遍历 s:从 s 的每个起点开始,取出长度为 len(words) * len(words[0]) 的子串,并检查是否是由 words 中的字符串组成的。
  3. 滑动窗口检查:对每个子串使用另一个哈希表记录匹配到的字符串次数,逐个比较是否与 word_count 一致。
  4. 返回结果:将所有符合条件的起始索引返回。

优化思路:

  1. 滑动窗口:通过滑动窗口,每次移动固定长度的窗口,而不是每次都重新构建和检查整个单词频率表。
  2. 跳过无效起点:如果某个起点的第一个单词不在 words 中,可以直接跳过,而不是逐个检查。

三、代码

class Solution {
    public List<Integer> findSubstring(String s, String[] words) {
        List<Integer> result = new ArrayList<>();
        if (s == null || s.length() == 0 || words == null || words.length == 0) {
            return result;
        }

        int wordLength = words[0].length();  // 单个单词的长度
        int totalWordsLength = wordLength * words.length;  // 所有单词拼接起来的总长度
        if (s.length() < totalWordsLength) {
            return result;
        }

        // 记录 words 中每个单词出现的次数
        Map<String, Integer> wordCount = new HashMap<>();
        for (String word : words) {
            wordCount.put(word, wordCount.getOrDefault(word, 0) + 1);
        }

        // 遍历可能的起始位置
        for (int i = 0; i < wordLength; i++) {
            // 利用滑动窗口遍历整个字符串
            int left = i, right = i;
            int count = 0;  // 记录匹配的单词数
            Map<String, Integer> seen = new HashMap<>();

            while (right + wordLength <= s.length()) {
                // 取出当前窗口右侧的单词
                String word = s.substring(right, right + wordLength);
                right += wordLength;

                if (wordCount.containsKey(word)) {
                    seen.put(word, seen.getOrDefault(word, 0) + 1);
                    count++;

                    // 如果某个单词的数量超出了 words 中的要求,则需要移动左侧窗口
                    while (seen.get(word) > wordCount.get(word)) {
                        String leftWord = s.substring(left, left + wordLength);
                        seen.put(leftWord, seen.get(leftWord) - 1);
                        count--;
                        left += wordLength;
                    }

                    // 当匹配到所有单词时,将当前左侧窗口的位置加入结果
                    if (count == words.length) {
                        result.add(left);
                    }
                } else {
                    // 如果当前单词不在 words 中,重置窗口
                    seen.clear();
                    count = 0;
                    left = right;
                }
            }
        }

        return result;
    }
}

四、复杂度分析

  • 外层循环遍历起始位置:O(wordLength)
  • 内层循环遍历所有字符,每次移动 wordLength 长度,整个遍历次数为 O(n),其中 n 是字符串 s 的长度。
  • 因此,整体时间复杂度为 O(wordLength * n),比之前直接比较每一个可能位置更高效。
  • 空间复杂度:O(m),因为我们需要额外的哈希表来记录 words 中的单词以及窗口中的单词。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值