首先还是先解释一下什么是回文串: 就是从左到右或者从右到左读, 都是同样的字符串. 比如: 上海自来水来自海上, bob 等等.
那么什么又是找出最长回文串呢?
例如: 字符串 abcdefedcfggggggfc, 其中 efe,defed,cdefedc,gg,ggg,gggg,ggggg,gggggg,fggggggf,cfggggggfc 都是回文串, 左右完全一样.
这其中, 有最短的 gg, 最长的 cfggggggfc, 还有其他长度的. 忽略长度为 1 的. 毕竟一个字符的都算回文了.
那么, 找出最长的, 就是找出这个 cfggggggfc.
说实话, 最开始想到的办法, 就是暴力的枚举, 也就是找出原字符串的所有子串, 然后逐一判断是否是回文串, 如果是就记录下来该字符串.
然后, 碰到下一个回文串的时候, 再对比两个字符串的长度, 谁长, 就把谁记录下来.
感觉遍历, 枚举这类操作真的是万能的...
先来段暴力的代码, 定个场.
通过两层循环, 逐一筛选字符串的子串, 找出所有回文串, 并不断判断, 记录最长的回文串
- function is_palindrome($str)
- {
- $strrev = strrev($str);// 逆序字符串
- return $strrev == $str ? 1 : 0;
- }
- function get_max_palindrome($str)
- {
- $len = strlen($str);
- $res = '';// 结果
- for ($i = 0; $i <$len - 2; $i++) {// $i 用于定义字符串起始位置, 倒数第二个和最后一个如果还不能组成回文串, 最后一个就不需要截取了
- for ($j = $i + 2; $j <= $len; $j++) {// $j 用于逐一延长子字符串的长度,($j=$i+2)截取子串长度 2 位起, 所以循环条件使用的是<= 不是<
- $tmp = substr($str, $i, $j - $i);// 逐一截取子串
- if (is_palindrome($tmp)) {// 判断当前截取的子串是否是回文串
- if (strlen($tmp)> strlen($res)) {// 是回文串, 则再判断是否长度大于结果中保存的回文串
- $res = $tmp;// 当前回文串大于结果中的, 将结果变量更新成当前的回文串
- }
- }
- }
- }
- return $res;
- }
- $str = "abcdefedcfggggggfc";
- echo get_max_palindrome($str);
这方法感觉还不错, 简单直观, 并且代码也算简单. 就是会被鄙视, 毕竟这个太初级, 太暴力. 越简单粗暴不是越好么?
简单粗暴的有了, 有没有可以装一下的? 有没有什么好玩的? 有没有...... 于是就有了下边的程序.
- function get_max_palindrome1($str)
- {
- $len = strlen($str);
- $res = [];// 结果数组
- $res2= [];// 偶数长度的结果
- // 使用 array_unshift 的目的是为了从前向数组插入每一次找到的答案. 也可以直接更新单个元素数组, 就是只要当前取到的字符串比原来的长, 就把原来的覆盖掉
- // 使用多维数组不是必须的, 以为数组或者变量也可以. 这里就是做一个简单的记录, 可以微调一下, 多完成另一个功能
- array_unshift($res, $str[0]);// 默认将第一个字符作为最长回文串写入数组
- array_unshift($res2, '');// 默认一个空字符串, 长度为 0, 初始化
- for ($i = 1; $i <$len - 1; $i++) {// 从第二个开始操作, 因为第一个左边没有字符, 只能算本身长度为 1 的回文串
- // 针对奇数长度的最长回文串
- $left = $right = $i;// 从中间向两边扩展, 默认起始位置为中间的这个位置
- $tmp = $str[$i];// 临时回文串, 用于中间数据处理, 默认是当前字符串
- while ($left> 0 and $right <$len - 1) {// 限定, 只要有任何一边到头, 循环结束
- $left--;// 左边向左扩展
- $right++;// 右边向?? 扩展
- if ($str[$left] == $str[$right]) {// 如果扩展以后左右相等, 说明当前是回文串
- $tmp = $str[$left] . $tmp . $str[$right];// 将当前符合的回文串组合在一起
- if (strlen($tmp)> strlen($res[0])) {// 如果当前得到的回文串比原来数组中最长的回文串长, 则记录该回文串信息
- array_unshift($res, $tmp);// 将当前回文串信息 (长度和内容) 记录在结果数组中
- }
- } else {// 不相等, 就不用处理后续了, 跳出循环
- break;
- }
- }
- // 针对偶数长度的最长回文串
- $left2 = $i - 1;
- $right2 = $i;// 从中间向两边扩展, 默认起始位置为中间的这个位置
- if ($str[$i] == $str[$i-1]) {// 如果当前的字符和他前一个字符相等, 说明很可能是一个偶数长度的回文串
- $tmp2 = $str[$i-1] . $str[$i];// 两个拼一起, 记录下来作为初始字符串
- if (count($res2) == 1) {// 如果只有一个, 说明是只有一个默认的无效的元素
- array_unshift($res2, $tmp2);// 相当于真正有效的第一个偶数长度的字符串
- }
- while ($left2> 0 and $right2 <$len - 1) {
- $left2--;
- $right2++;
- if ($str[$left2] == $str[$right2]) {// 如果扩展以后左右相等, 说明当前是回文串
- $tmp2 = $str[$left2] . $tmp2 . $str[$right2];// 将当前符合的回文串组合在一起
- if (strlen($tmp2)> strlen($res2[0])) {// 如果当前得到的回文串比原来数组中最长的回文串长, 则记录该回文串信息
- array_unshift($res2, $tmp2);// 将当前回文串信息 (长度和内容) 记录在结果数组中
- }
- } else {// 不相等, 就不用处理后续了, 跳出循环
- break;
- }
- }
- }
- }
- // 结果数组中, 第一个元素就是 str 是最长的回文串, 谁长就返回谁
- if (strlen($res[0])>= strlen($res2[0])) {
- return $res[0];// 奇数长度回文数组
- } else {
- return $res2[0];// 偶数长度回文数组
- }
- }
- $str = "abcdefedcfggggggfc";
- echo get_max_palindrome1($str);
这段代码厉害了, 自己写完调试通了, 自己都飒了一下!
看着就多吧? 看这阵容, 单单代码行数就比 "简单粗暴" 翻了三倍啊. 是不是复杂粗暴?
虽然代码多了一些, 但是也算好理解.
简单解释一下大致的思路, 从第二个字符开始一直到倒数第二个, 循环的假设他们是回文串的最中间的那个字符(左右根据它对称)
1, 当前指针指向的字符串为假设的回文串的中心
2, 将左右两个指针同时向两边相同步长的移动一下
3, 对比左右两个指针指向的字符是否相同
4, 如果相同, 说明是回文串, 然后将左右指针指向的值和两个值中间的值拼接到一起, 生成回文串, 并统计长度
5, 生成新回文串的长度, 和结果数组中最长回文串的长度对比, 如果够长, 则将当前回文串写入数组前边
直到左右指针向两边移动后, 左右指针对应的字符不相同, 则回文串结束, 跳出回文串验证循环, 将外层循环加 1, 将中心移到下一个, 重复 1-5 步, 完成下一组回文串的验证
直到中心移动到倒数第二个, 完成比对后. 最终数组第一个元素, 就是最长的回文串.
当然, 代码复杂, 是因为还有一个情况要考虑, 回文串分奇数长度回文串和偶数长度回文串例如: bob 这个, o 是中心, 长度为 3, 奇数.
还有一种情况就是 noon, 这样中间可以看做是空字符串, 也可以理解成两个 oo, 长度为 4, 偶数.
总之处理奇数长度回文串和偶数长度回文串稍微有一点区别, 思路一样, 代码很像, 但是区别还是有的.
所以, 这种通过不断移动回文串假设的中心的方法, 看着挺有想法的, 也算是很巧妙, 但实际上稍微复杂啰嗦了一点, 考虑的情况也多了一些.
按照常理, 很难一下子就写出最终的代码, 一般只要留心, 仔细想想, 都有可能有优化的空间, 于是, 上边的代码就变成了下边的代码了.
- function get_max_palindrome2($str)
- {
- $len = strlen($str);// 获取字符串的长度, 用于右边界设定
- $res = $res2 ='';// 初始化
- for ($i = 1; $i <$len - 1; $i++) {// 从第二个开始操作, 因为第一个左边没有字符, 只能算本身长度为 1 的回文串
- // 针对奇数长度的最长回文串
- $left = $right = $i;// 从中间向两边扩展, 默认起始位置为中间的这个位置
- $tmp = $str[$i];// 临时回文串, 用于中间数据处理, 默认是当前字符串
- while ($left--> 0 and $right++ <$len - 1) {// 限定, 只要有任何一边到头, 循环结束($left-- 左边向左扩展, right++ 右边向右扩展)
- if ($str[$left] != $str[$right]) break;// 不相等, 不是回文串, 就不用处理后续了, 跳出循环
- $tmp = $str[$left] . $tmp . $str[$right];// 将当前符合的回文串组合在一起
- if (strlen($tmp)> strlen($res)) $res = $tmp;// 当前得到的回文串比原来的回文串长, 记录当前回文串
- }
- // 针对偶数长度的最长回文串
- $left2 = $i - 1;
- $right2 = $i;// 从中间向两边扩展, 默认起始位置为中间的这个位置
- if ($str[$i] == $str[$i - 1]) {// 如果当前的字符和他前一个字符相等, 说明是一个偶数长度的回文串
- $tmp2 = $str[$i - 1] . $str[$i];// 两个拼一起, 记录下来作为初始字符串
- if (strlen($tmp2)> strlen($res2)) $res2 = $tmp2;// 如果当前得到的回文串比原来最长的回文串长, 则记录该回文串信息
- while ($left2--> 0 and $right2++ <$len - 1) {
- if ($str[$left2] != $str[$right2]) break;// 不相等, 不是回文串, 就不用处理后续了, 跳出循环
- $tmp2 = $str[$left2] . $tmp2 . $str[$right2];// 将当前符合的回文串组合在一起
- if (strlen($tmp2)> strlen($res2)) $res2 = $tmp2;// 如果当前得到的回文串比原来最长的回文串长, 则记录该回文串信息
- }
- }
- }
- // 两个 (奇数长度回文串和偶数长度回文串) 结果中, 谁长谁是最长的回文串
- return strlen($res)>= strlen($res2) ? $res : $res2;
- }
- $str = "abcdefedcfggggggfc";
- echo get_max_palindrome2($str);
这个就看着简洁紧凑了吧? 不止是代码减少了冗余, 也换了个别地方的写法, 同样可以节省代码量和空间的使用量. 因为思路和实现方法与上一个一样, 只是对代码做了个二次优化.
就不重复说明了, 好在注释够详细. 只说一点, 正常情况下, 没有人能保证第一次就写出最合适的代码, 很有可能要优化一次以上, 代码越多, 逻辑越复杂, 可以优化的空间就越大.
正常, 代码写到这里, 就算完事了, 结束了. 想做的都实现了. 感觉上没什么了. 但是, 人外有人啊. 不一定谁就有什么牛逼的思路呢...
这一百度,,, 还真是, 有一个牛人叫 Manacher, 在 1975 年, 弄出个马拉车算法. 真是有想法啊. 很羡慕.
看了几篇文章的解释吧, 说实话, 打眼一看, 都很专业, 大篇幅, 带表格, 带图解, 很高级的感觉. 但是大部分都不好理解. 所以, 基本上一篇都没看完.
现在现忽略这个牛人的牛思路, 毕竟他这个给每个字符两边包上特殊符号的办法, 确实就已经是一个很新奇的思路了. 只用这个方法, 就会精简很多代码的.
毕竟, 通过加入特殊符号, 所有的回文串就不区分是偶数长度, 还是奇数长度了, 都统一按照奇数长度处理, 最后将特殊符号过滤掉即可.
那么这个特殊符号是以什么形式加入原来的字符串中呢? 例如: 123321 加入特殊符号 "#", 结果是:#1#2#3#3#2#1#;121 加入 "#" 号, 结果是:#1#2#1#.
字符串这么处理以后, 就都是奇数长度的回文串了. 下面就根据这样的字符串写一个新的方法
- function get_max_palindrome_m($str)
- {
- $res = $str[0];
- // 用 "#" 包上字符串的每一个字符, 比如 abc 转换成 #a#b#c#. 这样就导致不管是奇数长度的回文还是偶数长度的都是可以按照奇数的处理
- $str = '#' . implode('#', str_split($str)) . '#';
- $len = strlen($str);// 获取处理后字符串的长度
- for ($i = 2; $i <$len; $i++) {
- $left = $right = $i;
- while ($left> 0 and $right <$len - 2) {// 只要任意一边不到字符串的边际, 就继续循环
- if ($str[$i] == '#') {// 如果是 #号, 说明相邻的左右两个是正常的字符串, 所以左右各扩展一位
- $left--;
- $right++;
- } else {// 如果不是 #号, 说明相邻的两个字符都是 #号, 直接左右两边各扩展两位来取字符比较
- $left -= 2;
- $right += 2;
- }
- if ($str[$left] != $str[$right]) break;// 只要有一对不相等了, 就跳出循环
- $tmp = substr($str, $left, $right - $left + 1);// 左右两边同样步长的字符相等, 则说明这区间是回文串, 截取符合条件部分的字符串
- if (strlen($tmp)> strlen($res)) {// 如果当前获取的回文串比记录的长, 则更新结果数组
- $res = $tmp;
- }
- }
- }
- return str_replace('#', '', $res);
- }
- $str = "abcdefedcfggggggfc";
- echo get_max_palindrome_m($str);
这代码, 明显看着简单清晰多了. 看着也相对好理解了. 因为只要理解一种情况就可以了. 当然, 为了减少一定的循环次数, while 循环里多一个判断, 如果没有这个判断, 统一加 1 减 1, 代码会少很多行.
那么, 以上这段代码就是根据网上的提示, 加工原字符串, 最终再一次优化了程序. 代码精简了, 思路也更简洁清晰了. 那么可以想想, 到这里, 还可以再优化么?
毕竟上边的代码是有了新思路后的第一版的代码, 仔细想想, 还是可以精简的.
- function get_max_palindrome_m1($str)
- {
- $res = $str[0];
- $str = '#' . implode('#', str_split($str)) . '#';// "#" 包上字符串的每一个字符, 比如 abc 转换成 #a#b#c#. 奇数或偶数长度的回文串都可以按奇数长度处理
- $len = strlen($str);// 获取处理后字符串的长度
- for ($i = 2; $i <$len; $i++) {// 因为第一个字符是 "#" 属于没意义字符因此从第三个开始正常处理
- $step = 0;// 初始化步长
- while ($i - $step> 0 and $i + $step <$len - 2) {// 任意一边不到字符串的边际, 继续循环(最后一位属于没意义字符因此处理到 $len-2 就算结尾)
- $str[$i] == '#' ? $step++ : $step += 2;// 当前字符是 "#", 步长左右各扩展 1 位即可, 否则扩展 2 位(因为 1 位是两个 "#" 没意义)
- if ($str[$i - $step] != $str[$i + $step]) break;// 只要有一对不相等了, 就跳出循环
- $tmp = substr($str, ($i - $step), $step * 2 + 1);// $tmp = substr($str, ($i - $step), ($i + $step) - ($i - $step) + 1);
- if (strlen($tmp)> strlen($res)) $res = $tmp;// 如果当前获取的回文串比记录的长, 则更新结果数组
- }
- }
- return str_replace('#', '', $res);
- }
- $str = "abcdefedcfggggggfc";
- echo get_max_palindrome_m1($str);
这样整理之后, 是不是看着又简洁了很多?
程序都是不断优化改进的, 随着掌握的技术, 了解了新思路, 熟能生巧的经验, 代码都会越来越精简, 越来越优化.
没有最好, 只有更好!
来源: http://www.bubuko.com/infodetail-2969148.html