【leetcode】字符串的常见算法问题总结(LIS、LCS、LCP、LPS、ED、KMP)

字符串有很多比较经典的算法问题,例如:LIS(最长递增子序列)、LCS(最长公共子序列、最长公共子串)、LCP(最长公共前缀)、LPS(最长回文子序列、最长回文子串)、ED(最小编辑距离,也叫 “Levenshtein 距离”)、 KMP(一种字符串匹配的高效算法)。

上面列举的经典问题,在 Leetcode 中都有对应题型,这些也是笔试面试经常会遇到的基本题型。

下面来详细讲解这些经典算法问题:

LIS(Longest Increasing Subsequence)

LIS 是最长递增子序列(Leetcode 300),不要求子序列连续。其实,连续的情况也是一个经典的问题(称为 “Longest Continuous Increasing Subsequence”,见 Leetcode 674)。下面对这两种情况分别进行讲解。先来看 LIS,这个问题(Leetcode 300)的描述如下:

Given an unsorted array of integers, find the length of longest increasing subsequence.
Example:
Input: [10,9,2,5,3,7,101,18]
Output: 4
Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4.
Note:
1.There may be more than one LIS combination, it is only necessary for you to return the length.
2.Your algorithm should run in O( n 2 n^2 n2) complexity.
Follow up: Could you improve it to O(n log n) time complexity?

这个题目的思路不难,用动态规划是很容易的。令 dp[ i i i] 表示从数组开始到第 i + 1 i+1 i+1 个元素的最大递增子序列长度(之所以定义第 i + 1 i+1 i+1 个元素, 是因为数组中第一个元素序号为 0,所以第 i + 1 i+1 i+1 个元素的序号为 i i i)。设数组为 nums,则它的第 i + 1 i+1 i+1 (即序号为 i i i ) 的元素为 nums[ i i i],由于要考虑到 “递增”,所以需要比较 nums[ i i i] 与它之前的所有元素的大小,所以需要再增加一个维度 dp[ i i i][ j j j],要大于它前面的某些元素,那么它对应的 dp[ i i i] 会基于前面的元素对应的 dp 上增加1。举个例子, nums = [1, 3, 5, 2, 4, 7, 6],容易知道:dp[0] = 1,dp[1] = 2,dp[2] = 3,dp[3] = 2,在判断 dp[4] 的时候,由于 nums[4] 比前面的元素 1、3、2 都要大,那么当 j j j 遍历到元素 1 时,dp[4] = dp[0]+1 = 2,当 j j j 遍历到元素 3 时,dp[4] = dp[1]+1 = 3,当 j j j 遍历到元素 2 时,dp[4] = dp[3]+1 = 3,而由于是求 “最长递增子序列”,所以应该取所有 dp[4] 中最大的作为最后 dp[4] 的取值,即 dp[4] = max(dp[4], dp[ j j j]+1) ( j j j = 0, 1, 3)。

Python 代码如下:

    def LIS(nums):
        if len(nums)==0:
            return 0
        dp = [1 for _ in range(len(nums))]
        for i in range(1,len(nums)):
            for j in range(i):
                if nums[i]>nums[j]:
                    dp[i] = max(dp[i],dp[j]+1)
        return max(dp)

上面这个解法是 O ( n 2 ) O(n^2) O(n2) 的时间复杂度,在题目中已经明确指出还有复杂度 O ( n log n ) O(n\text{log}n) O(nlogn) 的算法,下面给出它的思路:

以 nums = [3,5,6,2,4,5,7] 作为例子,从第一个元素 3 开始,逐步添加后续元素,看 “最长递增序列” 的长度变化,记录如下:
(1) 当第一个元素时, LCS 的列表为 [3];
(2) 当加入第二个元素时,LCS 的列表为 [3, 5],保留原有的一个长度的列表:
长度为1:[3]
长度为2:[3,5]
(3) 当加入第三个元素时,LCS 的列表为 [3, 5, 6],列表情况为:
长度为1:[3]
长度为2:[3, 5]
长度为3:[3, 5, 6]
(4) 当加入第四个元素时,元素 2 比任何一个元素都小,故只能生成长度为 1 的递增序列,由于比之前长度为 3 的元素要小,故将其替换掉,得到列表情况如下:
长度为1:[2]
长度为2:[3, 5]
长度为3:[3, 5, 6]
(5) 当加入第五个元素时,元素 4 只比上面长度为 1 里的元素大,得到 [3, 4],由于此时得到的长度为 2 的 [3, 4] 的尾端元素 4 比之前长度为 2 的 [3, 5] 的尾端元素 5 要小,故替换掉之前的长度为 2 的列表,得到的列表情况如下:
长度为1:[2]
长度为2:[3, 4]
长度为3:[3, 5, 6]
(4) 当加入第六个元素时,元素 5 比长度为 2 的 [3, 4] 的尾端大,比长度为 3 的 [3, 5, 6] 的尾端小,故长度为 3 的列表被替换成 [3, 4, 5],列表情况如下:
长度为1:[2]
长度为2:[3, 4]
长度为3:[3, 4, 5]
(4) 当加入第七个元素时,元素 7 比长度为 3 的 [3, 4, 5] 的尾端大,而且此时长度 3 是最大长度,故会生成新的长度为 4 的 [3, 4, 5, 7],列表情况如下:
长度为1:[2]
长度为2:[3, 4]
长度为3:[3, 4, 5]
长度为4:[3, 4, 5, 7]
所以最后的 LIS 的长度为 4。

