力扣 - 30. 串联所有单词的子串
题目
思路1(滑动窗口+哈希表)
- 使用HashMap,这样子就可以不用纠结字符串的顺序了
- 先计算words中的每个单词出现的个数
- 每次确定starts位置,然后依次截取wordLen长度的字串,比较是否再words中,不存在的话,直接break,进入下一个窗口;存在的话,就加入到窗口中,添加进去还要比较是否数量抄了,如果超了的话也是不行的
- 遍历字符串s,每次每个窗口的最大容量为words的大小,每次的窗口用一个新的哈希表来表示
- 移动窗口时候右移一个字符
代码
class Solution {
public List<Integer> findSubstring(String s, String[] words) {
List<Integer> res = new ArrayList<>();
// 获取s字符串长度
int length = s.length();
// 获取words中的单词个数
int wordNum = words.length;
// 如果字符串长度为0或者words中的单词个数为0,直接返回空结果
if (length == 0 || wrodNum == 0) {
return res;
}
// 获取words中的单词长度
int wordLen = words[0].length();
// 统计words中的每个不同单词出现个数
Map<String, Integer> wordCount = new HashMap<>();
for (String word : words) {
wordCount.put(word, wordCount.getOrDefault(word, 0) + 1);
}
// 从0开始,末尾只要道 strLen - wordNum * wordLen + 1 即可
// wordNum * wordLen 是待匹配的单词串联总长度,末尾的 +1 是因为如果刚好最后一段匹配上,如果不 +1 ,那么就会导致最后一段匹配不了
// 滑动窗口每次向右移动一格
for (int i = 0; i < strLen - wordNum * wordLen + 1; i++) {
// 用于临时存储滑动窗口中的子字符串及其对应的出现次数
Map<String, Integer> temp = new HashMap<>();
// num用于统计临时map中加入了多少个字符串
int num = 0;
// 记录此次遍历窗口的起始位置
int start = i;
// 当temp中的单词数量没有超过待串联的单词数量时,就可以继续添加
while (num < wordNum) {
// 获取一个wordLen长度的子字符串
String subWord = s.substring(start, start + wordLen);
// 如果待串联的单词中包含该单词,就添加到temp哈希表中
if (wordCount.containsKey(subWord)) {
temp.put(subWord, temp.getOrDefault(subWord, 0) + 1);
// 如果一个单词出现次数超过了待串联的这个单词的最多个数,那么也直接不匹配
if (temp.get(subWord) > wordCount.get(subWord)) {
break;
}
} else {
// 不包含subWord就直接进入下一个滑动窗口
break;
}
// 添加的word数+1
num++;
// start也右移wordLen,为了获取下一个subWord
start += wordLen;
}
// 如果每个都符合,且添加到temp哈希表中,那么就该字串符合条件,将起始索引添加到res集合中
if (num == wordNum) {
res.add(i);
}
}
// 得到结果
return res;
}
}
复杂度分析
- 时间复杂度:\(O(N*M)\),其中 N 为字符串s的长度, M 为每个单词的长度
- 空间复杂度:\(O(M)\),M 单词的个数,哈希表大小
思路2(滑动窗口优化)
- 第一种思路是每次窗口只向右移动一个,然后再在每个窗口里面截取wordLen的字串,这样的复时间杂度很高m*n
- 为什么要每次只能移动一个呢?因为如果s字符串单词大小如果有的不为word的长度,那么按照word长度为跨度来移动窗口就会导致匹配不到正确结果。例:
s="afoobar" words=["foo", "bar"],此时afo不匹配,接下来是oba也不匹配,可是正确结果是有匹配的,这就导致了bug - 我们介绍另一种方法可以实现窗口每次移动的跨度为
word的长度,但是还要分wordLen种情况,此时就符合题意了
代码
class Solution {
public List<Integer> findSubstring(String s, String[] words) {
List<Integer> res = new ArrayList<>();
int length = s.length();
int wordNum = words.length;
if (length == 0 || wordNum == 0) {
return res;
}
int wordLen = words[0].length();
Map<String, Integer> wordCount = new HashMap<>();
for (String word : words) {
wordCount.put(word, wordCount.getOrDefault(word, 0) + 1);
}
// 将所有移动分成wordLen类情况
for (int i = 0; i < wordLen; i++) {
Map<String, Integer> temp = new HashMap<>();
// 记录当前哈希表种的字串个数,且不能超过wordNum
int num = 0;
// 从i开始计算,且窗口每次右移wordLen长度
for (int j = i; j < length - wordLen * wordNum + 1; j += wordLen) {
// 用于标记是否出现单词符合,但是个数超过了的情况
boolean hasRemoved = false;
while (num < wordNum) {
// 动态获取字串
String subWord = s.substring(j + num * wordLen, j + (num+1) * wordLen);
// 如果字串在words中,就添加
if (wordCount.containsKey(subWord)) {
temp.put(subWord, temp.getOrDefault(subWord, 0) + 1);
// 并且判断是否超过了words中的个数
if (temp.get(subWord) > wordCount.get(subWord)) {
// 字串存在words中,但数量超了,要删减,将hasRemoved置为true
hasRemoved = true;
// 记录删除了几个字串
int removeNum = 0;
// 只要个数还是大于words中的个数就一直删除窗口的最前面的字串
while (temp.get(subWord) > wordCount.get(subWord)) {
// 获取当前窗口最前面的字串
String firstWord = s.substring(j + removeNum * wordLen, j + (removeNum+1) * wordLen);
// -1即删除
temp.put(firstWord, temp.get(firstWord) - 1);
// 同时记录删除了一个字串
removeNum++;
}
// +1因为已经将那个超出个数的那个字串加入到哈希表中了,removeNum是代表删除了多少个字串
num = num - removeNum + 1;
// 然后将j的值重新修改了
j = j + (removeNum - 1) * wordLen;
break;
}
} else {
// 如果字串不在words中,那么窗口起始位置就可以直接跳到这个字串之后位置就可以了
j = j + num * wordLen;
// 清空窗口(哈希表)
temp.clear();
// 将num计数置为0
num = 0;
break;
}
num++;
}
// 如果匹配的话,那么num就会等于words
if (num == wordNum) {
res.add(j);
}
// 如果字串完全匹配,那么将窗口的最前面的一个字串删除,进入下一轮判断
if (num > 0 && !hasRemoved) {
String subWord = s.substring(j, j + wordLen);
temp.put(subWord, temp.get(subWord)-1);
num--;
}
}
}
return res;
}
}
复杂度分析
- 时间复杂度:\(O(N)\),其中 N 为字符串 s 的长度
- 空间复杂度:\(O(M)\),其中 M 为单词的个数
我走得很慢,但我从不后退!

浙公网安备 33010602011771号