1. 程式人生 > >演算法君帶你學演算法(1):最長迴文字串

演算法君帶你學演算法(1):最長迴文字串

演算法君:小白同學,給你出道演算法題,看你小子演算法能力有沒有長進。

演算法小白:最近一直在研究演算法,刷了很多演算法題,正好活動活動大腦,來來來,趕快出題!

演算法君:聽好了,題目是:求一個字串中最長的迴文字串。

演算法小白:這個演算法好像很簡單,就是有一個概念不太明白,啥叫“迴文字串”。

演算法君:哈哈,你說的很簡單,一定是題目的字數很少的意思。

演算法小白:哦,又被老大猜中了。還是先給我講一下什麼是迴文字串吧!

演算法君:迴文字串嗎!首先是一個字串(廢話),然後,核心就是迴文。“回”嗎,就是來來回回的意思。其實就是正向和反向遍歷字串中的每一個字元,然後嘛,如果遍歷的結果都一樣,就是迴文字串。例如,有一個字串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,即可獲得相關資源。除此之外,還有更多精彩內容等著你哦!