题目描述:
给定一个字符串 s 和一些 长度相同 的单词 words 。找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置。
注意子串要与 words 中的单词完全匹配,中间不能有其他字符 ,但不需要考虑 words 中单词串联的顺序。
示例:
示例 1:
输入:s = "barfoothefoobarman", words = ["foo","bar"]
输出:[0,9]
解释:
从索引 0 和 9 开始的子串分别是 "barfoo" 和 "foobar" 。
输出的顺序不重要, [9,0] 也是有效答案。
示例 2:
输入:s = "wordgoodgoodgoodbestword", words = ["word","good","best","word"]
输出:[]
示例 3:
输入:s = "barfoofoobarthefoobarman", words = ["bar","foo","the"]
输出:[6,9,12]
解析:
这道题虽然属于难题,但实际上只是由多个步骤叠加起来而已,现在先逐步分析。
(1)子串要与 words 中的单词完全匹配,中间不能有其他字符。
关于这点,我们完全可以当做是一个长度为words总长相同的滑动窗口在s上滑动,然后,再在这个窗口上陆续截取与单个单词长度相同的字符串再判断截取字符串是否属于words中的一员即可。
因为截取的长度与words是相等的,只要截取中的一个字符串不同,就表明截取的字符串无法满足题目要求。
(2)words中部分单词重复
由例2中可以看到,word在words中出现了两次。也就是说,不能单纯判断words某个字符串已经出现在截取的字符串中,而是需要对出现的次数进行计数。
只有截取的字符串可分成所有的单词且每个单词出现的次数与words中的相等才表明截取的该字符串满足要求。
这里就需要用map(字典)了,通过map存储<单词,该单词出现次数>,我们就能解决这一问题。
现在来看粗略代码:
vector<int> findSubstring(string s, vector<string>& words)
{
//当单词长度
int word_size = words[0].size();
vector<int> ans;
//总长度不足可返回
if(s.size()<words.size()*word_size)
{
return ans;
}
//words字典
unordered_map<string, int> my_map;
vector<string> s_map(s.size());
//将words压入字典
for(int i=0;i<words.size();i++)
{
my_map[words[i]]++;
}
//预先记入s中各个长度为word_size的短语
//不计也行,不过后面就需要多次调用substr,需要时间
for(int i=0;i<=s.size()-word_size;i++)
{
s_map[i] = s.substr(i, word_size);
}
unordered_map<string, int> temp;
int count=0;
string ss="";
for(int i=0;i<=s.size()-words.size()*word_size;i++)
{
temp.clear();
count = words.size();
//words有多少个单词就需要读入多少次
for(int j=0;j<words.size();j++)
{
ss = s_map[i + j * word_size];
//string ss=s.substr(i + j * word_size,word_size);
//当这个单词不在words内,表明不连续或者出现次数过多,可以断开
if(!my_map[ss])
{
break;
}
else
{
//使用count是可以简化temp中所有单词和words的进行对比
temp[ss]++;
if(temp[ss]<=my_map[ss])
{
count--;
}
else
{
break;
}
}
}
if(!count)
{
ans.push_back(i);
}
}
return ans;
}
这样虽能过,但最终得出的结果是300ms左右,显然这很不自然(刷题的时候但凡题目表示用时超过2位数就表明不是最佳答案)。因此我们可以再分析超时原因。
我们可以看到每次截取字符串s[a : b]时得到的结果,都需要重头到脚扫描一遍,但其结果与字符串s[a-单词长度 : b-单词长度]差不多,可视作字符串s[a-单词长度 : b-单词长度]去掉前面的一个单词后又再尾部接上了一个单词。
因此,为了计数方便,我们在s进行窗口滑动时,直接位移一个单词长度的距离,并减去头一个单词的计数,且有:
1-1:当读入的新的单词是属于words且总长度不足且该单词出现的次数不大于应该出现的次数时,计数
1-2:当读入的新的单词是属于words且总长度相同时,则表示当前截取字符串符合要求,输出头部位置
1-3:当读入的新的单词是属于words且总长度不足但该单词出现的次数大于应该出现的次数时时,则表示当前截取字符串部分符合要求,应该让头部前进几个单词长度,并修改对应记录,直到新加入的单词计数合理为止
2-1:当读入的新的单词不属于words时,表明截取的字符串到该单词尾部都无法符合要求的组合,此时可以直接从单词的尾部开始重新计数。
代码:
vector<int> findSubstring(string s, vector<string>& words)
{
int s_length = s.length();
//需要的单词总数、一个单词长度,所需总长度
int words_size = words.size(), word_length = words[0].length(),
sum_length=words_size*word_length;
vector<int> ans;
if(s_length< sum_length)
{
return ans;
}
//建造words字典,临时字典
unordered_map<string, int> word_map, temp;
for(string word:words)
{
word_map[word]++;
}
//根据word长度,实际上就是余数的情况,做分组
for(int i=0;i<word_length;i++)
{
//记录截取字符串首尾
int head = i;
int tail = i;
//计数
int count = 0;
while (head+sum_length<=s_length)
{
//获取单词与更新尾部
string ss = s.substr(tail, word_length);
tail += word_length;
//当这个单词不存在,直接查看下一个单词
//小技巧:当判断一个元素是否存在,用map.count()比较好,
//因为直接看map[]的话,如果不存在还需要花时间创建
if(!word_map.count(ss))
{
//更新头部位置,单词计数,
head = tail;
count = 0;
temp.clear();
}
else
{
//计数
temp[ss]++;
count++;
//当这个单词计数不大于需要计数时,才增加
if(temp[ss]<=word_map[ss])
{
//当数目合格后,将头部加入并后移一个单位,且更新计数
if(count==words_size)
{
ans.push_back(head);
temp[s.substr(head, word_length)]--;
head += word_length;
count--;
}
}
//当过多,则应该让前段移动到该计数合格为止
else
{
while (temp[ss]>word_map[ss])
{
temp[s.substr(head,word_length)]--;
head += word_length;
count--;
}
}
}
}
//新的情况要清空表
temp.clear();
}
return ans;
}