30. 串联所有单词的子串

133 阅读6分钟

【题目】

给定一个字符串 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 由小写英文字母组成

【题目解析】

思路

本问题可以通过滑动窗口和哈希表来解决。关键思路是遍历字符串s,对于每个可能的起始位置,检查其后的子串是否可以由words中的所有单词组成。

  1. 哈希表统计:首先,使用哈希表统计words中每个单词出现的次数。
  2. 滑动窗口搜索:然后,遍历s的每个字符作为起始位置,使用滑动窗口的方法来检查后续的子串。窗口的大小等于所有words的长度之和。
  3. 匹配单词:对于每个窗口,使用另一个哈希表记录当前窗口中单词出现的次数,与words中的次数进行比较,如果完全匹配,则将当前窗口的起始索引添加到结果列表中。
class Solution:
    def findSubstring(self, s: str, words: List[str]) -> List[int]:
        # 如果输入的字符串或单词列表为空,则直接返回空列表
        if not words or not s:
            return []
        
        # 单词的长度和所有单词的总长度
        word_len = len(words[0])
        words_len = word_len * len(words)
        # 使用 Counter 来统计 words 中每个单词出现的次数
        word_count = Counter(words)
        
        # 结果列表
        result = []
        
        # 遍历每个可能的开始位置,最多有 word_len 个不同的开始位置
        for i in range(word_len):
            # 初始化左右指针
            left = i
            right = i
            # 当前窗口中单词出现的次数
            current_count = Counter()
            
            # 遍历字符串,直到右指针达到字符串末尾
            while right + word_len <= len(s):
                # 获取当前窗口的单词
                word = s[right:right+word_len]
                # 移动右指针
                right += word_len
                
                # 如果当前单词在 words 中
                if word in word_count:
                    current_count[word] += 1
                    
                    # 如果当前单词出现的次数超过了它在 words 中的次数,移动左指针,直到次数匹配
                    while current_count[word] > word_count[word]:
                        current_count[s[left:left+word_len]] -= 1
                        left += word_len
                        
                    # 如果当前窗口的长度等于所有单词的总长度,将左指针添加到结果列表中
                    if right - left == words_len:
                        result.append(left)
                else:
                    # 如果当前单词不在 words 中,清空当前计数器,移动左指针到右指针的位置
                    current_count.clear()
                    left = right
            
        return result

执行

image.png

【总结】

适用问题类型

"串联所有单词的子串"问题属于复杂的字符串搜索和匹配类问题,特别适合于需要在一个主字符串中找出由一个字符串数组中所有元素组成的所有可能子串的情况。这种问题类型常见于文本分析、日志文件处理、数据挖掘、以及需要复杂模式匹配的编程场景中。具体来说,这种方法适用于以下几种情形:

  • 需要识别并验证字符串中包含的模式或单词组合。
  • 在大型文本中寻找满足特定顺序或组合条件的词汇序列。
  • 解决需要同时考虑多个字符串匹配且对匹配顺序有要求的问题。

解决算法

解决此问题主要采用了滑动窗口哈希表的算法。算法的核心思想包括:

  • 哈希表统计:利用哈希表预先统计字符串数组words中每个单词出现的次数,为之后的匹配提供快速查找的可能。
  • 滑动窗口遍历:通过滑动窗口遍历主字符串s,窗口大小为所有words单词长度之和,窗口每次移动一个单词的长度。
  • 匹配与验证:在每个窗口中,利用另一个哈希表记录窗口内单词出现的次数,并与words的哈希表进行比较,确保当前窗口内的单词组合符合条件。
  • 索引记录:一旦发现符合条件的窗口,记录当前窗口的起始索引。

算法性能

  • 时间复杂度:最坏情况下为O(N×M×L),其中NN是字符串s的长度,M是words数组的长度,L是words中单词的长度。这是因为需要遍历s并在每个可能的位置检查长度为M×L的子串。
  • 空间复杂度:O(M),主要是存储words中单词出现次数的哈希表和当前窗口内单词出现次数的哈希表。

总结与扩展

通过结合滑动窗口和哈希表技术,我们可以高效地解决复杂的字符串搜索和匹配问题。这种方法不仅适用于本题,也可以扩展到其他需要复杂模式匹配的问题,如查找具有特定排列的子串、多模式字符串搜索等。掌握这种技巧可以大大提高解决字符串处理问题的能力,尤其是在处理大规模文本数据时,能有效地优化程序的性能和效率。此外,对于更高级的字符串匹配算法,如KMP、Rabin-Karp等,理解本题的解题思路也会有所帮助,为进一步学习复杂算法打下坚实的基础。

题目链接

串联所有单词的子串