目录
滑动窗口:
1. 无重复字符的最长子串(中等)
2. 找到字符串中所有字母异位词(中等)
暴力解法
优化
3. 串联所有单词的子串(困难)
暴力解法
优化
4. 最小覆盖子串(困难)
暴力解法
优化
回文串:
1. 验证回文串(简单)
2. 验证回文串 II(简单)
3. 回文子串(中等)
用滑动窗口定位子串,用哈希表记录子串中每个字符出现的次数。
进窗口后判断窗口中有没有重复字符,如果已经有重复字符了,要出窗口,直到没有重复字符。这时就找到了没有重复字符的子串,更新结果。
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int n = s.size();
int ans = 0;
int left = 0;
int right = 0;
vector hash(256, 0); // 下标表示ASCII码值
while (right < n)
{
// 进窗口
hash[s[right]]++;
// 判断窗口中有没有重复字符
while (hash[s[right]] > 1 && left <= right)
{
// 出窗口
hash[s[left]]--;
left++;
}
// 更新结果
ans = max(ans, right - left + 1);
// 更新右边界
right++;
}
return ans;
}
};
字母异位词:
用固定的p.size()大小的窗口定位s的子串,逐个扫描子串中的字母,并把字母在哈希表中对应的值-1,如果子串是p的排列,哈希表中所有的值为0。
class Solution {
public:
vector findAnagrams(string s, string p) {
int nS = s.size();
int nP = p.size();
if (nS < nP)
return {};
vector ans;
vector hash(26, 0); // hash[0]存储'a'出现的次数,hash[1]存储'b'出现的次数……
for (auto& ch : p)
{
hash[ch - 'a']++;
}
int left = 0;
int right = nP - 1;
while (right < nS)
{
vector tmp(hash);
for (int i = left; i <= right; i++)
{
tmp[s[i] - 'a']--;
}
if (areAllZero(tmp))
{
ans.push_back(left);
}
left++;
right++;
}
return ans;
}
private:
bool areAllZero(vector& v)
{
for (auto& i : v)
{
if (i)
return false;
}
return true;
}
};
用hash1记录字符串p中每个字母出现的次数,hash2记录字符串s的窗口中每个字母出现的次数。
用count记录窗口中有效字母的个数。窗口中的某一字母,它在窗口中出现的次数不能比它在p中出现的次数多,这就是有效字母。
进窗口的同时要维护count,然后判断窗口的大小是否> p.size(),如果已经> p.size()了,要出窗口并维护count,直到窗口的大小== p.size()。这时只能说明窗口的大小是合法的,不能说明窗口定位的子串一定是p的异位词。所以,接下来要判断有效字母的个数是否== p.size(),如果相等,说明找到了字母异位词,更新结果。
class Solution {
public:
vector findAnagrams(string s, string p) {
int nS = s.size();
int nP = p.size();
if (nS < nP)
return {};
vector ans;
vector hash1(26, 0); // 记录p中的字母出现的次数
vector hash2(26, 0); // 记录窗口中的字母出现的次数
for (auto& ch : p)
{
hash1[ch - 'a']++;
}
int left = 0;
int right = 0;
int count = 0;
while (right < nS)
{
// 进窗口+维护count
char in = s[right];
if (++hash2[in - 'a'] <= hash1[in - 'a']) // 判断有效字母
{
count++;
}
// 判断窗口大小是否>p.size()
if (right - left + 1 > nP)
{
// 出窗口+维护count
char out = s[left++];
if (hash2[out - 'a']-- <= hash1[out - 'a']) // 判断有效字母
{
count--;
}
}
// 有效字母的个数==p.size(),说明找到了字母异位词,更新结果
if (count == nP)
{
ans.push_back(left);
}
// 更新右边界
right++;
}
return ans;
}
};
和“找到字符串中所有字母异位词”类似。区别在于:
用固定的words.size() * len大小的窗口定位s的子串,逐个扫描子串中的单词,并把单词在哈希表中对应的值-1,如果子串是words中的单词的排列,哈希表中所有的值为0。
暴力解法会超时,但是代码本身没有问题。
class Solution {
public:
vector findSubstring(string s, vector& words) {
int nS = s.size(); // 字符串s的长度
int nW = words.size(); // 数组words中单词的个数
int len = words[0].size(); // 一个单词的长度
if (nS < nW * len)
return {};
vector ans;
unordered_map hash; // 记录words中单词出现的次数
for (auto& s : words)
{
hash[s]++;
}
// 滑动窗口执行len次
for (int i = 0; i < len; i++)
{
int left = i;
int right = i + nW * len - 1;
while (right < nS)
{
unordered_map tmp(hash);
for (int i = left; i <= right - len + 1; i += len)
{
tmp[s.substr(i, len)]--;
}
if (areAllZero(tmp))
{
ans.push_back(left);
}
left += len;
right += len;
}
}
return ans;
}
private:
bool areAllZero(unordered_map ump)
{
for (auto& e : ump)
{
if (e.second)
return false;
}
return true;
}
};
用hash1记录数组words中每个单词出现的次数,hash2记录字符串s的窗口中每个单词出现的次数。
用count记录窗口中有效单词的个数。窗口中的某一单词,它在窗口中出现的次数不能比它在words中出现的次数多,这就是有效单词。
进窗口的同时要维护count,然后判断窗口的大小是否> words.size() * len,如果已经> words.size() * len了,要出窗口并维护count,直到窗口的大小== words.size() * len。这时只能说明窗口的大小是合法的,不能说明窗口定位的子串一定是串联子串。所以,接下来要判断有效单词的个数是否== words.size(),如果相等,说明找到了串联子串,更新结果。
class Solution {
public:
vector findSubstring(string s, vector& words) {
int nS = s.size(); // 字符串s的长度
int nW = words.size(); // 数组words中单词的个数
int len = words[0].size(); // 一个单词的长度
if (nS < nW * len)
return {};
vector ans;
unordered_map hash1; // 记录words中单词出现的次数
for (auto& s : words)
{
hash1[s]++;
}
// 滑动窗口执行len次
for (int i = 0; i < len; i++)
{
unordered_map hash2; // 记录窗口中单词出现的次数
int left = i;
int right = i;
int count = 0;
while (right + len - 1 < nS)
{
// 进窗口+维护count
string in = s.substr(right, len);
hash2[in]++;
if (hash1.count(in) && hash2[in] <= hash1[in]) // 判断有效单词
{
count++;
}
// 判断窗口大小是否大于words.size()*len
if (right + len - left > nW * len)
{
// 出窗口+维护count
string out = s.substr(left, len);
if (hash1.count(out) && hash2[out] <= hash1[out]) // 判断有效单词
{
count--;
}
hash2[out]--;
left += len;
}
// 有效单词的个数==words.size(),说明找到了串联子串,更新结果
if (count == nW)
{
ans.push_back(left);
}
// 更新右边界
right += len;
}
}
return ans;
}
};
用哈希表记录字符串t每个字符出现的次数。用滑动窗口定位s的子串,逐个扫描子串中的字符,并把字符在哈希表中对应的值-1。
如果哈希表中有值>0,说明子串没包含t的所有字符,则让右边界往右滑动,直到全部包含t的字符(哈希表中最终所有值都<=0)。
如果子串包含t的所有字符,则让左边界往右滑动,然后判断删除子串最左边的字符后是否仍然包含t的所有字符。
class Solution {
public:
string minWindow(string s, string t) {
int nS = s.size();
int nT = t.size();
if (nS < nT)
return "";
vector hash(256, 0); // 下标表示ASCII码值
for (auto& ch : t)
{
hash[ch]++;
}
int left = 0;
int right = 0;
int min = INT_MAX; // 最小子串长度
int begin = -1; // 最小子串起始位置
while (right < nS)
{
hash[s[right]]--;
while (areAllNotMoreThanZero(hash)) // 子串包括t的所有字符
{
if (right - left + 1 < min)
{
min = right - left + 1;
begin = left;
}
hash[s[left]]++;
left++;
}
right++;
}
if (begin == -1)
return "";
else
return s.substr(begin, min);
}
private:
bool areAllNotMoreThanZero(vector& v)
{
for (auto& i : v)
{
if (i > 0)
return false;
}
return true;
}
};
用hash1记录字符串t中每个字符出现的次数,hash2记录字符串s的窗口中每个字符出现的次数。
用kind记录t中的字符的种类。用count记录窗口中有效字符的种类。注意,是种类,和之前的题不一样!窗口中的某一字符,只有它在窗口中出现的次数 == 它在t中出现的次数时,才记录。
例如,
t = "ABCCC",s = "ECDBACBCBD"
假设,红色表示的是左边界,蓝色表示的是右边界。现在正在进窗口:
ECDBACBCBD,E在窗口中出现1次,在t中出现0次
ECDBACBCBD,C在窗口中出现1次,在t中出现3次
ECDBACBCBD,D在窗口中出现1次,在t中出现0次。
ECDBACBCBD,B在窗口中出现1次,在t中出现1次,count++
ECDBACBCBD,A在窗口中出现1次,在t中出现1次,count++
ECDBACBCBD,C在窗口中出现2次,在t中出现3次
ECDBACBCBD,B在窗口中出现2次,在t中出现1次
ECDBACBCBD,C在窗口中出现3次,在t中出现3次,count++
此时窗口中有效字符的种类 == 字符串t中字符的种类,显然"ECDBACBC"涵盖了"ABCCC"的所有字符。
进窗口的同时要维护count,然后判断count是否== kind,如果相等,说明找到了覆盖子串,更新结果,然后出窗口并维护count,直到count < kind。
class Solution {
public:
string minWindow(string s, string t) {
int nS = s.size();
int nT = t.size();
if (nS < nT)
return "";
vector hash1(256, 0); // 记录t中的字符出现的次数
vector hash2(256, 0); // 记录窗口中的字符出现的次数
int kind = 0; // 记录t中的字符的种类
for (auto& ch : t)
{
if (hash1[ch]++ == 0)
{
kind++;
}
}
int left = 0;
int right = 0;
int count = 0;
int min = INT_MAX; // 最小子串长度
int begin = -1; // 最小子串起始位置
while (right < nS)
{
// 进窗口+维护count
char in = s[right];
if (++hash2[in] == hash1[in])
{
count++;
}
// 判断count是否== kind
while (count == kind)
{
// 找到了覆盖子串,更新结果
if (right - left + 1 < min)
{
min = right - left + 1;
begin = left;
}
// 出窗口+维护count
char out = s[left++];
if (hash2[out]-- == hash1[out])
{
count--;
}
}
right++;
}
if (begin == -1)
return "";
else
return s.substr(begin, min);
}
};
从两端向里逐个比较,跳过非字母数字字符,如果出现了不同的字符,则不是回文串。
class Solution {
public:
bool isPalindrome(string s) {
int left = 0; // 首指针
int right = s.size() - 1; // 尾指针
while (left < right)
{
while (!isalnum(s[left]) && left < right)
{
left++;
}
while (!isalnum(s[right]) && left < right)
{
right--;
}
int chLeft = tolower(s[left]);
int chRight = tolower(s[right]);
if (chLeft != chRight)
{
return false;
}
left++;
right--;
}
return true;
}
};
从两端向里逐个比较,如果字符不相同,判断删除这两个字符的其中一个能否形成回文。
class Solution {
public:
bool validPalindrome(string s) {
int left = 0; // 首指针
int right = s.size() - 1; // 尾指针
while (left < right)
{
if (s[left] != s[right])
{
break;
}
left++;
right--;
}
return left == s.size() / 2 || isPalindrome(s, left, right - 1) || isPalindrome(s, left + 1, right);
}
private:
bool isPalindrome(string s, int start, int end)
{
while (start < end)
{
if (s[start] != s[end])
{
return false;
}
start++;
end--;
}
return true;
}
};
枚举所有可能的回文中心,向两边拓展,形成回文子字符串。
回文长度是奇数,回文中心是一个字符;回文长度是偶数,回文中心是两个字符。
假设字符串长度为4,枚举所有可能的回文中心:
"abcd"
0123
编号i | 回文中心左起始位置li | 回文中心右起始位置ri |
---|---|---|
0 | 0 | 0 |
1 | 0 | 1 |
2 | 1 | 1 |
3 | 1 | 2 |
4 | 2 | 2 |
5 | 2 | 3 |
6 | 3 | 3 |
可以看出li = i/2,ri = i/2 + i % 2,长度为n的字符串,一共有2n-1个可能的回文中心。
class Solution {
public:
int countSubstrings(string s) {
int n = s.size();
int ans = 0;
for (int i = 0; i < 2 * n - 1; i++)
{
int left = i / 2;
int right = i / 2 + i % 2;
while (left >= 0 && right < n && s[left] == s[right])
{
left--;
right++;
ans++;
}
}
return ans;
}
};