数据结构与算法:(待更)字符串和KMP算法原理(附有leetcode344、541、151、459题解)

字符串交换相关

leetcode344.反转字符串

注意:这里的s是str数组

class Solution(object):
    def reverseString(self, s):
        """
        :type s: List[str]
        :rtype: None Do not return anything, modify s in-place instead.
        """
        n = len(s)
        i = 0
        j = n-1
        while  i < j:
            s_list[i], s_list[j] = s_list[j], s_list[i]
            i+=1
            j-=1

leetcode541.反转字符串Ⅱ

注意:虽然swap操作是直接在原str数组上就进行交换,但是因为切片操作产生的是新的数组,所以比起344.反转字符串的交换操作,本题的swap操作会多一步返回。且注意最后要返回字符串形式。

class Solution(object):
    def swap(self, s):
        n = len(s)
        i = 0
        j = n-1
        while i<j:
            s_list[i], s_list[j] = s_list[j], s_list[i]
            i+=1
            j-=1
        return s
    def reverseStr(self, s, k):
        """
        :type s: str
        :type k: int
        :rtype: str
        """
        n = len(s)
        s_list = list(s)
        # 分好组的前2k个字符有m组
        m = n / (2*k)
        i = 0
        # 反转前m组的字符
        while i<(2*k*m):
            tmp_str = s_list[i:i+k]
            s_list[i:i+k] = self.swap(tmp_str)
            i+=2*k
        # 剩余字符属于哪种情况
        left_nums = n % (2*k)
        # 少于k个
        if left_nums < k:
            tmp_str = s_list[i:n]
            s_list[i:n] = self.swap(tmp_str)
        else:
            tmp_str = s_list[i:i+k]
            s_list[i:i+k] = self.swap(tmp_str)
        return ''.join(s_list)

leetcode151.反转字符串中的单词

知识点:直接用s.split()来进行切片,而不是s.split(" "),这样前者会自动去除末尾空格。同时,' '.join默认在两两元素之间插入' '

class Solution(object):
    def reverseWords(self, s):
        """
        :type s: str
        :rtype: str
        """
        # 
        s_list = s.split()
        n = len(s_list)
        i = 0
        j = n-1
        while i<j:
            if s_list[i] and s_list[j]:
                s_list[i], s_list[j] = s_list[j], s_list[i]
                i += 1
                j -= 1
            elif s_list[i]=="":
                i += 1
            elif s_list[j]=="":
                j -= 1
        return ' '.join(s_list)

KMP算法

原理

场景和暴力解法

场景:当我们给到一个待匹配的长字符串aabaabaaf(下文统称为s)和一个用于匹配的模式串aabaaf(下文统称为p),我们需要返回第一个s匹配成功的索引开始下标和结束下标。
暴力解法:两层for循环ij分别遍历待匹配字符串和模式串,匹配成功则i和j同时向后移动,匹配失败则i自己+1,j回退到i的位置。譬如这里第一次匹配失败时i=5,j=5,随后i从0跳转到1,j从5回退到1,然后继续遍历。
在这里插入图片描述
在这里插入图片描述
可以看到,暴力解法的i的遍历会重复遍历到我们已知的部分(譬如这里第二次循环i跳转到1,就是第一次循环j遍历过的位置)。于是,我们想要知道i应该跳转到什么位置,才能避免重复遍历。为此,我们引出前缀表的概念。

前缀表

前缀:对于一个字符串,含有首字母且不含有尾字母的所有子串都称之为该字符串的前缀。
后缀:对于一个字符串,含有尾字母且不含有首字母的所有子串都称之为该字符串的前缀。

Att:所以像是:a这种单字母,就没有前后缀的概念

在这里,我们模式串aabaaf含有的子串分别如下:

a
aa
aab
aaba
aabaa
aabaaf

拿其中aabaa字串来举例,其前缀有:

a
aa
aab
aaba

其后缀有:

a
aa
baa
abaa

那么相等的前后缀就有:

a
aa

其中,最长相等前后缀即aa
那么对于模式串aabaaf含有的子串都得到其最长相等前后缀,分别是:

a			0
aa			1,a
aab			0
aaba		1,a
aabaa		2,aa
aabaaf		0

于是我们得到了,这个模式串的前缀表为0 1 0 1 2 0
我们取前缀表中最大的数值2,则i要跳转的位置下标就为2。

为什么前缀表中最大数值即i要跳转的下标位置?

要回答这个问题,我们其实就是要回答,暴力解法中某些位置为什么被跳过,譬如第一次匹配失败后,i往右顺移一位的i=1为什么会是无用的尝试。

那么我们来看i往右顺移一位的情况:

第一次循环时,在s[i]p[j]处我们发现了不匹配,那么就意味着我们得到了条件①。
条件①s的红色部分和p的红色部分是匹配的。

而因为条件①,那么我们可以推出条件②。
条件②s的紫色部分等于p的紫色部分。

