字符串相关题目

本文探讨了多种字符串处理的编程技巧,包括反转字符串、字符串翻转与替换、KMP算法及其应用。通过实例展示了如何使用双指针、双端队列等数据结构解决问题,并深入解析了KMP算法的前缀表构建与模式匹配过程。此外,还涉及了如何判断字符串是否由重复子串组成。这些内容涵盖了字符串操作的基础与进阶知识。
摘要由CSDN通过智能技术生成

1、指针相关

344. 反转字符串.
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。
必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
示例 1:
输入:[“h”,“e”,“l”,“l”,“o”]
输出:[“o”,“l”,“l”,“e”,“h”]

双指针即可

class Solution:
    def reverseString(self, s: List[str]) -> None:
        """
        Do not return anything, modify s in-place instead.
        """
        len_s = len(s)
        j = len_s - 1
        for i in range(len_s // 2):
            s[i], s[j] = s[j], s[i]
            j -= 1

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

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

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

题目要求每隔2k个字符就将前k个字符进行反转,我们可以直接遍历字符串,让i每次移动2k,反转每2k区间的前k个字符

class Solution:
    def reverseStr(self, s: str, k: int) -> str:
        s = list(s)
        def reverse(s):
            left, right = 0, len(s) - 1
            while left < right:
                s[left], s[right] = s[right], s[left]
                left += 1
                right -= 1
            return s
        
        for i in range(0, len(s), 2*k):
            s[i:i+k] = reverse(s[i:i+k])
        
        return "".join(s)

这里要注意不要因为要统计2k和前k个字符串,而把代码搞得很复杂;这道题虽然只是按照固定的规律去处理每一段字符串,但也需要考虑如何在for循环里下心思

剑指 Offer 05. 替换空格.
实现一个函数,把字符串 s 中的每个空格替换成"%20"。
示例 1:
输入:s = “We are happy.”
输出:“We%20are%20happy.”

在python/java中,字符串是不可变对象,无法原地修改(java可以使用StringBuilder类进行修改),需要新建字符串空间来解决
我们初始化一个列表res用来保存结果,遍历字符串中的每个字符,分两种情况:

  • 当c为空格时,在res后添加字符串“%20”
  • 当c不为空格时,向res后添加该字符

最后需要将结果列表转化为字符串,这样时间复杂度和空间复杂度都为O(N)

class Solution:
   def replaceSpace(self, s: str) -> str:
       res = []
       for c in s:
           if c == ' ':
               res.append("%20")
           else:
               res.append(c)
       
       return "".join(res)

151. 翻转字符串里的单词.
给一个字符串 s ,逐个翻转字符串中的所有 单词 。
单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。
返回一个翻转 s 中单词顺序并用单个空格相连的字符串。

说明:

  • 输入字符串 s 可以在前面、后面或者单词间包含多余的空格。
  • 翻转后单词间应当仅用一个空格分隔。 翻
  • 转后的字符串中不应包含额外的空格。

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

如果使用语言自带的库函数,如split(拆分)先将字符串按空格分割成字符串数组、reverse(翻转)将字符串数组进行翻转和join(连接)将字符串数组拼接成一个字符串等方法,简单地调用内置API就能完成操作:

class Solution:
    def reverseWords(self, s: str) -> str:
        return " ".join(reversed(s.split()))

如果不想使用语言自带的API,也可以自己编写对应的函数,对于java/python这种字符串不可变的语言,需要先将字符串转化为其他可变的数据结构,在转化的过程中去掉空格(对于字符串可变的语言,不需要额外开辟空间,可以原地同时完成反转字符串和去除空格的操作)。

这里主要介绍双端队列的做法,双端队列支持从队列头部插入,因此可以沿着字符串处理每个单词,用一个数组来维护每一个单词,将单词压入队列头部,再将队列转成字符串
在这里插入图片描述

class Solution:
    def reverseWords(self, s: str) -> str:
        left, right = 0, len(s) - 1
        #去掉字符串开头结尾的空白字符
        while left <= right and s[left] == ' ':
            left += 1
        while left <= right and s[right] == ' ':
            right -= 1
        
        d, word = collections.deque(), []
        #将单词压入队列头部
        while left <= right:
        	#每遇到一个单独的单词字符串,压入队列头部
            if s[left] == ' ' and word:
                d.appendleft(''.join(word))
                word = []
            #维护单词字符串数组
            elif s[left] != []:
                word.append(s[left])
            left += 1
        
        d.appendleft(''.join(word))

        return ' '.join(d)

2.KMP算法

实现 strStr().
给两个字符串 haystack 和 needle ,在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从 0 开始)。如果不存在,则返回 -1 。
示例 1:
输入:haystack = “hello”, needle = “ll”
输出:2

做这道题之前首先要了解一下KMP算法。

首先概述KMP算法的思想就是:当字符串不匹配时,先记录一部分之前已经匹配的文本内容,利用这些内容避免再去从头开始匹配。
下面对KMP进行讲解:

2.1为什么叫KMP

三个人发明的,取这三个人名字的首字母。

2.2KMP作用

主要应用在字符串匹配。
主要思想上面已经说过,这里面也凸显了KMP的重点,就是如何记录已匹配的文本内容,也就是算法里的next数组。

2.3前缀表

上文提到的next数组就是一个前缀表,前缀表的作用:用于回退,记录了模式串与主串(文本串)不匹配时,模式串应该从何处开始重新匹配。
举个栗子:
在文本串:aabaabaafa 中查找是否出现过模式串:aabaaf
在这里,文本串中的第六个字符’b’和模式串中的第六个字符’f’不匹配,如果暴力匹配,此时就要从头匹配了
但如果使用前缀表,就会从上次已经匹配的内容开始匹配,找到模式中第三个字符’b’继续开始匹配
这里有个问题就是:前缀表如何记录的?
首先要知道前缀表的作用是:当前位置匹配失败时,找到之前已经匹配的位置,再重新匹配。也就是在某个字符不匹配时,告知模式串下一步匹配应该跳到哪个位置
所以前缀表就是:记录下标i及之前的字符串中,有多大长度的前缀后缀。

2.4最长公共前后缀

前缀:不包含最后一个字符的所有以第一个字符开头的连续子串
后缀:不包含第一个字符的所有以最后一个字符结尾的连续子串
“最长公共前后缀”这个词是在网上看到的,但是也不清楚在KMP里具体什么意思,这里理解前缀、后缀的概念就好。
然后需要理解:
字符串a的最长相等前后缀长度为0
字符串aa的最长相等前后缀长度为1
字符串aaa的最长相等前后缀长度为2

2.5为什么一定要用前缀表

为什么前缀表可以告知上次匹配的位置,并跳到该位置?
拿2.3中例子,文本匹配的时候在下标5的地方不匹配,模式串指向’f’:
在这里插入图片描述
然后找到下标2,指向b,继续匹配:
在这里插入图片描述
下标5之前的的字符串(aabaa)的最长相等的前缀和后缀字符串是子字符串aa,由于找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串之后,因此找到与其相同的前缀之后从新匹配即可。
因此前缀表可以告知我们当前位置匹配失败,并跳到之前已经匹配过的地方

2.6如何计算前缀表

对于模式串:aabaaf
模式串a:最长相同前后缀长度为0
模式串aa:最长相同前后缀长度为1
模式串aab:最长相同前后缀长度为0
模式串aaba:最长相同前后缀长度为1
模式串aabaa:最长相同前后缀长度为2
模式串aabaaf:最长相同前后缀长度为0
求得的最长相同前后缀长度就是对应前缀表的元素:
在这里插入图片描述
因此,模式串与前缀表对应位置的数字表示:下标i及之前的字符串中,有多大长度的相同前后缀
如何利用前缀表找到字符不匹配时,指针应该移动的位置:
找到不匹配的位置,看它前一个字符的前缀表数值是多少(因为要找前面字符串的最长相同的前后缀),比如前一个字符的前缀表数值是2,把指针移动到下标为2的位置继续匹配,具体可以看动画:gif

2.7 前缀表与next数组

next数组与前缀表的关系:next数组可以是前缀表,也可以把前缀表统一右移一位之后作为next数组

2.7.1 前缀表减一后作为next数组

设n:文本串长度,m:模式串长度,在匹配的过程中,根据前缀表不断调整匹配的位置,匹配的过程是O(n);在这之前还要单独生成next数组,该时间复杂度为O(m);因此KMP算法的时间复杂度为O(n+m)。
如果使用暴力解法,时间复杂度为O(n*m),因此KMP能够极大地提高搜索效率。

以下将haystack作为文本串,needle作为模式串。构造next数组:
定义函数getNext用于构造next数组,函数参数分别为:指向next数组的指针,以及一个字符串:

def getNext(next, s)

构造next数组就是计算模式串s与前缀表的过程:
1.初始化
2.处理前后缀不同的情况
3.处理前后缀相同的情况
以下是详细过程:
1.初始化:
定义两个指针分别指向前缀起始位置、后缀起始位置,同时对next数组继续初始化赋值:

j = -1;	#前缀起始位置
next[0] = j;

j初始化为-1是因为这里是将前缀表减一作为next数组的实现方式。
next[i]表示i之前(包括i)最长相等的前后缀,相当于j,因此初始化next[0]=j。
2.处理前后缀不同的情况
j初始化为-1,因此i从1开始,进行s[i]与s[j+1]的比较:

for i in range(1, len(s)):

