串联所有单词的子串

标签: 哈希表 字符串 滑动窗口

难度: Hard

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

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

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

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

示例 1:

输入:s = "barfoothefoobarman", words = ["foo","bar"]
输出:[0,9]
解释:因为 words.length == 2 同时 words[i].length == 3,连接的子字符串的长度必须为 6。
子串 "barfoo" 开始位置是 0。它是 words 中以 ["bar","foo"] 顺序排列的连接。
子串 "foobar" 开始位置是 9。它是 words 中以 ["foo","bar"] 顺序排列的连接。
输出顺序无关紧要。返回 [9,0] 也是可以的。

示例 2:

输入:s = "wordgoodgoodgoodbestword", words = ["word","good","best","word"]
输出:[]
解释:因为 words.length == 4 并且 words[i].length == 4,所以串联子串的长度必须为 16。
s 中没有子串长度为 16 并且等于 words 的任何顺序排列的连接。
所以我们返回一个空数组。

示例 3:

输入:s = "barfoofoobarthefoobarman", words = ["bar","foo","the"]
输出:[6,9,12]
解释:因为 words.length == 3 并且 words[i].length == 3,所以串联子串的长度必须为 9。
子串 "foobarthe" 开始位置是 6。它是 words 中以 ["foo","bar","the"] 顺序排列的连接。
子串 "barthefoo" 开始位置是 9。它是 words 中以 ["bar","the","foo"] 顺序排列的连接。
子串 "thefoobar" 开始位置是 12。它是 words 中以 ["the","foo","bar"] 顺序排列的连接。

提示:

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

Submission

运行时间: 804 ms

内存: 15.1 MB

from collections import defaultdict


class Solution:
    def findSubstring(self, s: str, words: List[str]) -> List[int]:
        ans = []
        need = defaultdict(int)
        for word in words:
            need[word] += 1
        word_length = len(words[0])
        word_count = len(words)
        total = word_length * word_count
        for i in range(len(s)-total+1):
            window = defaultdict(int)
            for j in range(i, i+total, word_length):
                window[s[j:j+word_length]] += 1
            if window == need:
                ans.append(i)
        return ans

Explain

这个题解使用了滑动窗口和哈希表的思路。首先用一个哈希表 need 统计 words 中每个单词出现的次数。然后遍历字符串 s,对于每个可能的起始位置 i,使用另一个哈希表 window 统计从 i 开始,以 words 中单词长度为步长,截取 words 总长度的子串中每个单词出现的次数。如果 window 和 need 相等,说明找到了一个符合要求的串联子串,将起始位置 i 加入答案数组 ans。

时间复杂度: O(n * m * w)

空间复杂度: O(m)

```python
from collections import defaultdict


class Solution:
    def findSubstring(self, s: str, words: List[str]) -> List[int]:
        ans = []
        # 统计 words 中每个单词出现的次数
        need = defaultdict(int)
        for word in words:
            need[word] += 1
        word_length = len(words[0])
        word_count = len(words)
        total = word_length * word_count
        # 遍历 s 的起始位置
        for i in range(len(s)-total+1):
            # 统计子串中每个单词出现的次数
            window = defaultdict(int)
            for j in range(i, i+total, word_length):
                window[s[j:j+word_length]] += 1
            # 如果 window 和 need 相等,说明找到了一个符合要求的串联子串
            if window == need:
                ans.append(i)
        return ans
```

Explore

哈希表(或称为字典)在这种场景下被使用是因为它支持快速的查找、插入和更新操作。对于本题中的需求来说,我们需要经常检查一个单词是否存在于集合中,并且追踪每个单词的出现次数。哈希表以近乎常数的时间复杂度进行这些操作,非常适合处理与单词对应的键值对。相比之下,使用数组或列表等其他数据结构可能会因为查找操作而导致更高的时间复杂度,尤其是当单词集合较大或单词长度不一时。

在本题的实现中,遍历字符串s的起始位置i是从0开始,直到`len(s)-total+1`。这里的`total`是所有单词的长度总和,确保了从任一起始位置i开始,即使截取至i+total的长度,也不会超出字符串s的界限。因此,在内部循环中,j从i开始,每次增加word_length,直到i+total,这保证了在访问`s[j:j+word_length]`时,j+word_length不会超过s的长度,从而避免了越界的情况。

在实现中,并没有直接限制window哈希表不包含need哈希表中不存在的单词。实际上,window哈希表会记录从s中截取的任意单词,无论它是否存在于need中。然而,在最终比较window和need是否相等时,如果window中包含了need中不存在的单词或单词的数量不匹配,这两个哈希表将不会相等,因此这样的子串不会被添加到结果列表中。简而言之,包含多余单词的窗口在最终比较中将自动被排除。

当words数组中包含重复单词时,哈希表need将正确地统计每个单词的期望出现次数。例如,如果一个单词出现两次,那么need中该单词的计数将为2。在遍历字符串s并填充window哈希表时,也会根据s中相应位置的单词增加计数。最后,只有当window中的每个单词的出现次数都恰好匹配need中的计数时,才认为找到了一个有效的子串。这意味着算法自然地处理了words中单词的重复情况,无需额外的逻辑处理。