上面的思路也很简单,相当于每次添加一个新的元素时,将这个元素与已有的所有列表中的尾端元素进行比较,如果它比最长的列表的尾端元素大,则以这个元素为尾端元素来新增加一个更长的列表,如果它介于长度为 m 和 m+1 的尾端元素大小之间,则在长度为 m 的列表尾端上增加上这个元素变成 m+1 的列表并替换掉之前的 m+1 的列表。

Python 代码如下:

    # 后缀数组解法
    def LIS(nums):
        tails = [0 for _ in range(len(nums))]  # 构建尾部数组(初始化)
        max_len = 0
        for x in nums:
            i, j = 0, max_len  # 二分查找的两个指针
            while i < j:
                m = (i + j) // 2  #  计算首尾指针对应的中间指针
                if tails[m] < x: 
                    i = m + 1 # 当查询的值比 x小时,改变首指针
                else:        
                    j = m     # 当查询的值不比 x小时,改变尾指针
            tails[i] = x  # 二分查找到准确的 i后,用 x替换掉其尾部
            max_len = max(i + 1, max_len)
        return max_len

下面再来看子序列连续的情况,即 “Longest Continuous Increasing Subsequence” (LCIS)。问题描述如下:

Given an unsorted array of integers, find the length of longest continuous increasing subsequence (subarray).
Example 1:
Input: [1,3,5,4,7]
Output: 3
Explanation: The longest continuous increasing subsequence is [1,3,5], its length is 3.
Even though [1,3,5,7] is also an increasing subsequence, it’s not a continuous one where 5 and 7 are separated by 4.
Example 2:
Input: [2,2,2,2,2]
Output: 1
Explanation: The longest continuous increasing subsequence is [2], its length is 1.
Note: Length of the array will not exceed 10,000.

这个 LICS 问题相对于上面的 LIS 而言,就非常简单了。对于数组 nums,LICS 问题只需要比较 nums[i] 与它前一个数 nums[i-1] 的大小就可以了。

Python 代码如下:

    def LCIS(nums):
        if len(nums)==0:
            return 0
        dp = [1]*len(nums)
        for i in range(1,len(nums)):
            if nums[i]>nums[i-1]:
                dp[i]=dp[i-1]+1
        return max(dp)
LCS(Longest Common Subsequence、Longest Common Substring)

对于LCS,这是两个问题。一个是求字符串的最长公共子序列,一个是求字符串的最长公共子串。先来讲解最长公共子串。

比如两个字符串, s1 为 “abcddeeb”,s2 为 “ababcddeb”,那么要计算公共子串长度,最容易想到的是动态规划的方法。令 dp[i][j] 为 s1 前 i 个字符和 s2 前 j 个字符构成的子字符串的最长公共子串的长度。那么,显然,当 s1[i] == s2[j] 时,它的公共子串的长度 dp[i][j] 肯定比 dp[i-1][j-1] 要大 1,这个公式就是解这个题的核心。

Python 代码如下:

    def LCS(s1, s2):
        len1 = len(s1); len2 = len(s2)
        max_len = 0
        dp = [[0 for _ in range(len2)] for _ in range(len1)]
        for i in range(len1):
            for j in range(len2):
                if s1[i] == s2[j]:
                    if i>0 and j>0:
                        dp[i][j] = dp[i-1][j-1] + 1
                    else:
                        dp[i][j] = 1   #  边界情况
                    if max_len < dp[i][j]:
                        max_len = dp[i][j]
        return max_len

