此道题目,我一开始用的解法是暴力法,通过两个map来判断符合条件的子串。时间复杂度为O(NM),其中N为字符串s的长度,M为words数组中字符串的个数。需要注意的一点是,在实际的运用中,unordered_map因为内存管理的问题,效率往往不如map高。我写的代码效率不太高。所以在这里只贴一下别人的代码,转自http://www.cnblogs.com/TenosDoIt/p/3807055.html。
暴力解法
从字符串s的每个位置都判断一次(如果从当前位置开始到s结束的子串长度小于words中所有单词长度,不用判断)。算法判断从当前位置开始的子串的前面那部分能不能由words里面的单词拼接而成。从某一个位置 i 判断时,依次判断单词s[i,i+L-1],s[i+L,i+2L-1],s[i+2L, i+3L-1]...是否在words中,如果单词在words中,就从words中删除该单词。我们用一个hash map来保存单词的出现次数,这样可以在O(1)时间内判断单词是否在words中。算法的时间复杂度是O(NM),N是字符串s的长度,M是单词的个数。注意,在主程序中,代码只定义了一个hash map。那么一个hash map,是如何起到两个hash map的作用的呢?
递归代码:
<pre name="code" class="cpp">class Solution {
private:
int wordLen;
public:
vector<int> findSubstring(string S, vector<string> &L) {
unordered_map<string, int>wordTimes;
for(int i = 0; i < L.size(); i++)
if(wordTimes.count(L[i]) == 0)
wordTimes.insert(make_pair(L[i], 1));
else wordTimes[L[i]]++;
wordLen = L[0].size();
vector<int> res;
for(int i = 0; i <= (int)(S.size()-L.size()*wordLen); i++)
if(helper(S, i, wordTimes, L.size()))
res.push_back(i);
return res;
}
//判断子串s[index...]的前段是否能由L中的单词组合而成
bool helper(string &s, const int index,
unordered_map<string, int>&wordTimes, const int wordNum)
{
if(wordNum == 0)return true;
string firstWord = s.substr(index, wordLen);
unordered_map<string, int>::iterator ite = wordTimes.find(firstWord);
if(ite != wordTimes.end() && ite->second > 0)
{
(ite->second)--;
bool res = helper(s, index+wordLen, wordTimes, wordNum-1);
(ite->second)++; //恢复hash map的状态。在代码中,正是通过递归使一个hash map起到了两个hash map的作用
return res;
}
else return false;
}
};
非递归代码
非递归代码的时间复杂度要大于递归代码,这是因为在非递归的helper函数中,hash map参数是以值的方式进行传递的,每次调用都要拷贝一次hash map,而递归代码中一直只存在一个hash map对象。代码如下:
class Solution {
private:
int wordLen;
public:
vector<int> findSubstring(string S, vector<string> &L) {
unordered_map<string, int>wordTimes;
for(int i = 0; i < L.size(); i++)
if(wordTimes.count(L[i]) == 0)
wordTimes.insert(make_pair(L[i], 1));
else wordTimes[L[i]]++;
wordLen = L[0].size();
vector<int> res;
for(int i = 0; i <= (int)(S.size()-L.size()*wordLen); i++)
if(helper(S, i, wordTimes, L.size()))
res.push_back(i);
return res;
}
//判断子串s[index...]的前段是否能由L中的单词组合而成
bool helper(const string &s, int index,
unordered_map<string, int>wordTimes, int wordNum) // 注意wordTimes是按值传递
{
for(int i = index; wordNum != 0 && i <= (int)s.size()-wordLen; i+=wordLen)
{
string word = s.substr(i, wordLen);
unordered_map<string, int>::iterator ite = wordTimes.find(word);
if(ite != wordTimes.end() && ite->second > 0)
{ite->second--; wordNum--;}
else return false;
}
if(wordNum == 0)return true;
else return false;
}
};
滑动窗口解法
回想LeetCode其他的题目:LeetCode:Longest Substring Without Repeating Characters 和 LeetCode:Minimum Window Substring ,都用了一种滑动窗口的方法。这一题也可以利用相同的思想。因为在本题中单词都是定长的,所以可以把每个单词当成单个字符来看待。假设字符串s的长度为N,words中单词的长度为L。因为不是一个字符,所以我们需要对字符串s中的所有长度为L的子串进行判断。做法是i从0到L-1个字符开始,得到开始index分别为i, i+L, i+2L, ...的长度为L的单词。这样就可以保证判断到所有的满足条件的子串。因为每次扫描的时间复杂度是O(2N/L)(每个单词不会被访问多于两次,一次是窗口右端,一次是窗口左端),总共扫描L次(i=0, ..., L-1),所以总复杂度是O(2N/L*L)=O(N),是一个线性算法。空间复杂度是words的大小,即O(M*L),其中M是words中的单词数量。
比如s = “a1b2c3a1d4” L={“a1”,“b2”,“c3”,“d4”}
窗口最开始为空,
a1在L中,加入窗口 【a1】b2c3a1d4
b2在L中,加入窗口 【a1b2】c3a1d4
c3在L中,加入窗口 【a1b2c3】a1d4
a1在L中,但是前面a1已经算了一次,再算一次的话,窗口里的a1就超了,因此此时只需要把窗口向右移动一个单词a1【b2c3a1】d4
d4在L中,加入窗口a1【b2c3a1d4】找到了一个匹配
如果把s改为“a1b2c3kka1d4”,那么在第四步中会碰到单词kk,kk不在L中,此时窗口起始位置移动到kk后面a1b2c3kk【a1d4。
注意之所以代码没有遗漏情形,是因为第一层循环为单词的长度!!!!
代码如下:
class Solution {
public:
vector<int> findSubstring(string S, vector<string> &L) {
unordered_map<string, int>wordTimes;//L中单词出现的次数
for(int i = 0; i < L.size(); i++)
if(wordTimes.count(L[i]) == 0)
wordTimes.insert(make_pair(L[i], 1));
else wordTimes[L[i]]++;
int wordLen = L[0].size();
vector<int> res;
for(int i = 0; i < wordLen; i++)
{//为了不遗漏从s的每一个位置开始的子串,第一层循环为单词的长度
unordered_map<string, int>wordTimes2;//当前窗口中单词出现的次数
int winStart = i, cnt = 0;//winStart为窗口起始位置,cnt为当前窗口中的单词数目
for(int winEnd = i; winEnd <= (int)S.size()-wordLen; winEnd+=wordLen)
{//窗口为[winStart,winEnd)
string word = S.substr(winEnd, wordLen);
if(wordTimes.find(word) != wordTimes.end())
{
if(wordTimes2.find(word) == wordTimes2.end())
wordTimes2[word] = 1;
else wordTimes2[word]++;
if(wordTimes2[word] <= wordTimes[word])
cnt++;
else
{//当前的单词在L中,但是它已经在窗口中出现了相应的次数,不应该加入窗口
//此时,应该把窗口起始位置想左移动到,该单词第一次出现的位置的下一个单词位置
for(int k = winStart; ; k += wordLen)
{
string tmpstr = S.substr(k, wordLen);
wordTimes2[tmpstr]--;
if(tmpstr == word)
{
winStart = k + wordLen;
break;
}
cnt--;
}
}
if(cnt == L.size())
res.push_back(winStart);
}
else
{//发现不在L中的单词
winStart = winEnd + wordLen;
wordTimes2.clear();
cnt = 0;
}
}
}
return res;
}