您的位置:首页 > 其它

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

2018-10-09 00:45 435 查看

字符串有很多比较经典的算法问题,例如: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(n2n^2n2) complexity.
Follow up: Could you improve it to O(n log n) time complexity?

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

上面这个解法是 O(n2)O(n^2)O(n2) 的时间复杂度,在题目中已经明确指出还有复杂度 O(nlogn)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
[/code]

下面再来看子序列连续的情况,即 “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)
[/code]
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
[/code]

再来看最长公共子序列。子序列与子串的不同在于它并不要求连续,只要有字符是公共的就算一个。其中,核心公式 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)]
[/code]
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(nnn)。

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
[/code]
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]
[/code]

另一个问题是最长回文子串(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]
[/code]

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

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)]
[/code]

另外一种时间复杂度为 O(nnn) 的算法是 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
[/code]
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)]
[/code]
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
[/code]

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

阅读更多
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: