当前位置: 首页 > news >正文

代码随想录算法训练营第九天 |【字符串】151.翻转字符串里的单词、卡码网55.右旋转字符串、28.实现strStr、459.重复的子字符串

代码随想录算法训练营第九天 |【字符串】151.翻转字符串里的单词、卡码网55.右旋转字符串、28.实现strStr、459.重复的子字符串

151.翻转字符串里的单词

思路
  • 我的想法是模拟,用状态机拾取单词,然后存到双重数组中,再反向读取输出
  • 状态机有两个状态,IN 和 OUT 如果状态机检测到分隔符,则一定转换为状态OUT,如果未检测到分隔符,则转换为状态IN,单词计数加一切开始
看完代码随想录之后的想法
想多了,三步走法
  • 移除多余空格,快慢指针,有两个版本

    • 版本一分别讨论了前面、中间、尾部的空格去除,比较繁琐
    //版本一 
    void removeExtraSpaces(string& s) {int slowIndex = 0, fastIndex = 0; // 定义快指针,慢指针// 去掉字符串前面的空格while (s.size() > 0 && fastIndex < s.size() && s[fastIndex] == ' ') {fastIndex++;}for (; fastIndex < s.size(); fastIndex++) {// 去掉字符串中间部分的冗余空格if (fastIndex - 1 > 0&& s[fastIndex - 1] == s[fastIndex]&& s[fastIndex] == ' ') {continue;} else {s[slowIndex++] = s[fastIndex];}}if (slowIndex - 1 > 0 && s[slowIndex - 1] == ' ') { // 去掉字符串末尾的空格s.resize(slowIndex - 1);} else {s.resize(slowIndex); // 重新设置字符串大小}
    }
    
    • 版本二是用27.移除元素的逻辑,遇到非空格就处理,手动添加空格,最后补上该单词
    // 版本二 
    void removeExtraSpaces(string& s) {//去除所有空格并在相邻单词之间添加空格, 快慢指针。int slow = 0;   //整体思想参考https://programmercarl.com/0027.移除元素.htmlfor (int i = 0; i < s.size(); ++i) { //if (s[i] != ' ') { //遇到非空格就处理,即删除所有空格。if (slow != 0) s[slow++] = ' '; //手动控制空格,给单词之间添加空格。slow != 0说明不是第一个单词,需要在单词前添加空格。while (i < s.size() && s[i] != ' ') { //补上该单词,遇到空格说明单词结束。s[slow++] = s[i++];}}}s.resize(slow); //slow的大小即为去除多余空格后的大小。
    }
    
  • 将整个字符串反转,见344.反转字符串541.反转字符串IIswap()

    // 反转字符串s中左闭右闭的区间[start, end]
    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; //removeExtraSpaces后保证第一个单词的开始下标一定是0。for (int i = 0; i <= s.size(); ++i) {if (i == s.size() || s[i] == ' ') { //到达空格或者串尾,说明一个单词结束。进行翻转。reverse(s, start, i - 1); //翻转,注意是左闭右闭 []的翻转。start = i + 1; //更新下一个单词的开始下标start}}return s;}

卡码网55.右旋转字符串

思路
  • 用双指针法,暴力每次向右移动一个字符,然后贴到字头
看完代码随想录之后的想法
双重倒叙法
  • 通过 整体倒叙,把两段子串顺序颠倒,两个段子串里的的字符在倒叙一把,负负得正,这样就不影响子串里面字符的顺序了。
# include <iostream>
# include <algorithm>
using namespace std;
int main() {int t; string s;cin >> t;cin >> s;reverse(s.begin(), s.end());reverse(s.begin(), s.begin() + t);reverse(s.end() - t, s.end());return 0;
}
  • 当然还有第二种做法,就是先反转局部,再反转整体

    #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.begin() + n); //  反转第一段长度为n reverse(s.begin() + n, s.end()); // 反转第二段长度为len-n reverse(s.begin(), s.end());  // 整体反转cout << s << endl;
    }
    

28.实现strStr

思路
  • 一个一个遍历,用文本串和模式串暴力匹配,没其他思路了
