串联所有单词的子串
给定一个字符串 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”]
输出:[]
分析: 最先想起用map先对word中的单词进行全排列 然后去s中找出每一个全排列的子串,但是这种的时间消耗较长。主要是每一个全排列都要在s中从头到尾匹配一遍。后面改进用滑动窗口依次从s中找到目标匹配大小的子串,从子串中依次提取长度固定的字符作为单词去map中查找,如果查找成功该单词数目在map中减一,若匹配不到则该子串不能匹配。时间复杂度o(n*m), n为s的长度,m为words的长度,滑动窗口依次向后移动一位。
class Solution {
public:
vector<int> findSubstring(string s, vector<string>& words) {
vector<int> rst;
if((s == "") || (words.size() == 0)){
return rst;
}
int word_num = words.size();
int word_one = words[0].size();
int window_len = word_one * word_num;
map<string,int> m_map;
map<string,int>::iterator iter;
for(auto word : words){
m_map[word]++;
}
for(int i = 0; i < s.size()-window_len+1; ++i){
map<string,int> t_map = m_map;
string subword = s.substr(i,window_len);
for(int j = 0; j < word_num; ++j){
string word_item = subword.substr(j*word_one,word_one);
if((iter=t_map.find(word_item)) != t_map.end()){
if(iter->second == 1){
t_map.erase(word_item);
}else{
--(iter->second);
}
}else{
break;
}
}
if(t_map.empty()){
rst.push_back(i);
}
}
return rst;
}
};
滑动窗口(双指针)伪代码
遇到子串问题,首先想到的就是滑动窗口技巧。滑动窗口算法的思路是这样:
-
我们在字符串 S 中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引闭区间 [left, right] 称为一个「窗口」。
-
我们先不断地增加 right 指针扩大窗口 [left, right],直到窗口中的字符串符合要求(包含了 T 中的所有字符)。
-
此时,我们停止增加 right,转而不断增加 left 指针缩小窗口 [left, right],直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同时,每次增加 left,我们都要更新一轮结果。
-
重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。
以总结出滑动窗口算法的抽象思想:
int left = 0, right = 0;
while (right < s.size()) {
window.add(s[right]);
while (valid) {
window.remove(s[left]);
left++;
}
right++;
}
最小覆盖子串
给你一个字符串 S、一个字符串 T,请在字符串 S 里面找出:包含 T 所有字母的最小子串。说明:如果 S 中不存这样的子串,则返回空字符串 “”。如果 S 中存在这样的子串,我们保证它是唯一的答案。
示例:
输入: S = “ADOBECODEBANC”, T = “ABC”
输出: “BANC”
分析1: 依次从s中往后查找,若该下标字符在map中匹配,如果查找成功该单词数目在map中减一,若匹配不到依次向后查找直到字符串结束。这种方法在字符串s较长的时候时间消耗大,时间复杂度o(n^2),在leetcode中最后几个测试用例会超时。该算法需改进。
class Solution {
public:
string minWindow(string s, string t) {
if (s == ""||t=="")
return "";
int length = t.size();
map<char, int> m_map;
map<char, int>::iterator iter;
bool find = false;
for (auto word : t)
m_map[word]++;
int min = INT_MAX;
int indexleft = 0;
for (int i = 0; i < s.size()-length+1; ++i) {
map<char, int> t_map = m_map;
int tlength = length;
for (int j = i; j < s.size()&&tlength<=s.size()-j+1; ++j) {
if ((iter = t_map.find(s[j])) != t_map.end()) {
tlength--;
if (iter->second == 1)
t_map.erase(s[j]);
else
--(iter->second);
}
if (tlength==0) {
if (j - i + 1 < min) {
min = j - i + 1;
indexleft = i;
}
find = true;
break;
}
}
if (find == false)
break;
}
return min==INT_MAX?"":s.substr(indexleft,min);
}
};
分析2: 滑动窗口,这个算法的时间复杂度是 O(M + N),M 和 N 分别是字符串 S 和 T 的长度。因为我们先用 for 循环遍历了字符串 T 来初始化 needs,时间 O(N),之后的两个while 循环最多执行2M 次,时间 O(M)。while 执行的次数就是双指针 left 和 right 走的总路程,最多是 2M 。
滑动窗口算法的思路是这样:
-
我们在字符串 S 中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引闭区间 [left, right] 称为一个「窗口」。
-
我们先不断地增加 right 指针扩大窗口 [left, right],直到窗口中的字符串符合要求(包含了 T 中的所有字符)。
-
此时,我们停止增加 right,转而不断增加 left 指针缩小窗口 [left, right],直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同时,每次增加 left,我们都要更新一轮结果。
-
重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。
class Solution {
public:
string minWindow(string s, string t) {
// 记录最短子串的开始位置和长度
int start = 0, minLen = INT_MAX;
int left = 0, right = 0;
unordered_map<char, int> window;
unordered_map<char, int> needs;
for (char c : t) needs[c]++;
int match = 0;
while (right < s.size()) {
char c1 = s[right];
if (needs.count(c1)) {
window[c1]++;
if (window[c1] == needs[c1])
match++;
}
while (match == needs.size()) {
if (right - left+1 < minLen) {
// 更新最小子串的位置和长度
start = left;
minLen = right - left+1;
}
char c2 = s[left];
if (needs.count(c2)) {
window[c2]--;
if (window[c2] < needs[c2])
match--;
}
left++;
}
right++;
}
return minLen == INT_MAX ?"" : s.substr(start, minLen);
}
};
找到字符串中所有字母异位词
给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。字符串只包含小写英文字母,并且字符串 s 和 p 的长度都不超过 20100。说明:字母异位词指字母相同,但排列不同的字符串。不考虑答案输出的顺序。
示例 :
输入: s: “cbaebabacd” p: “abc”
输出: [0, 6]
解释:起始索引等于 0 的子串是 “cba”, 它是 “abc” 的字母异位词。起始索引等于 6 的子串是 “bac”, 它是 “abc” 的字母异位词。
分析: 滑动窗口
vector<int> findAnagrams(string s, string t) {
// 用数组记录答案
vector<int> res;
int left = 0, right = 0;
unordered_map<char, int> needs;
unordered_map<char, int> window;
for (char c : t) needs[c]++;
int match = 0;
while (right < s.size()) {
char c1 = s[right];
if (needs.count(c1)) {
window[c1]++;
if (window[c1] == needs[c1])
match++;
}
while (match == needs.size()) {
// 如果 window 的大小合适
// 就把起始索引 left 加入结果
if (right - left+1 == t.size()) {
res.push_back(left);
}
char c2 = s[left];
if (needs.count(c2)) {
window[c2]--;
if (window[c2] < needs[c2])
match--;
}
left++;
}
++right;
}
return res;
}
无重复字符的最长子串
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 :输入: “abcabcbb” 输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
分析: 滑动窗口
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int left = 0, right = 0;
unordered_map<char, int> window;
int res = 0; // 记录最长长度
while (right < s.size()) {
char c1 = s[right];
window[c1]++;
// 如果 window 中出现重复字符
// 开始移动 left 缩小窗口
while (window[c1] > 1) {
char c2 = s[left];
window[c2]--;
left++;
}
res = max(res, right - left+1);
++right;
}
return res;
}
};