Leetcode学习之字符串

字符串学习

1. 反转字符串

题目链接

编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。

不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。

你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。

示例 1:
输入:[“h”,“e”,“l”,“l”,“o”]
输出:[“o”,“l”,“l”,“e”,“h”]

示例 2:
输入:[“H”,“a”,“n”,“n”,“a”,“h”]
输出:[“h”,“a”,“n”,“n”,“a”,“H”]

1.1 思路

直接reverse函数解决问题!!! 最好还是不要啦!!!

但是也不是说库函数就不能用,是要分场景的。

如果在现场面试中,我们什么时候使用库函数,什么时候不要用库函数呢?

如果题目关键的部分直接用库函数就可以解决,建议不要使用库函数。

毕竟面试官一定不是考察你对库函数的熟悉程度, 如果使用python和java 的同学更需要注意这一点,因为python、java提供的库函数十分丰富。

如果库函数仅仅是 解题过程中的一小部分,并且你已经很清楚这个库函数的内部实现原理的话,可以考虑使用库函数。

建议大家平时在leetcode上练习算法的时候本着这样的原则去练习,这样才有助于我们对算法的理解。

不要沉迷于使用库函数一行代码解决题目之类的技巧,不是说这些技巧不好,而是说这些技巧可以用来娱乐一下。

真正自己写的时候,要保证理解可以实现是相应的功能。

接下来再来讲一下如何解决反转字符串的问题。

大家应该还记得,我们已经讲过了206.反转链表 (opens new window)。

在反转链表中,使用了双指针的方法

那么反转字符串依然是使用双指针的方法,只不过对于字符串的反转,其实要比链表简单一些。

因为字符串也是一种数组,所以元素在内存中是连续分布,这就决定了反转链表和反转字符串方式上还是有所差异的。

对于字符串,我们定义两个指针(也可以说是索引下表),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素。

以字符串hello为例,过程如下:

在这里插入图片描述

1.2 代码

class Solution{
public:
	void reverseString(vector<char> & s){
		int left = 0, right = s.size()-1;
		while (right > left){
			swap(s[left++], s[right--]);
		}
	}
};

2. 反转字符串II

题目链接

给定一个字符串 s 和一个整数 k,你需要对从字符串开头算起的每隔 2k 个字符的前 k 个字符进行反转。

如果剩余字符少于 k 个,则将剩余字符全部反转。

如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。

示例:

输入: s = “abcdefg”, k = 2
输出: “bacdfeg”

2.1 思路

这道题目其实也是模拟,实现题目中规定的反转规则就可以了。

一些同学可能为了处理逻辑:每隔2k个字符的前k的字符,写了一堆逻辑代码或者再搞一个计数器,来统计2k,再统计前k个字符。

其实在遍历字符串的过程中,只要让 i += (2 * k),i 每次移动 2 * k 就可以了,然后判断是否需要有反转的区间。

因为要找的也就是每2 * k 区间的起点,这样写,程序会高效很多。

所以当需要固定规律一段一段去处理字符串的时候,要想想在在for循环的表达式上做做文章。

2.2 代码

class Solution{
public:
	void reverseStr(string& s, int k){
		int len = s.size();
		for (int i = 0; i < len; i+=2*k){
			reverse(s.begin()+i, s.bein()+min(i+k, len));
		}
	}
};

3 替换空格

请实现一个函数,把字符串 s 中的每个空格替换成"%20"。

示例 1: 输入:s = “We are happy.”
输出:“We%20are%20happy.”

3.1 思路

如果想把这道题目做到极致,就不要只用额外的辅助空间了!

首先扩充数组到每个空格替换成"%20"之后的大小。

然后从后向前替换空格,也就是双指针法,过程如下:

i指向新长度的末尾,j指向旧长度的末尾。

请添加图片描述
有同学问了,为什么要从后向前填充,从前向后填充不行么?

从前向后填充就是O(n^2)的算法了,因为每次添加元素都要将添加元素之后的所有元素向后移动。

其实很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。

这么做有两个好处:

不用申请新数组。
从后向前填充元素,避免了从前先后填充元素要来的 每次添加元素都要将添加元素之后的所有元素向后移动。

3.2 代码

class Solution {
public:
    string replaceSpace(string s) {
        int count = 0;
        int curr = s.size()-1;
        
        for (char c: s){
            if (c == ' ')
                count++;
        }

        int prev = curr + 2*count;
        s.resize(prev+1);

        while(curr > -1){
            if (s[curr] != ' '){
                s[prev--] = s[curr--];
            }
            else if (s[curr] == ' '){
                s[prev] = '0';
                s[prev-1] = '2';
                s[prev-2] = '%';
                prev -= 3;
                curr--;
            }
        }
        return s;
    }
};

