算法君:小白同學,給你出道算法題,看你小子算法能力有沒有長進。
算法小白:最近一直在研究算法,刷了很多算法題,正好活動活動大腦,來來來,趕快出題!
算法君:聽好了,題目是:求一個字符串中最長的回文字符串。
算法小白:這個算法好像很簡單,就是有一個概念不太明白,啥叫“回文字符串”。
算法君:哈哈,你說的很簡單,一定是題目的字數很少的意思。
算法小白:哦,又被老大猜中了。還是先給我講一下什么是回文字符串吧!
算法君:回文字符串嗎!首先是一個字符串(廢話),然后,核心就是回文。“回”嗎,就是來來回回的意思。其實就是正向和反向遍歷字符串中的每一個字符,然后嘛,如果遍歷的結果都一樣,就是回文字符串。例如,有一個字符串abcba,無論正向遍歷,還是反向遍歷,結果都是abcba,如果還不清楚,可以看下圖。

算法小白:太好了,我終於知道什么叫回文字符串了,現在可以做這道題了。只要正向和反向分別遍歷一遍字符串,然后比較一下結果,如果兩次遍歷的結果相同,就是回文字符串,哈哈哈,對嗎?老大。
算法君:着什么急,你看清題目了嗎?不是讓你判斷是否為回文字符串,是要在一個字符串中尋找最長回文字符串,例如,akbubk是一個字符串,這里邊有很多回文字符串,其實單個字母就是一個回文字符串,還有就是bub、kbubk,很顯然,最長的回文字符串就是kbubk。
算法小白:懂了,懂了,題目還要一個條件,就是要找到所有回文字符串中最長的一個。
算法君:真懂了嗎? 好吧,說說你怎么設計這個算法。
算法小白:我只用了0.01秒,就想到如何設計了,當然是得到字符串中的所有子串,然后找到所有的回文字符串,最后,當然是對這些回文字符串按長度排序,並找到最大的回文字符串了。哈哈哈,我聰明嗎!!!
算法君:Are you crazy?(此時的算法君內心一定是有一萬匹草泥馬跑過,氣得都飆英文了),這么做當然沒錯,但.....,效率太低了。這要是字符串長點,是要去租用超級計算機運行算法程序嗎?
算法小白:不好意思啊,我只想到了這種算法,反正能實現就行唄,管它效率怎樣呢!
算法君:設計算法可不光是實現就行,要注重效率、效率、還是效率,重要的事情說三遍。這才能真正體現出算法之美,否則,直接用最笨的方法誰都會。
算法小白:那么老大有什么更好的實現嗎?
算法君:當然,我作為老大,自然是肚子里有貨了,先給你講一種效率較高的算法。
算法君:要優化一個算法,首先要知道原來的算法哪里效率低了。就拿前面給出的字符串akbubk來說。u、bub和kbubk都是回文字符串,我們前面說了,判斷回文字符串就是要分別正向和反向遍歷一遍字符串,然后比較遍歷結果,如果結果相同,就是回文字符串。對於單個字符,直接就是回文字符串,對於bub來說,按常規的判斷方法,需要正向循環3次(得到正向字符串),反向循環3次(得到反向字符串)。一共6次才能判斷該字符串是否為回文字符串(不過正向可以省略了,因為就用字符串本身就可以了),而對於kbubk來說,需要遍歷10次才可以。
算法君:這是按常規的做法。但是,我們仔細觀察,bub和u的關系,u是包含在bub中的,而且u肯定是回文字符串,所以以u為軸,只要判斷該字符串的首字符與尾字符是否相等即可。也就是說,這樣一來,就只需要比較一次(首字符和尾字符進行比較)就可以了,效率大大提升。
算法小白:好像是有點明白了。
算法君:別急別急,我還沒說完呢!如果感覺bub和u的關系太簡單,可以再看一下bub和kbubk的關系。bub是包含在kbubk中的,如果按常規的做法,bub正反遍歷兩次判斷是否為回文字符串,而kbubk同樣需要遍歷兩次,但對於kbubk來說,遍歷的這兩次,是包含bub的兩次遍歷的,也就是說,這里重復遍歷了。這就是我們應該優化的地方:避免重復遍歷字符串。
算法小白:真是醍醐灌頂啊,那么如何避免重復遍歷字符串呢?
算法君:當然是保存歷史了,我們是怎么知道秦皇漢武、唐宗宋祖的,不就是通過歷史書嗎?咱們又沒見過這幾位老哥,對不!
算法小白:保存歷史?讓我來猜一猜,是不是將已經確認的回文字符串保存起來呢,如果下次再遇到這些已經確認的回文字符串,就不需要再進行遍歷了,直接取結果就行了!
算法君:算你小子聰明一回,沒錯,是要將已經確認的回文字符串保存起來,但並不是保存回文字符串本身。而是要保存字符串是否為回文的結果。還拿kbubk為例,我們可以將kbubk看成3部分,首字符k是一部分,尾字符k是一部分,中間的子字符串bub是一部分,如下圖所示。
算法君:對於這個特例來說,bub是回文字符串。而尋找akbubk中最長回文字符串的過程中,肯定是從長度為1的子字符串開始搜索,然后是長度為2的字符串,以此類推,所以bub一定比kbubk先搜索到,所以需要將bub是回文字符串的結果保存起來,如果要判斷kbubk是否為回文字符串,只需要經過如下2步就可以了:
1. 判斷首尾兩個字符是否相同
2. 判斷夾在首尾字符中間的子字符串是否為回文字符串
算法君:如果這兩步的結果都是yes,那么這個字符串就是回文字符串,將該模型泛化,如下圖所示。
算法小白:這下徹底明白了,不過應該如何保存歷史呢?設計算法和實現算法還是有一定差別的,這就是理論派和實踐派的差距,中間差了一個特斯拉的距離。
算法君:哈哈,理論與實踐是有差別的,但好像也沒那么大。其實理論與實踐很多時候是相輔相成的。
算法小白:快快,給我講講到底如何保存歷史,給我一架從理論到實踐的梯子吧!
算法君:梯子!沒有,我這有一壺涼水,澆下去就灌頂了!
算法小白:別逗了,趕快說說!
算法君:要想確定如何保存歷史記錄,首先要確定如何獲取這些數據,然后再根據獲取的方式確定具體的數據結構。我們期望知道字符串中任意的子串是否是回文字符串,這個子串的第一個字符在原字符串中的索引是i,最后一個字符在原字符串中的索引是j。所以我們期望有一個函數is_palindrome_string,通過將i和j作為參數傳入該函數,如果i和j確定的字符串是回文,返回true,否則返回false。
算法小白:老大的意思是說將i和j作為查詢歷史記錄的key嗎?
算法君:沒錯,這次終於說對了一回。下面就看is_palindrome_string函數如何實現了!
算法小白:我想想啊,那么如何實現這個is_palindrome_string函數呢?通過key搜索是否為回文的歷史記錄,也就是搜索value,在Python中字典可以實現這個功能。用字典可以嗎?
算法君:字典算是一種實現,你想想用字典具體應該如何實現呢?
算法小白:這個我知道,Python我已經很熟悉了。可以將i和j作為一個列表,然后作為字典的key,不不不,該用元組,Python中是不支持將列表作為字典的key的。
例如:history_record = {(1,1):True, (1,3):False}
算法小白:然后通過元組(i,j)查詢歷史,例如,要想知道索引從1到3的子串是否為回文字符串,只需要搜索history_record即可,代碼如下:
history_record[(1,3)]
算法君:沒錯,這算是一種存儲歷史的方法,不過搜索字典仍然需要時間,盡管時間不是線性的。想想還有沒有更快的定位歷史記錄的方法呢?
算法小白:快速定位?..... 這個,比字典還快,難道是用魔法嗎? 哈哈哈!這個還真一時想不出。
算法君:其實在數據結構中,已經清楚地闡述了最快定位的數據結構,這就是數組,由於數組是在內存中的一塊連續空間,所以可以根據偏移量瞬間定位到特定的位置。
算法小白:嗯,數組我當然知道,不過如何用數組來保存回文字符串的歷史呢?
算法君:前面提到的is_palindrome_string函數有兩個參數i和j。i和j是字符串中某一個字符的索引,從0開始,取值范圍都是0 <= i,j < n(這里假設字符串的長度是n),其實這也符合二維數組的索引取值規則。假設有一個n*n的正方形二維數組P(每個元素初始值都是0)。如果從i到j的字符串是回文字符串,那么就將P[i,j]設為1,如果要知道從i到j的字符串是否為回文字符串,也只需要查詢P[i,j]即可。
算法君:舉個具體的例子,有一個字符串acxxcd,要求該字符串的最大回文字符串,可以創建如下圖的6*6的二維數組,初始化為0。
然后將長度為1的字符串標記為回文字符串(主對角線上),如P[0,0],P[1,1]等。
接下來將長度為2 的字符串是回文的做一下標記,也就是兩個字符相等的字符串,這里只有一個,那就是xx,也就是P[2,3]。如下圖所示。
在字符串acxxcd中,並沒有長度為3的回文字符串,所以直接搜索長度為4的回文字符串,如果搜索長度為4的字符串,按着前面的描述,先要判斷首尾字符是否相等,如果相等,再判斷夾在中間的字符串是否為回文字符串,夾在中間的字符串的長度肯定是2,所以可以直接在這個二維數組上定位。例如,搜索到cxxc,首尾字符都是c,中間的xx在二維數組中的坐標是P[2,3],這個位置剛剛在前面設置為1,所以xx是回文字符串,從而判定cxxc是回文字符串。而cxxc在整個字符串中首尾字符的位置是(1,4),所以需要將P[1,4]設置為1,如下圖所示。
繼續掃描長度為5的回文字符串(不存在),然后是長度為6的回文字符串(不存在),所以這個唯一的長度為4的回文字符串就是acxxcd的最長回文字符串。
算法君:這種算法還有一個名字:動態規划法
算法小白:哈哈,還可以這么做。又學了一招!!老大就是老大!
算法君:不過這種存儲方案也有缺點,就是比較浪費內存空間,因為需要額外申請n*n的數組空間。另外,你能說出這個算法的時間復雜度和空間復雜度嗎?
算法小白:復雜度?我想想,所謂復雜度就是值隨着算法輸入數據的多少,時間和空間的變化關系吧。如是線性變化的,那么時間復雜度就是O(n)。
算法君:是的,可以這么理解。那么這個算法的復雜度是多少呢?
算法小白:由於該算法需要申請n*n的數組,所以空間復雜度應該是O(n^2),對於每一個字符串,都需要從長度為1的回文字符串開始搜索,需要雙重循環,所以時間復雜度也是O(n^2)。
算法君:嗯,這回說得沒錯,那么還有什么更好的算法可以降低空間復雜度嗎?例如,將空間復雜度降為O(1),也就是不需要申請額外的內存空間。
算法小白:我現在已經用腦過度了,這個要回去好好考慮下。感謝老大耐心講解。
算法君:好吧,回去多想想還有沒有更好的算法。下面給出一個具體的算法實現(Python語言)。
#動態規划法求最長回文字符串的完整代碼 class MaxPalindromeString: def __init__(self): self.start_index = None self.array_len = None def get_longest_palindrome(self,s): if s == None: return size = len(s) if size < 1: return self.start_index = 0 self.array_len = 1 # 用於保存歷史記錄(size * size) history_record = [([0] * size) for i in range(size)] # 初始化長度為1的回文字符串信息 i= 0 while i< size: history_record[i][i] = 1 i += 1 # 初始化長度為2的回文字符串信息 i = 0 while i < size - 1: if s[i] == s[i+1]: history_record[i][i+1] = 1 self.start_index = i self.array_len = 2 i += 1 # 查找從長度為3開始的回文字符串 p_len = 3 while p_len <= size: i = 0 while i < size-p_len + 1: j = i + p_len-1 if s[i] == s[j] and history_record[i+1][j-1] == 1: history_record[i][j] = 1 self.start_index = i self.array_len = p_len i += 1 p_len += 1 s = 'abcdefgfedxyz' s1 = 'akbubk' p = MaxPalindromeString() p.get_longest_palindrome(s1) if p.start_index != -1 and p.array_len != -1: print('最長回文字符串:',s1[p.start_index:p.start_index + p.array_len]) else: print('查找失敗')
算法君:如果想知道這些算法的具體實現細節,可以掃描下面的二維碼,並關注“極客起源”公眾號,然后輸入229893,即可獲得相關資源。除此之外,還有更多精彩內容等着你哦!
