大家好,今天我們來聊一聊最長迴文子串這個問題。
前幾天,有個校招的小夥伴問到了這個問題。今天,我們就來分析一下。
最長迴文子串不論是在校招還是社招中都是各大廠出現頻率比較高的題目。所以對於正在找工作的同學來說,這是必須要準備的一道題。
Tips:迴文串就是正反讀都是一樣的字串,比如"上海自來水來自海上"。
問題描述
給你一個字串 s,找到s中最長的迴文子串。
示例:
輸入:s = "cbbd"
輸出:"bb"
提示:
1 <= s.length <= 1000
s 僅由數字和英文字母(大寫和/或小寫)組成
分析問題
一看到這個問題,我們直接從定義出發,這是最容易想到的解決方案。我們把這個“最長迴文子串”這幾個字,拆開來看,它的要求有三點:“最長”、“迴文”、“子串”。首先它是“子串”,然後再“迴文”,最後是“最長”。所以,這就很容易理解了,我們把所有的子串求出來,然後判斷是不是“迴文”,把最長的“迴文”拿過來,這個問題不就迎刃而解了。想通這個邏輯,那程式碼寫起來就簡單多了。下面我們來看看程式碼如何實現。
def isPalindromic(ss):
n=len(ss)
for i in range(int(n/2)):
if(ss[i]!=ss[n-i-1]):
return False
return True
def longestPalindrome(s):
ans=""
maxlen=0
n=len(s)
#求出所有子串
for i in range(n):
for j in range(i+1,n):
ss=s[i:j]
#判斷子串是否是“迴文”
if(isPalindromic(ss)):
#拿出最長的
if(len(ss)>maxlen):
ans=ss
maxlen=len(ss)
return ans
print(longestPalindrome("cbbd"))
這個演算法的實現複雜度是O(n^3),空間複雜度是O(1)。那我們有什麼優化方式來降低時間複雜度嗎?
優化
既然你正反讀是一樣的,我們把原字串翻轉生成一個新的字串,然後拿新的字串和原字串求最長公共子串不就可以了嗎?那我們如何求兩個字串的最長公共子串呢?既然要找公共子串,那不就是求出兩個字串的所有子串,然後比較他們是否相同,找出最長的相同的子串。顯而易見,這個演算法的時間複雜度也是O(n^3),這不和上面那個演算法複雜度一樣嗎?也沒優化上啊。
def longestCommonSubstring(s1,s2):
n1=len(s1)
n2=len(s2)
maxlen=0
ans=""
for i in range(n1):
for j in range(n2):
length=0
m=i
n=j
while m<n1 and n<n2:
if s1[m]!=s2[n]:
break
m=m+1
n=n+1
length=length+1
if length> maxlen:
maxlen=length
ans=s1[i:i+maxlen]
return ans
不要著急,我們慢慢往下看。可以先喝口茶休息休息。
有了上面的這個解答,面試官肯定是不滿意嘛。要想拿到offer,我們還得加把勁。
我們來看上面的解答有哪些問題呢?我們可以知道在進行子串比較的過程中,多了很多重複的比較。也就是程式碼裡的while迴圈部分。比如我們在比較以i和j分別為起點的子串時,有可能會進行i+1
和j+1
以及i+2
和j+2
位置的字元的比較。而在比較以 i+1和
j+1分別為起始點字串時,這些字元又會被比較一次了。這就說明了該問題有“重疊子問題”的特徵,那我們是不是可以用動態規劃來解決呢?
我們假設兩個字串分別為s和t,s[i]和t[j]分別表示第i和第j個字元。我們用a[i] [j]表示以s[i]和t[j]為結尾的相同子串的最大長度。那我們就可以很容易推匯出a[i+1] [j+1]之間的關係,這兩者之間就差a[i+1]和t[j+1]這一對字元,如果a[i+1]和t[j+1]相等,那麼a[i+1] [j+1]=a[i] [j] +1,如果不相等,則a[i+1] [j+1]=0。當i=0或者j=0時,對應字元相等的話a[i] [j]=1,不相等的話,則為0。下面,我們來看一下程式碼如何實現。
def longestPalindrome(s):
if s=="":
return ""
#字串反轉
t=s[::-1]
n=len(s)
a=[[0 for _ in range(n)] for _ in range(n)]
maxlen=0
maxend=0
for i in range(n):
for j in range(n):
if(s[i]==t[j]):
if i == 0 or j == 0:
a[i][j]=1
else:
a[i][j]=a[i-1][j-1]+1
if a[i][j] > maxlen:
maxlen=a[i][j]
#以i為結尾的子串
maxend=i
return s[maxend-maxlen+1:maxend+1]
我們來看下面這個例子S1=“abcedcba”,S2=“abcdecba”。最長的公共子串是“abc”和“cba”,但很明顯這不是迴文子串。所以,我們在求出最長公共子串後,還需要判斷一下該子串倒置前後的下標是否相匹配。比如S1="daba",S2="abad"。S2中aba的下標是0,1,2,倒置前是3,2,1,和S1中的aba的下標符合,所以aba是最長迴文子串。其實,我們不需要每個字元都判斷,我們只需要判斷末尾字元就可以了。
如圖所示,我們來看最長公共子串“aba”在翻轉前後末尾字元“a”對應的下標分別為i和j。翻轉字串t中該子串的末尾字元“a”對應的下標是j=2,在翻轉前s中對應的下標為m=length-1-j=4-1-2=1。m對應的是公共子串在原字串s中首位字元的下標,再加上子串的長度,即m+a[i] [j]-1對應的是公共子串在原字串s中末尾字元的下標,如果它和i相等,則說明該子串是迴文子串。下面,我們來看一下程式碼的實現。
def longestPalindrome(s):
if s=="":
return ""
#字串反轉
t=s[::-1]
n=len(s)
a=[[0 for _ in range(n)] for _ in range(n)]
maxlen=0
maxend=0
for i in range(n):
for j in range(n):
if(s[i]==t[j]):
if i == 0 or j == 0:
a[i][j]=1
else:
a[i][j]=a[i-1][j-1]+1
if a[i][j] > maxlen:
#翻轉前對應的下標
m=n-1-j
#判斷下標是否對應
if m+a[i][j]-1==i:
maxlen=a[i][j]
maxend=i
return s[maxend-maxlen+1:maxend+1]
print(longestPalindrome("abacd"))
我們從程式碼可以看出,時間複雜度為O(n2),空間複雜度也是O(n2)。
我們來看一下空間複雜度還能進行優化嗎?
我們來分析一下迴圈邏輯,i=0,然後遍歷j=0,1,2,3。更新一列,然後i=1,再更新一列,更新的時候,我們只需要上一列的資訊。所以,我們可以用一個一維陣列來儲存就好了。我們看一下程式碼實現。
def longestPalindrome(s):
if s=="":
return ""
#字串反轉
t=s[::-1]
n=len(s)
a=[0 for _ in range(n)]
maxlen=0
maxend=0
for i in range(n):
for j in range(n-1,-1,-1):
if(s[i]==t[j]):
if i == 0 or j == 0:
a[j]=1
else:
a[j]=a[j-1]+1
else:
a[j]=0
if a[j] > maxlen:
#翻轉前對應的下標
m=n-1-j
#判斷下標是否對應
if m+a[j]-1==i:
maxlen=a[j]
maxend=i
return s[maxend-maxlen+1:maxend+1]
print(longestPalindrome("abacd"))
所以,我們可以把空間複雜度降低為O(n)。
到此為止,我們就把最長迴文子串這個問題說完了。
最後
最長迴文子串作為校招和社招中常見的演算法題,是需要我們掌握的。對於有“重疊子問題”特徵的問題,我們首先要考慮採用動態規劃的方法來解決。
今天,我們就聊到這裡。更多有趣知識,請關注公眾號【程式設計師學長】。我給你準備了上百本學習資料,包括python、java、資料結構和演算法等。如果需要,請關注公眾號【程式設計師學長】,回覆【資料】,即可得。
你知道的越多,你的思維也就越開闊,我們下期再見。