题目
给定一个字符串 s 和一些长度相同的单词 words。找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置。
注意子串要与 words 中的单词完全匹配,中间不能有其他字符,但不需要考虑 words 中单词串联的顺序。
示例
输入:
s = “wordgoodgoodgoodbestword”,
words = [“word”,“good”,“best”,“word”]
输出:[]
算法思路
刚开始拿到这个题,我的思路是找到单词words中所有能组合成的字符串,然后逐个在s中比较,得到结果。这种方法明显的不足在于将words中的所有单词进行排列组合,会耗费大量的和空间,当单词数量较多是,组合数也会特别多,很多组合都是不必要的,所有排除这种了思路。
另一种思路:滑动窗口,引入HashMap的键值对来解题。具体步骤如下:
- 首先将words中的每个单词及其出现的次数作为键值对加入map中,将单词word作为键,对应单词出现的次数作为值,使用getOrDefault(key, default value)方法;
- 从目标字符串s的第一个字符开始,不用循环到最后一个字符,到s的长度减去words组成的字符串长度即可;
- 然后从i开始,长度为所有word组成的字符串的长度,每次讲j加单个word的长度,依次取出单个word及其出现的次数加入新的map中;
- 最后判断两个map是否相等即可。
该方法可以在第二层循环中优化一下,减少没必要的遍历。首先是判断子串substr是否是map中的key,不存在就直接跳出当循环,遍历下一个i,存在就判断当前map中的key对应出现的次数是否比map中的大,大于就天出循环。
代码
class Solution {
public List<Integer> findSubstring(String s, String[] words) {
List<Integer> res = new ArrayList<>();
if(s.isEmpty() || words.length==0 || s.length()<words[0].length()) return res;
Map<String,Integer> map = new HashMap<>();
int oneLen = words[0].length();
int allLen = oneLen * words.length;
for(String word:words ){
map.put(word,map.getOrDefault(word, 0)+1); //将words中每一个子单词作为键,出现的次数作为值
}
for(int i = 0;i<s.length()-allLen+1;i++){
Map<String,Integer> temp = new HashMap<>();
for(int j = i;j < i+allLen;j+=oneLen){//从i开始逐个单词加入hashMap中,然后和map进行比较
String substr = s.substring(j,j+oneLen);
temp.put(substr,temp.getOrDefault(substr,0)+1);
if(map.containsKey(substr)){//判断substr在map中是否存在,如果存在然后再判断对应的值是否比map中的大,如果大,就结束当前循环
if(temp.get(substr) > map.get(substr))
break;
}
else break;//substr不在map的键中,结束当前循环
}
if(map.equals(temp)) res.add(i); //HashMap集合中上的键值对与顺序无关
}
return res;
}
}
复杂度
时间复杂度:O(MN),M是字符串s的长度,N是words数组的长度;
空间复杂度: O(N),引入了HashMap,N个键值对。