LeetCode练习六:字符串

参考《算法通关手册》字符串篇

一、字符串基础

1.1 字符串基础知识

1.1.1 字符串简介
  1. 字符串的表示:字符串是由0个或多个字符组成的有序字符序列,由一对单引号或一对双引号表示
    在这里插入图片描述

  2. 字符串有2类共4种表示方法:

    • 由1对单引号或双引号表示,仅表示单行字符串;
    • 由1对三单引号或三双引号表示,可表示多行字符串;
  3. 字符串是字符的有序序列,可以对其中的字符进行索引。

  4. 特殊字符:使用转义符\\b表示回退;\n表换行(光标移动到下行首;\r表示回车(光标移动到本行首),\t表示制表符(4格)

1.1.2 字符串处理

format官方文档re文档

  1. 字符串操作符
操作符及查找描述
x+y连接两个字符串x和y
nx或xn复制n次字符串x
x in s如果x是s的子串,返回True,否则返回False
str.index(‘’)子串第一次出现的位置,不存在报错ValueError
str.rindex(‘’)子串最后一次出现的位置,不存在报错ValueError
str.find(‘’)子串第一次出现的位置,不存在返回-1
str.rfind(‘’)子串最后一次出现的位置,不存在返回-1
  1. 字符串处理函数
函数及使用描述
len(x)返回字符串的长度,非空字符串都有长度(换行符、空格和符号长度1)
str(x)将x转换为string类型
hex(x)或oct(x)将整数x转换成16进制或8进制字符串
chr(x)将一个(0-255)10或16进制整数(Unicode编码),转换成对应ASCII字符
ord(x)x为字符,返回其对应的Unicode编码(原始值0-65535)
  1. 字符串大小写
大小写转换描述
str.lower()或str.upper()全部字符小写或大写,产生新字符串
str.swapcase()字符串大写改小写,小写改大写,产生新字符串
str.capitalize()首字符大写,其余小写,产生新字符串
str.title()每个字符首字符大写,其余小写,产生新字符串
  1. 字符串对齐
字符串对齐描述
str.center(width,’stp’)居中对齐,width指定宽度,stp指定分隔符,默认空格分割。
str.ljust(width,’stp’)左对齐,width指定宽度,stp指定分隔符,默认空格分割。
str.rjust(width,’stp’)右对齐,width指定宽度,stp指定分隔符,默认空格分割。
str.zfill(width)右对齐,左侧用0填充,width指定宽度。

以上对齐方式,指定宽度小于实际宽度时返回原字符串

  1. 字符串处理函数
函数及使用描述
str.split(‘sep’,maxsplit)从str左侧开始分割,分隔符为sep,默认空格。第二个参数指定最大分割次数。最大分割后剩余字符串成一个元素
str.rsplit(‘sep’,maxsplit)同上,从右侧开始分割。直接写’/’或者sep=’/’都可以。最大分割后的元素不同。没有指定最大分割时是一样的
str.count(sub)返回子串sub在str中的出现的次数
str.replace(old,new,num)返回字符串str副本,所有old子串被替换为new,num为最大替换次数
str.center(width[,fillchar])字符串str根据宽度width居中,fillchar可选填
str.strip(chars)从str中去掉在其左侧和右侧chars中列出的字符
str.join(iter)在iter变量除最后元素外每个元素后增加一个str
str.isidentifier()判断字符串是否是合法标识符(汉字也算)
str.isspace()判断字符串是否都是由空白字符组成(回车、换行、水平制表符)
str.isalpha()判断字符串是否都是由字母组成(汉字也算)
str.isdecimal()判断字符串是否都是由十进制数字组成
str.isnumeric()判断字符串是否都是由数字组成(包括罗马数字)
str.isalnum()判断字符串是否都是由数字和字母组成
  1. 字符串的比较
    字符串可以用<、>、=、>=、<=、==、!=来比较。比较原理是比较每个字符的原始值(unicode编码),可用ord查看
  2. 编码与解码
    • 编码:将字符串转换成二进制数据(byte)(gbk中文两个字节,utf-8中文三个字节)
    • 格式:str.encode(encoding=’gbk’)(gbk大小写都可以,开头b表二进制)
    • 解码:将二进制数据(byte)转换成字符串
    • 格式:byte.decode((encoding=’gbk’)(byte为二进制编码),解码格式应该与编码格式一致
1.1.3 字符串类型的格式化

字符串格式化有两种方式:

  1. 用%占位输出
    如print(‘我是%s,今年%d岁,%(name,age))
    print(‘我今年%10.2f岁’ % age)
    结果:我是name,今年age岁
  2. 用{}输出,如
    print(f‘我是(name),今年(age)岁)
    print(‘我是{0},今年{1}岁’.format(name,age))(槽中0 和1可以不写)

整数类型输出格式:

  • b:输出整数的二进制方式
  • c:输出整数对应的Unicode字符
  • d:输出整数的十进制方式
  • o:输出整数的八进制方式
  • x:输出整数的小写十六进制方式
  • X:输出整数的大写十六进制方式

浮点数类型输出格式:

  • e:输出浮点数对应的小写字母e的指数形式
  • E:输出浮点数对应的大写字母E的指数形式
  • f:输出浮点数的标准浮点形式
  • %:输出浮点数的百分比形式
    • {:.2f}表示小数点后两位的小数
    • {:.2}表示一共两位数,有十位时表示为3e+01的形式

在这里插入图片描述

②格式控制

在这里插入图片描述

1.2 字符串基础题目

1.2.1 字符串基础题目列表
题号标题题解标签难度
0125验证回文串Python字符串、双指针简单
0005最长回文子串Python字符串、动态规划中等
0003无重复字符的最长子串Python字符串、哈希表、双指针、字符串、滑动窗口中等
0344反转字符串Python字符串简单
0557反转字符串中的单词 IIIPython字符串简单
0049字母异位词分组Python字符串、哈希表中等
0415字符串相加Python字符串、大数加法简单
0151颠倒字符串中的单词Python双指针、字符串中等
0043字符串相乘Python数学、字符串、模拟中等
0014最长公共前缀Python字符串简单
1.2.2 验证回文串

  如果将所有大写字符转换为小写字符、并移除所有非字母数字字符之后,短语正着读和反着读都一样。则可以认为该短语是一个 回文串 。
  给你一个字符串 s,如果它是 回文串 ,返回 true ;否则,返回 false 。

class Solution:
    def isPalindrome(self, s: str) -> bool:
        s = "".join(ch.lower() for ch in s if ch.isalnum())
        return s == s[::-1]

作者:LeetCode-Solution
链接:https://leetcode.cn/problems/valid-palindrome/solution/yan-zheng-hui-wen-chuan-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

1.2.3 最长回文子串

  给你一个字符串 s,找到 s 中最长的回文子串。如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。

输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。

思路 1:动态规划

  • 初始化一个 n * n 大小的布尔类型数组 dp[][]dp[i][j] 表示字符串 s 上 从位置 ij 的子串 s[i...j] 是否是一个回文串,下面进行判断:

    • 当子串只有 1 位或 2 位的时候,如果 s[i] == s[j],该子串为回文子串, dp[i][j] = (s[i] == s[j])

    • 如果子串大于 2 位,则如果 s[i + 1...j - 1] 是回文串,且 s[i] == s[j],则 s[i...j] 也是回文串,dp[i][j] = (s[i] == s[j]) and dp[i + 1][j - 1]

  • 当判断完 s[i: j] 是否为回文串时,判断并更新最长回文子串的起始位置和最大长度。

class Solution(object):
    def longestPalindrome(self, s):
        """
        :type s: str
        :rtype: str
        """

        n=len(s)
        if n<2:
            return s
        
        # 定义dp[i][j]表示从i到j的子串是否是回文串,初始设为False
        dp=[[False]*n for _ in range(n)]
        max_len=1 # 回文串最大长度
        start=0   # 最长回文串起始位置

        for j in range(1,n):
            for i in range(j):
                if s[i]==s[j]:
                    # 子串长度只有1或2时,一定是回文串
                    if j-i<3:
                        dp[i][j]=True
                    else:
                        dp[i][j]=dp[i+1][j-1]

                if dp[i][j] and j-i+1>max_len:
                   max_len=j-i+1
                   start=i
                   
        return s[start:start+max_len]
1.2.4 无重复字符的最长子串

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3
class Solution(object):
    def lengthOfLongestSubstring(self, s):
        """
        :type s: str
        :rtype: int
        """
        left,right=0,0
        se=set()
        ans=0
        while right<len(s):
            if s[right] not in se:
                se.add(s[right])
                right+=1
                ans=max(ans,right-left)
            else:
                se.remove(s[left])
                left+=1
        return ans
class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        l,r=0,0
        ans=0
        se=set()
        for r in range(len(s)):
            while s[r] in se:
                se.remove(s[l])
                l+=1
            se.add(s[r])
            ans=max(ans,r-l+1)
        return ans
1.2.5 字符串相加

  给定两个字符串形式的非负整数 num1num2 ,计算它们的和并同样以字符串形式返回。

  你不能使用任何內建的用于处理大整数的库(比如 BigInteger), 也不能直接将输入的字符串转换为整数形式。
  解题思路:本题不能将字符串直接转为整数之后,在整数之间相加计算。只能对两个大整数模拟「竖式加法」的过程,如下所示:
在这里插入图片描述
算法流程:

  • 设定 ij 两指针分别指向 num1num2 尾部,模拟人工加法;
  • 计算进位: 计算 add= tmp // 10,代表当前位相加是否产生进位
  • 添加当前位: 计算 tmp = n1 + n2 + add,即当前位的结果。但是这其中要除去进位,所以当前位实际结果是 tmp % 10 ,将其添加至 ans 头部;(比如计算9+4=13,当前位计算结果是3,但是进位add=1,累积到高一位的计算中)
  • 索引溢出处理: 当指针 ij 走过数字首部后,给 n1n2 赋值为 0,相当于给 num1num2 中长度较短的数字前面填 0,以便后续计算。
  • 当遍历完 num1num2 后跳出循环,并根据add 值决定是否在头部添加进位 1,最终返回 res 即可。

  这其中的关键点,就是用add来表示前一位是否对当前位产生了进位,并将这个进位状态不断累加到后续每一位的计算中。

class Solution:
    def addStrings(self, num1: str, num2: str) -> str:
        ans=""
        i,j,add=len(num1)-1,len(num2)-1,0
        while i>=0 or j>=0:# 只要有一个数还可以计算
            #位数减为负则补0
            n1=int(num1[i]) if i>=0 else 0 
            n2=int(num2[j]) if j>=0 else 0
            
            temp=n1+n2+add 				# 当前位计算结果
            add=temp//10 				#判断是否要进一位
            ans=str(temp%10)+ans		# 当前位写入的结果
            i-=1
            j-=1
        return "1"+ans if add==1 else ans
1.2.6 字符串相乘

  给定两个以字符串形式表示的非负整数 num1 和 num2,返回 num1 和 num2 的乘积,它们的乘积也表示为字符串形式。

  注意:不能使用任何内置的 BigInteger 库或直接将输入转换为整数。

输入: num1 = "123", num2 = "456"
输出: "56088"

思路一:先算乘数的每一位相乘结果再相加

  • 将较长的字符作为被乘数(num1),较短的作为乘数(num2)。
  • 遍历num2的每一位j,将num2[j]乘以num1的每一位,得到结果ans
  • 将结果ans存在列表ls中,最后再将ls的所有中间结果依次相加(借用上一题的相加函数)

比如996*32=1992+29980=31872

class Solution(object):    

    def multiply(self, num1, num2):
        """
        :type num1: str
        :type num2: str
        :rtype: str
        """
        if num1=='0' or num2=='0':
        	return '0' # 如果没有这一步,会算出'00000'之类的
        # 将nums1设为更长的数
        if len(num1)-1<len(num2)-1:
            num1,num2=num2,num1

        ls=[]  # 记录nums2每一位和nums1相乘的结果
        count=0 # 记录nums2的位数,每进一位中间结果要乘以10
        for j in range(len(num2)-1,-1,-1):
            n2=int(num2[j]) # 用nums2的每一位乘以nums1的所有位
            add=0
            ans=''
            for i in range(len(num1)-1,-1,-1): 
                n1=int(num1[i]) 
                #print(n1,n2)          
                temp=n1*n2+add
                add=temp//10
                ans=str(temp%10)+ans
            ans=str(add)+ans if add!=0 else ans # 因为是相乘,进位可以是1到8(9*9=81)
            ls.append(ans+'0'*count)
            count+=1  
        #print(ls)

        res='0'            
        for i in range(len(ls)):
            res=self.addStrings(res,ls[i])
        return  res 

	def addStrings(self, num1, num2): # 加法函数
        ans=""
        i,j,add=len(num1)-1,len(num2)-1,0
        while i>=0 or j>=0:# 只要有一个数还可以计算
            #位数减为负则补0
            n1=int(num1[i]) if i>=0 else 0 
            n2=int(num2[j]) if j>=0 else 0
            
            temp=n1+n2+add 				# 当前位计算结果
            add=temp//10 				#判断是否要进一位
            ans=str(temp%10)+ans		# 当前位写入的结果
            i-=1
            j-=1
        return "1"+ans if add==1 else ans

思路二:各位相乘后,再相加

  长度为 len(num1) 的整数 num1 与长度为 len(num2) 的整数 num2 相乘的结果长度为 len(num1) + len(num2) - 1len(num1) + len(num2)。所以我们可以使用长度为 len(num1) + len(num2) 的整数数组 nums 来存储两个整数相乘之后的结果。(相当于首位可能补了0,如果最终结果没有进位的话)

整个计算流程的步骤如下:

  1. 从个位数字由低位到高位开始遍历 num1,取得每一位数字 digit1。从个位数字由低位到高位开始遍历 num2,取得每一位数字 digit2
  2. digit1 * digit2 的结果累积存储到 nums 对应位置 i + j + 1 上。(比如1996*23时,nums=[0, 2, 21, 45, 39, 18]
  3. len(num1) + len(num2) - 1 的位置由低位到高位遍历数组 nums。将每个数位上大于等于 10 的数字进行进位操作,然后对该位置上的数字进行取余操作。
  4. 最后判断首位是否有进位。如果首位为 0,则从第 1 个位置开始将答案数组拼接成字符串。如果首位不为 0,则从第 0 个位置开始将答案数组拼接成字符串。并返回答案字符串。
class Solution:
    def multiply(self, num1: str, num2: str) -> str:
        if num1 == "0" or num2 == "0":
            return "0"

        len1, len2 = len(num1), len(num2)
        nums = [0 for _ in range(len1 + len2)] # 首位补0

        for i in range(len1 - 1, -1, -1):
            digit1 = int(num1[i])
            for j in range(len2 - 1, -1, -1):
                digit2 = int(num2[j])
                # 比如num1的十位*num2的个位,和num2的十位*num1的个位,结果都在十位上,要累加起来
                nums[i + j + 1] += digit1 * digit2 
                
		# nums是每一位乘法计算之后的结果,比如[0, 2, 21, 45, 39, 18]
        for i in range(len1 + len2 - 1, 0, -1): # 从个位遍历到最高位的前一位
            nums[i - 1] += nums[i] // 10        # 先计算进位,并累加到下一位
            nums[i] %= 10						# 再将当前位结果取余数

        if nums[0] == 0:
            ans = "".join(str(digit) for digit in nums[1:])
        else:
            ans = "".join(str(digit) for digit in nums[:])
            
        return ans    
1.2.7 最长公共前缀

  编写一个函数来查找字符串数组中的最长公共前缀。如果不存在公共前缀,返回空字符串 ""
思路一:纵向比较

  从前往后遍历所有字符串的每一列,比较相同列上的字符是否相同,如果相同则继续对下一列进行比较,如果不相同则当前列不再属于公共前缀

class Solution(object):
    def longestCommonPrefix(self, strs):
        """
        :type strs: List[str]
        :rtype: str
        """
        
        if not strs:
            return ""


        le=[len(x) for x in strs]
        min_le=min(le) # 最短的长度

        i=0
        while i <min_le:
            se=set(strs[0][i]) # 从第一个字符串的第一个字符开始比较
            for ch in strs[1:]:
                if ch[i] not in se:
                    return strs[0][:i]
            i+=1

        return strs[0][:i]

或者是:

class Solution:
    def longestCommonPrefix(self, strs: List[str]) -> str:
        s = ""
        for i in zip(*strs): # 直接取出所有字符串的第i列
            if len(set(i)) == 1:
                s += i[0]
            else:
                break           
        return s

二、单模式串匹配算法

参考《算法通关手册》单模式串匹配篇

2.1 Brute Force 算法(暴力匹配)

2.1.1 算法介绍
  • Brute Force 算法:简称为 BF 算法。中文意思是暴力匹配算法,也可以叫做朴素匹配算法。

  • BF 算法思想:对于给定文本串 T 与模式串 p,从文本串的第一个字符开始与模式串 p 的第一个字符进行比较,如果相等,则继续逐个比较后续字符,否则从文本串 T 的第二个字符起重新和模式串 p 进行比较。依次类推,直到模式串 p 中每个字符依次与文本串 T 的一个连续子串相等,则模式匹配成功。否则模式匹配失败。

  • Brute Force 算法步骤

    1. 对于给定的文本串 T 与模式串 p,求出文本串 T 的长度为 n,模式串 p 的长度为 m
    2. 同时遍历文本串 T 和模式串 p,先将 T[0]p[0] 进行比较。
      1. 如果相等,则继续比较 T[1]p[1]。以此类推,一直到模式串 p 的末尾 p[m - 1] 为止。
      2. 如果不相等,则将文本串 T 移动到上次匹配开始位置的下一个字符位置,模式串 p 则回退到开始位置,再依次进行比较。
    3. 当遍历完文本串 T 或者模式串 p 的时候停止搜索。
2.1.2 代码实现
def bruteForce(T: str, p: str) -> int:
    n, m = len(T), len(p)
    
    i, j = 0, 0                     # i 表示文本串 T 的当前位置,j 表示模式串 p 的当前位置
    while i < n and j < m:          # i 或 j 其中一个到达尾部时停止搜索
        if T[i] == p[j]:            # 如果相等,则继续进行下一个字符匹配
            i += 1					# i和j同步右移
            j += 1
        else:
            i = i - (j - 1)         # 如果匹配失败则将 i 移动到上次匹配开始位置的下一个位置,
            j = 0                   # 匹配失败 j 回退到模式串开始位置

    if j == m:
        return i - j                # 匹配成功,返回匹配的开始位置
    else:
        return -1                   # 匹配失败,返回 -1
2.2.3 算法分析

  BF 算法非常简单,容易理解,但其效率很低。主要是因为在匹配过程中可能会出现回溯:当遇到一对字符不同时,模式串 p 直接回到开始位置,文本串也回到匹配开始位置的下一个位置,再重新开始比较。

  • 最坏时间复杂度为 O ( m × n ) O(m \times n) O(m×n)。在回溯之后,文本串和模式串中一些部分的比较是没有必要的。由于这种操作策略,导致 BF 算法的效率很低。最坏情况是每一趟比较都在模式串的最后遇到了字符不匹配的情况,每轮比较需要进行 m 次字符对比,总共需要进行 n - m + 1 轮比较,总的比较次数为 m * (n - m + 1)

  • 最佳时间复杂度是 O ( m ) O(m) O(m)。最理想的情况下(第一次匹配直接匹配成功)。

  • 平均时间复杂度为 O ( n + m ) O(n + m) O(n+m)。在一般情况下,根据等概率原则,平均搜索次数为 ( n + m ) 2 \frac{(n + m)}{2} 2(n+m)

2.2 KMP 算法介绍

参考:

  KMP 算法:全称叫做 「Knuth Morris Pratt 算法」,是由它的三位发明者 Donald Knuth、James H. Morris、 Vaughan Pratt 的名字来命名的。KMP 算法是他们三人在 1977 年联合发表的。

   KMP 算法思想:对于给定文本串 T 与模式串 p,当发现文本串 T 的某个字符与模式串 p 不匹配的时候,可以利用匹配失败后的信息,尽量减少模式串与文本串的匹配次数,避免文本串位置的回退,以达到快速匹配的目的。

2.2.1 朴素匹配算法的缺陷

  在朴素匹配算法的匹配过程中,我们分别用指针 i 和指针 j 指示文本串 T 和模式串 p 中当前正在对比的字符。当发现文本串 T 的某个字符与模式串 p 不匹配的时候,j 回退到开始位置,i 回退到之前匹配开始位置的下一个位置上(下图的B),继续匹配,直到能够与匹配串对上位置(下图第二个A),如下图所示。

  那么有没有哪种算法,可以让 i 不发生回退,一直向右移动呢?

2.2.1 改进算法:KMP

  如果我们可以通过每一次的失配而得到一些「信息」,并且这些「信息」可以帮助我们跳过那些不可能匹配成功的位置,那么我们就能大大减少模式串与文本串的匹配次数,从而达到快速匹配的目的。

  1. 每一次失配所告诉我们的信息是:主串的某一个子串等于模式串的某一个前缀
  • 比如文本串 T[i: i + m] 与模式串 p 的失配是下标位置 j 上发生的,那么文本串 T 从下标位置 i 开始连续的 j - 1 个字符,一定与模式串 p 的前 j - 1 个字符一模一样,即:T[i: i + j] == p[0: j]

  • 例如上图中,失配是在下标i+5这个位置发生的,那么失配位置的前5个字符,一定与模式串 p 的前 5 个字符一模一样,即:"ABCAB" == "ABCAB"

  1. 模式串的前 5 个字符中,前 2 位前缀和后 2 位后缀又是相同的,即 "AB" == "AB"

  所以根据上面的信息,我们可以推出:文本串子串的后 2 位后缀和模式串子串的前 2 位是相同的,即 T[i + 3: i + 5] == p[0: 2],而这部分(即下图中的蓝色部分)是之前已经比较过的,不需要再比较了,可以直接跳过。

  那么我们就可以将文本串中的 T[i + 5] 对准模式串中的 p[2],继续进行对比。这样 i 就不再需要回退了,可以一直向右移动匹配下去。在这个过程中,我们只需要将模式串 j 进行回退操作即可。

  实际上,我们会创建一个next数组作为「部分匹配表」,next[j] 表示的含义是:**记录下标 j 之前(包括 j)的模式串 p 中,最长相等前后缀的长度。**下一节会详细说明。
   由于模式串数组中,next[4] == 2,所以不用回退 i,而是将 j 移动到下标为 2 的位置,让 T[i + 5] 直接对准 p[2],然后继续进行比对。

下图参考《【宫水三叶】简单题学 KMP 算法》

  也就是说,匹配失败时,匹配串会检查之前已经匹配成功的部分中里是否存在相同的「前缀」和「后缀」。如果存在,则跳转到「前缀」的下一个位置继续往下匹配:
在这里插入图片描述
  跳转到下一匹配位置后,尝试匹配,发现两个指针的字符对不上,并且此时匹配串指针前面不存在相同的「前缀」和「后缀」,这时候只能回到匹配串的起始位置重新开始:
在这里插入图片描述

  KMP 算法就是使用了这样的思路,对模式串 p 进行了预处理,计算出一个 「部分匹配表」(也叫PMT:Partial Match Table)**,用一个数组 next 来记录。然后在每次失配发生时,不回退文本串的指针 i,而是根据「部分匹配表」中模式串失配位置 j 的前一个位置的值,即 next[j - 1] 的值来决定模式串可以向右移动的位数。

  • KMP 利用已匹配部分中相同的「前缀」和「后缀」来加速下一次的匹配
  • KMP 的原串指针不会进行回溯(没有朴素匹配中回到下一个「发起点」的过程)
2.2.3 next 数组

  上文提到的「部分匹配表PMT」,也叫做「前缀表」,在 KMP 算法中使用 next 数组存储。next[j] 表示的含义是:记录下标 j 之前(包括 j)的模式串 p 中,最长相等前后缀的长度。 也可以理解为,PMT中的值是 字符串的前缀集合与后缀集合的交集中最长元素的长度。

  • 前缀
    • 如果字符串A和B,存在A=BS,其中S是任意的非空字符串,那就称BA的前缀。
    • 例如,”Harry”的前缀包括{”H”, ”Ha”, ”Har”, ”Harr”},我们把所有前缀组成的集合,称为字符串的前缀集合
  • 后缀
    • 若有A=SB, 其中S是任意的非空字符串,那就称BA的后缀
    • 例如,”Potter”的后缀包括{”otter”, ”tter”, ”ter”, ”er”, ”r”},然后把所有后缀组成的集合,称为字符串的后缀集合
  • 对于字符串”ababa”,它的前缀集合为{”a”, ”ab”, ”aba”, ”abab”},它的后缀集合为{”baba”, ”aba”, ”ba”, ”a”}, 两个集合的交集为{”a”, ”aba”},其中最长的元素为”aba”,长度为3
  • 要注意的是,字符串本身并不是自己的前缀或者后缀。
    在这里插入图片描述

举个例子来说明一下,以 p = "ABCABCD" 为例。

  • next[0] = 0,因为 "A" 中无有相同前缀后缀,最大长度为 0
  • next[1] = 0,因为 "AB" 中无相同前缀后缀,最大长度为 0
  • next[2] = 0,因为 "ABC" 中无相同前缀后缀,最大长度为 0
  • next[3] = 1,因为 "ABCA" 中有相同的前缀后缀 "a",最大长度为 1
  • next[4] = 2,因为 "ABCAB" 中有相同的前缀后缀 "AB",最大长度为 2
  • next[5] = 3,因为 "ABCABC" 中有相同的前缀后缀 "ABC",最大长度为 3
  • next[6] = 0,因为 "ABCABCD" 中无相同前缀后缀,最大长度为 0

  同理也可以计算出 "ABCABDEF" 的前缀表为 [0, 0, 0, 1, 2, 0, 0, 0]"AABAAAB" 的前缀表为 [0, 1, 0, 1, 2, 2, 3]"ABCDABD" 的前缀表为 [0, 0, 0, 0, 1, 2, 0]

  在之前的例子中,当 p[5]T[i + 5] 匹配失败后,根据模式串失配位置 j 的前一个位置的值,即 next[4] = 2,我们直接让 T[i + 5] 直接对准了 p[2],然后继续进行比对,如下图所示。

但是这样移动的原理是什么?

如果文本串 T[i: i + m] 与模式串 p 的失配是在第 j 个下标位置发生的,那么:

  • 文本串 T 从下标位置 i 开始连续的 j 个字符,一定与模式串 p 的前 j 个字符一模一样,即:T[i: i + j] == p[0: j](上图中的"ABCAB" == "ABCAB")。

  • 而如果模式串 p 的前 j 个字符中,前 k 位前缀和后 k 位后缀相同,("ABCAB"中有相同的前后缀"AB",即k=2)那么可以断言:文本串中i指针失配位置之前的 k 位(“AB”)一定与模式字符串的第0位至第 k位是相同的(“AB”),即长度为 k的后缀与前缀相同。

  • 这样一来,我们就可以将这些字符段的比较省略掉。具体的做法是,保持i指针不动,然后将j指针指向模式字符串的next[j −1]位即可(表示模式串中,前j-1个子符里,最长相同前后缀的长度k)。

其实相当于因为模式串存在相同的前后缀,所以失配后,模式串不用退回起始位置,退到相同前缀的下一位置就行。

2.2.4 next 数组的构造

  其实,求next数组的过程完全可以看成字符串匹配的过程,即以模式字符串为文本串串,以模式字符串的前缀为目标字符串,一旦字符串匹配成功,那么当前的next值就是匹配成功的字符串的长度。
  具体来说,就是从模式字符串的第一位(注意,不包括第0位)开始对自身进行匹配运算。 在任一位置,能匹配的最长长度就是当前位置的next值。如下图所示。

数组下标从0开始,所以图中应该是next[5]=4,以此类推。下图模式串有next=[0, 0, 1, 2, 3, 4, 0, 1]

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

这样我们就可以使用KMP本身的匹配原理来计算next数组。

  • 我们将模式串p本身即作为文本串也作为模式串,同样用指针ij来遍历。因为文本串第一个位置即使匹配上 ,也有next[0]=0,而不等于1,所以初始时令j = 0i = 1
  • 遍历文本串和模式串:
    • 如果 p[i] != p[j],说明文本串在此位置失配,同上面所讲, i 不动,模式串指针 j 不断回退到 next[j - 1] 位置。
      • 如果回退几次后,有 p[i] == p[j] ,说明匹配上了一个字符,令j右移,此时此时 j 既是前缀下一次进行比较的下标位置,又是当前最长前后缀的长度,所以next[i]=j。最后移动指针i遍历下一个位置;
      • 如果一直回退到j=0,表示文本串在i位置匹配不到任何一个字符,next[i]=0=ji+=1
    • 如果 p[i] == p[j],同样先将 j += 1next[i]=ji+=1
  • 如果p[j]==p[i]j先后移,next[i]=j,然后i后移;
  • 如果不匹配,前缀指针回退,退到前一位置的next值,即j=next[j-1],不停回退,直到p[j]==p[i],或者j=0表示退到模式串开头位置,此时next[i]=0(因为j=0,所以依旧有next[i]=j),表示没有匹配的共同前后缀。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
2.2.5 KMP 算法整体步骤和代码实现
  1. 根据 next 数组的构造步骤生成「前缀表」next
  2. 使用两个指针 ij,其中 i 指向文本串中当前匹配的位置,j 指向模式串中当前匹配的位置。初始时,i = 0j = 0
  3. 循环判断模式串前缀是否匹配成功,如果模式串前缀匹配不成功,将模式串进行回退,即 j = next[j - 1],直到 j == 0 时或前缀匹配成功时停止回退。
  4. 如果当前模式串前缀匹配成功,则令模式串向右移动 1 位,即 j += 1
  5. 如果当前模式串 完全 匹配成功,则返回模式串 p 在文本串 T 中的开始位置,即 i - j + 1
  6. 如果还未完全匹配成功,则令文本串向右移动 1 位,即 i += 1,然后继续匹配。
  7. 如果直到文本串遍历完也未完全匹配成功,则说明匹配失败,返回 -1
# 生成 next 数组
# next[j] 表示下标 j 之前的模式串 p 中,最长相等前后缀的长度
def generateNext(p: str):
    m = len(p)
    next = [0 for _ in range(m)]                # 初始化数组元素全部为 0
    
    left = 0                                    # left 表示前缀串开始所在的下标位置
    for right in range(1, m):                   # right 表示后缀串开始所在的下标位置
        while left > 0 and p[left] != p[right]: # 匹配不成功, left 进行回退, left == 0 时停止回退
            left = next[left - 1]               # left 进行回退操作
        if p[left] == p[right]:                 # 匹配成功,找到相同的前后缀,先让 left += 1,此时 left 为前缀长度
            left += 1
        next[right] = left                      # 记录前缀长度,更新 next[right], 结束本次循环, right += 1

    return next

# KMP 匹配算法,T 为文本串,p 为模式串
def kmp(T: str, p: str) -> int:
    n, m = len(T), len(p)
    
    next = generateNext(p)                      # 生成 next 数组
    
    j = 0                                       # j 为模式串中当前匹配的位置
    for i in range(n):                          # i 为文本串中当前匹配的位置
        while j > 0 and T[i] != p[j]:           # 如果模式串前缀匹配不成功, 将模式串进行回退, j == 0 时停止回退
            j = next[j - 1]
        if T[i] == p[j]:                        # 当前模式串前缀匹配成功,令 j += 1,继续匹配
            j += 1
        if j == m:                              # 当前模式串完全匹配成功,返回匹配开始位置
            return i - j + 1
    return -1                                   # 匹配失败,返回 -1            
2.2.5 KMP 算法分析
  • KMP 算法在构造前缀表阶段的时间复杂度为 O ( m ) O(m) O(m),其中 m m m 是模式串 p 的长度。
  • KMP 算法在匹配阶段,是根据前缀表不断调整匹配的位置,文本串的下标 i 并没有进行回退,可以看出匹配阶段的时间复杂度是 O ( n ) O(n) O(n),其中 n n n 是文本串 T 的长度。
  • 所以 KMP 整个算法的时间复杂度是 O ( n + m ) O(n + m) O(n+m),相对于朴素匹配算法的 O ( n ∗ m ) O(n * m) O(nm) 的时间复杂度,KMP 算法的效率有了很大的提升。

参考资料

三、单模式串匹配练习

3.1 单模式串匹配题目

题号标题题解标签难度
0028找出字符串中第一个匹配项的下标Python字符串、双指针简单
0459重复的子字符串Python字符串、字符串匹配简单
0686重复叠加字符串匹配Python字符串、字符串匹配中等
1668最大重复子字符串
0796旋转字符串Python字符串、字符串匹配简单
1408数组中的字符串匹配Python字符串、字符串匹配简单
2156查找给定哈希值的子串Python字符串、滑动窗口、哈希函数、滚动哈希中等

3.2 找出字符串中第一个匹配项的下标

class Solution(object):
    def strStr(self, haystack, needle):
        """
        :type haystack: str
        :type needle: str
        :rtype: int
        """
        # KMP 匹配算法,haystack 为文本串,needle 为模式串
        m,n = len(haystack), len(needle)
        next = self.next(needle)  # 生成 next 数组
        j = 0                                             # j 为模式串中当前匹配的位置

        for i in range(m):                                # i 为文本串中当前匹配的位置
            while j > 0 and haystack[i] != needle[j]:     # 如果模式串前缀匹配不成功, 将模式串进行回退, j == 0 时停止回退
                j = next[j - 1]
            if haystack[i] == needle[j]:                  # 当前模式串前缀匹配成功,令 j += 1,继续匹配
                j += 1
            if j == n:                              	  # 当前模式串完全匹配成功,返回匹配开始位置
                return i - j + 1
        return -1

    def next(self,p):
        # 等同于模式串自己和自己匹配,只不过文本串从1开始(第一位匹配到结果也应该是0不是1),模式串从0开始
        next=[0 for _ in range(len(p))]
        j=0 # 模式串匹配起始位置,也是匹配到的长度
        for i in range(1,len(p)):
            #次序反过来,left右移,下一步判定就不对了
            #if p[left]==p[right]:
                #left+=1
            # 先判定不相等就回退,直到相等再右移模式串指针,进行下一步比较
            while j>0 and p[j]!=p[i]:
                j=next[j-1]
            if p[i]==p[j]:
                j+=1 			# 匹配上了,公共前后缀长度+1
            next[i]=j
        return next 

3.3 重复的子字符串

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

输入: s = "abcabcabcabc"
输出: true
解释: 可由子串 "abc" 重复四次构成。 (或子串 "abcabc" 重复两次构成。)
  1. 思路一:官方题解
    在这里插入图片描述
class Solution:
    def repeatedSubstringPattern(self, s: str) -> bool:
        return (s+s).find(s,1)!=len(s)

思路二:KMP(参考《算法通关手册》
  我们知道 next[j] 表示的含义是:记录下标 j 之前(包括 j)的模式串 p 中,最长相等前后缀的长度。
  而如果整个模式串 p 的最长相等前后缀长度不为 0,即 next[len(p) - 1] != 0 ,则说明整个模式串 p 中有最长相同的前后缀。假设 next[len(p) - 1] == k,则说明 p[0: k] == p[m - k: m]。比如字符串 “abcabcabc”,最长相同前后缀为 “abcabc” = “abcabc”。

  • 如果最长相等的前后缀是重叠的,比如之前的例子 “abcabcabc”。
    • 如果我们去除字符串中相同的前后缀的重叠部分,剩下两头前后缀部分(这两部分是相同的)。然后再去除剩余的后缀部分,只保留剩余的前缀部分。比如字符串 “abcabcabc” 去除重叠部分和剩余的后缀部分之后就是 “abc”。实际上这个部分就是字符串去除整个后缀部分的剩余部分。
    • 如果整个字符串可以通过子串重复构成的话,那么这部分就是最小周期的子串。
    • 我们只需要判断整个子串的长度是否是剩余部分长度的整数倍即可。也就是判断 len(p) % (len(p) - next[size - 1]) == 0 是否成立,如果成立,则字符串 s 可由 s[0: len(p) - next[size - 1]] 构成的子串重复构成,返回 True。否则返回 False
  • 如果最长相等的前后缀是不重叠的,那我们可将重叠部分视为长度为 0 的空串,则剩余的部分其实就是去除后缀部分的剩余部分,上述结论依旧成立。
class Solution:
    def repeatedSubstringPattern(self, s: str) -> bool:
    	# 创建next数组
        le=len(s)
        if le==1:
            return False
        j=0
        next=[0 for _ in range(le)]
        for i in range(1,le):
            while j>0 and s[i]!=s[j]:
                j=next[j-1]
            if s[i]==s[j]:
                j+=1
            next[i]=j
        
        # next数组最后一位的值,表示模式串的最长相等前后缀长度。
        # 将其除去前后缀重叠部分和剩余的后缀部分,就是剩余前缀部分。
        # 这部分就是最小周期长度,可以被模式串长度整除
        if next[le-1]!=0 and le%(le-next[le-1])==0:
            return True
        return False

3.4 重复叠加字符串匹配

  给定两个字符串 a 和 b,寻找重复叠加字符串 a 的最小次数,使得字符串 b 成为叠加后的字符串 a 的子串,如果不存在则返回 -1。

输入:a = "abcd", b = "cdabcdab"
输出:3
解释:a 重复叠加三遍后为 "abcdabcdabcd", 此时 b 是其子串。    

首先,可以分析复制次数的「下界」和「上界」为何值:

  • 「下界」:至少将 a 复制长度大于等于 b 的长度,才有可能匹配
  • 「上界」:由于主串是由 a 复制多次而来,并且是从主串中找到子串 b,因此可以明确子串的起始位置,不会超过 a 的长度。即长度越过 a 长度的起始匹配位置,必然在此前已经被匹配过了。由此,我们可知复制次数「上界」最多为「下界 + 1」
    在这里插入图片描述
class Solution:
    def repeatedStringMatch(self, a: str, b: str) -> int:
    # 将b看做模式串,a最低重复次数是len([a]*count)=len(b),即至少的是一样长
    # 最大重复次数是count+1。
    # 将a复制(n+1)次后匹配b,如果匹配下标超过n,则表示无法重复,否则返回count
        m,n=len(a),len(b)
        count=(n//m)+1 if n%m!=0 else n//m
        a=a*(count+1)
        idx=a.find(b)
        
        if idx ==-1:
            return -1
        else:
            idx=idx+n-1 # 匹配串的结束位置
            return count if idx<m*count else count +1

如果用KMP算法代替find函数写,就是:

class Solution:
    def repeatedStringMatch(self, a: str, b: str) -> int:
    # 将b看做模式串,a最低重复次数是len([a]*count)=len(b),即至少的是一样长
    # 最大重复次数是count+1。
    # 将a复制(n+1)次后匹配b,如果匹配下标超过n,则表示无法重复,否则返回count
        m,n=len(a),len(b)
        count=(n//m)+1 if n%m!=0 else n//m
        a=a*(count+1)
        idx=self.KMP(a,b)
        print(idx)
        if idx ==-1:
            return -1
        else:        
            return count if idx<m*count else count +1
    
    def KMP(self, t, p):
        """
        :type haystack: str
        :type needle: str
        :rtype: int
        """
        # KMP 匹配算法,haystack 为文本串,needle 为模式串
        m,n = len(t), len(p)
        next = self.next(p)  # 生成 next 数组
        j = 0                                               # j 为模式串中当前匹配的位置

        for i in range(m):                          # i 为文本串中当前匹配的位置
            while j > 0 and t[i] != p[j]:           # 如果模式串前缀匹配不成功, 将模式串进行回退, j == 0 时停止回退
                j = next[j - 1]
            if t[i] == p[j]:                        # 当前模式串前缀匹配成功,令 j += 1,继续匹配
                j += 1
            if j == n:                              # 当前模式串完全匹配成功,返回匹配结束位置
                return i
        return -1

    def next(self,p):
        # 等同于模式串自己和自己匹配看前缀,只不过文本串从1开始(第一位匹配到结果也应该是0不是1),模式串从0开始
        next=[0 for _ in range(len(p))]
        j=0 # 共同前缀下标
        for i in range(1,len(p)):
            #次序反过来,left右移,下一步判定就不对了
            #if p[left]==p[right]:
                #left+=1
            # 先判定不相等就回退,直到相等再右移模式串指针,进行下一步比较
            while j>0 and p[j]!=p[i]:
                j=next[j-1]
            if p[i]==p[j]:
                j+=1 # 匹配时,公共前后缀长度+1
            next[i]=j
        #print(next)
        return next

3.5 最大重复子字符串

  给你一个字符串 sequence ,如果字符串 word 连续重复 k 次形成的字符串是 sequence 的一个子字符串,那么单词 word 的 重复值为 k 。单词 word 的 最大重复值 是单词 word 在 sequence 中最大的重复值。如果 word 不是 sequence 的子串,那么重复值 k 为 0 。

  给你一个字符串 sequence 和 word ,请你返回 最大重复值 k 。

输入:sequence = "ababc", word = "ba"
输出:1
解释:"ba""ababc" 的子字符串,但 "baba" 不是 "ababc" 的子字符串。
class Solution(object):
    def maxRepeating(self, sequence, word):
        """
        :type sequence: str
        :type word: str
        :rtype: int
        """
        ans=0
        while (ans*word) in sequence:
            ans+=1
        return ans -1
class Solution(object):
    def maxRepeating(self, sequence, word):
        """
        :type sequence: str
        :type word: str
        :rtype: int
        """
        m,n=len(sequence),len(word)
        max_count=m//n
        ans,count=0,1
        
        while count<=max_count:
            temp=word*count
            if sequence.find(temp)!=-1:
                ans=max(ans,count)
                count+=1
            else:
                break            
        return ans

3.6 旋转字符串

  • 给定两个字符串, s 和 goal。如果在若干次旋转操作之后,s 能变成 goal ,那么返回 true 。
  • s 的 旋转操作 就是将 s 最左边的字符移动到最右边。
输入: s = "abcde", goal = "cdeab"
输出: true
class Solution(object):
    def rotateString(self, s, goal):
        """
        :type s: str
        :type goal: str
        :rtype: bool
        """
        return goal in s+s if len(s)==len(goal) else False
  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

神洛华

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

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

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

打赏作者

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

抵扣说明:

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

余额充值