今天刷到了这道题,算是滑动窗口和哈希结合,很标准的能够锻炼优化思路的题,下面写一下自己的解体思路。
给定一个字符串 s 和一些长度相同的单词 words。找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置。
注意子串要与 words 中的单词完全匹配,中间不能有其他字符,但不需要考虑 words 中单词串联的顺序。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/substring-with-concatenation-of-all-words
这道题简单分析一下可以看出来,题的最终目标就是找到所有符合两个条件的子串。
- 条件1:长度为words中全部字的长度
- 条件2:完全由words中的字构成
这样就有了一个简单的思路就是先找到全部符合条件1 的子串然后对符合条件1的子串进行条件2的判断如果也符合条件2 就将这个子串的首地址加入到结果集中。
接下来我们的工作就放在如何进行条件2 的比对,根据条件2的描述可以看出这个比对对于顺序是没有要求的他要求的就是能够一对一找到彼此中的字,那么就可以使用哈希表来做
首先将words存入哈希表然后将目标子串也根据字长分割后存入哈希表,比对哈希表是否一致就可以判断出是否符合条件2.
根据这个思路就可以写出第一个版本的代码
class Solution {
public List<Integer> findSubstring(String s, String[] words) {
List<Integer> res = new ArrayList<>();
if (s == null || s.length() == 0 || words == null || words.length == 0) return res;
HashMap<String, Integer> map = new HashMap<>();
int one_word = words[0].length();
int word_num = words.length;
int all_len = one_word * word_num;
for (String word : words) {
map.put(word, map.getOrDefault(word, 0) + 1);
}
for (int i = 0; i < s.length() - all_len + 1; i++) {
String tmp = s.substring(i, i + all_len);
HashMap<String, Integer> tmp_map = new HashMap<>();
for (int j = 0; j < all_len; j += one_word) {
String w = tmp.substring(j, j + one_word);
tmp_map.put(w, tmp_map.getOrDefault(w, 0) + 1);
}
if (map.equals(tmp_map)) res.add(i);
}
return res;
}
}
随后就是对我们的思路进行分析看看有没有什么地方可以优化
第一个自然而然的想法就是如果我们在条件2判断中的哈希表填充时就可以提前判断这个目标串已经不符合条件了那么后面的填充就没有必要继续进行了。下面是这个版本的代码
class Solution {
public List<Integer> findSubstring(String s, String[] words) {
List<Integer> res = new ArrayList<Integer>();
int sLength = s.length();
int wordNum = words.length;
int wordSize = words[0].length();
int wordsSize = wordSize*wordNum;
if(sLength==0||wordNum==0) return res;
Map<String,Integer> allWords = new HashMap<String,Integer>();
for(String ss:words){
if(allWords.containsKey(ss)){
allWords.put(ss,allWords.get(ss)+1);
}else allWords.put(ss,1);
}
for(int i=0;i<sLength-wordsSize+1;i++){
int num = 0;
Map<String,Integer> checkWords = new HashMap<String,Integer>();
while(num<wordNum){
int beg = i+num*wordSize;
int end = i+(num+1)*wordSize;
String tmp = s.substring(beg,end);
if(allWords.containsKey(tmp)){
if(checkWords.containsKey(tmp)){
checkWords.put(tmp,checkWords.get(tmp)+1);
if(checkWords.get(tmp)>allWords.get(tmp)) break;
}else{
checkWords.put(tmp,1);
}
}else {
break;
}
num++;
}
if(num==wordNum) res.add(i);
}
return res;
}
}
最后根据信息的思路来分析,如果我们在之前就已经在某种判断中获取到过某些信息那么就可以避免后面在对这些信息进行重复的获取,以此来减少获取全部信息的时间。
我们获取信息的操作共有两个部分一个是条件1一个是条件2,在条件1中并没有多余的的操作,就是对全部长度为length的子串进行遍历。
在看条件2,我们在获取第一个子串时对每个字长的子串中的字串都进行了获取和判断,而在再次移动子串起始长度为字长时我们又一次对之前以及获取过的字进行了获取和判断,这个重复获取每隔字长就出现总串长-1次。
那么就可以把获取条件1的过程分为字长个阶段。for (int j = 0; j < wordLen; j++)
在条件2的判断中的每个字的判断中也可以分为两个情况:匹配成功,匹配失败(1.由于次数超了,2.单纯的匹配失败)
- 如果匹配成功我们完全可以保留当前的除了第一个字以外的哈希表
- 如果是单纯的字就匹配失败了,那么就可以直接将起始位置跳到匹配失败的那个字的后面重新开始匹配
- 如果是字都对上了,但是次数多了,那么就可以一直往后移动起始位置直到次数不超的那个地方在开始匹配
具体的实现如下
class Solution {
public List<Integer> findSubstring(String s, String[] words) {
List<Integer> res = new ArrayList<Integer>();
int wordNum = words.length;
if (wordNum == 0) {
return res;
}
int wordLen = words[0].length();
HashMap<String, Integer> allWords = new HashMap<String, Integer>();
for (String w : words) {
int value = allWords.getOrDefault(w, 0);
allWords.put(w, value + 1);
}
//将所有移动分成 字长个 情况
for (int j = 0; j < wordLen; j++) {
HashMap<String, Integer> hasWords = new HashMap<String, Integer>();
int num = 0; //记录当前 HashMap2中有多少个单词
//每次移动一个字长
for (int i = j; i < s.length() - wordNum * wordLen + 1; i = i + wordLen) {
while (num < wordNum) {
String word = s.substring(i + num * wordLen, i + (num + 1) * wordLen);
if (allWords.containsKey(word)) {
int value = hasWords.getOrDefault(word, 0);
hasWords.put(word, value + 1);
//遇到了符合的单词,但是次数超了
if (hasWords.get(word) > allWords.get(word)) {
int removeNum = 0;
//一直移除单词,直到次数符合了
while (hasWords.get(word) > allWords.get(word)) {
String firstWord = s.substring(i + removeNum * wordLen, i + (removeNum + 1) * wordLen);
int v = hasWords.get(firstWord);
hasWords.put(firstWord, v - 1);
removeNum++;
}
num = num - removeNum + 1;
i = i + (removeNum - 1) * wordLen;
break;
}
//出现情况二,遇到了不匹配的单词,直接将 i 移动到该单词的后边
} else {
hasWords.clear();
i = i + num * wordLen;
num = 0;
break;
}
num++;
}
if (num == wordNum) {
res.add(i);
String firstWord = s.substring(i, i + wordLen);
int v = hasWords.get(firstWord);
hasWords.put(firstWord, v - 1);
num = num - 1;
}
}
}
return res;
}
}
下面给出对应版本的c++实现:
版本2:
class Solution {
public:
vector<int> findSubstring(string s, vector<string>& words) {
vector<int> res;
int word_num = words.size();//字的个数2
if(word_num==0) return res;
map<string,int> map1;
//初始化信息
vector<string>::iterator itr = words.begin();
while(itr!=words.end()){
string s = *itr;
map1[s]++;
itr++;
}
int word_size = words[0].length();//单个字长3
int words_size = word_size*word_num; //拼接后总字长6
//---------------
for(int i=0;i<s.length()-words_size+1;i++){
map<string,int> map2;
int num = 0;
while(num<word_num){
string tmp = s.substr(i+num*word_size,word_size);
if(map1.count(tmp)==0) break;
else {
map2[tmp]++;
if(map1[tmp]<map2[tmp]) break;
}
num++;
}
if(num==word_num) res.push_back(i);
}
return res;
}
};
版本3
class Solution {
public:
vector<int> findSubstring(string s, vector<string>& words) {
if(words.empty()) return {};
unordered_map<string,int> wordmap,smap;
for(string word:words) wordmap[word]++;
int wordlen = words[0].size();
int wordnum = words.size();
vector<int> ans;
for(int k=0;k<wordlen;k++){
int i=k,j=k;
while(i<s.size()-wordnum*wordlen+1){
while(j<i+wordnum*wordlen){
string temp = s.substr(j,wordlen);
smap[temp]++;
j+=wordlen;
if(wordmap[temp]==0){//情况二,有words中不存在的单词
i=j;//对i加速
smap.clear();
break;
}
else if(smap[temp]>wordmap[temp]){//情况三,子串中temp数量超了
while(smap[temp]>wordmap[temp]){
smap[s.substr(i,wordlen)]--;
i+=wordlen;//对i加速
}
break;
}
}
//正确匹配,由于情况二和三都对i加速了,不可能满足此条件
if(j==i+wordlen*wordnum){
ans.push_back(i);
smap[s.substr(i,wordlen)]--;
i+=wordlen;//i正常前进
}
}
smap.clear();
}
return ans;
}
};