看完代码随想录之后的想法
KMP经典思想
  • KMP的经典思想就是:当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。

  • 什么是KMP——因为是由这三位学者发明的:Knuth,Morris和Pratt

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

  • 什么是前缀表——next数组

    • 前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。
    • 举例:要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf
    • 前缀表:记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。

KMP详解1

  • 最长公共前后缀

    • 前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串
    • 后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串
  • 为什么一定要用前缀表

    • 下标5之前这部分的字符串(也就是字符串aabaa)的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面重新匹配就可以了。
    • KMP精讲2
  • 如何计算前缀表

    • 下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。
    • KMP精讲8
    • 找到的不匹配的位置, 那么此时我们要看它的前一个字符的前缀表的数值是多少
    • 前一个字符的前缀表的数值是2, 所以把下标移动到下标2的位置继续比配
  • 前缀表与next数组

    • next数组是新前缀表(旧前缀表统一减一了)
    • KMP精讲4
  • 构造next数组——构造next数组其实就是计算模式串s,前缀表的过程

    • 初始化:定义两个指针i和j,j指向前缀末尾位置,i指向后缀末尾位置,next[i] 表示 i(包括i)之前最长相等的前后缀长度(其实就是j)

      • int j = -1;
        next[0] = j;
        
    • 处理前后缀不相同的情况:

      • 因为j初始化为-1,那么i就从1开始,进行s[i] 与 s[j+1]的比较。

      • 如果 s[i] 与 s[j+1]不相同,也就是遇到 前后缀末尾不相同的情况,就要向前回退。

      • next[j]就是记录着j(包括j)之前的子串的相同前后缀的长度。

      • for (int i = 1; i < s.size(); i++) {// 这里不能写成if,因为这是一个连续递归回退的过程,如果不相等要继续回退while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了j = next[j]; // 向前回退}
    • 处理前后缀相同的情况

      • 如果 s[i] 与 s[j + 1] 相同,那么就同时向后移动i 和j 说明找到了相同的前后缀,同时还要将j(前缀的长度)赋给next[i], 因为next[i]要记录相同前后缀的长度。

      • if (s[i] == s[j + 1]) { // 找到相同的前后缀j++;
        }
        next[i] = j;
        
    • 整体代码如下:

      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]}
      }
      
    • 逻辑动画如下:

    • KMP精讲3

  • 使用next数组来匹配——在文本串s里找是否出现过模式串t

    • 定义两个下标j 指向模式串起始位置,i指向文本串起始位置,j初始值依然为-1,因为next数组里记录的起始位置为-1

    • 接下来就是 s[i] 与 t[j + 1] (因为j从-1开始的) 进行比较。

      • 如果 s[i] 与 t[j + 1] 不相同,j就要从next数组里寻找下一个匹配的位置。
      • 如果 s[i] 与 t[j + 1] 相同,那么i 和 j 同时向后移动
      • 如果j指向了模式串t的末尾,那么就说明模式串t完全匹配文本串s里的某个子串了,所以返回当前在文本串匹配模式串的位置i 减去 模式串的长度,就是文本串字符串中出现模式串的第一个位置。
    • 整体代码如下:

      int j = -1; // 因为next数组里记录的起始位置为-1
      for (int i = 0; i < s.size(); i++) { // 注意i就从0开始while(j >= 0 && s[i] != t[j + 1]) { // 不匹配j = next[j]; // j 寻找之前匹配的位置}if (s[i] == t[j + 1]) { // 匹配,j和i同时向后移动j++; // i的增加在for循环里}if (j == (t.size() - 1) ) { // 文本串s里出现了模式串treturn (i - t.size() + 1);}
      }
      
  • 前缀表统一减一代码实现

    class Solution {
    public: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]}}int strStr(string haystack, string needle) {if (needle.size() == 0) {return 0;}vector<int> next(needle.size());getNext(&next[0], needle);int j = -1; // // 因为next数组里记录的起始位置为-1for (int i = 0; i < haystack.size(); i++) { // 注意i就从0开始while(j >= 0 && haystack[i] != needle[j + 1]) { // 不匹配j = next[j]; // j 寻找之前匹配的位置}if (haystack[i] == needle[j + 1]) { // 匹配,j和i同时向后移动j++; // i的增加在for循环里}if (j == (needle.size() - 1) ) { // 文本串s里出现了模式串treturn (i - needle.size() + 1);}}return -1;}
    };
    
  • 前缀表不减一代码实现:

    // 1.next数组
    // 定义j为前缀末尾,设置next第一个为0
    // 循环next数组,当前后缀不相等时,j跳转到前一位的next值,连续跳转
    // 当前后缀相等时,j指针向后移一位,最后next要等与j的值
    // 2.判断模式串和子串
    // 先判断needle的长度不能为0哦,否则返回0
    // 生成next数组,和模式串一样大就行
    // 循环子串,和next数组一样,当前后缀不相等时,j跳转到前一位的next值,连续跳转
    // 当前后缀相等时,j指针向后移一位,最后next要等与j的值
    // 如果j走到了模式数组的最后一位,返回第一位即可,否则返回1
    class Solution {
    public:// next使用指针传递,needle使用引用传递,减少空间占用void getNext(int* next, const string& s) {int j = 0;next[0] = 0;// 注意这里要从i=1开始循环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;}vector<int> next(needle.size());// 这里因为要把vector类型的next传进来,所以用&next[0],当然int数组可以直接传nextgetNext(&next[0], 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;}
    };
    

    这么做有两个变化:

    1. next[j-1]的判断变成了前一位,而且判断也有所变化。
    2. 从后向前填充元素,避免了从前向后填充元素时,每次添加元素都要将添加元素之后的所有元素向后移动的问题。
  • 时间复杂度: O(n + m)

  • 空间复杂度: O(m)