4. 翻转字符串里的单词

题目链接

给定一个字符串,逐个翻转字符串中的每个单词。

示例 1:
输入: “the sky is blue”
输出: “blue is sky the”

示例 2:
输入: " hello world! "
输出: “world! hello”
解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。

示例 3:
输入: “a good example”
输出: “example good a”
解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。

4.1 思路

这道题目可以说是综合考察了字符串的多种操作。

一些同学会使用split库函数,分隔单词,然后定义一个新的string字符串,最后再把单词倒序相加,那么这道题题目就是一道水题了,失去了它的意义。

所以这里我还是提高一下本题的难度:不要使用辅助空间,空间复杂度要求为O(1)。

不能使用辅助空间之后,那么只能在原字符串上下功夫了。

想一下,我们将整个字符串都反转过来,那么单词的顺序指定是倒序了,只不过单词本身也倒叙了,那么再把单词反转一下,单词不就正过来了。

所以解题思路如下:

移除多余空格
将整个字符串反转
将每个单词反转
举个例子,源字符串为:"the sky is blue "

移除多余空格 : “the sky is blue”
字符串反转:“eulb si yks eht”
单词反转:“blue is sky the”
这样我们就完成了翻转字符串里的单词。

4.2 代码

class Solution{
public:
	string reverseWords(string s){
		// 删除头空格
		int slow = 0, fast = 0;
		while (s.size() > 0 && fast < s.size() && s[fast] == ' ') right++;

		// 删除中间空格
		for (; fast < s.size(); fast++){
		//至少有3个字符才能实现数值,
			if (fast > 1 && s[fast] == s[fast-1] && s[fast] == ' ') {
				continue;
			}
			else{
				s[slow++] == s[fast];
			}
		}

		// 删除尾空格,  如果最后有空格,在中间空格的条件下,会保留一个空格个left,可以自己画图看一下
		if (s[left-1] == ' '){
			s.resize(left-1);
		}
		else{
			s.resize(left);
		}

		// 全部翻转
		reverse(s.begin(), s.end());

		//局部翻转
		int left = 0, right = 0;
		while (right < s.size()){
			// 考虑中间空格
			if (s[right] == ' '){
				reverse(s.begin()+left, s.begin()+right);
				left = right+1;
			}
			//最后一个string没有空格,补充条件
			if (right == s.size()-1){
				reverse(s.begin()+left, s.end()):
			}
		}
		return s;
	}
};

5. 左旋转字符串

题目链接

字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。

示例 1:
输入: s = “abcdefg”, k = 2
输出: “cdefgab”

示例 2:
输入: s = “lrloseumgh”, k = 6
输出: “umghlrlose”

限制:
1 <= k < s.length <= 10000

5.1 思路

为了让本题更有意义,提升一下本题难度:不能申请额外空间,只能在本串上操作。

不能使用额外空间的话,模拟在本串操作要实现左旋转字符串的功能还是有点困难的。

那么我们可以想一下上一题目字符串:花式反转还不够! (opens new window)中讲过,使用整体反转+局部反转就可以实现,反转单词顺序的目的。

这道题目也非常类似,依然可以通过局部反转+整体反转 达到左旋转的目的。

具体步骤为:

反转区间为前n的子串
反转区间为n到末尾的子串
反转整个字符串
最后就可以得到左旋n的目的,而不用定义新的字符串,完全在本串上操作。

例如 :示例1中 输入:字符串abcdefg,n=2

如图:

在这里插入图片描述
最终得到左旋2个单元的字符串:cdefgab

思路明确之后,那么代码实现就很简单了

5.2 代码

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

总结

此时我们已经反转好多次字符串了,来一起回顾一下吧。

在这篇文章344.反转字符串,第一次讲到反转一个字符串应该怎么做,使用了双指针法。

然后发现541. 反转字符串II ,这里开始给反转加上了一些条件,当需要固定规律一段一段去处理字符串的时候,要想想在在for循环的表达式上做做文章。

后来在151.翻转字符串里的单词中,要对一句话里的单词顺序进行反转,发现先整体反转再局部反转 是一个很妙的思路。

最后再讲到本题,本题则是先局部反转再 整体反转,与151.翻转字符串里的单词类似,但是也是一种新的思路。

好了,反转字符串一共就介绍到这里,相信大家此时对反转字符串的常见操作已经很了解了。

6. 实现 strStr()

