长大后想做什么?做回小孩!

0%

LeetCode——串联所有单词的子串

NO.30 串联所有单词的子串 困难

3E2X5D.png

这道题要把每个单词看成整体,每个不同的单词看作是不同的字符,单词串就看成是特殊的字符串。

==注意:s中的单词未必是长度相等。words中可能存在相同的单词。==

思路一:暴力法 words中的单词长度都一样,大幅降低了这道题的难度,所以这个特点要充分利用。所以遍历s的每个子串,分别检查每个字串中是否符合要求。

用一个hashmap存储words中的每个单词及其在words中出现的次数;每遍历一个子串都要用一个hashmap存储被遍历子串中出现的words中存在的单词及其在子串中出现的次数。

重点是理解这个“要求”:1.words中的每个单词都必须出现一次。2.words中的每个单词必须连续出现

反言之:检查每个子串的过程中,出现words中的不存在的单词则结束检查;出现与words中相等的单词,但是出现的次数超过其在words中出现的次数则结束检查。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public List<Integer> findSubstring(String s, String[] words) {
List<Integer> res=new ArrayList<>();
if (words==null||words.length==0)return res;
//单词个数、单词长度
int wordNum = words.length,wordLen=words[0].length();
//将words每个单词及其个数存入hashmap
HashMap<String,Integer> allWords=new HashMap<>();
for (String word : words) {
Integer value = allWords.getOrDefault(word, 0);
allWords.put(word,++value);
}
//遍历s每一个子串,剩余不足wordNum*wordLen个字符的子串不需要遍历
for (int i = 0; i < s.length() - wordNum * wordLen + 1; i++) {
//将子串中出现的和words中相等的单词及其出现次数存入hashmap
HashMap<String,Integer> hasWords=new HashMap<>();
//记录字串中和words中相等单词数量
int count=0;
//统计字串中连续和words中相等的单词
while (count<wordNum){
String word = s.substring(i + count * wordLen, i + (count + 1) * wordLen);
//如果word匹配words中的单词,就统计其出现次数
if (allWords.containsKey(word)){
Integer value = hasWords.getOrDefault(word, 0);
hasWords.put(word,++value);
//如果word出现次数超过words中这个单词的总数量则结束统计
if (hasWords.get(word)>allWords.get(word))break;
}else {
//如果字串中出现于words中所有单词都不匹配的word则结束统计
break;
}
//增加成功与words中匹配的单词数量
count++;
}
if (count==wordNum)res.add(i);
}
return res;
}

时间复杂度:O(n*m) n是s长度,m是words中单词个数。

思路二:滑动窗口优化暴力法 用循环内的map(haswords)来保存窗口中匹配的单词,再用一个指针标记窗口当前的起始位置。

暴力方法中有几个需要优化的地方:

  1. 匹配成功:

    3My6je.png

    判断i=0这个子串符合要求,如果继续按照思路一的方法判断。当i=3的时候,依然一次校验每个单词,但是“foofoo”这两个单词已经在i=0子串的时候校验过了。所以暴力法中的hasword这个map并不需要每次都清空,只需要移除“bar“之后,从i=9的单词开始判断就好了。

  2. 匹配失败,有不匹配的单词:

    3MyycD.png

    判断i=0子串时出现了“the”这个不匹配的单词导致匹配失败。i=3、i=6这些子串都包含“the”这个单词,所以都不能匹配成功,所以窗口直接移动到i=9继续校验即可。

  3. 匹配失败,单词匹配但是数量超出:

    3Mys1O.png

    i=0字串中“bar”出现两次,但是words中只有一个”bar”所以匹配失败。窗口移动到i=3,移除了“foo”但是“bar”依然多出一个,所以一定不匹配。窗口移动到i=6的时候移除了“bar”,就可以按照正常流程继续判断了。

不难发现,上述几种情况的描述时,不再是每次移动一个字符,而是每次移动单词长度。但是s中的单词不一定都是刚好符合wordLen,如何解决这种情况?

答:分成wordLen种情况,分别进行判断。分别从i=0开始每次移动一个单词长度、从i=1开始每次移动一个单词长度、从i=2开始每次移动一个单词长度、、、直至从i=wordLen-1开始每次移动一个单词长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public List<Integer> findSubstring(String s, String[] words) {
List<Integer> result=new ArrayList<>();
if (s==null||words==null||words.length==0)return result;
int wordsNum = words.length,wordLen=words[0].length();
//将words中的单词及其数量存入hashmap
HashMap<String,Integer> allWords=new HashMap<>();
for (String word : words) {
Integer value = allWords.getOrDefault(word, 0);
allWords.put(word,value+1);
}
//分成wordLen中情况,分别从0开始每次移动一个单词长度~从wordLen-1开始每次移动一个单词长度
for (int j=0;j<wordLen;j++){
//haswords存放当前子串中匹配的单词及其个数,count当前子串匹配的单词数量
HashMap<String,Integer> haswords=new HashMap<>();
int count=0;
//遍历从j开始的每个子串,每次动一个单词长度
for (int i=j;i<s.length()-wordLen*wordsNum+1;i+=wordLen){
//防止情况三出现之后,情况一继续移除
boolean hasRemoved=false;
while (count<wordsNum){
String curWord = s.substring(i + count * wordLen, i + (count + 1) * wordLen);
//当前单词匹配,加入haswords
if (allWords.containsKey(curWord)) {
Integer value = haswords.getOrDefault(curWord, 0);
haswords.put(curWord,value+1);
count++;
//情况三,当前单词匹配,但是数量超了
if (haswords.get(curWord) > allWords.get(curWord)) {
hasRemoved=true;
//从i开始逐个单词,从haswords中移除,removeNum记录移除的单词个数
int removeNum=0;
while (haswords.get(curWord) > allWords.get(curWord)) {
String fristWord = s.substring(i + removeNum * wordLen, i + (removeNum + 1) * wordLen);
Integer v = haswords.get(fristWord);
haswords.put(fristWord,v-1);
removeNum++;
}
//移除完毕之后,更新count
count-=removeNum;
//移动i的位置(注意removeNum要-1,因为跳出当前循环之后,i还要+wordLen)
i+=(removeNum-1)*wordLen;
break;
}
}else{//情况二,当前单词不匹配
//清空haswords
haswords.clear();
//i移动到当前单词位置(因为跳出当前循环之后,i还要+wordLen)
i+=count*wordLen;
count=0;
break;
}
}
//情况一,匹配成功
if (count==wordsNum)result.add(i);
//如果情况三没有出现
if (count>0&&!hasRemoved){
//移除成功匹配子串的第一个元素
String fristWord = s.substring(i, i + wordLen);
Integer v = haswords.get(fristWord);
haswords.put(fristWord,v-1);
count--;
}
}
}
return result;
}

时间复杂度:O(n*wordLen) 这个时间复杂度不敢确定算的对。。。


本人菜鸟,有错误请告知,感激不尽!

更多题解和学习记录博客:博客github