当s[i]与s[j+1]不相同时,即前后缀末尾不相同,需要向前回退:next[j]记录j之前(包括j)的子串的相同前后缀的长度,因此当s[i]与s[j+1]不相同时,需要找j+1的前一个元素在next数组里的值,即next[j]:

while j >= 0 and s[i] != s[j + 1] : // 前后缀不相同了
    j = next[j] // 向前回退
}

3.处理前后缀相同的情况
当s[i]与s[j+1]相同,同时向后移动i和j,将j(前缀长度)赋给next[i],因为next[i]需要记录相同前后缀长度:

if s[i] == s[j + 1]:  // 找到相同的前后缀
    j++
next[i] = j

整体构造next数组的函数代码如下:

def getNext(next, s):
	j = -1
	next[0] = j
	for i in range(1, len(s)):
		while j >= 0 and s[i] != s[j+1]:	# 前后缀不同
			j = next[j]	# 向前回退
		if s[i] == s[j+1]:	# 找到相同前后缀
			j += 1
		next[i] = j	# 将前缀的长度赋给next[i]

构造next数组的动画展示:link.

2.7.2使用next数组进行匹配

在文本串s里查找是否出现过模式串t。
定义下标j指向模式串起始位置,下标i指向文本串起始位置,j初始值为-1,因为next数组里记录的初始位置为-1;i则从0开始遍历文本串:

for i in range(len(s)):

然后是s[i]与t[j+1]进行比较,二者不同时,j从next数组里寻找下一个匹配的位置:

while j >= 0 and s[i] != t[j+1]:
	j = next[j]

如果二者相同,则i与j同时向后移动:

if s[i] == t[j+1]:
	j += 1

如果j指向了模式串的末尾,说明模式串t完全匹配文本串s中的某个子串。
题目要求在文本串中找出模式串出现的第一个位置,所以返回当前在文本串匹配模式串的位置i 减去 模式串的长度,即为文本串中首次出现模式串的位置:

if j == len(t) - 1:
	return i - len(t) + 1

使用next数组,用模式串匹配文本串的代码如下:

j = -1
for i in range(len(s)):
	while j >= 0 and s[i] != t[j+1]:	#不匹配
		j = next[j]	#找上次匹配的位置
	if s[i] == t[j+1]:	#匹配
		j += 1	#i与j后移
	if j == len(t) - 1:	#文本串s中出现了模式串t
		return i - len(t) - 1

本题最终实现代码:

class Solution:
    def strStr(self, haystack: str, needle: str) -> int:
        a=len(needle)
        b=len(haystack)
        if a==0:
            return 0
        next=self.getnext(a,needle)
        p=-1
        for j in range(b):
            while p>=0 and needle[p+1]!=haystack[j]:
                p=next[p]
            if needle[p+1]==haystack[j]:
                p+=1
            if p==a-1:
                return j-a+1
        return -1

    def getnext(self,a,needle):
        next=['' for i in range(a)]
        k=-1
        next[0]=k
        for i in range(1,len(needle)):
            while (k>-1 and needle[k+1]!=needle[i]):
                k=next[k]
            if needle[k+1]==needle[i]:
                k+=1
            next[i]=k
        return next

总结:
介绍了KMP算法,分析了next数组即为前缀表(减1),用next数组求出文本串s是否出现过模式串,并实现了具体代码。

459. 重复的子字符串.
给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成。给定的字符串只含有小写英文字母,并且长度不超过10000。

输入: “abab”
输出: True
解释: 可由子字符串 “ab” 重复两次构成。

解题思路:
从上述内容我们知道,KMP中的next数组记录的就是最长相同前后缀,如果(数组长度 - 最长相等前后缀的长度)可以被数组长度整除,说明该字符串有重复的字符串。
原因:(数组长度 - 最长相等前后缀的长度)的结果相当于一个周期的长度,如果这个周期可以被整除,说明整个数组就是这个周期的循环。
数组长度:len
最长相等前后缀的长度:next[len - 1] + 1
可以打印出next数组,看看数组规律:
在这里插入图片描述
图中next[len - 1] = 7, next[len - 1] + 1 = 8即为此时字符串的最长相同前后缀的长度。
(len - (next[len - 1] + 1))= (12 - 8)= 4,4可被12整除说明有重复的字符串(asdf)。

class Solution:
    def repeatedSubstringPattern(self, s: str) -> bool:
        n = len(s)
        next = [0] * n
        self.getNet(next, s)
        if next[n - 1] != -1 and n % (n - (next[-1] + 1)) == 0:
            return True
        return False

    def getNet(self, next, s):
        j = -1
        next[0] = j
        for i in range(1, len(s)):
            while j >= 0 and s[i] != s[j + 1]:  # 前后缀不同
                j = next[j]  # 向前回退
            if s[i] == s[j + 1]:  # 找到相同前后缀
                j += 1
            next[i] = j  # 将前缀的长度赋给next[i]
        return next
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值