再来看最长公共子序列。子序列与子串的不同在于它并不要求连续,只要有字符是公共的就算一个。其中,核心公式 dp[i][j] = dp[i-1][j-1] + 1 没变,但增加了 s1[i] 与 s2[j] 不相等的情况,此时有 dp[i][j] = max(dp[i-1][j], dp[i][j-1]),也就是说,。

Python 代码如下:

    def LCS(s1, s2):
        # 做了 padding 处理,使得包含空串的情况(自含了边界情况)
        dp = [[0 for _ in range(len(s2)+1)] for _ in range(len(s1)+1)]
        for i in range(1,len(s1)+1):
            for j in range(1,len(s2)+1):
                if s1[i-1] == s2[j-1]:
                    dp[i][j] = dp[i-1][j-1] + 1
                else:
                    dp[i][j] = max(dp[i-1][j], dp[i][j-1])
        return dp[len(s1)][len(s2)]
LCP(Longest Common Prefix)

这个问题(Leetcode 14)的描述如下:

Write a function to find the longest common prefix string amongst an array of strings.
If there is no common prefix, return an empty string “”.
Example 1:
Input: [“flower”,“flow”,“flight”]
Output: “fl”
Example 2:
Input: [“dog”,“racecar”,“car”]
Output: “”
Explanation: There is no common prefix among the input strings.
Note:
All given inputs are in lowercase letters a-z.

这个问题比较简单,只需要统计两个字符串前面公共字符的个数就可以了。时间复杂度是 O( n n n)。

Python 代码如下:

    def LCP(strs):
        lens = list(map(len, strs))
        if lens==[]:  #  如果strs中没有单词,则肯定没有前缀,返回''
            return ''
        min_lens = min(lens)  # 取最小长度的单词的对应长度作为循环长度
        prefix = ''
        for i in range(min_lens):
            char = list(map(lambda s:s[i],strs)) # 依次取每个单词的第i个字符
            if char==[char[0]]*len(strs): # 如果所有单词的第i个字符相同,就存入prefix
                prefix+=char[0]
            else:     # 当出现有不相同的字符时,就跳出循环
                break
        return prefix
LPS(Longest Palindrome Subsequence、Longest Palindrome Substring)

LPS 也有两个问题,一个是最长回文子序列(Leetcode 516),一个是最长回文子串(Leetcode 5)。下面先讲解最长回文子序列,其问题如下:

Given a string s, find the longest palindromic subsequence’s length in s. You may assume that the maximum length of s is 1000.
Example 1:
Input: “bbbab”
Output: 4
One possible longest palindromic subsequence is “bbbb”.
Example 2:
Input: “aaabcdbcb”
Output: 5
One possible longest palindromic subsequence is “bcdcb”.

Python 代码如下:

    def LPS(s):
        if s==s[::-1]:
            return len(s)
        # dp[i][j] 表示s[i..j]中的最大回文长度
        dp = [[0 for _ in range(len(s))] for _ in range(len(s))]
        for i in range(len(s)):
            dp[i][i] = 1  # 单个字符肯定是回文(动态规划的base条件)
        for l in range(1,len(s)+1):  # s[i..j]的长度范围从 1到 len(s)
            for i in range(len(s)-l+1): # 确定s[i..j]的长度为l后,i的取值最大为len(s)-l
                j = i+l-1   # 因为s[i..j]的长度为l,所以j-i应该为 l-1
                if i<j:
                    if s[i] == s[j]:
                        dp[i][j] = dp[i+1][j-1]+2
                    else:
                        dp[i][j] = max(dp[i+1][j],dp[i][j-1])
        return dp[0][len(s)-1]

另一个问题是最长回文子串(Leetcode 5),这个问题相对之前的要简单许多,当然它的解法也有很多(可参考:最长回文子串(Longest Palindromic Substring))。下面先给出这个问题的描述,然后再用三种方法来解这个问题。问题描述如下:

Given a string s, find the longest palindromic substring in s. You may assume that the maximum length of s is 1000.
Example 1:
Input: “babad”
Output: “bab”
Note: “aba” is also a valid answer.
Example 2:
Input: “cbbd”
Output: “bb”

下面通过三种方法来解这个问题。

先考虑动态规划方法,与之前的最长回文子序列类似。

