代码随想录day09|151.反转字符串中的单词、右旋字符串

提示:DDU,供自己复习使用。欢迎大家前来讨论~


字符串Part02

学习字符串的基础操作。

一、题目

题目一:151.反转字符串中的单词

leetcode题目链接

正常思路:

采用split库函数,将单词分割出来,然后倒序输入到一个新的字符串中。

增加题目难度,不要使用辅助空间,空间复杂度要求为O(1)。(为了不使用split库函数,只能在原字符串上下功夫了)

解题思路:

  • 移除字符串中的多余空格

  • 反转字符串

  • 反转字符串中的单词

举个例子,源字符串为:"the sky is blue "

移除多余空格 : “the sky is blue”
字符串反转:“eulb si yks eht”
单词反转:“blue is sky the”

细节:

删除元素,是可以在O(n)的时间复杂度实现,使用双指针。

erase()函数的本身的时间复杂度就是O(n),再嵌套一个for循环的情况下就是O(n^2)。

class Solution {
public:
    void reverseString(vector<char>& s) {
        for (int i = 0, j = s.size() - 1; i < s.size() / 2; i++, j--) {
            swap(s[i], s[j]); //这里使用了函数,也可以使用中间变量进行两个数的交换
        }
    }
};
  • 时间复杂度: O(n)
  • 空间复杂度: O(1)

题目二: 右旋字符串

题目链接

​ 为了让本题更有意义,提升一下本题难度:不能申请额外空间,只能在本串上操作。 (Java不能在字符串上修改,所以使用java一定要开辟新空间)

整体反转+局部反转就可以实现反转单词顺序的目的。

解题思路:

  1. 字符串分割:将字符串视为两部分,右移n位意味着将后n个字符移到前部。例如,n=2时,后两个字符成为新的前缀。

  2. 初步倒序:实现右移,可以先将整个字符串倒序,这样原本的第二段变为开头,第一段移到末尾,而字符的相对位置保持不变。

  3. 局部倒序:经过初步倒序后,虽然各段的位置正确,但字符顺序颠倒了。接下来,分别对这两段进行倒序操作,以恢复字符的正确顺序。

通过这三步,我们能有效地将字符串向右循环移动n位,同时保持字符的内部顺序。

通过 整体倒叙,把两段子串顺序颠倒,两个段子串里的的字符在倒叙一把,负负得正,这样就不影响子串里面字符的顺序了。

代码细节:reverse 函数接受两个迭代器作为参数,表示要反转的序列的范围。这个范围是==左闭右开。==

1.第一种实现方法:

先整体反转,在局部反转。

// 版本一
#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;

} 

2.第二种实现方式(也是两种):

先局部反转,再整体反转。

细节注意:

区间一定要留意。

s.begin()s.end() 是两个迭代器,它们分别指向字符串 s 的第一个字符的开始位置字符串末尾的下一个位置(即字符串的结束之后的位置)。

// 版本2.1
#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()); // 再反转后一段
    reverse(s.begin(), s.end()); // 整体反转
    

    cout << s << endl;

} 
// 版本2.2
#include<iostream>
#include<algorithm>
using namespace std;
int main() {
    int n;
    string s;
    cin >> n;
    cin >> s;
    int len = s.size(); //获取长度
    reverse(s.end() - n , s.end()); // 先反转后一段,n,注意这里是和版本一的区别
    reverse(s.begin(), s.end() - n); // 再反转前一段
    reverse(s.begin(), s.end()); // 整体反转
    cout << s << endl;

}

最后:剑指offer的题目是左反转,那么左反转和右反转 有什么区别呢?

其实思路是一样一样的,就是反转的区间不同而已。

代码如下:

#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;

}

为啥能移动到b的原因:你已经知道了f前面的字符都匹配上了

那么1.文本串不会从第二个a开始,而是可以直接从当前的b开始

2.模式串也不需要从第一个a开始,而是从b开始,因为知道文本串前面的一定是aa

二、 KMP算法的基础理论

在这里插入图片描述

思路:

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