接下来i往右顺移一位使得i=1,同时j直接回退到i的这种遍历,意味着我们的目标①如下:
目标①s的蓝色部分和p的红色部分匹配成功。

又因为满足目标①的话,我们也必须满足目标②:
目标②s的紫色部分和p的绿色部分匹配成功。

此时根据条件②和目标②,我们可以推出我们需要满足的是目标③:
目标③p的紫色部分和p的绿色部分匹配成功。

可以看到,目标③其实就是不匹配位置前面字符串有个公共前后缀。

在这里插入图片描述
也就是说,当我们某次循环没有匹配成功时,下一次循环想要匹配成功,就一定满足不匹配位置前面字符串有个公共前后缀,我们从这个前缀的后一位再继续遍历尝试即可。

那么不匹配位置前面字符串的公共前后缀可能有多个,为什么我们i跳转的位置是上次匹配失败位置前面最长公共前后缀的下一位,而不是随便一个公共前后缀的下一位呢?

很明显的是,题目要求的是第一个匹配成功的位置,那么只会有唯一的答案。
如下,当第一次循环匹配失败时,存在两个公共前后缀aaa,他们两个都是符合目标③的答案集的一部分,如果i跳转到a的下一位继续遍历,肯定会遇到不匹配情况的(不然就不会出现第一次循环匹配失败了),之后i需要继续跳转,又因为aa是包含a在内的,所以这时i依旧会跳转到最开始我们说的aa的下一位。
在这里插入图片描述
所以我们i跳转的位置是上次匹配失败位置前面最长公共前后缀的下一位。那么又因为数组下标是从0开始的,最长公共前后缀的长度即i的坐标。所以i跳转的位置就是上次匹配失败位置前面最长公共前后缀的长度。

前缀表的不同处理

如上例子,我们的前缀表为,但是有些题目存在其他两种处理方式:
处理方式1:将该数组先初始化为-1,然后整体右移一位
处理方式2:整体-1

原始前缀表:0	1	0	1	2	0
处理方式1:-1	0	1	0	1	2
处理方式2:-1	0	-1	0	1	-1

处理方式1的原因是让我们遇到匹配失败时,不用找匹配失败位置前面最长公共前后缀的长度,而是直接找匹配失败位置最长公共前后缀的长度。
处理方式2的原因仍旧是找匹配失败位置前面最长公共前后缀的长度,但是会+1回来(感觉变麻烦了,实际上这个做法也确实不常见,主要是针对某些特殊题目的)
目前最主流的做法是处理方式1。

代码求解ne数组

p[ne[j-1]] == p[j]

假设前面ne[0]~ne[4]都是已知的,现在我们要求解ne[5]是多少,那么其实我们可以通过ne[4]=2知道,橙色下划线部分和紫色下划线部分是相等的,此时如果橙色下划线和紫色下划线后一位分别相等,即bf相等,则红色方框和绿色方框p[0]~p[a]和p[j-a+1]~p[j]就相等,那么ne[5]就应该是ne[4]+1,即3了。

如上逻辑是:

  1. 通过next[j-1]=a知道,j下标前面字符串的最长公共前后缀为p[0]~p[a-1],准确来说,前缀为p[0]~p[a-1],即橙色下划线部分,后缀为p[j-a]~p[j-1],即紫色下划线部分。
  2. 接下来我们比较橙色下划线部分后一位,即p[a],和紫色下划线后一位,即p[j]是否相等
  3. 如果相等,则ne[j]=ne[j-1]+1

在这里插入图片描述
我们进一步简化逻辑,可以得到:

if p[a] == p[j]:
	ne[j]=ne[j-1]+1

其中p[a]a是如何找到的,是通过ne[j-1]=a找到的,所以代码应该是:

if p[ne[j-1]] == p[j]:
	ne[j]=ne[j-1]+1

p[ne[j-1]] != p[j]

那么如果p[a]p[j]不相等呢?就如同上述图片,bf不相等,此时我们应该怎么求解ne[j]呢?

这个时候我们就要从长度为ne[j-1]的前后缀开始逐渐长度递减地考虑,如下:
在这里插入图片描述
既然因为p[a]p[j]不相等,所以红色方框和绿色方框不相等,那么我们就应该考虑红色椭圆和绿色椭圆是否相等。

为什么知道是考虑红色椭圆和绿色椭圆,解答如下:
ne[j-1]=ap[a]p[j]不相等得知,我们ne[j]不可能等于a+1了,所以只能考虑从长度为a的前后缀开始考虑,此时我们确定了红色椭圆/绿色椭圆含有的元素的长度为a。然后确定红色椭圆的下标为p[0]~p[a-1],绿色椭圆的下标范围为p[j-a+1]~p[j],也就是说此时我们找到了应该考虑红色椭圆和绿色椭圆,并且知道了两个椭圆的长度和下标范围。接着我们开始正式考虑红色椭圆和绿色椭圆是否相等。