Python 代码如下:

    def LPS(s):
        max_len = 1 # 最长回文子串长度
        start = 0 # 最长回文子串起点
        if s==s[::-1]:
            return s
        # dp[i][j] 表示s[i..j]中的最大回文子序列长度
        dp = [[0 for _ in range(len(s))] for _ in range(len(s))]
        for l in range(1,len(s)+1):  # s[i..j]的长度范围从 1到 len(s)
            for i in range(len(s)-l+1): # 确定s[i..j]的长度为l后,i的取值最大为len(s)-l
                j = i+l-1   # 因为s[i..j]的长度为l,所以j-i应该为 l-1
                if i==j:
                    dp[i][j] = True  # 单个字符肯定是回文(动态规划的base条件)
                elif j-i==1:
                    dp[i][j] = (s[i]==s[j])
                else:
                    dp[i][j] = ((s[i]==s[j]) and dp[i+1][j-1])
                if(dp[i][j] and (max_len<j-i+1)):
                    max_len = j-i+1
                    start = i
        return s[start:start+max_len]

上面的解法时间复杂度是 O( n 2 n^2 n2),在效率上并不高效,下面来介绍两种时间复杂度为 O( n n n) 的解法。

Python 代码如下:

    def longestPalindrome(s):
        max_len = 0   # 最长回文子串长度
        start = 0   # 最长回文子串起点
        for i in range(len(s)):
            if i - max_len >= 0 and s[i-max_len:i+1] == s[i-max_len:i+1][::-1]:
                start = i - max_len
                max_len += 1
            if i - max_len >= 1 and s[i-max_len-1:i+1] == s[i-max_len-1:i+1][::-1]:
                start = i - max_len -1
                max_len += 2
        return s[start:(start+max_len)]

另外一种时间复杂度为 O( n n n) 的算法是 Manacher 算法。

Python 代码如下:

    def Manacher(s):
        s='#'+'#'.join(s)+'#'  # 字符串首尾和中间都插入字符'#'
        RL=[0 for _ in range(len(s))] #  RL是回文半径数组 
        MaxRight=0
        Pos=0 
        Maxlen=0 
        for i in range(len(s)):
            if i<MaxRight:
                RL[i]=min(RL[2*Pos-i],MaxRight-i)
            else:  #i在maxright右边,以i为中心的回文串还没扫到,此时,以i为中心向两边扩展 
                RL[i]=1 #RL=1:只有自己     
            #以i为中心扩展,直到左!=右or达到边界(先判断边界)
            while i-RL[i]>=0 and i+RL[i]<len(s) and s[i-RL[i]]==s[i+RL[i]]: 
                RL[i]+=1  
            #更新Maxright pos: 
            if RL[i]+i-1>MaxRight: 
                MaxRight=RL[i]+i-1 
                Pos=i             
            #更新最长回文子串的长; 
            Maxlen=max(Maxlen,RL[i])
        s=s[RL.index(Maxlen)-(Maxlen-1):RL.index(Maxlen)+(Maxlen-1)] 
        s=s.replace('#','') 
        return s 
ED(Edit Distance)

最小编辑距离,又称 Levenshtein 距离,它是指两个字符串之间通过增、删、替换(改)三种变换能够使字符串相同的最小编辑次数,用来衡量两个字符串的字符距离(每一次增、删、替换,都算作距离为1)。问题(Leetcode )描述如下:

Given two words word1 and word2, find the minimum number of operations required to convert word1 to word2.
You have the following 3 operations permitted on a word:
1.Insert a character
2.Delete a character
3.Replace a character
Example:
Input: word1 = “intention”, word2 = “execution”
Output: 5
Explanation:
intention -> inention (remove ‘t’)
inention -> enention (replace ‘i’ with ‘e’)
enention -> exention (replace ‘n’ with ‘x’)
exention -> exection (replace ‘n’ with ‘c’)
exection -> execution (insert ‘u’)

Python 代码如下:

    def minDistance(word1, word2):
        dp = [[0 for _ in range(len(word2)+1)] for _ in range(len(word1)+1)]
        for i in range(len(word1)+1):
            for j in range(len(word2)+1):
                if i==0:
                    dp[i][j] = j
                if j==0:
                    dp[i][j] = i
                if i !=0 and j!=0 and word1[i-1] == word2[j-1]:
                    dp[i][j] = dp[i-1][j-1]
                if i !=0 and j!=0 and word1[i-1] != word2[j-1]:
                    dp[i][j] = min(dp[i][j-1],dp[i-1][j],dp[i-1][j-1])+1
        return dp[len(word1)][len(word2)]
KMP(D.E.Knuth,J.H.Morris,V.R.Pratt)