将以如下顺序来讲解KMP,需要二刷,第一遍先大概知道。

  • 什么是KMP:

    说到KMP,先说一下KMP这个名字是怎么来的,为什么叫做KMP呢。

    因为是由这三位学者发明的:Knuth,Morris和Pratt,所以取了三位学者名字的首字母。所以叫做KMP

  • KMP有什么用:

    当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了

    所以如何记录已经匹配的文本内容,是KMP的重点,也是next数组肩负的重任。

  • 什么是前缀表:

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

    记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀

  • 为什么一定要用前缀表:

    前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力。

  • 如何计算前缀表:

    下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。

  • 前缀表与next数组

    next数组就可以是前缀表,但是很多实现都是把前缀表统一减一(右移一位,初始位置为-1)之后作为next数组。

  • 使用next数组来匹配

  • 时间复杂度分析

    整个KMP算法的时间复杂度是O(n+m)的。

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

  • 构造next数组

  • 使用next数组来做匹配

  • 前缀表统一减一 C++代码实现

  • 前缀表(不减一)C++实现

  • 总结

精髓:

只是比较模式串,比较指针i永不回退。

前缀来到后缀相同的位置

代码实现

next数组:

a a b a a f

0 1 0 1 2 0 原始的最长相等的前后缀

-1 0 1 0 1 2 初始化第一个为-1,其他的右移操作

-1 0 -1 0 1 -1 在原始的基础上进行减一

求next[]数组的步骤

  • 初始化

  • 前后缀相同

  • 前后缀不同

  • 更新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]
        }
    }
    

    关于指针回溯求next的理解

    每次求next【i】,可看作前缀与后缀的一次匹配,在该过程中就可以用上之前所求的next,若匹配失败,则像模式串与主串匹配一样,将指针移到next【j-1】上。

    求next过程实际上是dp(动态规划),只与前一个状态有关:

    若不匹配,一直往前退到0或匹配为止

    若匹配,则将之前的结果传递: 因为之前的结果不为0时,前后缀有相等的部分,所以j所指的实际是与当前值相等的前缀,可视为将前缀从前面拖了过来,就不必将指针从前缀开始匹配了,所以之前的结果是可以传递的。

三、字符串总结

1.什么是字符串?
字符串是若干字符组成的有限序列,也可以理解为是一个字符数组,把一个字符串存入一个数组时,也把结束符 '\0’存入数组,并以此作为该字符串是否结束的标志。
2.要不要使用库函数?
不要太迷恋于库函数。习惯于调用substr,split,reverse之类的库函数,却不知道其实现原理,也不知道其时间复杂度,这样实现出来的代码,如果在面试现场,面试官问:“分析其时间复杂度”的话,一定会一脸懵逼!所以建议如果题目关键的部分直接用库函数就可以解决,建议不要使用库函数。如果库函数仅仅是 解题过程中的一小部分,并且你已经很清楚这个库函数的内部实现原理的话,可以考虑使用库函数。
3.双指针法
使用双指针法实现了反转字符串的操作,双指针法在数组,链表和字符串中很常用。
很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。
会使用for循环里调用库函数erase来移除元素,这其实是O(n^2)的操作,因为erase就是O(n)的操作,所以这也是典型的不知道库函数的时间复杂度。
4.反转系列
当需要固定规律一段一段去处理字符串的时候,要想想在在for循环的表达式上做做文章。
先整体反转再局部反转,达到了左旋的效果

  • 字符串类类型的题目,往往想法比较简单,但是实现起来并不容易,复杂的字符串题目非常考验对代码的掌控能力。
  • 双指针法是字符串处理的常客。
  • KMP算法是字符串查找最重要的算法。

四、双指针回顾

数组篇

​ 原地移除数组上的元素,我们说到了数组上的元素,不能真正的删除,只能覆盖。

字符串篇

​ 注意这里强调要原地反转。使用双指针法,定义两个指针(也可以说是索引下标),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素。,时间复杂度是O(n)。

链表篇

​ 翻转链表是现场面试,白纸写代码的好题,考察了候选者对链表以及指针的熟悉程度,而且代码也不长,适合在白纸上写。

N数之和篇


总结

  • 字符串章节过了一遍

  • KMP算法需要好好看第二遍,掌握一下代码。

  • 双指针的使用,需要好好掌握。使用情况和使用方法。

1.学完字符串及之前的算法, 感觉自己学的太泛了, 只是听懂了,然后自己在看了解题思路之后,跟着打了一遍代码,提交上去。

2.感觉这样意义不大, 但要掌握住实在太难了,不要灰心,还是要从基础一点点积累, 先掌握“1+1=2”的题目,再进阶吧。

看到这里的未来的各位老板,可不可以留下一句鼓励我的话(随心留,简单一句话,可以是建议、“鸡汤”、学习方法等等)
如果可以请扶我起来,我还能学!!送花送花

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值