代码随想录学习记录——字符串篇

344、反转字符串

这道题比较简单,但是最好还是不要通过直接调用库函数reserve来解决,这样对这道题就没什么理解可言。反转字符串,也就是将头尾对应位置的元素一一交换,那么可以采用双指针法,即一个指针从头开始,一个指针从尾开始,然后不断在中间移动,同时交换元素,代码如下:

class Solution {
public:
    void reverseString(vector& s) {
        int lenStr = s.size() / 2;
        for(int i = 0; i < lenStr; i++ ){
            swap(s[i],s[s.size()- 1 - i]);
        }
    }
};
541、反转字符串2

这道题对于特殊情况的处理比较多,比较繁杂。但是分段处理的问题时就最好在for遍历时做文章,而不是一个个遍历然后一个个加再增加判断条件。下面的代码就是每一次循环i都是直接加上2k,因为就是要每隔2k然后处理前k个,那么只需要在循环判断当前的情况就明确如何处理了。

class Solution {
public:
    string reverseStr(string s, int k) {
        for( int i = 0 ; i < s.size(); i += (2*k)){
            if ( i + k < s.size()){
                //当前剩余的字符大于k个,那么就翻转前k个
                reverse(s.begin() + i, s.begin() + i + k);
            }
            else{
                //当前剩余的字符不大于k个,那么就全部翻转
                reverse(s.begin() + i, s.end());
            }
        }
        return s;
    }
};

这里补充一下reverse函数的用法:

//翻转数组
reverse(a,a+n);//n为数组a的长度
//翻转字符串
reverse(s.begin(),s.end());//用迭代器,区间都是左闭右开,因此用end()
剑指offer05、替换空格

这道题最简单的思路当然是遍历字符串然后每次看到空格就将当前字符串后移,加入“%20”,而这样的问题是每次移动串时间消耗太大,并且在未知空格数量时不知道需要多少额外的空间。那么就同样可以采用双指针的方法:

  • 先统计字符串出现的空格数目,再将字符串变形为目标大小的字符串
  • 第一个指针从原来的字符串的最后面开始循环,第二个指针从变形后的字符串的最后开始循环
  • 如果第一个指针遇到了空格,就从第二个指针开始的位置开始向前逐步填充%20
  • 如果第一个指针遇到的不是空格,那么就将元素移动到第二个指针的位置。
class Solution {
public:
    string replaceSpace(string s) {
        int numP = 0;//统计空格的个数
        for(int i = 0; i < s.size(); i ++){
            if (s[i] == ' '){
                numP++;
            }
        }
        int sOldSize = s.size();
        s.resize(s.size() + numP * 2);
        int sNewSize = s.size();
        for(int i = sNewSize - 1, j = sOldSize - 1; j < i; i--,j--){
            if(s[j] != ' '){
                s[i] = s[j];
            }else{
                s[i] = '0';
                s[i-1] = '2';
                s[i-2] = '%';
                i -= 2;
            }
        }
        return s;
    }
};
151、反转字符串中的单词

这道题比较有难度,其首要的思想就是想将整个字符串反转过来,再逐个将里面的单词反转成原来的顺序。那么主要的问题就是如何去除多余的空格以及如何将单词逐个反转

class Solution {
public:
    void removeExtraSpaces(string &s){
        int slow = 0;
        for(int i = 0; i < s.size() ; i++){
            if( s[i] != ' '){
                if(slow != 0){ //说明前面刚填充了一个单词,要补充一个空格
                    s[slow++] = ' ';
                }
                while( i < s.size() && s[i] != ' '){
                    s[slow++] = s[i++];
                }
            }
        }
        s.resize(slow);
    }
    
    void reverse(string & s, int start, int end){//这里定义的是左闭右闭的区间
        for(int i = start, j = end; i < j ; i++,j--){
            swap(s[i],s[j]);
        }
    }

    string reverseWords(string s) {
        removeExtraSpaces(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);//由于定义是右闭,而i是走到单词结束的下一个才判断出来,因此i-1
                start = i + 1;
            }
        }
        return s;
    }
};
剑指offer58-左旋转字符串

这道题比较简单,为了避免一个个移动字符,我尝试用时间复杂度为O(n)来解决这个问题,即先将前n个字符用另外的数组存储起来,再利用双指针移动字符,最后再将前n个字符放到后n个位置上,时间复杂度为O(s.size())。但缺点就是利用了额外的空间。

