网址
题目
给定一个字符串和一堆单词相同长度的单词,找出包含所有单词组合的字符串的索引
这道题有点难度,我刚开始想着先找出每个单词在字符串中的索引,然后遍历找到的索引差是否一致来判断,但是这样貌似无法实现。看了看讨论区大神的解法是这样的(我加了点注释):
解法1
class Solution {
public List<Integer> findSubstring(String s, String[] words) {
//最终的结果
List<Integer> indexes = new ArrayList<>();
int n = s.length(), num = words.length;
if (num == 0 || n == 0) return indexes;
//hashmap 1 存储word及存在的次数
Map<String, Integer> counts = new HashMap<>();
for (String word : words) {
counts.put(word, counts.getOrDefault(word, 0) + 1);
}
int len = words[0].length();
//由于每个word长度相等,只需要遍历n-num*len+1次字符串即可
//word的排列顺序巧妙地通过hash来解决
for (int i = 0; i < n - num * len + 1; i++) {
//hashmap 2 存储原字符串中的匹配情况
Map<String, Integer> seen = new HashMap<>();
int j = 0;//已经匹配了几个word
while (j < num) {
//每次匹配一个word
String word = s.substring(i + j * len, i + (j + 1) * len);
if (counts.containsKey(word)) {
seen.put(word, seen.getOrDefault(word, 0) + 1);
//如果当前识别的word次数超过最大匹配限度 整体来说也是不匹配的
if (seen.get(word) > counts.getOrDefault(word, 0)) {
break;
}
} else {
break;
}
j++;
}
//匹配成功 进行下一轮匹配
if (j == num) {
indexes.add(i);
}
}
return indexes;
}
}
解法2
上面的做法外层循环每次都挪一位其实是很低效的,我们可以考虑一下场景可以跳过一些无效的遍历
class Solution {
public List<Integer> findSubstring(String s, String[] words) {
//最终的结果
List<Integer> indexes = new ArrayList<>();
int n = s.length(), num = words.length;
if (num == 0 || n == 0) return indexes;
//hashmap 1 存储word及存在的次数
Map<String, Integer> counts = new HashMap<>();
for (String word : words) {
counts.put(word, counts.getOrDefault(word, 0) + 1);
}
int len = words[0].length();
//考虑每次以word长度来移动 效率会更高 而这样移动需要遍历word的长度次数
for (int k = 0; k < len; k++) {
//hashmap 2 存储原字符串中的匹配情况
Map<String, Integer> seen = new HashMap<>();
int j = 0;//已经匹配了几个word
for (int i = k; i < n - num * len + 1; i += len) {
boolean hasRemoved = false;
while (j < num) {
//每次匹配一个word
String word = s.substring(i + j * len, i + (j + 1) * len);
if (counts.containsKey(word)) {
seen.put(word, seen.getOrDefault(word, 0) + 1);
//如果当前识别的word次数超过最大匹配限度 则移除hashmap2中已匹配的word直至没有超过最大匹配限度
if (seen.get(word) > counts.get(word)) {
hasRemoved = true;
int removeNum = 0;
while (seen.get(word) > counts.get(word)) {
String firstword = s.substring(i + removeNum * len, i + (removeNum + 1) * len);
seen.put(firstword, seen.get(firstword) - 1);
removeNum++;
}"+removeNum);
j = j - removeNum + 1; //当前单词已经加入hashmap中 所以要加一
i = i + (removeNum - 1) * len;
break;
}
} else {
//遇到不匹配的word i直接移动到不匹配的下一个word
seen.clear();
i = i + j * len;
j = 0;
break;
}
j++;
}
//匹配成功
if (j == num) {
indexes.add(i);
}
//一直匹配 则把第一个word移除
if (j > 0 && !hasRemoved) {
String firstword = s.substring(i, i + len);
seen.put(firstword, seen.get(firstword) - 1);
j = j - 1;
}
}
}
return indexes;
}
}
该方法过leetcode的测试用例只用了24ms,而上一种方法用了88ms。它们的思路基本一致,不过就是相较于第一种方法每次从头取,这种方法采用的是一个滑动窗口,一直往右滑,由于每个单词长度一致,可以每次移动一个长度单位,除此以外,还考虑了一些特殊情况:
1⃣️如果heatmap2检测到有单词超出最大次数,则滑动窗口一直往右移动直至没有超过最大限制
2⃣️在不满足1⃣️的条件下,去掉窗口中第一个匹配的单词,留出空间以匹配下一个单词
3⃣️在遇到单词不匹配的情况下,直接移动滑动窗口至不匹配单词的下一个单词
这种做法折腾了好久好久,怎么看都没问题,debug好久才发现我用了-=
是默认右边的式子是连在一块的!以后在-=
或者\=
连写时注意⚠️