459.重复的子字符串

思路
  • KMP算法,如果有重复子串,next数组中就会有大于等于2的值,计算即可
  • 该想法不对,问题是判断是否完全由重复子串构成,next是计算最大前后缀的
  • 暴力解法是for循环,获取子串,去和主串匹配,时间复杂度O(N2)
  • 字符串结束位置,而且只到中间即可,因为子串长度不可能大于二分之一,而且子串一定是从第一个字符开始的
看完代码随想录之后的想法
移动匹配法
  • 当一个字符串s:abcabc,内部由重复的子串组成,也就是由前后相同的子串组成。

  • 那么既然前面有相同的子串,后面有相同的子串,用 s + s,这样组成的字符串中,后面的子串做前串,前面的子串做后串,就一定还能组成一个s,

  • 在判断 s + s 拼接的字符串里是否出现一个s的的时候,要刨除 s + s 的首字符和尾字符

  • 使用库函数 find,他就是用KMP算法实现的

  • 判断字符串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; // rreturn false;}
    };
    
KMP算法
  • 如果一个字符串s是由重复子串组成,那么 最长相等前后缀不包含的子串一定是字符串s的最小重复子串
  • 最长相等前后缀不包含的子串的长度 可以被 字符串s的长度整除,那么不包含的子串 就是s的最小重复子串。
  • 如果 next[len - 1] != 0,则说明字符串有最长相同的前后缀(就是字符串里的前缀子串和后缀子串相同的最长长度)
  • 最长相等前后缀的长度为:next[len - 1] 。(这里的next数组是以正常的方式计算的,不需要+1
  • len - next[len - 1] 是最长相等前后缀不包含的子串的长度。
  • 如果len % (len - (next[len - 1] + 1)) == 0 ,则说明数组的长度正好可以被 最长相等前后缀不包含的子串的长度 整除 ,说明该字符串有重复的子字符串。
// 可以用移动匹配法,也可以用KMP法class Solution {
public:// 移动匹配法// 先拼接+,再去头尾erase,最后查找子串find std::string::nposbool repeatedSubstringPattern2(string s) {string t = s + s;t.erase(t.begin());t.erase(t.end() - 1);if (t.find(s) != std::string::npos) return true;return false;}// KMP法// 算next数组,如果next数组最后一位不等于0,且长度能被最长相等前后缀不包含的字串的长度整除,那这就是个重复的字符串void getNext(int* next, const string& s) {int j = 0;next[0] = j;// 注意这里i初始化为1,错两次了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;}}bool repeatedSubstringPattern(string s) {if (s.size() <= 0) return false;// 要判断s大小别是0了vector<int> next(s.size());getNext(&next[0], s);int len = next.size();if (next[len - 1] != 0 && (len % (len - next[len - 1])) == 0) {return true;}return false;}
};