[题目链接](https://leetcode-cn.com/problems/implement-strstr/)

实现 strStr() 函数。

给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。

示例 1: 输入: haystack = “hello”, needle = “ll” 输出: 2

示例 2: 输入: haystack = “aaaaa”, needle = “bba” 输出: -1

说明: 当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。 对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与C语言的 strstr() 以及 Java的 indexOf() 定义相符。

6.1 思路

本题是KMP 经典题目。

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

本篇将以如下顺序来讲解KMP,

  • 什么是KMP
  • KMP有什么用
  • 什么是前缀表
  • 为什么一定要用前缀表
  • 如何计算前缀表
  • 前缀表与next数组
  • 使用next数组来匹配
  • 时间复杂度分析
  • 构造next数组
  • 使用next数组来做匹配
  • 前缀表统一减一 C++代码实现
  • 前缀表(不减一)C++实现
  • 总结
什么是KMP

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

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

KMP有什么用

KMP主要应用在字符串匹配上。

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

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

其实KMP的代码不好理解,一些同学甚至直接把KMP代码的模板背下来。

没有彻底搞懂,懵懵懂懂就把代码背下来太容易忘了。

不仅面试的时候可能写不出来,如果面试官问:next数组里的数字表示的是什么,为什么这么表示?

估计大多数候选人都是懵逼的。

下面就带大家把KMP的精髓,next数组弄清楚。

什么是前缀表

写过KMP的同学,一定都写过next数组,那么这个next数组究竟是个啥呢?

next数组就是一个前缀表(prefix table)。

前缀表有什么作用呢?

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

为了清楚的了解前缀表的来历,我们来举一个例子:

要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。

请记住文本串和模式串的作用,对于理解下文很重要,要不然容易看懵。所以说三遍:

要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。

要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。

要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。

如动画所示:

请添加图片描述
动画里,我特意把 子串aa标记上了,这是有原因的,大家先注意一下,后面还会说道。

可以看出,文本串中第六个字符b 和 模式串的第六个字符f,不匹配了。如果暴力匹配,会发现不匹配,此时就要从头匹配了。

但如果使用前缀表,就不会从头匹配,而是从上次已经匹配的内容开始匹配,找到了模式串中第三个字符b继续开始匹配。

首先要知道前缀表的任务是当前位置匹配失败,找到之前已经匹配上的位置,在重新匹配,此也意味着在某个字符失配时,前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置。

那么什么是前缀表:记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。

最长公共前缀

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

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

正确理解什么是前缀什么是后缀很重要!

那么网上清一色都说 “kmp 最长公共前后缀” 又是什么回事呢?

我查了一遍 算法导论 和 算法4里KMP的章节,都没有提到 “最长公共前后缀”这个词,也不知道从哪里来了,我理解是用“最长相等前后缀” 更准确一些。

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

而最长公共前后缀里面的“公共”,更像是说前缀和后缀公共的长度。这其实并不是前缀表所需要的。

所以字符串a的最长相等前后缀为0。 字符串aa的最长相等前后缀为1。 字符串aaa的最长相等前后缀为2。 等等…。

为什么一定要用前缀表

这就是前缀表那为啥就能告诉我们 上次匹配的位置,并跳过去呢?

回顾一下,刚刚匹配的过程在下标5的地方遇到不匹配,模式串是指向f,如图:

在这里插入图片描述
然后就找到了下标2,指向b,继续匹配:如图:

在这里插入图片描述
以下这句话,对于理解为什么使用前缀表可以告诉我们匹配失败之后跳到哪里重新匹配 非常重要!

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

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

很多介绍KMP的文章或者视频并没有把为什么要用前缀表?这个问题说清楚,而是直接默认使用前缀表。

如何计算前缀表

接下来就要说一说怎么计算前缀表。

如图:

在这里插入图片描述
长度为前1个字符的子串a,最长相同前后缀的长度为0。(注意字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串;后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。

在这里插入图片描述
长度为前2个字符的子串aa,最长相同前后缀的长度为1。
在这里插入图片描述
长度为前3个字符的子串aab,最长相同前后缀的长度为0。以此类推: 长度为前4个字符的子串aaba,最长相同前后缀的长度为1。 长度为前5个字符的子串aabaa,最长相同前后缀的长度为2。 长度为前6个字符的子串aabaaf,最长相同前后缀的长度为0。

那么把求得的最长相同前后缀的长度就是对应前缀表的元素,如图:
在这里插入图片描述
可以看出模式串与前缀表对应位置的数字表示的就是下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。

再来看一下如何利用 前缀表找到 当字符不匹配的时候应该指针应该移动的位置。如动画所示:

在这里插入图片描述
找到的不匹配的位置, 那么此时我们要看它的前一个字符的前缀表的数值是多少。

为什么要前一个字符的前缀表的数值呢,因为要找前面字符串的最长相同的前缀和后缀。

所以要看前一位的 前缀表的数值。

前一个字符的前缀表的数值是2, 所有把下标移动到下标2的位置继续比配。 可以再反复看一下上面的动画。

最后就在文本串中找到了和模式串匹配的子串了。

前缀表与next数组

很多KMP算法的时间都是使用next数组来做回退操作,那么next数组与前缀表有什么关系呢?

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

为什么这么做呢,其实也是很多文章视频没有解释清楚的地方。

其实这并不涉及到KMP的原理,而是具体实现,next数组即可以就是前缀表,也可以是前缀表统一减一(右移一位,初始位置为-1)。

使用next数组来匹配

以下我们以前缀表统一减一之后的next数组来做演示。

有了next数组,就可以根据next数组来 匹配文本串s,和模式串t了。

注意next数组是新前缀表(旧前缀表统一减一了)。

匹配过程动画如下:

请添加图片描述

时间复杂度分析

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

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

为了和力扣题目28.实现strStr保持一致,方便大家理解,以下文章统称haystack为文本串, needle为模式串。

都知道使用KMP算法,一定要构造next数组。

B站学习视频链接:https://www.bilibili.com/video/BV1jb411V78H?from=search&seid=8944714227303953817&spm_id_from=333.337.0.0

相对好理解一些。

6.2 代码

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

class Solution {
public:
    void getNext(vector<int>& next, const string& s){
		// i表示s中前一个指针,j表示后一个指针,i负责从1位置开始遍历s,j负责与i匹配寻找最长公共前后缀并记录返回位置
        int j = -1;
        // i从1开始,从0开始,两个指针重叠,没有意义,会导致无法区别,始终重叠
        for (int i = 1; i < s.size(); i++){
            while (j >= 0 && s[j+1] != s[i]){
                j = next[j]; // 返回对应位置,继续匹配
            }
            if (s[j+1] == s[i]) j++;
            // 对应的位置上相应的返回位置
            next[i] = j;
        }
    }

    int strStr(string haystack, string needle) {
    	// 字符串为0的情况
        if (needle.size() == 0) return 0;
        //初始化
        vector<int> next(needle.size(), -1);
        getNext(next, needle);
        // i表示文本串指针,j表示模式串指针
        int j = -1;
        for (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里出现了模式串t,自己手写一个例子就明白为什么这么判断相等了,记得上面有j++哦,在最后的基础上j是+1的
                return (i - needle.size() + 1);
            }
        }
        return -1;
    }
};

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

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

7. 重复的子字符串

题目链接

7.1 思路

这又是一道标准的KMP的题目。

那么寻找重复子串怎么也涉及到KMP算法了呢?

假设考虑减一情况

这里就要说一说next数组了,next 数组记录的就是最长相同前后缀( 字符串:KMP算法精讲 这里介绍了什么是前缀,什么是后缀,什么又是最长相同前后缀), 如果 next[len - 1] != -1,则说明字符串有最长相同的前后缀(就是字符串里的前缀子串和后缀子串相同的最长长度)。

最长相等前后缀的长度为:next[len - 1] + 1

数组长度为:len。

如果len % (len - (next[len - 1] + 1)) == 0 ,则说明 (数组长度-最长相等前后缀的长度) 正好可以被 数组的长度整除,说明有该字符串有重复的子字符串。

数组长度减去最长相同前后缀的长度相当于是第一个周期的长度,也就是一个周期的长度,如果这个周期可以被整除,就说明整个数组就是这个周期的循环。(如果它是重复的字符串,必定满足该要求)

强烈建议大家把next数组打印出来,看看next数组里的规律,有助于理解KMP算法

举个例子
在这里插入图片描述
next[len - 1] = 7,next[len - 1] + 1 = 8,8就是此时字符串asdfasdfasdf的最长相同前后缀的长度。

(len - (next[len - 1] + 1)) 也就是: 12(字符串的长度) - 8(最长公共前后缀的长度) = 4, 4正好可以被 12(字符串的长度) 整除,所以说明有重复的子字符串(asdf)。

7.2 代码

减一

class Solution{
public:
	bool repeatedSubstringPattern(string s){
		// next模块
		int j = -1;
		int len = s.size();
		vector<int> next(len, -1);
		for (int i = 1; i < len; i++){
			while (j >= 0 && s[i] != s[j]){
				j = next[j];
			}
			if (s[i] == s[j+1]) j++;
			next[i] = j;
		}
	
		if ((s[len-1] != -1) && (len % (len - next[len-1] - 1) == 0))
			return true;
		else
			return false;
	}
};

总结

其实我们已经学习了十天的字符串了,从字符串的定义到库函数的使用原则,从各种反转到KMP算法,相信大家应该对字符串有比较深刻的认识了。

那么这次我们来做一个总结。

什么是字符串

字符串是若干字符组成的有限序列,也可以理解为是一个字符数组,但是很多语言对字符串做了特殊的规定,接下来我来说一说C/C++中的字符串。

在C语言中,把一个字符串存入一个数组时,也把结束符 '\0’存入数组,并以此作为该字符串是否结束的标志。

例如这段代码:

char a[5] = "asd";
for (int i = 0; a[i] != '\0'; i++) {
}

在C++中,提供一个string类,string类会提供 size接口,可以用来判断string类字符串是否结束,就不用’\0’来判断是否结束。

例如这段代码:

string a = "asd";
for (int i = 0; i < a.size(); i++) {
}

那么vector< char > 和 string 又有什么区别呢?

其实在基本操作上没有区别,但是 string提供更多的字符串处理的相关接口,例如string 重载了+,而vector却没有。

所以想处理字符串,我们还是会定义一个string类型。


要不要使用库函数

在文章344.反转字符串 (opens new window)中强调了打基础的时候,不要太迷恋于库函数。

甚至一些同学习惯于调用substr,split,reverse之类的库函数,却不知道其实现原理,也不知道其时间复杂度,这样实现出来的代码,如果在面试现场,面试官问:“分析其时间复杂度”的话,一定会一脸懵逼!

所以建议如果题目关键的部分直接用库函数就可以解决,建议不要使用库函数。

如果库函数仅仅是 解题过程中的一小部分,并且你已经很清楚这个库函数的内部实现原理的话,可以考虑使用库函数。


双指针法

344.反转字符串 (opens new window),我们使用双指针法实现了反转字符串的操作,双指针法在数组,链表和字符串中很常用。

接着在字符串:替换空格 (opens new window),同样还是使用双指针法在时间复杂度O(n)的情况下完成替换空格。

其实很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。

那么针对数组删除操作的问题,其实在27. 移除元素 (opens new window)中就已经提到了使用双指针法进行移除操作。

同样的道理在151.翻转字符串里的单词 (opens new window)中我们使用O(n)的时间复杂度,完成了删除冗余空格。

一些同学会使用for循环里调用库函数erase来移除元素,这其实是O(n^2)的操作,因为erase就是O(n)的操作,所以这也是典型的不知道库函数的时间复杂度,上来就用的案例了。


翻转字符

在反转上还可以在加一些玩法,其实考察的是对代码的掌控能力。

541.反转字符串II (opens new window)中,一些同学可能为了处理逻辑:每隔2k个字符的前k的字符,写了一堆逻辑代码或者再搞一个计数器,来统计2k,再统计前k个字符。

其实当需要固定规律一段一段去处理字符串的时候,要想想在在for循环的表达式上做做文章。

只要让 i += (2 * k),i 每次移动 2 * k 就可以了,然后判断是否需要有反转的区间。

因为要找的也就是每2 * k 区间的起点,这样写程序会高效很多。

在151.翻转字符串里的单词 (opens new window)中要求翻转字符串里的单词,这道题目可以说是综合考察了字符串的多种操作。是考察字符串的好题。

这道题目通过 先整体反转再局部反转,实现了反转字符串里的单词。

后来发现反转字符串还有一个牛逼的用处,就是达到左旋的效果。

在字符串:反转个字符串还有这个用处? (opens new window)中,我们通过先局部反转再整体反转达到了左旋的效果。


KMP

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

KMP的精髓所在就是前缀表,在KMP精讲 (opens new window)中提到了,什么是KMP,什么是前缀表,以及为什么要用前缀表。

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

那么使用KMP可以解决两类经典问题:

匹配问题:28. 实现 strStr()(opens new window)
重复子串问题:459.重复的子字符串(opens new window)
再一次强调了什么是前缀,什么是后缀,什么又是最长相等前后缀。

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

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

然后针对前缀表到底要不要减一,这其实是不同KMP实现的方式,我们在KMP精讲 (opens new window)中针对之前两个问题,分别给出了两个不同版本的的KMP实现。

其中主要理解j=next[x]这一步最为关键!


总结

字符串类类型的题目,往往想法比较简单,但是实现起来并不容易,复杂的字符串题目非常考验对代码的掌控能力。

双指针法是字符串处理的常客。

KMP算法是字符串查找最重要的算法,但彻底理解KMP并不容易,我们已经写了五篇KMP的文章,不断总结和完善,最终才把KMP讲清楚。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fighting_1997

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值