字符串
遇到的问题无非就这么几类:字符串翻转/公共子序列/回文子串/重复子串。
首先我们发现,字符串经常和动态规划挂钩!还有一些独特的算法,如中轴法,滑动窗口(分固定和不固定)。此外,我们还要关注一些经典问题:KMP算法,马拉车算法,表达式求解等。
字符串问题经常用到unordered_map和unordered_set两种stl容器。
练手题:1143(dp出现过,高频) 225 696(题解很有趣)3(滑动窗口问题)
下面进行具体讲解
-
1143
:求给定字符串的最长公共子序列这是一个非常明显的动态规划问题。以测试数据text1=”abcde“,text2=”bde“为例,在有两个字符串/数组的问题中,我们常见的模拟策略是固定一个字符串text1,将text2的字符串从头开始依次尝试和整个text1匹配并填充dp数组,即先将”b“与text1全部匹配,填满dp[1],然后根据dp[1]与”bd“将dp[2]填充,循序渐进。总时间复杂度是O(mn)。边界情况直接当作一个字符串为空,按照常规思路去填充即可。
动态规划看起来是就某个中间状态进行分析,但是实际填充的时候也是从无到有,从最小的子问题开始逐步解决的,所以分析初始状态对求解是非常有帮助的。
int longestCommonSubsequence(string text1, string text2) { int len1=text1.length(); int len2=text2.length(); vector<vector<int> > dp(len1+1,vector<int>(len2+1,0)); for(int i=1;i<=len1;i++){ for(int j=1;j<=len2;j++){ if(text1[i-1]==text2[j-1])//注意要-1 dp[i][j]=dp[i-1][j-1]+1;//状态转移方程 else dp[i][j]=max(dp[i-1][j],dp[i][j-1]); } } return dp[len1][len2]; }
-
696:给定一个 0-1 字符串,求有多少非空子字符串的 0 和 1 数量相同。
dp和动态规划都可以,先看看中心扩展法(后面有具体讲)。此外还有一种偏数学的解法。
class Solution { public: int countBinarySubstrings(string s) { int sum=0; for(int i=0;i<s.length()-1;++i){ int j=i+1; if(s[i]!=s[j]){//符合初始状态 ++sum; int k=i-1; j++;//开始扩展 while(k>=0 && j<s.length() && s[k]==s[k+1] && s[j]==s[j-1]){ ++sum; k--; j++; } } } return sum; } };
我们可以从左往右遍历数组,记录和当前位置数字相同且连续的长度,以及其之前连续的不同数字的长度。举例来说,对于 00110 的最后一位,我们记录的相同数字长度是 1,因为只有一个连续 0; 我们记录的不同数字长度是 2,因为在 0 之前有两个连续的 1。若不同数字的连续长度大于等于 当前数字的连续长度,则说明存在一个且只存在一个以当前数字结尾的满足条件的子字符串。
正确性:因为以每个字符结尾的符合要求的子字符串最多只有1个。回文子串这个方法就不能用。
int countBinarySubstrings(string s) { int pre = 0, cur = 1, count = 0; //pre:与当前位置不同的字符串长度,cur:与当前位置不同的字符串长度 for (int i = 1; i < s.length(); ++i) { if (s[i] == s[i-1]) ++cur; else { pre = cur;//原来的最大长度变成了之前的最大长度 cur = 1;//重新标记 } if (pre >= cur) ++count;//存在以当前字符结尾的满足要求的子字符串 } return count; }
-
3:找出给定字符串S中不含有重复字符的最长子串 的长度。
初始想法是动态规划,但是其实由于下一个要检测的子串开头位置必然是递增的,故用滑动窗口法。按次序依次将字符串的字母放入队列,然后从队列头开始弹出元素直到当前队列中没有重复元素为止。至于是否有重复元素直接用set迭代器进行查询即可。
class Solution { public: int lengthOfLongestSubstring(string s) { unordered_set<char> hash;//用来查找很好用的一个stl //不用map的原因是虽然可以直接定位重复的前一个位置,但是之前需要删除的元素无法一一删除 int begin=0,end=-1; int len=0; for(auto& c:s){ ++end; if(!hash.count(c)){//没找到 hash.insert(c); len=max(len,end-begin+1);//更新长度 } else{//找到了 while(s[begin]!=c)//还不相等 hash.erase(s[begin++]);//删除 begin++; } } return len; } };
回文类
练手题:
647:给定一个字符,求其有多少个回文子字符串。回文的定义是左右对称。
409:返回由字符串 s
中的字母构造成的最长的回文串 (用不同的stl容器怎么解)
下面具体讲解
-
647:解法有动态规划或中轴法
先来讲讲动态规划。这里dp数组的含义应该是s[i:j]是否是回文子串,不能是子串个数,因为a(aba)a此时两边新增的字母还可以和中间的字符串拆分开后重新组合成回文子串。另外,初始状态应该都是1(默认可以构成),否则aa此类字符串会被当作不是回文子串。
class Solution { public: int countSubstrings(string s) { int len=s.length(); //默认要改成1表示成立,但是加法时要小心不能加进去 vector<vector<int>> dp(len,vector<int>(len,1));//含义变一下,变成是否是回文 //当发现转移方程需要用到后面的结果,或者步长全部为1的情况,不妨试试倒过来遍历 int sum=0; for(int i=len-1;i>=0;--i){ dp[i][i]=1; ++sum; for(int j=i+1;j<len;++j){ if(s[i]==s[j]) dp[i][j]=dp[i+1][j-1]; else dp[i][j]=0; sum+=dp[i][j]; } } return sum; } };
另一种解法:中轴法(后面还会涉及)我们可以依次以字符串的每个位置为中心向左向右同步延长,判断存在多少以当前位置为中轴的回文子字符串。注意要分奇数和偶数两种情况来讨论,然后依次向外扩展,直到不再相等为止。
int countSubstrings(string s) { int count = 0; for (int i = 0; i < s.length(); ++i) { count += extendSubstrings(s, i, i); // 奇数长度 count += extendSubstrings(s, i, i + 1); // 偶数长度 } return count; } int extendSubstrings(string s, int l, int r) { int count = 0; while (l >= 0 && r < s.length() && s[l] == s[r]) { --l; ++r; ++count; } return count; }
-
409:不难,主要是学习一下unordered_map的遍历和unordered_set
class Solution { public: int longestPalindrome(string s) { unordered_set<char>un; for(auto& c:s) { if(!un.count(c)) un.insert(c); else un.erase(c); } return s.size()-max(0,(int)un.size()-1); } };
KMP算法
字符串匹配算法:28题
next[i]表示下一次需要匹配的字母下标(从1开始),相当于从头开始长度为next[i]的真前缀和从当前位置开始向前长度为next[i]的真后缀(包括当前字符)是完全相等的,故可以直接移动,使得当前字符和下标为next[i]的字符进行匹配。
a | a | b | a | a | a | b | |
---|---|---|---|---|---|---|---|
next | 0 | 1 | 0 | 1 | 2 | 2 | 3 |
π(0)=0,因为 a 没有真前缀和真后缀,根据规定为 0(可以发现对于任意字符串 π(0)=0 必定成立);
π(1)=1,因为 aa 最长的一对相等的真前后缀为 aa,长度为 1;
π(2)=0,因为 aab 没有对应真前缀和真后缀,根据规定为 0;
π(3)=1,因为 aaba 最长的一对相等的真前后缀为 a,长度为 1;
π(4)=2,因为 aabaa 最长的一对相等的真前后缀为 aa,长度为 2;
π(5)=2,因为 aabaaa 最长的一对相等的真前后缀为 aa,长度为 2;
π(6)=3,因为 aabaaab 最长的一对相等的真前后缀为 aab,长度为 3。
void calNext(const string &needle, vector<int> &next) {
//p表示当前已匹配的相同前后缀长度
for (int j = 1, p = 0; j < needle.length(); ++j) {
while (p > 0 && needle[p] != needle[j]) {
p = next[p-1]; // 如果下一位不同,往前回溯
}
if (needle[p] == needle[j]) {
++p; // 如果下一位相同,更新相同的最大前缀和最大后缀长
}
next[j] = p;
}
}
π(0)=0,a:不参与遍历,默认为0。
π(1)=1,aa :p=0直接进入if语句,needle[0] == needle[1]即当前字符和之前匹配的相同,p=1
π(2)=0,aab:进入时p=1,说明之前有匹配前缀(0和1已经匹配),于是比较当前needle[1] != needle[2]不相同,p=next[0]=0,回退。
π(3)=1,因为 aaba 最长的一对相等的真前后缀为 a,长度为 1;
π(4)=2,因为 aabaa 最长的一对相等的真前后缀为 aa,长度为 2;
π(5)=2,因为 aabaaa 最长的一对相等的真前后缀为 aa,长度为 2;
π(6)=3,因为 aabaaab 最长的一对相等的真前后缀为 aab,长度为 3。
全部代码如下:
class Solution {
public:
int strStr(string haystack, string needle) {
int n = haystack.size(), m = needle.size();
if (m == 0) return 0;
vector<int> pi(m);
for (int i = 1, j = 0; i < m; i++) {
while (j > 0 && needle[i] != needle[j])
j = pi[j - 1];
if (needle[i] == needle[j])
j++;
pi[i] = j;
}
for (int i = 0, j = 0; i < n; i++) {
while (j > 0 && haystack[i] != needle[j])
j = pi[j - 1];
if (haystack[i] == needle[j])
j++;
if (j == m)//找到了
return i - m + 1;
}
return -1;
}
};
表达式计算
经典解法:分别定义运算符栈和操作数栈,并定义运算符的优先级。优先级定义为:’<'优先级低,可以先计算前面的式子;‘>‘要等待下一个操作数进入;’='表示当前运算已完成直接弹出即可。
+ | - | * | / | ( | ) | # | |
---|---|---|---|---|---|---|---|
+ | > | > | < | < | < | > | > |
- | > | > | < | < | < | > | > |
* | > | > | > | > | < | > | > |
/ | > | > | > | > | < | > | > |
( | < | < | < | < | < | = | |
) | > | > | > | > | > | > | |
# | < | < | < | < | < | = |
有很多细节需要注意!
//获取优先级
stack<char> op;
stack<int> num;
char pre[7][7]={
{'>','>','<','<','<','>','>'},
{'>','>','<','<','<','>','>'},
{'>','>','>','>','<','>','>'},
{'>','>','>','>','<','>','>'},
{'<','<','<','<','<','=','0'},
{'>','>','>','>','0','>','>'},
{'<','<','<','<','<','0','='}};
char Precede(char a,char b) //判断优先级
{
int i,j;
switch(a)
{
case '+':i=0;break;
case '-':i=1;break;
case '*':i=2;break;
case '/':i=3;break;
case '(':i=4;break;
case ')':i=5;break;
case '=':i=6;break;
default:break;
}
switch(b)
{
case '+':j=0;break;
case '-':j=1;break;
case '*':j=2;break;
case '/':j=3;break;
case '(':j=4;break;
case ')':j=5;break;
case '=':j=6;break;
default:break;
}
return pre[i][j];
}
//进行计算
void eval()
{
int a = num.top(); num.pop();
int b = num.top(); num.pop();
char c = op.top(); op.pop();
int r;
switch(c)
{
case '+':r=b+a;break;
case '-':r=b-a;break;
case '*':r=b*a;break;
case '/':r=b/a;break;
default:break;
}
num.push(r);
return;
}
int calculate(string str){
str='0'+str;//开头是负数的操作
int i=0;
while(i<str.size())
{
char c=str[i];
if(c==' ') i++;//跳过空格
else if(isdigit(c)) //c是数字,读取一个连续的数字
{
int x = 0;
while(i < str.size() && isdigit(str[i])) x = x * 10 + (str[i++] - '0');
num.push(x); //加入数字栈中
//i--; //这里不能--,因为不是for循环
}
else //c是操作符
{ //op栈非空并且栈顶操作符优先级大于等于当前操作符c的优先级,进行eval()计算
if(op.empty()){//加判断条件!
op.push(c);
++i;
}
else{
switch(Precede(op.top(),c)){
case '>'://计算之前的值
eval();
break;//先不进行++i,相当于for循环中的while内部循环
case '<':
op.push(c);//当前元素入栈,等待后续处理
++i;
break;
case '=':
op.pop();//弹出栈顶元素和去掉当前元素
++i;//相当于计算完了括号内的内容
break;
}}
}
}
while(op.size()) eval();//最后要统一计算一次
return num.top();
}
``