而又因为ne[4]=2,我们得知,红色椭圆不就是橙色下划线部分吗?不就进一步等于紫色下划线部分吗?
所以问题转换为了,我们应该考虑紫色下划线部分和绿色椭圆是否相等。
也就是说接下来需要看aaf的第一个af是否相等,因为不相等,所以我们知道红色椭圆和绿色椭圆也是不会相等的,所以我们进一步考虑,红色三角和绿色三角是否会相等呢?
在这里插入图片描述
同样的,也是因为ne[4]=2,我们得知,红色三角一定等于黄色三角。
所以问题转换为了,我们应该考虑黄色三角和绿色三角是否会相等。
也就是说接下来需要看aaf的第一个af是否相等,因为不相等,所以我们知道红色三角和绿色三角也是不相等的。
所以ne[5]=0

从如上逻辑,我们可以看到,其实就是不断地在求绿色方框p[j-a+1]~p[j]的最长公共前后缀,或者说就是不断地在求红色方框p[0]~p[a]的最长公共前后缀。注意,这里的a是由ne[j-1]=a得到的,所以本质是不断地在求p[0]~p[ne[j-1]]的最长公共前后缀。
那么带上之前p[ne[j-1]] == p[j]的逻辑,整个求解ne[j]的逻辑就变成了如下:

求解p[0]~p[j]的最长公共前后缀(就是求解ne[j]嘛):
	if p[ne[j-1]] == p[j]:
		ne[j]=ne[j-1]+1
	else:
		试图求解p[0]~p[ne[j-1]]的最长公共前后缀

所以我们看到,ne[j]的值只跟ne[j-1]和p[j]有关。

同时,我们根据ne数组的定义知道,ne[0]应该是0,因为下标为0的前面字符串的最长公共前后缀的长度为0,因为下标为0前面都没有字符串。
那么我们从左到右赋值ne数组,用j来表示当前需要赋值的ne数组的下标,则j从1开始,因为ne[0]=0已知。
然后我们用i来表示p[0]~p[j]的最长公共前后缀的前缀的最后元素的下标,那么i应该从0开始。
那么如果ne[j]存在最长公共前后缀的话,s[j]应该等于s[i],意味着前缀的最后元素和后缀的最后元素相同,那么此时i应该+1,因为相当于当前遍历到的前缀的最后元素属于最长公共前后缀的一部分。

如果s[j]不等于s[i]的话,意味着我们模式串少了一位,应该去求解p[0]~p[i-1]的最长公共前后缀。i应该往前回退,那么回退到哪里呢?回退到ne[i-1]的位置。假设我们要回退的位置下标是k。
那么
要移动的下一个位置k。存在着这样的性质:最前面的k个字符和j之前的最后k个字符是一样的。

如果用数学公式来表示是这样的

P[0 ~ k-1] == P[j-k ~ j-1]

因为i的值代表模式串中已经匹配成功的部分的长度,又代表最长公共前后缀的前缀的最后元素的下标?
有ne[i-1]得p[0]~p[i-1]=p[j-i]~p[j-1]
那么p[j-k ~ j-1] == P[0 ~ k-1]

find_next(ne, s):
	i = 0
	ne[0] = 0
	for j in range(1, len(s)):
		while i>0 and s[i]!=s[j]:
			i = ne[i-1]
		if s[i]==s[j]:
			i+=1
		ne[j]=i

可以看到,ne数组的赋值本质上是一个动态规划的过程,对于动态规划,我们要做好边界的规范。这里就是ne[0]是多少,ne[0]应该是0。

字符串搜索子串

leetcode459.重复的子字符串

题目:

给定一个非空的字符串 s ,检查是否可以通过由它的一个子串重复多次构成。

示例 1:

输入: s = “abab”
输出: true
解释: 可由子串 “ab” 重复两次构成。
示例 2:

输入: s = “aba”
输出: false
示例 3:

输入: s = “abcabcabcabc”
输出: true
解释: 可由子串 “abc” 重复四次构成。 (或子串 “abcabc” 重复两次构成。)

提示:

1 <= s.length <= 104
s 由小写英文字母组成

思路:

对于由子串重复组成的字符串(我们称之为s),那么拼接两个s得到的更大的新字符串ss,我们一定可以在ss中找到和原来的s相等的部分,且不是s+s的s本身。
举例如下:
在这里插入图片描述
其中:
“在ss中找到和原来的s相等的部分”就是红框部分
“且不是s+s的s本身”,即这个红框部分不是绿框或者紫框

代码:

class Solution(object):
    def repeatedSubstringPattern(self, s):
        """
        :type s: str
        :rtype: bool
        """
        ss = s+s
        n = len(s)
        # 注意搜索时删去首尾元素,以免搜到的是s+s拼接的第一个s或者第二个s,即s本身
        if s in ss[1:-1]:
            return True
        return False
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

shanshandeisu

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

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

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

打赏作者

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

抵扣说明:

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

余额充值