演算法君帶你學演算法(1):最長迴文字串
阿新 • • 發佈:2019-12-24
演算法君:小白同學,給你出道演算法題,看你小子演算法能力有沒有長進。
演算法小白:最近一直在研究演算法,刷了很多演算法題,正好活動活動大腦,來來來,趕快出題!
演算法君:聽好了,題目是:求一個字串中最長的迴文字串。
演算法小白:這個演算法好像很簡單,就是有一個概念不太明白,啥叫“迴文字串”。
演算法君:哈哈,你說的很簡單,一定是題目的字數很少的意思。
演算法小白:哦,又被老大猜中了。還是先給我講一下什麼是迴文字串吧!
演算法君:迴文字串嗎!首先是一個字串(廢話),然後,核心就是迴文。“回”嗎,就是來來回回的意思。其實就是正向和反向遍歷字串中的每一個字元,然後嘛,如果遍歷的結果都一樣,就是迴文字串。例如,有一個字串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,如下圖所示。
#動態規劃法求最長迴文字串的完整程式碼 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,即可獲得相關資源。除此之外,還有更多精彩內容等著你哦!