字符串总结

字符串基础类
  • 在C语言中,把一个字符串存入一个数组时,也把结束符 '\0’存入数组,并以此作为该字符串是否结束的标志。
  • 在C++中,提供一个string类,string类会提供 size接口,可以用来判断string类字符串是否结束,就不用’\0’来判断是否结束。
  • vector< char > 和 string 又有什么区别呢?
    • string提供更多的字符串处理的相关接口,例如string 重载了+,而vector却没有。
    • 所以想处理字符串,还是会定义一个string类型
  • 打基础的时候,不要太迷恋于库函数
双指针法
  • 使用双指针法实现了反转字符串的操作,双指针法在数组,链表和字符串中很常用。
  • 在字符串:替换空格 (opens new window),其实很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作
  • 151.翻转字符串里的单词 (opens new window)中我们使用O(n)的时间复杂度,完成了删除冗余空格
  • 一些同学会使用for循环里调用库函数erase来移除元素,这其实是O(n^2)的操作,因为erase就是O(n)的操作
反转系列
  • 通过 先整体反转再局部反转,实现了反转字符串里的单词
  • 通过先局部反转再整体反转达到了左旋的效果
KMP
  • KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了

  • 前缀表:起始位置到下标i之前(包括i)的子串中,有多大长度的相同前缀后缀

  • 前缀:指不包含最后一个字符的所有以第一个字符开头的连续子串。

    后缀:指不包含第一个字符的所有以最后一个字符结尾的连续子串。

  • 理解j=next[x-1]这一步最为关键

遇到困难

  • 一些字符串操作的库函数,需要总结

今日收获

  • 今日打卡终于赶上了进度,加油

相关文章:

  • 星纪魅族新品发布会定档5月13日,Note 16系列战神归来
  • 第七章,VLAN技术
  • 驱动开发系列57 - Linux Graphics QXL显卡驱动代码分析(四)显示区域更新
  • C#中从本地(两个路径文件夹)中实时拿图显示到窗口中并接收(两个tcp发送的信号)转为字符串显示在窗体中实现检测可视化
  • 【C语言】推箱子小游戏
  • C++ 构造函数
  • # YOLOv1:开启实时目标检测的新时代
  • tp8+swoole搭建
  • 如何提升丢包网络环境下的传输性能:从 TCP 到 QUIC,再到 wovenet 的实践
  • OpenGl实战笔记(2)基于qt5.15.2+mingw64+opengl实现纹理贴图
  • 【C++核心技术深度解析:从继承多态到STL容器 】
  • Nacos源码—4.Nacos集群高可用分析四
  • Python爬虫+代理IP+Header伪装:高效采集亚马逊数据
  • 【AI知识库云研发部署】RAGFlow + DeepSeek
  • python打卡day17
  • 解决android studio 中gradle 出现task list not built
  • 使用Unsloth微调DeepSeek-R1蒸馏模型:低显存高效训练实践
  • 效率提升利器:解锁图片处理新姿势
  • x-cmd install | Tuistash - Logstash 实时监控,告别图形界面,高效便捷!
  • 餐饮部绩效考核管理制度与综合评估方法
  • 公积金利率降至历史低位,百万房贷30年省5万
  • 世界银行最新营商环境体检单:59个测评点,上海22项达全球最优水平
  • 涉个人信息收集使用问题,15款App和16款SDK被通报
  • 【社论】跑赢12级狂风,敦煌做对了什么
  • 沪幼升小网上报名明起开始,是否参与民办摇号怎么定?
  • 从陈毅杯到小应氏杯,五一长假上海掀起围棋热