文章目录
1、最长回文子串
题目
分析
首先可以使用暴力法,双指针遍历,但时间上堪忧。
再来就是动态规划,
状态转移方程如下所示:
dp[i][j] = (s[i] == s[j]) and dp[i + 1][j - 1]
注:
①「动态规划」事实上是在填一张二维表格,由于构成子串,因此 i 和 j 的关系是 i <= j ,因此,只需要填这张表格对角线以上的部分。
②看到 dp[i + 1][j - 1] 就得考虑边界情况。
边界条件:
如果子串 s[i + 1..j - 1] 只有 1 个字符,即去掉两头,剩下中间部分只有 1 个字符,显然是回文;
如果子串 s[i + 1..j - 1] 为空串,那么子串 s[i, j] 一定是回文子串。
注意事项:
要小心“有后效应”,即求dp[i][j]时dp[i + 1][j - 1]还未求出来,这就肯定会出现错误,所以填表顺序很重要。
见下图:
复杂度
时间复杂度:O(n^2)
空间复杂度:O(n^2)
2、atoi
题目
分析
这种题目根据规则可以有n种变体,其实比较好的方法是用有限状态机,但那个一来不熟,二来很费时间分析,所以还是if-else解决。
这种题目主要是要把各种情况都给考虑清楚。
还有一点是需要考虑越界问题,可以用下面这种方案处理:
if(out>INT_MAX/10||out==INT_MAX/10&&str[i]-'0'>=INT_MAX%10) return INT_MAX;
if(out<INT_MIN/10||out==INT_MIN/10&&str[i]-'0'>=-(INT_MIN%10)) return INT_MIN;
注: 在开始循环前最好能把正负号给判断出来,然后定义一个sym,正取1,负取-1,在循环中将字符转int后先乘sym再加到out上面。
复杂度
时间复杂度:O(n)
空间复杂度:O(1)
3、翻转字符串里的单词
题目
分析
这题有两种要求,一种就是不设限,另一种就是原地求解。
不设限的情况下使用双指针法从后往前遍历即可,但要注意字符串可能是以空格开始和字符串中值包含空格这两种特殊情况,代码如下:
string reverseWords(string s) {
int n=s.size();
if(n==0) return s;
int left=n-1,right=n-1;
string ret;
while(right>=0){
while(right>=0&&s[right]==' ') --right;
if(right>=0){
left=right;
while(left>=0&&s[left]!=' ') --left;
string a=s.substr(left+1,right-left);
ret+=a;
ret+=' ';
right=left;
}
}
if(ret.size()>0) ret.pop_back();
return ret;
}
而原地求解则需要用到reverse函数,
reverse函数用于反转在[first,last)范围内的顺序(包括first指向的元素,不包括last指向的元素),reverse函数无返回值。
先整体翻转,再移位,插入空格后进行局部翻转,最后注意要将末尾的多余的位置全部删除。
复杂度
时间复杂度:O(N)
空间复杂度:O(N)或O(1)
4、字符串相乘
题目
分析
这个题目不必说,最大的麻烦就是越界,解决方法是维护维护一个数组,如下按位乘后逐个加到数组里面。鉴于可能传入的num1和num2本身就已经越界了,所以应该把第一个string放在数组中,然后将第二个string的每个位和第一个string的每个位依次做乘法。
下图是按位运算的优化方法:
1)乘数 num1 位数为 M,被乘数 num2 位数为 N, num1 x num2 结果 res 最大总位数为 M+N
2)num1[i] x num2[j] 的结果为 tmp(位数为两位,"0x","xy"的形式),其第一位位于 res[i+j],第二位位于 res[i+j+1]。
代码如下:
string multiply(string num1, string num2) {
if(num1=="0"||num2=="0") return "0";
int n1 = num1.size(), n2 = num2.size();
vector<int> out(n1 + n2, 0);
for (int i = 0; i < n1; ++i) {
for (int j = 0; j < n2; ++j) {
int tem = (num1[i] - '0')*(num2[j] - '0');
out[i + j + 1] += tem % 10;
out[i + j] += tem / 10;
}
}
//因为可能出现10,所以这里要再遍历一遍,并进行处理
for(int m=n1+n2-1;m>=0;--m){
if(out[m]>=10){
out[m-1]+=out[m]/10;
out[m]=out[m]%10;
}
}
string mul;
int start = 0;
while (start<out.size()&&out[start] == 0) ++start;
for (int k = start; k < out.size(); ++k) {
mul += out[k] + '0';
}
return mul;
}
复杂度
时间复杂度:O(M* N)。M,N分别为 num1 和 num2 的长度。
空间复杂度:O(M+N)。
总结
总得来说,字符串的题目以转换和查找为主。
对于需要转换成int的题目,一定要小心越界的发生,而对于查找类的题目则考虑使用DFS、BFS和DP来处理,FS的难点在于这时可能需要进行数学建模,而DP自然就是状态转移方程了。
如果实在想不出,这种题目通常也可以暴力遍历来解,可以尝试。
5、从一个string找另一个string的任意排序的子字串(ZJ)
题目
给定长度为m的字符串aim,以及一个长度为n的字符串str。
问能否在str中找到一个长度为m的连续子串使得这个子串刚好由aim的m个字符组成,顺序无所谓。
返回任意满足条件的一个子串的起始位置,未找到返回-1。
分析
这个题目有两种思路:
法一
窗口遍历,在str中找到每个和aim一样长的子串同aim进行比较,比较可以用位图来完成,如下:
bool is_equal(string a, string b) {
if (a.size() != b.size()) return false;
//字符的ASCII范围为0-255
int count[256] = { 0 };
for (int i = 0; i < a.size(); ++i) {
++count[a[i]];
}
for (int j = 0; j < b.size(); ++j) {
if (count[b[j]]-- == 0) return false;
}
return true;
}
时间复杂度为O(N*M) N和M分别是aim和str的size
法二
使用备忘录(欠债表)一边遍历一边记录,
我在这里理了好久,用这个方法一定要先确定好怎样规定欠债。
首先记录aim中各个字符的个数,如下:
int count[256] = { 0 };
for (int i = 0; i < len; ++i) {
++count[aim[i]];
}
定义备忘录并初始化,这里规定多出来的数记为负,负数总和即为欠债数量,并用invalidnums来记录。如下:
//invalidnums用来记录count中负数的总和,
//当invalidnums为0时,表示欠债已还清,这时返回true
int invalidnums = 0;
//初始化备忘录,将str窗口中的值一一减一,如果出现负数就记录到invalidnums中
//这里要搞懂负数时表示多出来的数,而正数则表示缺少的数
for (int i = 0; i < len; ++i) {
if ((count[str[i]]--) <= 0) ++invalidnums;
}
if (invalidnums == 0) return 0;
注:因为这里用了窗口,所以str子串的长度和aim是相等的,而str子串中的每个字符都会在count中减一,如果没了负数,说明子串已经和aim相抵消了,即两数包含的字符相同,顺序随意。
接下里就是滑动窗口的步骤了,这里要弄清多出来的数就是欠的债,还有判断的地方也需要注意,如下:
for (int i = 0, j = len ; j <= n; ++i, ++j) {
if (invalidnums == 0) return i;
//i是从窗口中清除掉的字符,如果它本来是负数,这里还了一次债,invalidnums-1
if ((count[str[i]]++) < 0) --invalidnums;
//j是窗口中新加入的字符,如果它是负数或0,这里表示要借出一份债,invalidnums+1
if ((count[str[j]]--) <= 0) ++invalidnums;
//因为这时的i是要被清除掉的,所以要在开头检查invalidnums是否为0
//if (invalidnums == 0) return i;
}
复杂度
时间复杂度:O(N)
空间复杂度:O(1),这里用到了256的数组记录字符的正负,为常数
6、外观数列
题目
分析
初始化
数字为1的时候结果也是1,所以先设置
string prev="1";
然后开始循环,外循环从1到n-1,而内循环则根据上一个的结果得出当前结果。
这里维护一个 pos 和 count,pos表示当前遍历的字符,而count则记录到目前为止,该字符出现了多少次。
注意,因为只有在遇到与pos不同的字符时才会更新pos,所以内循环结束后还要记录最后的字符,同时还要更新prev。
代码如下:
for(int i=1;i<n;++i){
string next="";
char pos=prev[0];
int count=0;
for(int j=0;j<prev.size();++j){
if(prev[j]==pos) ++count;
else{
next+=to_string(count);
next+=pos;
pos=prev[j];
count=1;
}
}
next+=to_string(count);
next+=pos;
prev=next;
}
复杂度
时间复杂度:O(N*M),这里N表示有多少项,M表示最后字符的长度。
空间复杂度:O(M)
7、正则表达式匹配
题目
分析
参考:link
这题让人颇为头痛,因为要讨论的情况太多,需要把逻辑理清才行。
状态:dp[i][j] 代表字符串 s 中前 i 个字符和 p 中前 j 个字符是否匹配。同时,记 s 第 i 个字符记为 s[m] == s[i - 1];p 第 j 个字符记为 p[n] == p[j - 1](这是为了在s,p前虚构出一个空格)。
初始化:dp[0][j] = dp[0][j - 2] and p[j - 1] == ‘ * ’;
即p 第 j 个字符记为 ‘*’ 且 dp[0][j - 2]为 True
转移公式:
注意,这里第一种情况中的两种小情况应该为与关系,也就是其中任一为真时,dp[i][j]都为真。因为这里即使可以匹配,我们也可以假设它不能匹配来进行处理。
代码如下:
bool isMatch(string s, string p) {
int n1=s.size(),n2=p.size();
//dp[i][j] 代表字符串 s 中前 i 个字符和 p 中前 j 个字符是否匹配
vector<vector<int>> dp(n1+1,vector<int>(n2+1,0));
dp[0][0]=1;
for(int j=2;j<=n2;++j) dp[0][j]=dp[0][j-2]&&p[j-1]=='*';
for(int i=1;i<=n1;++i){
for(int j=1;j<=n2;++j){
int m=i-1,n=j-1;
if(s[m]==p[n]||p[n]=='.') dp[i][j]=dp[i-1][j-1];
else if(p[n]=='*'){
dp[i][j]=dp[i][j-2]||((p[n-1]==s[m]||p[n-1]=='.')&&dp[i-1][j]);
}
}
}
return dp[n1][n2];
}
这里的初始化让人感到疑惑,为什么要这么写?
答:这里要理解好‘*’的作用,它可以匹配零个或多个前面的那一个元素,这里的意思是它可以将前面的字符抵消的,如下题目给的例3:
输入:
s = "aab"
p = "c*a*b"
输出: true
解释: 因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。
这里第一个 * 将 c 给抵消掉了。同时dp[0][1]是一定为false的,因为一个字符无论如何都不可能和空字符匹配。
而如果第二个字符是‘*’的话,就能将第一个字符抵消掉,从而匹配上空字符。
复杂度
时间复杂度:O(MN)
空间复杂度:O(MN)
8、串联所有单词的子串
题目
分析
参考:link
因为words中的字符串的长度都是相同的,因此可以使用滑动窗口配合哈希map从前往后进行查找,代码如下:
vector<int> findSubstring(string s, vector<string>& words) {
int n = words.size();
if (n == 0 || s.size() == 0) return{};
unordered_map<string, int> need;
int len = words[0].size();
for (string a : words) {
++need[a];
}
vector<int> ret;
for (int i = 0; i < len; ++i) {
int left = i, right = i, valid = 0;
unordered_map<string, int> win;
while (right+len <= s.size() ) {
string a = s.substr(right, len);
right += len;
if (need.count(a)) {
++win[a];
}
else {
//重置
valid = 0;
win.clear();
left = right;
continue;
}
if (win[a] == need[a]) ++valid;
else if (win[a] > need[a]) {
while (win[a] > need[a]) {
string b = s.substr(left, len);
left += len;
if (win[b] == need[b]) --valid;
--win[b];
}
}
if (valid == need.size()) {
ret.push_back(left);
}
}
}
return ret;
}
这里有几点需要注意,
①外围的for循环存在的原因在于只用从0到len-1开始遍历即可实现将所有长度为len的字符串都查找一遍。
②一旦遇到不在words中的字符串,这时应该将该字符串及前面的字符串应全部抛弃,因为只要包含了该字符串都不满足要求。
③如果查找过的字符串多了,这时就要右移left指针,直到多出来的字符串变成等于为止。
复杂度
时间复杂度:O(N)
空间复杂度:O(M),其中M为words中字符总长,而N表示s的长度。
9、单字符重复子串的最大长度
题目
分析
参考:link
这里先遍历整个字符串,记录索引i的左侧连续重复的字符的个数和右边连续字符的个数,并记录连续的字符的最长长度,记索引i的左侧连续的重复个数为left[i-1],右侧连续重复的个数为right[i+1],同时记录下各个字符的总数。
然后对每个字符进行判断,分为以下三种情况:
1)text[i-1]的字符总数大于left[i-1],证明左侧并非所有的字符都是一样的,而且在其他位置还有至少一个单独的text[i-1]元素,这时从其他位置交换一个字符即可,即ans = max(ans,left[i-1]+1)。
2)text[i+1]的字符总数大于right[i+1],这个情况和上面的是对称的。
3)text[i-1] == text[i+1],这时就需要比较left[i-1]与right[i+1]的和同text[i-1]的字符总数的大小,如果小于表示可以从其他地方调一个字符过来,否则就只能从左连续字符串的最左端或者右连续字符串的最右端调一个字符来填补,长度刚好是left[i-1]与right[i+1]的和。
这里还要注意先要将maxlen初始化一下,
int maxlen=max(left[n-1],right[0]);
代码如下:
int maxRepOpt1(string text) {
int n=text.size();
if(n==0) return 0;
vector<int> left(n,0),right(n,0);
vector<int> count(26,0);
for(int i=0;i<n;++i){
++count[text[i]-'a'];
}
left[0]=1;
for(int i=1;i<n;++i){
if(text[i]==text[i-1]){
left[i]=left[i-1]+1;
}
else{
left[i]=1;
}
}
right[n-1]=1;
for(int j=n-2;j>=0;--j){
if(text[j]==text[j+1]){
right[j]=right[j+1]+1;
}
else{
right[j]=1;
}
}
int maxlen=max(left[n-1],right[0]);
for(int i=1;i<n-1;++i){
if(text[i-1]==text[i+1]&&(left[i-1]+right[i+1])<=count[text[i-1]-'a']){
if((left[i-1]+right[i+1])==count[text[i-1]-'a']){
maxlen=max(maxlen,left[i-1]+right[i+1]);
}
else maxlen=max(maxlen,left[i-1]+right[i+1]+1);
}
else{
if(left[i-1]<count[text[i-1]-'a']){
maxlen=max(maxlen,left[i-1]+1);
}
if(right[i+1]<count[text[i+1]-'a']){
maxlen=max(maxlen,right[i+1]+1);
}
}
}
return maxlen;
}
复杂度
时间复杂度:O(N)
空间复杂度:O(N)