leetcode
leetcode 1 ~ 50
串联所有单词的子串

串联所有单词的子串

难度:

标签:

题目描述

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

代码结果

运行时间: 804 ms, 内存: 15.1 MB


/*
 * Problem: Given a string 's' and a list of words 'words', find all starting indices of substrings in 's' that are a concatenation of each word in 'words' exactly once and without any intervening characters.
 * 
 * Approach:
 * 1. Calculate the length of each word and the total length of the concatenated substring.
 * 2. Use a sliding window of the total length to traverse the string 's'.
 * 3. For each window, check if the substring can be broken into words in 'words'.
 * 4. Use Java Streams to check the frequency of each word in the window.
 */
 
import java.util.*;
import java.util.stream.*;
 
public class SolutionStream {
    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 totalWords = words.length;
        int totalLength = wordLength * totalWords;
        Map<String, Long> wordCount = Arrays.stream(words)
                                             .collect(Collectors.groupingBy(w -> w, Collectors.counting()));
        for (int i = 0; i <= s.length() - totalLength; i++) {
            String current = s.substring(i, i + totalLength);
            List<String> parts = IntStream.range(0, totalWords)
                                           .mapToObj(j -> current.substring(j * wordLength, (j + 1) * wordLength))
                                           .collect(Collectors.toList());
            Map<String, Long> seenWords = parts.stream()
                                               .collect(Collectors.groupingBy(p -> p, Collectors.counting()));
            if (seenWords.equals(wordCount)) {
                result.add(i);
            }
        }
        return result;
    }
}

解释

方法:

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

时间复杂度:

O(n * m * w)

空间复杂度:

O(m)

代码细节讲解

🦆
为什么在统计words中每个单词出现次数时选择使用哈希表而不是其他数据结构?
哈希表(或称为字典)在这种场景下被使用是因为它支持快速的查找、插入和更新操作。对于本题中的需求来说,我们需要经常检查一个单词是否存在于集合中,并且追踪每个单词的出现次数。哈希表以近乎常数的时间复杂度进行这些操作,非常适合处理与单词对应的键值对。相比之下,使用数组或列表等其他数据结构可能会因为查找操作而导致更高的时间复杂度,尤其是当单词集合较大或单词长度不一时。
🦆
在遍历字符串s的过程中,是否有可能出现越界的情况,比如j+word_length超出字符串s的长度?
在本题的实现中,遍历字符串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哈希表不包含need哈希表中不存在的单词。实际上,window哈希表会记录从s中截取的任意单词,无论它是否存在于need中。然而,在最终比较window和need是否相等时,如果window中包含了need中不存在的单词或单词的数量不匹配,这两个哈希表将不会相等,因此这样的子串不会被添加到结果列表中。简而言之,包含多余单词的窗口在最终比较中将自动被排除。
🦆
给定words数组中可能包含重复单词,这种情况下算法如何处理?
当words数组中包含重复单词时,哈希表need将正确地统计每个单词的期望出现次数。例如,如果一个单词出现两次,那么need中该单词的计数将为2。在遍历字符串s并填充window哈希表时,也会根据s中相应位置的单词增加计数。最后,只有当window中的每个单词的出现次数都恰好匹配need中的计数时,才认为找到了一个有效的子串。这意味着算法自然地处理了words中单词的重复情况,无需额外的逻辑处理。

相关问题

最小覆盖子串

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 ""

 

注意:

  • 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
  • 如果 s 中存在这样的子串,我们保证它是唯一的答案。

 

示例 1:

输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。

示例 2:

输入:s = "a", t = "a"
输出:"a"
解释:整个字符串 s 是最小覆盖子串。

示例 3:

输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。

 

提示:

  • m == s.length
  • n == t.length
  • 1 <= m, n <= 105
  • st 由英文字母组成

 

进阶:你能设计一个在 o(m+n) 时间内解决此问题的算法吗?