字符串->算法实现
综述
反转字符串:leetcode344
反转字符串 II:leetcode541
替换数字:非leetcode原题
反转字符串中的单词:leetcode151
右旋字符串:非leetcode原题
找出字符串中第一个匹配项的下标:leetcode28
重复的子字符串:leetcode459
引言
刷题总结
反转字符串
题目
题解
简单题不多说
class Solution {
public:
void reverseString(vector<char>& s) {
int i = 0;
int j = s.size() - 1;
while (i < j) {
std::swap(s[i++], s[j--]);
}
}
};
反转字符串 II
题目
题解
每 2k 个字符串就反转前 k 个字符串
循环中记得处理最后的边界,即“最后剩余的字符串的处理”
这里面有个小技巧,如果自己实现 reverse 函数的话,尽量保持和库函数一样的“左闭右开”,这样方便,而且像使用库函数一样,不容易出错
class Solution {
public:
string reverseStr(string s, int k) {
int index = 0;
//每2k个反转前k个字符串
while (index < s.size()) {
//处理最后剩余的字符串
if (s.size() - index < k) {
reverse(s, index, s.size());
break;
}
if (s.size() - index >= k && s.size() - index < 2 * k) {
reverse(s, index, index + k);
break;
}
reverse(s, index, index + k);
index += 2 * k;
}
return s;
}
private:
void reverse(string& s, int begin, int end) { //左闭右开反转
for (int i = begin, j = end - 1; i < j; i++, j--) {
std::swap(s[i], s[j]);
}
}
};
时间复杂度: O(n)
空间复杂度: O(1)
替换数字
题目
题解
很多数组填充类的问题,其做法都是先预先给数组扩容到填充后的大小,然后在从后向前进行操作。
这么做有两个好处:
- 不用申请新数组。
- 从后向前填充元素,避免了从前向后填充元素时,每次添加元素都要将添加元素之后的所有元素向后移动的问题。
ACM 代码:
#include<iostream>
using namespace std;
class Solution {
public:
void ti_huan_number(string& s) {
//统计数字的个数
int count = 0;
for (char ele : s) {
if (ele >= '0' && ele <= '9') count++;
}
//扩充字符串
int beforeSize = s.size();
s.resize(beforeSize + count * 5);
//从后往前赋值
for (int i = beforeSize - 1, j = s.size() - 1; i >= 0; i--) {
if (s[i] >= '0' && s[i] <= '9') {
s[j--] = 'r';
s[j--] = 'e';
s[j--] = 'b';
s[j--] = 'm';
s[j--] = 'u';
s[j--] = 'n';
} else {
s[j--] = s[i];
}
}
}
};
int main() {
string s;
Solution solution;
while (cin >> s) {
solution.ti_huan_number(s);
cout << s <<endl;
}
return 0;
}
反转字符串中的单词
题目
题解
要求:不要使用辅助空间,空间复杂度要求为O(1)
如果使用辅助空间,那这道题就是简单题了,没意义
这道题得多练,里面坑很多,不容易一下子写出来,得debug较长时间
思路就是:先总体反转,再挨个反转单词,反转单词的过程中需要处理多个空格的情况
class Solution {
public:
string reverseWords(string s) {
//先删除尾随空格
int index = s.size() - 1;
while (s[index] == ' ') index--;
s.resize(index + 1);
//字符串整体颠倒顺序
reverse(s.begin(), s.end());
//按照空格为间隔,给每个单词再次颠倒顺序,需要注意多个空格删成一个空格
for (int i = 0, j = 0; j < s.size(); j++) {
if (s[j] == ' ' && s[j - 1] == ' ') continue; //处理多余空格
if (s[j] == ' ') {
reverse(s.begin() + i, s.begin() + j);
while (s[i] != ' ') i++;
i++;
}
if (j == s.size() - 1) reverse(s.begin() + i, s.end());
}
//删除尾随空格
index = s.size() - 1;
while (s[index] == ' ') index--;
s.resize(index + 1);
return s;
}
};
右旋字符串
题目
题解
如果允许额外开辟空间,即再定义一个字符串,那这道题太简单了
因此作出限制:不能申请额外空间,只能在本串上操作
那么这道题就类似于上道题:整体反转 + 局部反转
也就是说:
- 整体反转
- 局部反转,前k个字符反转
- 局部反转,后面的字符反转
ACM 代码:
#include<iostream>
#include<algorithm>
using namespace std;
class Solution {
public:
void rightRerverse(string& s, int k) {
//整体反转
reverse(s.begin(), s.end());
//局部反转,前k个字符反转
reverse(s.begin(), s.begin() + k);
//局部反转,后面的字符反转
reverse(s.begin() + k, s.end());
}
};
int main() {
int k;
string s;
Solution sol;
cin >> k >> s;
sol.rightRerverse(s, k);
cout << s << endl;
return 0;
}
剑指 offer 中这个题是 左反转,其实思路是一样的,只不过局部反转时的区间不一样
找出字符串中第一个匹配项的下标
题目
题解
这道题是 KMP 算法的经典题目
一般面试中可能很难直接写出 KMP,可以先试试暴力,暴力也可以过 leetcode 的这道题
暴力:
思路就是一个一个的匹配,如果 haystack[i] == needle[j]
,就 j++,然后继续匹配后面的字符串
如果全部匹配,直接返回
如果中间有的不匹配,终止内循环,将 j 置为0,继续从 i + 1 的位置开始匹配
class Solution {
public:
int strStr(string haystack, string needle) {
for (int i = 0, j = 0; i < haystack.size(); i++) {
if (haystack[i] == needle[j]) { //两个字符串的第一个匹配,继续匹配后面的
for (int k = i; j < needle.size(); k++) {
if (haystack[k] == needle[j]) j++; //每匹配一次 j 就 ++
else break; //有一次不匹配就 break
if (j == needle.size()) return i; //全部匹配就直接返回
}
j = 0; //中间有一次不匹配,需要将 j 置为0,后序继续从 第一位开始匹配
}
}
return -1;
}
};
KMP 算法:
KMP 算法介绍
KMP 主要应用在字符串匹配上。
KMP 的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容(通过前缀表也就是 next 数组),可以利用这些信息避免从头再去做匹配了。
我们需要注意的是:next 数组里的数字表示的是什么,为什么这么表示?
面试有可能会问
前缀表是什么
前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。
举个例子:
文本串中第六个字符 b 和 模式串的第六个字符 f 不匹配了。如果暴力匹配,发现不匹配,此时就要从头匹配了。但是通过前缀表可以知道直接退回到第三个字符 b 的位置即可
首先明确概念:
前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串。
后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。
对于 aabaaf 来说,f 的前缀是 aabaa,f 的后缀是 abaaf
前缀表(这里直接用 next 数组表示前缀表)记录的就是最长相等前后缀的长度
比如 aabaaf 的前缀表:
举个例子:对于下标为 3 的 a 来说,前缀是 aab,后缀是 aba
从后往前匹配 aab 和 aba,发现第三位的 b 和 第三位的 a 不匹配,第二位的 a 和 第二位的 b 不匹配,第一位的 a 和第一位的 a 匹配,所以最长相等前后缀的长度是 1,next[3] = 1;
再举个例子:对于下标为 4 的 a 来说,前缀是 aaba,后缀是 abaa,第四位和第一位都相等,所以 next[4] = 2;
next 数组(前缀表)为什么这样表示
那 next 数组为什么这样表示呢?
因为如果找到不匹配的位置, 那么此时我们要看它的前一个字符的前缀表的数值是多少,从而进行回退,比如动图中的 b 和 f 不匹配,那么就从 f 回退到下标为 2 的 b,因为 next[4] = 2; 也就是说 f 前面的一位的 next 数值是 2,所以回退到下标为 2 的位置。
但是为什么 next 这样表示就可以正确回退呢?
因为 f 不匹配,但是前面两位的 aa 和 后面两位的 aa 相等,所以回退到 前面两位的 aa 后面那个下标位置最合适。我们想直接跳到 前缀aa 的后面,而 前缀aa 的后面那个下标 就是前缀的长度,f 位置不匹配,那么找前缀表中前一位的数值,也就是 f 前面的 a 对应的数值 2,而前缀表的数值代表的是
最长相等前后缀的长度,所以通过 next 可以直接找到 下标为 2 的位置。
构造 next 数组
构造 next 数组其实就是计算模式串 needle 前缀表的过程。 主要有如下 4 步:
- 初始化
- 处理前后缀不相同的情况
- 处理前后缀相同的情况
- 更新 next 数组
- 需要明确的一点
定义两个指针 i 和 j,j 指向前缀末尾位置(同时也代表前缀的长度),i 指向后缀末尾位置。
而 next[i] 表示 i(包括 i)之前的最长相等前后缀的长度,其实就是 j,所以更新 next 数组时肯定是这样的:next[i] = j;
- 初始化
对于模式串 aabaaf,第一位 a 只有后缀没有前缀,所以初始化为 0,即next[0] = 0;
- 处理前后缀不相同的情况
j 是从 0 开始的,i 是从 1 开始的(因为第一位已经初始化了,肯定是从第二位进行对比的,类似于动态规划)
所以:for (int i = 1; i < needle.size(); i++)
如果前后缀不相等即needle[i] != needle[j]
,需要向前回退,也就是找到 j 前面的一个元素的 next 的数值 即 next[j - 1],进行回退(和上面讲的模式串和文本串不匹配时的回退操作是一样的)
但是此时需要注意的是,只要不相等,就需要回退,所以使用的不是 if,而是 while。
并且使用的是 next[j - 1] 所以 j 需要 > 0,一来是防止数组角标越界,二来是意味着回退到 j = 0 就不再回退
所以代码是:
while (j > 0 && needle[i] != needle[j]) {
j = next[j - 1];
}
- 处理前后缀相同的情况
找到相等的前后缀即needle[i] == needle[j]
,则 j++,意味着 前后缀的长度加1
所以代码是:
if (needle[i] == needle[j]) j++;
- 更新 next 数组