关于 KMP 算法是做什么的,以及 KMP 算法的原理这些基本问题,可以参考这两篇文章:[1][2]。下面主要给出 KMP 算法的实现过程以及代码:

    def strStr(self, strings, strs):
        # strings 是需要匹配的串,而 strs 是模式串
        # KMP算法的核心是公式:若i为恰不匹配序号,则下一步匹配为 strs[next[i]]
        # 这个next数是针对模式串而言的,通过计算最大公共前后缀来得到 next 数组
        # 举例如下:
        # 比如模式串为 strs=“ABCABEFAC”,长度为9,那么next数组就是长度为9的数组
        # next的元素得到如下:
        # 首先:令 next[0] = -1
        # next[i+1]表示needle[0..i]的最大公共前后缀长度
        # 由于strs[0] = “A”,没有前后缀,故next[1] = 0
        # 由于strs[0..1]=“AB”,前缀为{A},后缀为{B},故最大公共前后缀为0,next[2]=0
        # 由于strs[0..2]=“ABC”,前缀为{A,AB},后缀为{BC,C},故next[3]=0
        # 后面同理,于是可得 next数组为 [-1,0,0,0,1,2,0,0,1]
        # 将 next数组加上序号,可得table如下:
        #-------------------------------------
        #| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |  <-- 这是 strs 模式串序号
        #-------------------------------------
        #| A | B | C | A | B | E | F | A | C |  <-- 这是 strs 模式串
        #-------------------------------------
        #|-1 | 0 | 0 | 0 | 1 | 2 | 0 | 0 | 1 |  <-- 这是 next 数组
        #-------------------------------------
        # 比如,需要匹配的字符串为 “ABCDABCABABCABEFACDAB”
        # 那么,有如下匹配:
        # “ABCDABCABABCABEFACDAB”
        # “ABCABEFAC”
        # 可以看到在 i=3 时(即'D'与'A')出现不匹配,由于needle[3]=0,那么将第0序号开始对齐
        # “ABCDABCABABCABEFACDAB”
        #    “ABCABEFAC”
        # 再由于 i=0 时(即'D'与'A')出现不匹配,由于needle[0]=-1,故将第-1序号开始对齐
        # “ABCDABCABABCABEFACDAB”
        #     “ABCABEFAC”
        # 此时,i=5 时(即'A'与'E')不匹配,由于needle[5]=2,故将第2序号开始对齐
        # “ABCDABCABABCABEFACDAB”
        #          “ABCABEFAC”
        # 此时已经找到匹配字符串,完成
        #============================================
        if not strs:
            return 0
        i, j, m, n = -1, 0, len(strings), len(strs)
        # i 是 next数组元素值,j是next数组序号
        # (由于 next在python中是关键字,此处写作nexts)
        nexts = [-1] * n   # next 数组初始化
        while j < n - 1:  
            if i == -1 or strs[i] == strs[j]:   
                i, j = i + 1, j + 1
                nexts[j] = i
            else:
                i = nexts[i]
        i = j = 0
        while i < m and j < n:
            if j == -1 or strings[i] == strs[j]:
                i, j = i + 1, j + 1
            else:
                j = nexts[j]
        if j == n:
            return i - j
        return -1

参考博文:
[1] KMP算法图解之过程实现
[2] 如果你看不懂KMP算法,那就看一看这篇文章

  • 14
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
KMP算法是一种字符串匹配算法,用于在一个文本串S内查找一个模式串P的出现位置。它的时间复杂度为O(n+m),其中n为文本串的长度,m为模式串的长度。 KMP算法的核心思想是利用已知信息来避免不必要的字符比较。具体来说,它维护一个next数组,其中next[i]表示当第i个字符匹配失败时,下一次匹配应该从模式串的第next[i]个字符开始。 我们可以通过一个简单的例子来理解KMP算法的思想。假设文本串为S="ababababca",模式串为P="abababca",我们想要在S中查找P的出现位置。 首先,我们可以将P的每个前缀和后缀进行比较,得到next数组: | i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | --- | - | - | - | - | - | - | - | - | | P | a | b | a | b | a | b | c | a | | next| 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 | 接下来,我们从S的第一个字符开始匹配P。当S的第七个字符和P的第七个字符匹配失败时,我们可以利用next[6]=4,将P向右移动4个字符,使得P的第五个字符与S的第七个字符对齐。此时,我们可以发现P的前五个字符和S的前五个字符已经匹配成功了。因此,我们可以继续从S的第六个字符开始匹配P。 当S的第十个字符和P的第八个字符匹配失败时,我们可以利用next[7]=1,将P向右移动一个字符,使得P的第一个字符和S的第十个字符对齐。此时,我们可以发现P的前一个字符和S的第十个字符已经匹配成功了。因此,我们可以继续从S的第十一个字符开始匹配P。 最终,我们可以发现P出现在S的第二个位置。 下面是KMP算法的C++代码实现:

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值