1 解题思路
子数组以及子字符串(子串)就是连续的序列。既然是连续,常使用到的方法就是使用滑动窗口,滑动窗口的滑动条件就是题目的要求,滑动条件可以借助有序的set、multiset或者无序的unordered_set等来实现。
对于子串的最值问题的求解常常会使用动态规划的思想,找出状态转移方程是关键。
滑动窗口参考链接:CSDN
对比子序列的题目:CSDN
2 子数组题目
2.1 最大连续1的个数
class Solution {
public:
int longestOnes(vector<int>& nums, int k) {
int ones = 0;
vector<int> counter(2,0);
queue<int> q;
for(auto& i : nums)
{
q.push(i);
counter[i]++;
if (counter[0] <= k)
ones = std::max(ones, counter[0]+counter[1]);
else
{
while (counter[0] > k)
{
counter[q.front()]--;
q.pop();
}
}
}
return ones;
}
};
2.2 绝对差不超过限制的最长连续子数组
//链接:https://leetcode-cn.com/problems/longest-continuous-subarray-with-absolute-diff-less-than-or-equal-to-limit/solution/jue-dui-chai-bu-chao-guo-xian-zhi-de-zui-5bki/
class Solution {
public:
int longestSubarray(vector<int>& nums, int limit) {
//数组中可能存在重复的数据,所以使用multiset
multiset<int> s;
int n = nums.size();
int left = 0, right = 0;
int ret = 0;
while (right < n)
{
s.insert(nums[right]);
while (*s.rbegin() - *s.begin() > limit)
{
s.erase(s.find(nums[left++]));
}
ret = max(ret, right - left + 1);
right++;
}
return ret;
}
};
2.3 滑动窗口的中位数
class Solution {
public:
vector<double> medianSlidingWindow(vector<int>& nums, int k) {
vector<double> r;
multiset<int> s;//因为需要排序且可能存在重复的数据,所以选择multiset
int left = 0;
for(auto& i : nums)
{
s.insert(i);
if (s.size() < k)
{
continue;
}
else if (s.size() > k)
{
//这里是关键,题目https://leetcode-cn.com/problems/longest-continuous-subarray-with-absolute-diff-less-than-or-equal-to-limit/ 与此操作类似
s.erase(s.find(nums[left++]));
}
auto it = s.begin();
double mid = 0.0;
if (k & 0x01 == 1)
{
std::advance(it, k/2);
mid = *it;
}
else
{
std::advance(it, k/2-1);
mid += *it;
std::advance(it, 1);
mid += *it;
mid /= 2;
}
r.push_back(mid);
}
return r;
}
};
2.4 最长连续递增序列
//使用滑动窗口
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
int maxLen = 1, left = 0, right = 0;
for (;right < nums.size();++right)
{
if (right > 0)
{
if (nums[right] > nums[right-1])
maxLen = std::max(maxLen, right-left+1);
else
left = right;
}
}
return maxLen;
}
};
2.5 最长重复子数组
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
int n = nums1.size(), m = nums2.size();
if (n*m == 0)
return 0;
//dp[i][j]:nums1的前i个元素和nums2的前j个元素的最长公共子数组的长度
vector<vector<int>> dp(n+1, vector<int>(m+1,0));
//base case 上面的默认值已经进行了初始化,这里不用再进行单独初始化了
// for (int i=0;i<=n;++i)
// dp[i][0] = 0;
// for (int i=0;i<=m;++i)
// dp[0][i] = 0;
int maxLen = 0;
for (int i=1;i<=n;++i)
{
for (int j=1;j<=m;++j)
{
if (nums1[i-1] == nums2[j-1])
{
dp[i][j] = 1 + dp[i-1][j-1];
maxLen = std::max(maxLen, dp[i][j]);
}
}
}
return maxLen;
}
};
2.6 最大子数组和
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int pre = nums[0], max = pre;
for(int i=1;i < nums.size();i++){
pre = std::max(pre + nums[i],nums[i]);
max = std::max(max,pre);
}
return max;
}
};
2.7
2.8
2.9
2.10
2.11
3 子字符串(也就是子串)
3.1 最长不含重复字符的子字符串
剑指 Offer II 016. 不含重复字符的最长子字符串
class Solution {
public:
int lengthOfLongestSubstring(string s) {
unordered_set<char> w;
int left = 0, right = 0, len = 0;
while(right < s.length())
{
char c = s[right];
if (w.count(c))
{
len = std::max(len, right - left);
w.erase(s[left++]);
continue;
}
else
{
w.insert(c);
}
++right;
}
len = std::max(len, right - left);
return len;
}
};
动态规划的解答:
class Solution {
public:
int lengthOfLongestSubstring(string s) {
vector<int> dp(128,-1);//存储每个字符最后出现的位置
int i=0,j=0,res=0;
for(;j<s.size();j++)
{
if(dp[s[j]]<i)//前面的子串不含新加的字符
res=max(res,j-i+1);
else//当前字符在之前的子串中出现过
i=dp[s[j]]+1;//更新i,使得i到j没有重复字符
dp[s[j]]=j;//更改当前字符出现的位置
}
return res;
}
};
3.2 最小覆盖子串
//从leetcode 567 https://leetcode-cn.com/problems/permutation-in-string/ 中的方法二修改而来
//因为方法二不要求字符串中全部是小写字母
class Solution {
public:
string minWindow(string s, string t) {
//方法一
string result;
unordered_map<char, int> need, window;
for (int i = 0;i<t.size();++i)
{
need[t[i]]++;
}
int left = 0, right = 0;
int valid = 0, len = INT_MAX;
while (right < s.size())
{
// c 是将移⼊窗⼝的字符
char c = s[right];
// 进⾏窗⼝内数据的⼀系列更新
if (need.count(c))
{
window[c]++;
//因为need中可能含有重复字符
if (window[c] == need[c])
{
valid++;
}
}
//这里的right还没右移(右移操作在外层while循环的最后一步),
//正是由于最后一个right字符导致valid==need.size(),所以滑动窗口的范围是[left, right],
//所以有效的字符串的长度就是:right-left+1
if (valid == need.size())
{
// s子串的长度与目标串相等,则肯定是s中最短的满足条件的子串
if (right-left+1 == t.size())
{
result = s.substr(left,right-left+1);
return result;
}
if (right-left+1 < len)
{
len = right-left+1;
result = s.substr(left,len);
}
}
// 判断左侧窗⼝是否要收缩
//当 valid == need.size() 时, 说明 T 中所有字符已经被覆盖, 已经得到
//一个可行的覆盖字串,现在应该开始收缩窗口了, 以便得到「最小覆盖字串」
while (valid == need.size())
{
// d 是将移出窗⼝的字符
char d = s[left];
// 进⾏窗⼝内数据的⼀系列更新
if (need.count(d))
{
if (window[d] == need[d])
{
//走到这里,说明left指向的是有效子串,
//正是由于最后一个right字符导致valid==need.size(),所以滑动窗口的范围是[left, right],
//所以有效的字符串的长度就是:right-left+1
// s子串的长度与目标串相等,则肯定是s中最短的满足条件的子串
if (right-left+1 == t.size())
{
result = s.substr(left,right-left+1);
return result;
}
if (right-left+1 < len)
{
len = right-left+1;
result = s.substr(left,len);
}
valid--;
}
window[d]--;
}
//从左侧收缩窗⼝
left++;
}
//从右侧扩展窗口
right++;
}
return result;
}
};
3.3 字符串的排列
/*
滑动窗口
由于排列不会改变字符串中每个字符的个数,所以只有当两个字符串每个字符的个数均相等时,一个字符串才是另一个字符串的排列。
根据这一性质,记 s1 的长度为 n,我们可以遍历 s2 中的每个长度为 n 的子串,判断子串和 s1
中每个字符的个数是否相等,若相等则说明该子串是 s1 的一个排列。
使用两个数组 cnt1 和 cnt2, cnt1 统计 s1 中各个字符的个数,cnt2统计当前遍历的子串中各个字符的个数。
由于需要遍历的子串长度均为 n,我们可以使用一个固定长度为n 的滑动窗口来维护 cnt2 :
滑动窗口每向右滑动一次,就多统计一次进入窗口的字符,少统计一次离开窗口的字符。
然后,判断 cnt1 是否与 cnt2相等,若相等则意味着 s1 的排列之一是 s2 的子串。
https://leetcode-cn.com/problems/permutation-in-string/solution/zi-fu-chuan-de-pai-lie-by-leetcode-solut-7k7u/
*/
class Solution {
public:
bool checkInclusion(string s1, string s2) {
//方法一
// int n = s1.length(), m = s2.length();
// if (n > m) {
// return false;
// }
// vector<int> cnt1(26), cnt2(26);
// for (int i = 0; i < n; ++i) {
// ++cnt1[s1[i] - 'a'];
// ++cnt2[s2[i] - 'a'];
// }
// if (cnt1 == cnt2) {
// return true;
// }
// for (int i = n; i < m; ++i) {
// ++cnt2[s2[i] - 'a'];
// --cnt2[s2[i - n] - 'a'];
// if (cnt1 == cnt2) {
// return true;
// }
// }
// return false;
//方法二,速度稍低于方法一
unordered_map<char, int> need, window;
for (int i = 0;i<s1.size();++i)
{
need[s1[i]]++;
}
int left = 0, right = 0;
int valid = 0;
while (right < s2.size())
{
// c 是将移⼊窗⼝的字符
char c = s2[right];
// 进⾏窗⼝内数据的⼀系列更新
if (need.count(c))
{
window[c]++;
//因为need中可能含有重复字符
if (window[c] == need[c])
{
valid++;
}
}
//这里的right还没右移(右移操作在外层while循环的最后一步),
//正是由于最后一个right字符导致valid==need.size(),所以滑动窗口的范围是[left, right],
//所以有效的字符串的长度就是:right-left+1
if (valid == need.size() && right-left+1 == s1.size())
{
return true;
}
// 判断左侧窗⼝是否要收缩
//当 valid == need.size() 时, 说明 T 中所有字符已经被覆盖, 已经得到
//一个可行的覆盖字串,现在应该开始收缩窗口了, 以便得到「最小覆盖字串」
while (valid == need.size())
{
// d 是将移出窗⼝的字符
char d = s2[left];
// 进⾏窗⼝内数据的⼀系列更新
if (need.count(d))
{
if (window[d] == need[d])
{
//走到这里,说明left指向的是有效子串,
//正是由于最后一个right字符导致valid==need.size(),所以滑动窗口的范围是[left, right],
//所以有效的字符串的长度就是:right-left+1
if (valid == need.size() && right-left+1== s1.size())
{
return true;
}
valid--;
}
window[d]--;
}
//从左侧收缩窗⼝
left++;
}
//从右侧扩展窗口
right++;
}
return false;
}
};
3.4 字符串中的所有变位词
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
vector<int> r;
do
{
int n = p.length(), m = s.length();
if (n > m)
{
break;
}
vector<int> cnt1(26), cnt2(26);
for (int i = 0; i < n; ++i)
{
++cnt1[p[i] - 'a'];
++cnt2[s[i] - 'a'];
}
if (cnt1 == cnt2)
{
r.push_back(0);
}
for (int i = n; i < m; ++i)
{
++cnt2[s[i] - 'a'];
--cnt2[s[i - n] - 'a'];
if (cnt1 == cnt2)
{
r.push_back(i-n+1);
}
}
}while(0);
return r;
}
};
3.5 重复的DNA序列
class Solution {
public:
vector<string> findRepeatedDnaSequences(string s) {
int n = s.length();
unordered_map<string, int> m;
unordered_set<string> st;
for (int i=0;i<=n-10;++i)
{
string str = s.substr(i,10);
if (m.count(str))
st.insert(str);
m[str]++;
}
return vector<string>(st.begin(), st.end());
}
};
3.6 最长重复子串
Rabin-Karp算法《算法导论3rd-p580》
参考链接:
https://leetcode-cn.com/problems/longest-duplicate-substring/solution/wei-rao-li-lun-rabin-karp-er-fen-sou-suo-3c22/
//参考链接:https://leetcode-cn.com/problems/longest-duplicate-substring/solution/wei-rao-li-lun-rabin-karp-er-fen-sou-suo-3c22/
class Solution {
public:
int n;
//选择的是一个素数,相当于Rabin-Karp算法中的31进制,《算法导论3rd-p580》介绍的是十进制
unsigned long long prime = 31;
string longestDupSubstring(string s) {
n = s.size();
int l = 1;
int r = n - 1;
int pos = -1;
int len = 0;
auto find = [&](int len)
{
unsigned long long hash = 0;
unsigned long long power = 1;
//将[0,len)这个len长度的字符串作为模式串,也就是RK算法中所说的P[1...m]《算法导论3rd-p580》
for (int i = 0; i < len; i++)
{
hash = hash * prime + (s[i] - 'a');
power *= prime;
}
unordered_set<unsigned long long> exist{ hash };
for (int i = len; i < n; i++)
{
hash = hash * prime - power * (s[i - len] - 'a') + (s[i] - 'a');
//如果已经存在该hash,则说明存在一个长度为 len 的字符串s.substr(i-len+1, len)等于 s.substr(0, len)
if (exist.count(hash))
return (i - len + 1);
exist.insert(hash);
}
return -1;
};
//二分查找
while (l <= r)
{
//将索引[0,mid)范围内长度为mid字符串作为模式串,然后在[1, n-1](l的初始值为1;r的初始值为n-1)查找是否存在与模式串相同的字符串
int mid = (l + r) / 2;
int start = find(mid);
if (start != -1)
{
//在[1, n-1]内找到与模式串相同的子串,也就是存在重复子串,需要增加模式串的长度(也就是扩展上面的mid)来看是否存在更长的重复子串,要扩展模式串的长度就需要递增左边界
len = mid;
pos = start;
l = mid + 1;
}
else
{
//在[1, n-1]内没有找到与模式串相同的子串,此时需要缩短模式串的长度(也就是缩短上面的mid),要缩短模式串的长度就需要递减右边界
r = mid - 1;
}
}
if (pos == -1)
return "";
else
return s.substr(pos, len);
}
};
3.7 最长回文子串
class Solution {
public:
string longestPalindrome(string s) {
//方法一
// string res;
// for (int i = 0; i < s.size(); i++)
// {
// // 以 s[i] 为中心的最长回文子串
// string s1 = palindrome(s, i, i);
// // 以 s[i] 和 s[i+1] 为中心的最长回文子串
// string s2 = palindrome(s, i, i + 1);
// res = res.size() > s1.size() ? res : s1;
// res = res.size() > s2.size() ? res : s2;
// }
// return res;
//方法二,马拉车
string T = preProcess(s);
int n = T.length();
int *P = new int[n];
int C = 0, R = 0;
int maxLen = 0, maxC = 0;
for (int i = 1; i < n - 1; i++)
{
int i_mirror = 2 * C - i;
if (R > i)
{
P[i] = std::min(R - i, P[i_mirror]);// 防止超出 R
}
else
{
P[i] = 0;// 等于 R 的情况
}
// 碰到之前讲的三种情况时候,需要利用中心扩展法
while (T[i + 1 + P[i]] == T[i - 1 - P[i]])
{
P[i]++;
}
// 判断是否需要更新 R
if (i + P[i] > R)
{
C = i;
R = i + P[i];
}
if (P[i] > maxLen)
{
maxLen = P[i];
maxC = i;
}
}
int start = (maxC - maxLen) / 2; //最开始讲的求原字符串下标
return s.substr(start, maxLen);
}
string palindrome(string& s, int l, int r) {
// 防止索引越界
while (l >= 0 && r < s.size() && s[l] == s[r])
{
// 向两边展开
l--; r++;
}
// 返回以 s[l] 和 s[r] 为中心的回文子串
return s.substr(l + 1, r - l - 1);
}
//原字符串:abcba ===> ^#a#b#c#b#a#$
string preProcess(string s) {
int n = s.length();
if (n == 0)
{
return "^$";
}
string ret = "^";
for (int i = 0; i < n; i++)
{
ret.push_back('#');
ret.push_back(s[i]);
}
ret.append("#$");
return ret;
}
};
3.8 回文子串的数目
class Solution {
public:
int countSubstrings(string s) {
int count = 0;
for (int i = 0; i < s.size(); i++)
{
count += palindrome(s, i, i) + palindrome(s, i, i + 1);
}
return count;
}
int palindrome(string& s, int l, int r) {
int count = 0;
// 防止索引越界
while (l >= 0 && r < s.size() && s[l] == s[r])
{
++count;
// 向两边展开
l--; r++;
}
return count;
}
string palindromeString(string& s, int l, int r) {
// 防止索引越界
while (l >= 0 && r < s.size() && s[l] == s[r])
{
// 向两边展开
l--; r++;
}
return s.substr(l+1,r-l-1);
}
};
3.9 分割回文子串
class Solution {
public:
vector<vector<string>> partition(string s) {
vector<string> path;
vector<vector<string>> result;
helper(s,0,path,result);
return result;
}
// [begin, end]
void helper(const string& str, int begin, vector<string>& path, vector<vector<string>>& result)
{
if (begin == str.size())
{
result.emplace_back(path);
return;
}
for (int end=begin;end<str.size();++end)
{
if (isPalindrome(str,begin,end))
{
path.emplace_back(str.substr(begin,end-begin+1));
helper(str,end+1,path,result);
path.pop_back();
}
}
}
// [begin, end]
bool isPalindrome(const string& str, int begin, int end)
{
if (begin > end)
{
return false;
}
while (begin < end)
{
if (str[begin++] != str[end--])
{
return false;
}
}
return true;
}
};
3.10 最少回文分割
//参考题目:https://leetcode-cn.com/problems/M99OJA/
class Solution {
public:
int minCut(string s) {
//方法一:借鉴 剑指 Offer II 086. 分割回文子字符串,https://leetcode-cn.com/problems/M99OJA/ 超时
// vector<string> path;
// int min = INT_MAX;
// dfs(s,0,path,min);
// return min-1;
//方法二,https://leetcode-cn.com/problems/palindrome-partitioning-ii/solution/fen-ge-hui-wen-chuan-ii-by-leetcode-solu-norx/
// https://leetcode-cn.com/problems/palindrome-partitioning-ii/solution/wei-rao-li-lun-yu-chu-li-dong-tai-gui-hu-akpu/
/********************************************************************************************
PalindromeTable[i][j] 表示 s[i..j] 是否为回文串
dp[i] 表示 s[0..i] 的最小分割
状态转移方程:dp[i] 遍历每一个j(0<=j<i),使得 s[j+1..i] 是回文串, 则 dp[i] = min(dp[i], dp[j] + 1) forall j
********************************************************************************************/
int n = s.size();
//使用了备忘录记录回文字符串的信息,防止重复计算0
vector<vector<bool>> PalindromeTable(n, vector<bool>(n, true));
//PalindromeTable[i][j]表示 s[i..j] 是否为回文串
for (int i = n - 1; i >= 0; --i)
{
for (int j = i + 1; j < n; ++j)
{ //从两边到中间来判断是否是回文
PalindromeTable[i][j] = (s[i] == s[j]) && PalindromeTable[i+1][j-1];
}
}
vector<int> dp(n, INT_MAX);
for (int i = 0; i < n; ++i)
{
//字符串s[0][i]本来就是回文字符串,不需要分割,故 dp[i] = 0
if (PalindromeTable[0][i])
{
dp[i] = 0;
}
else
{ // 0<=j<i, s[j+1..i] 是回文串,则 dp[i] = min(dp[i], dp[j] + 1)
for (int j = 0; j < i; ++j)
{
if (PalindromeTable[j+1][i])
{
dp[i] = min(dp[i], dp[j] + 1);
}
}
}
}
return dp[n-1];
}
// [begin, end]
void dfs(const string& str, int begin, vector<string>& path, int& min)
{
if (begin == str.size())
{
min = min > path.size() ? path.size() : min;
return;
}
for (int end=begin;end<str.size();++end)
{
if (isPalindrome(str,begin,end))
{
path.emplace_back(str.substr(begin,end-begin+1));
dfs(str,end+1,path,min);
path.pop_back();
}
}
}
// [begin, end]
bool isPalindrome(const string& str, int begin, int end)
{
if (begin > end)
{
return false;
}
while (begin < end)
{
if (str[begin++] != str[end--])
{
return false;
}
}
return true;
}
};
3.11 最多删除一个字符得到回文
class Solution {
public:
bool validPalindrome(string s) {
bool isValidPalindrome = false;
int n = s.length(), outBegin = 0, outEnd = 0;
isValidPalindrome = isPalindrome(s,0,n-1,outBegin,outEnd);
if (isValidPalindrome)
{
return true;
}
else
{
int tmp1,tmp2;
isValidPalindrome = (isPalindrome(s,outBegin,outEnd-1,tmp1,tmp2) || isPalindrome(s,outBegin+1,outEnd,tmp1,tmp2));
}
return isValidPalindrome;
}
// [begin, end]
bool isPalindrome(const string& str, int begin, int end, int& outBegin, int& outEnd)
{
if (begin > end)
{
outBegin = begin;
outEnd = end;
return false;
}
while (begin < end)
{
if (str[begin] != str[end])
{
outBegin = begin;
outEnd = end;
return false;
}
++begin;
--end;
}
return true;
}
};
3.12 将字符串反转到单调递增
class Solution {
public:
int minFlipsMonoIncr(string s) {
// 方法一
// int n = s.length();
// //dp[i][0]表示第i个元素被设置为0,使得s为单调递增的最小翻转次数
// //dp[i][1]表示第i个元素被设置为1,使得s为单调递增的最小翻转次数
// vector<vector<int>> dp(n, vector<int>(2,0));
// //base case
// if (s[0]=='0')
// dp[0][0] = 0, dp[0][1] = 1;
// else
// dp[0][0] = 1, dp[0][1] = 0;
// for (int i=1;i<n;++i)
// {
// if (s[i] == '0')
// {
// //0的前面只能是0才能保证s单调递增
// dp[i][0] = dp[i-1][0];
// //0的前面是0或者1都可能保证s单调递增
// dp[i][1] = std::min(1 + dp[i-1][0], 1 + dp[i-1][1]);
// }
// else
// {
// //0的前面只能是0才能保证s单调递增
// dp[i][0] = 1 + dp[i-1][0];
// //0的前面是0或者1都可能保证s单调递增
// dp[i][1] = std::min(dp[i-1][0], dp[i-1][1]);
// }
// }
// return dp[n-1][0] > dp[n-1][1] ? dp[n-1][1] : dp[n-1][0];
// 方法二,不用dp数组,而用两个变量进行优化
int n = s.length(), pre0, pre1, cur0, cur1;
//base case
if (s[0]=='0')
pre0 = 0, pre1 = 1;
else
pre0 = 1, pre1 = 0;
for (int i=1;i<n;++i)
{
if (s[i] == '0')
{
//0的前面只能是0才能保证s单调递增
cur0 = pre0;
//0的前面是0或者1都可能保证s单调递增
cur1 = std::min(1 + pre0, 1 + pre1);
}
else
{
//0的前面只能是0才能保证s单调递增
cur0 = 1 + pre0;
//0的前面是0或者1都可能保证s单调递增
cur1 = std::min(pre0, pre1);
}
pre0 = cur0;
pre1 = cur1;
}
return pre0 > pre1 ? pre1 : pre0;
}
};
3.13
3.14
3.15
3.16
3.17
3.18
3.19
3.20
3.21
3.22
3.23
3.24
3.25
3.26
3.27
3.28
3.29
3.30
3.31