提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
提示:这里可以添加本文要记录的大概内容:
1、在C语言中,把一个字符串存入一个数组时,也把结束符 '\0’存入数组,并以此作为该字符串是否结束的标志。
2、很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。
3、
什么时候用库函数?
库函数仅仅是解题的一部分,并且已经很清楚库函数的内部实现原理时可以使用。
题目的关键部分可以用库函数解决,就不要使用库函数
4.1 344反转字符串(3.26)
206反转链表使用了双指针的方法,这里也可以使用
定义两个指针,分别从数组的头尾,向中间移动,并交换元素
void reverseString(vector<char>& s) {
for (int i = 0, j = s.size() - 1; i < s.size()/2; i++, j--) {
swap(s[i],s[j]);
}
}
最后不需要return,因为函数时按引用传递(有&)的,对参数的修改会直接影响到原始变量,所以在函数内部修改s,原始变量也会被修改,无需显式返回
可以使用swap(s[i],s[j]);
不能使用reverse库函数,reverse(s.begin(),s.end());
时间复杂度: O(n)
空间复杂度: O(1)
swap函数的两种实现方式
1、交换数值
int tmp = s[i];
s[i] = s[j];
s[j] = tmp;
2、通过位运算
s[i] ^= s[j];
s[j] ^= s[i];
s[i] ^= s[j];
4.2 541反转字符串II(3.26)
其实在遍历字符串的过程中,只要让 i += (2 * k),i 每次移动 2 * k 就可以了,然后判断是否需要有反转的区间
class Solution {
public:
string reverseStr(string s, int k) {
for (int i = 0; i < s.size(); i += (2 * k)) {
// 1. 每隔 2k 个字符的前 k 个字符进行反转
// 2. 剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符
if (i + k <= s.size()) {
reverse(s.begin() + i, s.begin() + i + k );
} else {
// 3. 剩余字符少于 k 个,则将剩余字符全部反转。
reverse(s.begin() + i, s.end());
}
}
return s;
}
};
时间复杂度: O(n)
空间复杂度: O(1)
4.3 替换数字(3.26)
许多数组填充类的问题,做法:先预先给数组扩容带填充后的大小,然后开始从后向前操作。
好处:
1、不用申请新数组。
2、从后向前填充元素,避免了从前向后填充元素时,每次添加元素都要将添加元素之后的所有元素向后移动的问题。
从前往后填充就是O(n^2)
首先扩充数组到每个数字字符替换成 “number” 之后的大小
#include<iostream>
using namespace std;
int main(){
string s;
while(cin>>s){
int count=0;//计算当前字符串中数字的个数
int soldsize=s.size();
for(int i=0;i<soldsize;i++){
if(s[i]>='0' && s[i]<='9'){
count++;
}
}
//扩充字符串s的大小,每个数字替换成number
s.resize(s.size()+count*5);
int snewsize=s.size();
//从后向前将空格替换为number
for(int i=snewsize-1,j=soldsize-1;j<i;i--,j--){
if(s[j]>'9' || s[j]<'0'){
s[i]=s[j];
}
else{
s[i]='r';
s[i-1]='e';
s[i-2]='b';
s[i - 3] = 'm';
s[i - 4] = 'u';
s[i - 5] = 'n';
i=i-5;
}
}
cout<<s<<endl;
}
时间复杂度:O(n)
空间复杂度:O(1)
字符串和数组的区别
在C语言中,把一个字符串存入一个数组时,也把结束符 '\0’存入数组,并以此作为该字符串是否结束的标志。
那么vector< char > 和 string 又有什么区别呢?
其实在基本操作上没有区别,但是 string提供更多的字符串处理的相关接口,例如string 重载了+,而vector却没有。
所以想处理字符串,我们还是会定义一个string类型。
4.4 反转字符串中的单词(3.27)
可以使用split库函数,分隔单词,然后定义一个新的string字符串,最后再把单词倒叙相加
不使用辅助空间: "the sky is blue "
1、移除多余空格 “the sky is blue”
2、将整个字符串反转 “eulb si yks eht”
3、将每个单词反转 “blue is sky the”
快指针指向我们想要获取的元素
慢指针指向我们获取快指针获取的元素所指向的新的位置
删除多余空格的逻辑:
如果遇到空格,
判断是否是第一个位置,不是就是单词的开头,加上空格
while循环在不是空格时候,将整个单词赋值给slow,slow和i加一(遍历完一个完整的单词就又遇到了空格,就会跳出while)
继续下一个单词的遍历判断
(相当于一个for循环中判断的是一整个单词,当循环到一个单词的首字母时才进入if(s[j]!=0)中)
class Solution {
public:
//反转字符串s的左闭右闭区间
void reverse(string& s,int start,int end){
for(int i=start,j=end;i<j;i++,j--){
swap(s[i],s[j]);
}
}
//移除多余的空格
void romveextraspaces(string& s){
int slow=0;
for(int i=0;i<s.size();++i){
if(s[i]!=' '){ //遇到非空格就处理,即删除所有空格
if(slow!=0) { //判断不是字符串的第一个字母的话,需要在单词前加空格
s[slow]=' ';
slow++;
}
while(i<s.size() && s[i]!=' '){ //补上单词,遇到空格说明该单词结束
s[slow]=s[i];
slow++;
i++;
}
}
}
s.resize(slow);//slow的大小就是去除多余空格后的大小,设置大小
}
string reverseWords(string s) {
romveextraspaces(s); //去除多余空格,保证单词之间之只有一个空格,且字符串首尾没空格。
reverse(s,0,s.size()-1); //整个字符串进行反转
int start=0;
for(int i=0;i<=s.size();i++){
if(i==s.size()||s[i]==' '){ //到达空格或者串尾,说明一个单词结束。进行翻最后一步转。
reverse(s,start,i-1); //反转,左闭合,右闭合的反转
start=i+1; //更新下一个单词的开始下标start,s[i]是空格
}
}
return s;
}
};
第一次提交错误,最后一个单词没有反转成功的原因:
因为reverseWords函数里的for循环i<=s.size(),少了=,当i=size的时候无法进入for循环中的if判断,所以最后一个单词无法反转
时间复杂度: O(n)
空间复杂度: O(1) 或 O(n),取决于语言中字符串是否可变
移除多余空格的另一种法方法:
void removeExtraSpaces(string& s) {
for (int i = s.size() - 1; i > 0; i--) {
if (s[i] == s[i - 1] && s[i] == ' ') {
s.erase(s.begin() + i);
}
}
// 删除字符串最后面的空格
if (s.size() > 0 && s[s.size() - 1] == ' ') {
s.erase(s.begin() + s.size() - 1);
}
// 删除字符串最前面的空格
if (s.size() > 0 && s[0] == ' ') {
s.erase(s.begin());
}
}
一个erase本来就是O(n)的操作。erase操作上面还套了一个for循环,那么以上代码移除冗余空格的代码时间复杂度为O(n^2)。
那么使用双指针法来去移除空格,最后resize(重新设置)一下字符串的大小,就可以做到O(n)的时间复杂度。
4.5 右旋字符串(3.27)
不申请额外的空间
字符串先整体反转
再将前一部分、后一部分都反转
#include <iostream>
#include <string>
using namespace std;
//反转字符串s的左闭右闭区间
void reverse1(string& s,int start,int end){
for(int i=start,j=end;i<j;i++,j--){
swap(s[i],s[j]);
}
};
int main(){
int n;
string s;
cin>>n;
cin>>s;
int length=s.size();
reverse1(s,0,length-1);
reverse1(s,0,n-1);
reverse1(s,n,length-1);
cout<<s<<endl;
}
// 版本一
#include<iostream>
#include<algorithm>
using namespace std;
int main() {
int n;
string s;
cin >> n;
cin >> s;
int len = s.size(); //获取长度
reverse(s.begin(), s.end()); // 整体反转
reverse(s.begin(), s.begin() + n); // 先反转前一段,长度n
reverse(s.begin() + n, s.end()); // 再反转后一段
cout << s << endl;
}
4.6 实现strStr()(3.28)
在一个串中查找是否出现过另一个串,这是KMP的看家本领。
实现 strStr() 函数。
给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。
示例 1: 输入: haystack = “hello”, needle = “ll” 输出: 2
示例 2: 输入: haystack = “aaaaa”, needle = “bba” 输出: -1
说明: 当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。 对于本题而言,当 needle 是空字符串时我们应当返回 0 。
4.6.1 KMP
当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。
本篇将以如下顺序来讲解KMP,
-
什么是KMP
由三位发明学者的首字母 -
KMP有什么用
当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。 -
什么是前缀表
next数组就是一个前缀表,记录下标i之前(包括i)的字符串长,有多大长度的相同前缀后缀
前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配
最长相等前后缀的后缀的后面是不匹配字符,跳到与后缀相等的前缀的后一个字母重新开始匹配(重新匹配的位置:就是最长相等前后缀的长度)
要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。
可以看出,文本串中第六个字符b 和 模式串的第六个字符f,不匹配了。如果暴力匹配,发现不匹配,此时就要从头匹配了。
但如果使用前缀表,就不会从头匹配,而是从上次已经匹配的内容开始匹配,找到了模式串中第三个字符b继续开始匹配
前缀表的作用:
跳到不匹配字母的前面的最长相等前后缀
前缀表是如何记录的?
首先要知道前缀表的任务是当前位置匹配失败,找到之前已经匹配上的位置,再重新匹配,此也意味着在某个字符失配时,前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置
也就是不匹配字符的位置为i,找到前缀表的i-1位置对应的数,就是最长相等前后缀,在跳到该最长相等前后缀的前缀的后一位,重新开始匹配。 这时[0,i-1]位置上的字符是匹配好的
前缀表要求的就是相同前后缀的长度
前缀表里,统计了各个位置为终点字符串的最长相同前后缀的长度。
-
为什么一定要用前缀表
下标5之前这部分的字符串(也就是字符串aabaa)的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面重新匹配就可以了。
所以前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力。 -
如何计算前缀表
要求的是最长相等前后缀
前缀:是指不包含最后一个字符的所有以第一个字符开头的连续子串。例如:a(0)、aa(1)、aab(0)、aaba(1)、aabaa(2)
后缀:不包含第一个字符的所有以最后一个字符结尾的连续字串。例如:f、af、aaf、baaf、abaaf
最长相等前后缀是2,所以跳到字符串s[2]位置重新进行匹配
先分析长度为前1、2、3、4、5个字符的字串的有多大长度的相同前后缀,组成前缀表
-
前缀表与next数组
next数组就可以是前缀表,但是很多实现都是把前缀表统一减一(右移一位,初始位置为-1)之后作为next数组。
第一行前缀表
第二行前缀表右移一位
第三行前缀表-1
都可以作为next数组去计算
-
使用next数组来匹配
-
时间复杂度分析
其中n为文本串长度,m为模式串长度,因为在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n),之前还要单独生成next数组,时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)的。
暴力的解法显而易见是O(n × m),所以KMP在字符串匹配中极大地提高了搜索的效率。
- 构造next数组
定义一个函数getNext来构建next数组,函数参数为指向next数组的指针,和一个字符串。
1、初始化
2、处理前后缀不相同的情况
3、处理前后缀相等的情况
4、更新next
j指向前缀末尾位置(还代表包括i之前的子串的最长相等前后缀的长度),i指向后缀末尾位置
void getNext(int* next, const string& s){
int j=0;
next[0]=0;
for(int i=1;i<s.size();i++){ //i从1开始,因为
//前后缀不相同
while(j>0 && s[i]!=s[j]){// j要保证大于0,因为下面有取j-1作为数组下标的操作
j=next[j-1]; //找它前一位需要回退的位置
}
//前后缀相同
if(s[i]==s[j]){
j++;
}
next[i]=j; //更新next数组的值
}
}
4.6.2
暴力进行文本串和模式串的匹配,复杂度是O(m*n)
class Solution {
public:
void getNext(int* next, const string& s) {
int j = 0;
next[0] = 0;
for(int i = 1; i < s.size(); i++) {
while (j > 0 && s[i] != s[j]) {
j = next[j - 1];
}
if (s[i] == s[j]) {
j++;
}
next[i] = j;
}
}
int strStr(string haystack, string needle) {
if (needle.size() == 0) {
return 0;
}
int next[needle.size()];
getNext(next, needle);
int j = 0;
for (int i = 0; i < haystack.size(); i++) {
while(j > 0 && haystack[i] != needle[j]) {
j = next[j - 1];
}
if (haystack[i] == needle[j]) {
j++;
}
if (j == needle.size() ) {
return (i - needle.size() + 1);
}
}
return -1;
}
};
时间复杂度: O(n + m)
空间复杂度: O(m)
4.7 重复的子字符串(3.29)
1、暴力解法。
一个for循环获取子串的终止位置,然后判断子串是否能重复构成字符串,,又嵌套一个for循环,所以是O(n^2)
有的同学可以想,怎么一个for循环就可以获取子串吗? 至少得一个for获取子串起始位置,一个for获取子串结束位置吧。
其实我们只需要判断,以第一个字母为开始的子串就可以,所以一个for循环获取子串的终止位置就行了。 而且**遍历的时候 都不用遍历结束,只需要遍历到中间位置,**因为子串结束位置大于中间位置的话,一定不能重复组成字符串。
2、移动匹配
如果一个字符串s:abcabc,能由内部子串组成,即 由前后相同的子串组成,用 s + s,这样组成的字符串中,后面的子串做前串,前面的子串做后串,就一定还能组成一个s。
所以判断字符串s是否由重复子串组成,只要两个s拼接在一起,里面还出现一个s的话,就说明是由重复子串组成。
当然,我们在判断 s + s 拼接的字符串里是否出现一个s的的时候,要刨除 s + s 的首字符和尾字符,这样避免在s+s中搜索出原来的s,我们要搜索的是中间拼接出来的s。(所以需要把首字母和尾字母都删掉,防止又搜索出来原来的字符串)
class Solution {
public:
bool repeatedSubstringPattern(string s) {
string t=s+s;
t.erase(t.begin());
t.erase(t.end() - 1); // 掐头去尾,删掉首字母和尾字母
if (t.find(s) != std::string::npos) return true; // r
return false;
}
};
不过这种解法还有一个问题,就是 我们最终还是要判断 一个字符串(s + s)是否出现过 s 的过程,大家可能直接用contains,find 之类的库函数。 却忽略了实现这些函数的时间复杂度(暴力解法是m * n,一般库函数实现为 O(m + n))
3、 KMP解法
前缀表里,统计了各个位置为终点字符串的最长相同前后缀的长度。
最小重复单元:就是他的最长相等前后缀不包含的子串
图解:
正是因为 最长相等前后缀的规则,当一个字符串由重复子串组成的,最长相等前后缀不包含的子串就是最小重复子串
假设字符串s使用多个重复子串构成(这个子串是最小重复单位),重复出现的子字符串长度是x,所以s是由n * x组成。
因为字符串s的最长相同前后缀的长度一定是不包含s本身,所以 最长相同前后缀长度必然是m * x,而且 n - m = 1,(这里如果不懂,看上面的推理:最长相等前后缀不包含的子串就是最小重复子串)
所以如果 nx % (n - m)x = 0,就可以判定有重复出现的子字符串
如果 next[len - 1] !=0,则说明字符串有最长相同的前后缀(就是字符串里的前缀子串和后缀子串相同的最长长度)。
最长相等前后缀的长度为:next[len - 1]
数组长度为:len。
如果len % (len - (next[len - 1] + 1)) == 0 ,则说明数组的长度正好可以被 (数组长度-最长相等前后缀的长度) 整除 ,说明该字符串有重复的子字符串。
数组长度减去最长相同前后缀的长度相当于是第一个周期的长度,也就是一个周期的长度,如果这个周期可以被整除,就说明整个数组就是这个周期的循环。
class Solution {
public:
void getnext(int* next,const string& s){
next[0]=0;
int j=0;
for(int i=1;i<s.size();i++){ //从1开始,因为next【0】已经被初始化为0
while(j>0&&s[i]!=s[j]){
j=next[j-1];
}
if(s[i]==s[j]){
j++;
}
next[i]=j;
}
}
bool repeatedSubstringPattern(string s) {
if(s.size()==0){
return false;
}
int next[s.size()];
getnext(next,s);
int len=s.size();
if(next[len-1]!=0 && (len%(len-(next[len-1]))==0)){
return true;
}
return false;
}
};
时间复杂度: O(n)
空间复杂度: O(n)