文章目录
- 一、题目
- 二、我的解答:双指针从头开始遍历
- 2.1 暴力循环:超出时间限制
- 2.2 优化后的暴力循环:虽然没有超时,但效率很低
- 三、双指针中心扩散法(从字符串中心开始遍历)
- 四、动态规划法
- 4.1 我的错误解答:正向遍历字符串
- 4.2 正确解答:逆向遍历字符串
- 五、判断回文子串的多种写法
【LeetCode】5,最长回文子串。 难度等级:中等。
一、题目
二、我的解答:双指针从头开始遍历
思路:采用双指针(双重for循环)列举所有的子串,判断其是否为回文串,保存最长的回文串。时间复杂度为 O(n3):双指针 O(n2),判断子串是否为回文串 O(n)
2.1 暴力循环:超出时间限制
直接暴力循环会超出时间限制,因为时间复杂度太高。但是代码是正确的:
class Solution:
def judgePalindrome_(self,s:str)->bool:
if len(s)==1:
return True
length=len(s)
for i in range(0,length//2+1):
if s[i]!=s[length-i-1]:
return False
return True
def longestPalindrome(self, s: str) -> str:
if len(s)<=1:
return s
# 判断s是否为单一元素字符串,如"aaaaaaaaaa"。若不使用该判断,会超出时间限制。
if s==s[0]*len(s):
return s
# 记录下maxLen和相应的起始位置
maxLen=1
startIndex=0
# 双指针
length=len(s)
for i in range(0,length-1):
subStr=s[i]
for j in range(1,length-i):
subStr+=s[i+j]
if self.judgePalindrome_(subStr):
if len(subStr)>=maxLen:
maxLen=len(subStr)
startIndex=i
return s[startIndex:startIndex+maxLen]
2.2 优化后的暴力循环:虽然没有超时,但效率很低
由于我们只需要找到最长的子串,所以2.1中的代码是可以优化的。我们只需要判断长度大于当前maxLen的子串是否为回文子串即可,因为长度小于maxLen的子串即使是回文子串,也不可能是最长的,所以不需要判断。
优化后的代码如下:
class Solution:
def judgePalindrome_(self,s:str)->bool:
if len(s)==1:
return True
length=len(s)
for i in range(0,length//2+1):
if s[i]!=s[length-i-1]:
return False
return True
def longestPalindrome(self, s: str) -> str:
if len(s)<=1:
return s
# 判断s是否为单一元素字符串,如"aaaaaaaaaa"。若不使用该判断,会超出时间限制。
if s==s[0]*len(s):
return s
# 记录下maxLen和相应的起始位置
maxLen=1
startIndex=0
# 双指针
length=len(s)
for i in range(0,length-1):
subStr=s[i]
for j in range(1,length-i):
subStr+=s[i+j]
# 只判断长度大于 maxLen 的子串即可
if len(subStr) > maxLen and self.judgePalindrome_(subStr):
if len(subStr)>=maxLen:
maxLen=len(subStr)
startIndex=i
return s[startIndex:startIndex+maxLen]
执行结果:
执行用时:9136 ms, 在所有 Python3 提交中击败了 4.98% 的用户
内存消耗:16 MB, 在所有 Python3 提交中击败了 49.25% 的用户
执行结果惨不忍睹,其实试行用例再多一点也超时了。所以这道题用双指针是行不通的。
三、双指针中心扩散法(从字符串中心开始遍历)
利用回文子串的对称性,以每一个位置为中心,用left和right双指针向两侧扩散。时间复杂度为 O(n2)
代码:
class Solution:
def expend_(self,s,left,right):
length=len(s)
while left>=0 and right<length and s[left]==s[right]:
left-=1
right+=1
# while循环会导致 left-=1 和 right+=1 多执行一次
return left+1, right-1
def longestPalindrome(self, s: str) -> str:
if len(s)<=1:
return s
if len(s)==2:
if s[0]==s[1]:
return s
else:
return s[0]
length=len(s)
begin=0
end=0
for i in range(length-1):
# 以奇数长度中心开始扩展, 此时初始扩展中心为 left=right=i
left1,right1=self.expend_(s,i,i)
# 以偶数长度中心开始扩展, 此时初始扩展中心分别为 left=i,right=i+1
left2,right2=self.expend_(s,i,i+1)
if right1-left1>end-begin:
begin=left1
end=right1
if right2-left2>end-begin:
begin=left2
end=right2
return s[begin:end+1]
关键思路:使用一个函数实现 奇数长度和偶数长度字符串的两种中心扩散情况。
执行结果:
执行用时:340 ms, 在所有 Python3 提交中击败了 89.70% 的用户
内存消耗:16.1 MB, 在所有 Python3 提交中击败了 45.40% 的用户
四、动态规划法
对于一个子串而言,如果它是回文串,并且长度大于 2,那么将它首尾的两个字母去除之后,它仍然是个回文串。所以本题涉及到重复计算,那么就可以提前把较短的子串是否为回文串的判断结果保存下来,判断更长的子串时就可以利用之前的结果,减少计算量。
4.1 我的错误解答:正向遍历字符串
下面是我写的答案,对于一个测试用例,其计算结果是错误的:
输入:"aaaaa"
输出:"aaaa"
预期结果:"aaaaa"
code:
class Solution:
def longestPalindrome(self, s: str) -> str:
if len(s)<=1:
return s
if len(s)==2:
if s[0]==s[1]:
return s
else:
return s[0]
# 记录最长回文子串的长度和起始下标
maxLen=1
startIndex=0
length=len(s)
dp=[[False]*length for _ in range(length)]
# dp[i][j]表示s[i:j+1]是否为回文串
# 初始状态
for i in range(length-1):
dp[i][i]=True # 单个字符 s[i:i+1]=s[i] 一定是回文串
if s[i]==s[i+1]:
dp[i][i+1]=True # 判断长度为2的子串是否为回文串
if maxLen<2:
maxLen=2
startIndex=i
for i in range(length-2):
for j in range(i+2,length):
# 状态转移方程, 前提条件是 len(s[i:j+1])>2
dp[i][j]=(dp[i+1][j-1]==True and s[i]==s[j])
if dp[i][j]==True and j-i+1>maxLen:
maxLen=j-i+1
startIndex=i
return s[startIndex:startIndex+maxLen]
错误原因分析:
显然对于输入为 s=“aaaaa” 的字符串,最长子串为 dp[0][4]=s[0:4+1]=“aaaaa”
但程序计算 dp[0][4]=(dp[1][3]==True and s[1]==s[3]) 时,外层 i 的循环只循环了 i=0 ,i=1的情况还没有遍历到,所以 dp[1][3]=False,则程序执行结果是 dp[0][4]=False;但显然 dp[0][4]=True 才是正确的。
其根本原因是在顺序遍历字符串的情况下,遍历 i+1 时无法用到 i 的结果,遍历 i 时也无法用到 i +1 的结果,导致失去了动态规划法的意义。
4.2 正确解答:逆向遍历字符串
要想让更长的字符串利用到短字符串的计算结果,必须逆向遍历字符串。代码如下:
class Solution:
def longestPalindrome(self, s: str) -> str:
if len(s)<=1:
return s
if len(s)==2:
if s[0]==s[1]:
return s
else:
return s[0]
# 记录最长回文子串的长度和起始下标
maxLen=1
startIndex=0
length=len(s)
dp=[[False]*length for _ in range(length)]
# dp[i][j]表示s[i:j+1]是否为回文串
# 初始状态
for i in range(length-1):
dp[i][i]=True # 单个字符 s[i:i+1]=s[i] 一定是回文串
if s[i]==s[i+1]:
dp[i][i+1]=True # 判断长度为2的子串是否为回文串
if maxLen<2:
maxLen=2
startIndex=i
# i 一定要逆向遍历
for i in range(length-3,-1,-1):
for j in range(i+2,length):
# 状态转移方程, 前提条件是 len(s[i:j+1])>2
dp[i][j]=(dp[i+1][j-1]==True and s[i]==s[j])
if dp[i][j]==True and j-i+1>maxLen:
maxLen=j-i+1
startIndex=i
return s[startIndex:startIndex+maxLen]
五、判断回文子串的多种写法
方法一:使用 for 循环遍历一半长度的字符串。
这种写法需要考虑是否区分字符串长度是奇数还是偶数(其实不需要区分),写起来不方便。代码如下:
def judgePalindrome_(self,s:str)->bool:
if len(s)==1:
return True
length=len(s)
for i in range(0,length//2+1):
if s[i]!=s[length-i-1]:
return False
return True
方法一:使用 left 和 right 的双指针,分别从头部和尾部开始遍历,直到两个指针汇合时停止循环。
这种写法简洁明了,写法简单,不需要额外思考是否需要区分字符串长度是奇数还是偶数。代码如下:
def judgePalindrome_(self,s:str)->bool:
if len(s)==1:
return True
left=0
right=len(s)-1
while left<right:
if s[left]!=s[right]:
return False
left+=1
right-=1
return True