class Solution {
public:
    string reverseLeftWords(string s, int n) {
        vectortemV;
        for(int i = 0; i < n ; i++){
            temV.push_back(s[i]);
        }
        int slowP = 0;
        int fastP = n;
        while(fastP < s.size()){
            s[slowP] = s[fastP];
            slowP++;
            fastP++;
        }
        for(int i = 0; i < n; i ++){
            s[slowP] = temV[i];
            slowP++;
        }
        return s;
    }
};

代码随想录的思路则是完全不需要用额外的空间,直接在原字符串上进行处理,其具体思路为先分别翻转前n个字符和从n+1到结尾的字符,再整个翻转字符串。可见下图理解:

代码随想录学习记录——字符串篇_第1张图片

class Solution {
public:
    string reverseLeftWords(string s, int n) {
        reverse(s.begin(), s.begin() + n);
        reverse(s.begin() + n, s.end());
        reverse(s.begin(), s.end());
        return s;
    }
};
28、实现strStr()

本道题就是KMP算法的经典题目,因此需要先彻底了解KMP算法的原理及各种细节。

KMP算法的思想就是当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配,降低时间复杂度

1、什么是前缀表

前缀表即代码中出现的next数组,其作用是用来记录模式串(短串)与主串(长串)不匹配的时候,模式串应该从哪里开始重新匹配,因此是用来实现回退操作的

例如在文本串:aabaabaafa中查找模式串:aabaaf

那么在第一次寻找时两者从头开始一一比较,当比较到第六个字符发现字符b和字符f不一致,那么如果是暴力解法就将模式串往后移动一个位置然后继续从头开始比较。但是KMP算法是找到了模式串第三个字符b然后继续与文本串的字符比较,就不用从头开始

那么前缀表是如何记录才能够实现这种跳转呢?:其实前缀表记录的就是最长的相等前后缀。即记录下标i之前(包括i)的字符串中有多大长度的相同前缀后缀

2、最长相等前后缀

这句首先要理解什么是前后缀:

  • 前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串,例如在字符串abcd中,就有{a,ab,abc}
  • 后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串,例如在字符串abcd中,就有{d,cd,bcd}

那么前缀表中记录的最长相等前后缀,就是要求前后缀相等且具有最大长度,例如字符串abcab的最长相等前后缀为2,字符串aaa的最长相等前后缀为2。

3、前缀表记录最长相等前后缀如何告知下次开始匹配的位置

回到刚才的例子:文本串:aabaabaafa中查找模式串:aabaaf,第一次找到的不匹配的位置为索引为5的位置,也就是在模式串中指向f,此时有最长相等前后缀aa,如下图:

代码随想录学习记录——字符串篇_第2张图片

那么为什么可以直接将模式串的指针移动到索引为2的b呢

因为最长相等前后缀为aa,那么直接将前缀的aa移动到后缀的aa的位置,即字符串的第一个字符对应到第四个字符,肯定aa这个前后缀串的字符是可以匹配上的,接下来只需要往后匹配即可

那么有没有可能不是最长前后缀匹配,而是例如将模式串的第一个字符匹配到文本串的第四个字符之前的位置呢?,这是不可能的,否则最长相等前后缀就不可能只有aa,可以尝试将模式串往后移动直到第一个字符对应上文本串的第三个字符b,可以发现这个时候是匹配不上的。

4、如何计算前缀表

以上面那个模式串为例:

代码随想录学习记录——字符串篇_第3张图片

从头开始:

  • 第一个前缀串:a,那么其最长相等公共前后缀为0
  • 第二个:aa,其最长相等前后缀为1
  • 第三个:aab,为0
  • 第四个:aaba,为1
  • 第五个:aabaa,为2
  • 第六个:aabaaf,为0

因此可以看出,模式串与前缀表对应位置的数组表示的是:下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀

因此找到不匹配的位置时,就要查看不匹配位置的前一个字符的索引在前缀表中的数值是多少,然后模式串进行匹配的指针就移动到该数值对应的索引位置,例如上面匹配到f发现不匹配,而前一个字符对应在前缀表中数值为2,因此移动到索引为2的b继续匹配。

那么**为什么要前一个字符的前缀表的数值呢?**因为我们在当前这个位置不匹配(即文本串中索引为5的位置),我们要找出模式串中哪一个字符应该来与当前这个不匹配的字符进行比较,因此不能考虑现在这个字符的前后缀,而应该找前面一个字符串对应的最长相同前后缀。

5、前缀表与next数组

next数组实际上和前缀表意义相同,只不过很多实现都是将前缀表的每一个元素减一之后作为next数组。但这并不涉及到KMP算法的原理,只是为了是线上的方便,当然也可以用前缀表来实现

6、构造next数组

先定义一个函数getNext来构建next数组,如下:

void getNext(int* next, const string& s){
    int j = -1;
    next[0] = j; // 完成初始化工作
    for(int i = 1; i < s.size(); i++) { // 注意i从1开始
        while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了
            j = next[j]; // 向前回退
        }
        if (s[i] == s[j + 1]) { // 找到相同的前后缀
            j++;
        }
        next[i] = j; // 将j(前缀的长度)赋给next[i]
    }
}

该函数完成的功能就是计算模式串s的前缀表,具体的代码解释如下:

  1. 首先是初始化,定义指针 i i i j j j,其中 j j j指向当前前缀末尾的位置,而 i i i指向后缀末尾的位置。那么 j j j初始化为-1,是为了对应前文说前缀表统一减一作为next的操作,而next[i]表示i(包括i)之前最长相等的前后缀长度,因此初始化next[0]=j,因此单独一个元素没有前后缀而言

  2. 处理前后缀不相同的情况:第一个寻找前后缀的子串应该是s[0]和s[1]组成的子串,那么初始化应该i=1,那么就是s[i]和s[j+1]进行比较。如果s[i]与s[j+1]不相同了,说明当前s[i]这个字符要回退,那么回退到哪里呢?回退到s[i-1]对应的相等前后缀的前缀的下一个,可以看下图来理解:

代码随想录学习记录——字符串篇_第4张图片

i = 12 , j + 1 = 8 i=12,j+1=8 i=12,j+1=8时此时发现不相等,我们可以发现s[i]前面的aabc和从头开始的aabc相等,因此 j + 1 j+1 j+1就要回退到第一个aabc 的下一个,来和 s [ i ] s[i] s[i]比较,如果相等还是可以继续续上相等前后缀的,那么也可以看到第一次回归 j + 1 j+1 j+1时并不相等,那么就再回退一次,因此就回退到开头,从头开始比较了。那么下一个问题就是我们知道应该回退到哪里了,缩印在哪里呢?,其实回退位置的索引就是在next[j]里,因为我们要找** j + 1 j+1 j+1前一个位置的公共前缀的长度**。

  1. 处理前后缀相同的情况:前后缀都相同了,那么肯定是接着往下寻找,继续匹配前后缀,因此要 j + + j++ j++,同时还要将当前 j j j(也就是前缀的长度)赋予给next[i]作为记录
7、使用next数组来做匹配

有了next数组,就可以同样定义两个指针i和j来依次比较两个串的内容了,不过这里同样是比较s[i]和t[j+1],因为next数组中记录的起始位置为-1,因此要匹配。

如果比较相同,那么同样i和j一起往后移动,如果比较不相等那么就利用前述的方法进行回退即可。同时还要注意判断j是否已经走到了模式串的末尾,来判断比较过程是否应结束。因此完整的代码如下:

class Solution {
public:
    void getNext(int* next, const string& s){
        int j = -1;
        next[0] = j;
        for(int i = 1; i < s.size(); i ++){
            while( j >= 0 && s[i] != s[j+1]){
                j = next[j];
            }
            if(s[i] == s[j+1]){
                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 = -1;
        for(int i = 0; i = 0 && haystack[i] != needle[j + 1]){
                j = next[j];
            }
            if(haystack[i] == needle[j + 1]){
                j++;
            }
            if( j == (needle.size() - 1)){
                return( i - needle.size() + 1);
            }
        }
        return -1;
    }

};

如果不要前缀表统一减一也是可以实现的,那么只需要一些细节问题处理好即可;

class Solution {
public:
    void getNext(int* next, const string& s){
        int j = 0;//由于不减一了那么初始化为0
        next[0] = j;//这里同理
        for(int i = 1; i < s.size(); i++){ // 这里i同样是从1开始的
            while( j > 0 && s[i] != s[j]){
                j = next[j - 1];//因为这里是j-1,因此j要大于0
            }
            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;
    }

};
459、重复的子字符串

这道题也是看了以下代码随想录的思想才完全透彻,主要思想就是如果一个字符串A可以由其子串来构成,那么把两个同样的该字符串A拼在一起,在其中间一定会出现一个字符串A。那么只需要在拼接而成的串中查找A即可。但为了防止找到原来拼接的那两个原先的字符串A,需要将首字符和尾字符进行剔除,因此具体代码如下:

class Solution {
public:
    bool repeatedSubstringPattern(string s) {
        string t = s + s;
        t.erase(t.begin());//移除首字符
        t.erase(t.end()- 1);//移除尾字符,主要end()是结尾字符的下一个位置
        if (t.find(s) != std::string::npos){
            return true;
        }
        return false;
    }
};

这里补充一下:

std::string::npos

是一个常数,代表该字符串的长度的最大值,用来表示不存在的位置,其实可以理解成find函数找不到会返回这个不存在的位置

接下来便是利用KMP来求解,这里也是比较难以理解的地方。

最重要是要能够理解:在由重复子串组成的字符串中,最长相等前后缀所差的那部分子串就是最小重复子串,例如下图:

代码随想录学习记录——字符串篇_第5张图片

那么为什么呢?下面的解释需要始终记住s是由重复子串构成的,设前缀子串为front,后缀子串为rear,即:

