最長(大)回文串的查找(字符串中找出最長的回文串)PHP實現
首先還是先解釋一下什麽是回文串:就是從左到右或者從右到左讀,都是同樣的字符串。比如:上海自來水來自海上,bob等等。
那麽什麽又是找出最長回文串呢?
例如:字符串abcdefedcfggggggfc,其中efe,defed,cdefedc,gg,ggg,gggg,ggggg,gggggg,fggggggf,cfggggggfc都是回文串,左右完全一樣。
這其中,有最短的gg,最長的cfggggggfc,還有其他長度的。忽略長度為1的。畢竟一個字符的都算回文了。
那麽,找出最長的,就是找出這個cfggggggfc。
說實話,最開始想到的辦法,就是暴力的枚舉,也就是找出原字符串的所有子串,然後逐一判斷是否是回文串,如果是就記錄下來該字符串。
然後,碰到下一個回文串的時候,再對比兩個字符串的長度,誰長,就把誰記錄下來。
感覺遍歷、枚舉這類操作真的是萬能的。。。
先來段暴力的代碼,定個場。
通過兩層循環,逐一篩選字符串的子串,找出所有回文串,並不斷判斷,記錄最長的回文串
1 function is_palindrome($str) 2 { 3 $strrev = strrev($str);// 逆序字符串 4 return $strrev == $str ? 1 : 0; 5 } 6 7 function get_max_palindrome($str) 8 { 9 $len= strlen($str); 10 $res = ‘‘;// 結果 11 for ($i = 0; $i < $len - 2; $i++) {// $i 用於定義字符串起始位置,倒數第二個和最後一個如果還不能組成回文串,最後一個就不需要截取了 12 for ($j = $i + 2; $j <= $len; $j++) {// $j 用於逐一延長子字符串的長度,($j=$i+2)截取子串長度2位起,所以循環條件使用的是<=不是< 13 $tmp = substr($str, $i, $j - $i);// 逐一截取子串 14if (is_palindrome($tmp)) {// 判斷當前截取的子串是否是回文串 15 if (strlen($tmp) > strlen($res)) {// 是回文串,則再判斷是否長度大於結果中保存的回文串 16 $res = $tmp;// 當前回文串大於結果中的,將結果變量更新成當前的回文串 17 } 18 } 19 } 20 } 21 return $res; 22 } 23 24 $str = "abcdefedcfggggggfc"; 25 echo get_max_palindrome($str);
這方法感覺還不錯,簡單直觀,並且代碼也算簡單。就是會被鄙視,畢竟這個太初級,太暴力。越簡單粗暴不是越好麽?
簡單粗暴的有了,有沒有可以裝一下的?有沒有什麽好玩的?有沒有......於是就有了下邊的程序。
1 function get_max_palindrome1($str) 2 { 3 $len = strlen($str); 4 $res = [];// 結果數組 5 $res2= [];// 偶數長度的結果 6 // 使用array_unshift的目的是為了從前向數組插入每一次找到的答案。也可以直接更新單個元素數組,就是只要當前取到的字符串比原來的長,就把原來的覆蓋掉 7 // 使用多維數組不是必須的,以為數組或者變量也可以。這裏就是做一個簡單的記錄,可以微調一下,多完成另一個功能 8 array_unshift($res, $str[0]);// 默認將第一個字符作為最長回文串寫入數組 9 array_unshift($res2, ‘‘);// 默認一個空字符串,長度為0,初始化 10 11 for ($i = 1; $i < $len - 1; $i++) {// 從第二個開始操作,因為第一個左邊沒有字符,只能算本身長度為1的回文串 12 // 針對奇數長度的最長回文串 13 $left = $right = $i;// 從中間向兩邊擴展,默認起始位置為中間的這個位置 14 $tmp = $str[$i];// 臨時回文串,用於中間數據處理,默認是當前字符串 15 while ($left > 0 and $right < $len - 1) {// 限定,只要有任何一邊到頭,循環結束 16 $left--;// 左邊向左擴展 17 $right++;// 右邊向??擴展 18 if ($str[$left] == $str[$right]) {// 如果擴展以後左右相等,說明當前是回文串 19 $tmp = $str[$left] . $tmp . $str[$right];// 將當前符合的回文串組合在一起 20 if (strlen($tmp) > strlen($res[0])) {// 如果當前得到的回文串比原來數組中最長的回文串長,則記錄該回文串信息 21 array_unshift($res, $tmp);// 將當前回文串信息(長度和內容)記錄在結果數組中 22 } 23 } else {// 不相等,就不用處理後續了,跳出循環 24 break; 25 } 26 } 27 // 針對偶數長度的最長回文串 28 $left2 = $i - 1; 29 $right2 = $i;// 從中間向兩邊擴展,默認起始位置為中間的這個位置 30 if ($str[$i] == $str[$i-1]) {// 如果當前的字符和他前一個字符相等,說明很可能是一個偶數長度的回文串 31 $tmp2 = $str[$i-1] . $str[$i];// 兩個拼一起,記錄下來作為初始字符串 32 if (count($res2) == 1) {// 如果只有一個,說明是只有一個默認的無效的元素 33 array_unshift($res2, $tmp2);// 相當於真正有效的第一個偶數長度的字符串 34 } 35 while ($left2 > 0 and $right2 < $len - 1) { 36 $left2--; 37 $right2++; 38 if ($str[$left2] == $str[$right2]) {// 如果擴展以後左右相等,說明當前是回文串 39 $tmp2 = $str[$left2] . $tmp2 . $str[$right2];// 將當前符合的回文串組合在一起 40 if (strlen($tmp2) > strlen($res2[0])) {// 如果當前得到的回文串比原來數組中最長的回文串長,則記錄該回文串信息 41 array_unshift($res2, $tmp2);// 將當前回文串信息(長度和內容)記錄在結果數組中 42 } 43 } else {// 不相等,就不用處理後續了,跳出循環 44 break; 45 } 46 } 47 } 48 } 49 // 結果數組中,第一個元素就是str是最長的回文串,誰長就返回誰 50 if (strlen($res[0]) >= strlen($res2[0])) { 51 return $res[0];// 奇數長度回文數組 52 } else { 53 return $res2[0];// 偶數長度回文數組 54 } 55 } 56 57 $str = "abcdefedcfggggggfc"; 58 echo get_max_palindrome1($str);
這段代碼厲害了,自己寫完調試通了,自己都颯了一下!
看著就多吧?看這陣容,單單代碼行數就比“簡單粗暴”翻了三倍啊。是不是復雜粗暴?
雖然代碼多了一些,但是也算好理解。
簡單解釋一下大致的思路,從第二個字符開始一直到倒數第二個,循環的假設他們是回文串的最中間的那個字符(左右根據它對稱)
1、當前指針指向的字符串為假設的回文串的中心
2、將左右兩個指針同時向兩邊相同步長的移動一下
3、對比左右兩個指針指向的字符是否相同
4、如果相同,說明是回文串,然後將左右指針指向的值和兩個值中間的值拼接到一起,生成回文串,並統計長度
5、生成新回文串的長度,和結果數組中最長回文串的長度對比,如果夠長,則將當前回文串寫入數組前邊
直到左右指針向兩邊移動後,左右指針對應的字符不相同,則回文串結束,跳出回文串驗證循環,將外層循環加1,將中心移到下一個,重復1-5步,完成下一組回文串的驗證
直到中心移動到倒數第二個,完成比對後。最終數組第一個元素,就是最長的回文串。
當然,代碼復雜,是因為還有一個情況要考慮,回文串分奇數長度回文串和偶數長度回文串例如:bob這個,o是中心,長度為3,奇數。
還有一種情況就是noon,這樣中間可以看做是空字符串,也可以理解成兩個oo,長度為4,偶數。
總之處理奇數長度回文串和偶數長度回文串稍微有一點區別,思路一樣,代碼很像,但是區別還是有的。
所以,這種通過不斷移動回文串假設的中心的方法,看著挺有想法的,也算是很巧妙,但實際上稍微復雜啰嗦了一點,考慮的情況也多了一些。
按照常理,很難一下子就寫出最終的代碼,一般只要留心,仔細想想,都有可能有優化的空間,於是,上邊的代碼就變成了下邊的代碼了。
1 function get_max_palindrome2($str) 2 { 3 $len = strlen($str);// 獲取字符串的長度,用於右邊界設定 4 $res = $res2 =‘‘;// 初始化 5 for ($i = 1; $i < $len - 1; $i++) {// 從第二個開始操作,因為第一個左邊沒有字符,只能算本身長度為1的回文串 6 // 針對奇數長度的最長回文串 7 $left = $right = $i;// 從中間向兩邊擴展,默認起始位置為中間的這個位置 8 $tmp = $str[$i];// 臨時回文串,用於中間數據處理,默認是當前字符串 9 while ($left-- > 0 and $right++ < $len - 1) {// 限定,只要有任何一邊到頭,循環結束($left--左邊向左擴展,right++右邊向右擴展) 10 if ($str[$left] != $str[$right]) break;// 不相等,不是回文串,就不用處理後續了,跳出循環 11 $tmp = $str[$left] . $tmp . $str[$right];// 將當前符合的回文串組合在一起 12 if (strlen($tmp) > strlen($res)) $res = $tmp;// 當前得到的回文串比原來的回文串長,記錄當前回文串 13 } 14 // 針對偶數長度的最長回文串 15 $left2 = $i - 1; 16 $right2 = $i;// 從中間向兩邊擴展,默認起始位置為中間的這個位置 17 if ($str[$i] == $str[$i - 1]) {// 如果當前的字符和他前一個字符相等,說明是一個偶數長度的回文串 18 $tmp2 = $str[$i - 1] . $str[$i];// 兩個拼一起,記錄下來作為初始字符串 19 if (strlen($tmp2) > strlen($res2)) $res2 = $tmp2;// 如果當前得到的回文串比原來最長的回文串長,則記錄該回文串信息 20 while ($left2-- > 0 and $right2++ < $len - 1) { 21 if ($str[$left2] != $str[$right2]) break;// 不相等,不是回文串,就不用處理後續了,跳出循環 22 $tmp2 = $str[$left2] . $tmp2 . $str[$right2];// 將當前符合的回文串組合在一起 23 if (strlen($tmp2) > strlen($res2)) $res2 = $tmp2;// 如果當前得到的回文串比原來最長的回文串長,則記錄該回文串信息 24 } 25 } 26 } 27 // 兩個(奇數長度回文串和偶數長度回文串)結果中,誰長誰是最長的回文串 28 return strlen($res) >= strlen($res2) ? $res : $res2; 29 } 30 31 $str = "abcdefedcfggggggfc"; 32 echo get_max_palindrome2($str);
這個就看著簡潔緊湊了吧?不止是代碼減少了冗余,也換了個別地方的寫法,同樣可以節省代碼量和空間的使用量。因為思路和實現方法與上一個一樣,只是對代碼做了個二次優化。
就不重復說明了,好在註釋夠詳細。只說一點,正常情況下,沒有人能保證第一次就寫出最合適的代碼,很有可能要優化一次以上,代碼越多,邏輯越復雜,可以優化的空間就越大。
正常,代碼寫到這裏,就算完事了,結束了。想做的都實現了。感覺上沒什麽了。但是,人外有人啊。不一定誰就有什麽牛逼的思路呢。。。
這一百度,,,還真是,有一個牛人叫Manacher,在1975年,弄出個馬拉車算法。真是有想法啊。很羨慕。
看了幾篇文章的解釋吧,說實話,打眼一看,都很專業,大篇幅,帶表格,帶圖解,很高級的感覺。但是大部分都不好理解。所以,基本上一篇都沒看完。
現在現忽略這個牛人的牛思路,畢竟他這個給每個字符兩邊包上特殊符號的辦法,確實就已經是一個很新奇的思路了。只用這個方法,就會精簡很多代碼的。
畢竟,通過加入特殊符號,所有的回文串就不區分是偶數長度,還是奇數長度了,都統一按照奇數長度處理,最後將特殊符號過濾掉即可。
那麽這個特殊符號是以什麽形式加入原來的字符串中呢?例如:123321加入特殊符號“#”,結果是:#1#2#3#3#2#1#;121加入“#”號,結果是:#1#2#1#。
字符串這麽處理以後,就都是奇數長度的回文串了。下面就根據這樣的字符串寫一個新的方法
1 function get_max_palindrome_m($str) 2 { 3 $res = $str[0]; 4 // 用“#”包上字符串的每一個字符,比如abc轉換成#a#b#c#。這樣就導致不管是奇數長度的回文還是偶數長度的都是可以按照奇數的處理 5 $str = ‘#‘ . implode(‘#‘, str_split($str)) . ‘#‘; 6 $len = strlen($str);// 獲取處理後字符串的長度 7 for ($i = 2; $i < $len; $i++) { 8 $left = $right = $i; 9 while ($left > 0 and $right < $len - 2) {// 只要任意一邊不到字符串的邊際,就繼續循環 10 if ($str[$i] == ‘#‘) {// 如果是#號,說明相鄰的左右兩個是正常的字符串,所以左右各擴展一位 11 $left--; 12 $right++; 13 } else {// 如果不是#號,說明相鄰的兩個字符都是#號,直接左右兩邊各擴展兩位來取字符比較 14 $left -= 2; 15 $right += 2; 16 } 17 if ($str[$left] != $str[$right]) break;// 只要有一對不相等了,就跳出循環 18 $tmp = substr($str, $left, $right - $left + 1);// 左右兩邊同樣步長的字符相等,則說明這區間是回文串,截取符合條件部分的字符串 19 if (strlen($tmp) > strlen($res)) {// 如果當前獲取的回文串比記錄的長,則更新結果數組 20 $res = $tmp; 21 } 22 } 23 } 24 return str_replace(‘#‘, ‘‘, $res); 25 } 26 27 $str = "abcdefedcfggggggfc"; 28 echo get_max_palindrome_m($str);
這代碼,明顯看著簡單清晰多了。看著也相對好理解了。因為只要理解一種情況就可以了。當然,為了減少一定的循環次數,while循環裏多一個判斷,如果沒有這個判斷,統一加1減1,代碼會少很多行。
那麽,以上這段代碼就是根據網上的提示,加工原字符串,最終再一次優化了程序。代碼精簡了,思路也更簡潔清晰了。那麽可以想想,到這裏,還可以再優化麽?
畢竟上邊的代碼是有了新思路後的第一版的代碼,仔細想想,還是可以精簡的。
1 function get_max_palindrome_m1($str) 2 { 3 $res = $str[0]; 4 $str = ‘#‘ . implode(‘#‘, str_split($str)) . ‘#‘;// “#”包上字符串的每一個字符,比如abc轉換成#a#b#c#。奇數或偶數長度的回文串都可以按奇數長度處理 5 $len = strlen($str);// 獲取處理後字符串的長度 6 for ($i = 2; $i < $len; $i++) {// 因為第一個字符是“#”屬於沒意義字符因此從第三個開始正常處理 7 $step = 0;// 初始化步長 8 while ($i - $step > 0 and $i + $step < $len - 2) {// 任意一邊不到字符串的邊際,繼續循環(最後一位屬於沒意義字符因此處理到$len-2就算結尾) 9 $str[$i] == ‘#‘ ? $step++ : $step += 2;// 當前字符是“#”,步長左右各擴展1位即可,否則擴展2位(因為1位是兩個“#”沒意義) 10 if ($str[$i - $step] != $str[$i + $step]) break;// 只要有一對不相等了,就跳出循環 11 $tmp = substr($str, ($i - $step), $step * 2 + 1);// $tmp = substr($str, ($i - $step), ($i + $step) - ($i - $step) + 1); 12 if (strlen($tmp) > strlen($res)) $res = $tmp;// 如果當前獲取的回文串比記錄的長,則更新結果數組 13 } 14 } 15 return str_replace(‘#‘, ‘‘, $res); 16 } 17 18 $str = "abcdefedcfggggggfc"; 19 echo get_max_palindrome_m1($str);
這樣整理之後,是不是看著又簡潔了很多?
程序都是不斷優化改進的,隨著掌握的技術,了解了新思路,熟能生巧的經驗,代碼都會越來越精簡,越來越優化。
沒有最好,只有更好!
最長(大)回文串的查找(字符串中找出最長的回文串)PHP實現