代码随想录刷题4--字符串

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

提示:这里可以添加本文要记录的大概内容:
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,

  1. 什么是KMP
    由三位发明学者的首字母

  2. KMP有什么用
    当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。

  3. 什么是前缀表
    next数组就是一个前缀表,记录下标i之前(包括i)的字符串长,有多大长度的相同前缀后缀

前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配

最长相等前后缀的后缀的后面是不匹配字符,跳到与后缀相等的前缀的后一个字母重新开始匹配(重新匹配的位置:就是最长相等前后缀的长度)

要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。
可以看出,文本串中第六个字符b 和 模式串的第六个字符f,不匹配了。如果暴力匹配,发现不匹配,此时就要从头匹配了。
但如果使用前缀表,就不会从头匹配,而是从上次已经匹配的内容开始匹配,找到了模式串中第三个字符b继续开始匹配

前缀表的作用:
跳到不匹配字母的前面的最长相等前后缀

前缀表是如何记录的?
首先要知道前缀表的任务是当前位置匹配失败,找到之前已经匹配上的位置,再重新匹配,此也意味着在某个字符失配时,前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置
也就是不匹配字符的位置为i,找到前缀表的i-1位置对应的数,就是最长相等前后缀,在跳到该最长相等前后缀的前缀的后一位,重新开始匹配。 这时[0,i-1]位置上的字符是匹配好的

前缀表要求的就是相同前后缀的长度

前缀表里,统计了各个位置为终点字符串的最长相同前后缀的长度。

  1. 为什么一定要用前缀表
    在这里插入图片描述
    下标5之前这部分的字符串(也就是字符串aabaa)的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面重新匹配就可以了。
    所以前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力。

  2. 如何计算前缀表
    要求的是最长相等前后缀
    前缀:是指不包含最后一个字符的所有以第一个字符开头的连续子串。例如:a(0)、aa(1)、aab(0)、aaba(1)、aabaa(2)
    后缀:不包含第一个字符的所有以最后一个字符结尾的连续字串。例如:f、af、aaf、baaf、abaaf
    最长相等前后缀是2,所以跳到字符串s[2]位置重新进行匹配

先分析长度为前1、2、3、4、5个字符的字串的有多大长度的相同前后缀,组成前缀表

在这里插入图片描述

  1. 前缀表与next数组
    next数组就可以是前缀表,但是很多实现都是把前缀表统一减一(右移一位,初始位置为-1)之后作为next数组。
    第一行前缀表
    第二行前缀表右移一位
    第三行前缀表-1
    都可以作为next数组去计算
    在这里插入图片描述

  2. 使用next数组来匹配

  3. 时间复杂度分析
    其中n为文本串长度,m为模式串长度,因为在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n),之前还要单独生成next数组,时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)的。

暴力的解法显而易见是O(n × m),所以KMP在字符串匹配中极大地提高了搜索的效率。

  1. 构造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)

  • 28
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值