  1. 首先,因为是相同的前后缀,因此有 f r o n t [ 0 ] = = r e a r [ 0 ] front[0] == rear[0] front[0]==rear[0] f r o n t [ 1 ] = = r e a r [ 1 ] front[1] == rear[1] front[1]==rear[1],那么从上图的位置中也可以看出必有 s [ 0 ] = = s [ 2 ] s[0]==s[2] s[0]==s[2] s [ 1 ] = = s [ 3 ] s[1]==s[3] s[1]==s[3]。那么就是 s [ 0 ] s [ 1 ] = = s [ 2 ] s [ 3 ] s[0]s[1]==s[2]s[3] s[0]s[1]==s[2]s[3]
  2. 而在往后,则有 f r o n t [ 2 ] = = r e a r [ 2 ] front[2]==rear[2] front[2]==rear[2] f r o n t [ 3 ] = = r e a r [ 3 ] front[3]==rear[3] front[3]==rear[3],并在由于位置的对应关系,还有 f r o n t [ 2 ] = = r e a r [ 0 ] front[2]==rear[0] front[2]==rear[0], f r o n t [ 3 ] = = r e a r [ 1 ] front[3]==rear[1] front[3]==rear[1]
  3. 因此有 s [ 2 ] s [ 3 ] = = s [ 4 ] s [ 5 ] s[2]s[3] == s[4]s[5] s[2]s[3]==s[4]s[5],那么结合1就有 s [ 0 ] s [ 1 ] = = s [ 2 ] s [ 3 ] = = s [ 4 ] s [ 5 ] s[0]s[1]==s[2]s[3]==s[4]s[5] s[0]s[1]==s[2]s[3]==s[4]s[5].继续往下推,可以得到s就是由 s [ 0 ] s [ 1 ] s[0]s[1] s[0]s[1]组成的。

因此如果一个字符串由重复子串构成那最长相等前后缀所不包含的子串就是最小重复子串

那么现在我们需要逆过来想,需要考虑如果知道了最长相等前后缀,那么如果判断该串是不是由子串构成的重复串呢。此部分仍然需要假设s为重复子串构成来推导其充要条件:

  1. 如果s是由多个重复子串构成的,假设子串的长度为 x x x,而s的长度为 n x nx nx
  2. 而s的最长相等前后缀必然也是由重复子串构成的,设其长度为 m x mx mx,并且根据之前的推导可以得知 n − m = 1 n-m = 1 nm=1
  3. 也就是说如果 n x % ( n − m ) x = = 0 nx \% (n-m)x==0 nx%(nm)x==0,那么s就必然是由重复子串构成的

而在KMP中可得知, n e x t [ s . s i z e ( ) − 1 ] next[s.size() - 1] next[s.size()1]的值就代表最长相等前后缀的长度,因此可以写出如下代码:

class Solution {
public:
    void getNext(int* next, const string& s){
        next[0] = -1;
        int j = -1;
        for(int i = 1; i < s.size(); i++){
            while( j >= 0 && s[i] != s[ j+1]){
                j = next[j];
            }
            if( s[i] == s[j + 1]){
                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] != -1 && len % (len - (next[len-1] + 1))== 0 ){
            return true;
        }
        return false;
    }
};

你可能感兴趣的:(力扣刷题记录,学习,算法,数据结构,leetcode,c++)