【算法】Longest Palindromic Substring

最近刷LeetCode遇到一个比较有意思的题目(Longest Palindromic Substring),求一个字符串的最大回文子串。题目本身并不难,但需要理清思路才好理解,借此文记录下。

题目

Given a string s, find the longest palindromic substring in s. You may assume that the maximum length of s is 1000.

给定一个字符串,找出其最长的回文子串。你可假定字符串长度最大不超过1000。

官方题目链接

什么是最长回文子串?

一个正反顺序打印输出结果相等的字符串。左右字符具备对称性。

举例: “aabbaa" 、 "bbcbb"、"bccb" 等等。

怎么处理字符串长度奇偶情况

  • 各字符间插入特殊字符。比如插入:#
  • 从相邻两个字符开始比较。

我的解法

大概说下思路:

  1. 原字符间插入特殊字符(#),处理长度为偶数情况;
  2. 循环遍历每个字符算出其最大回文子串的长度;
  3. 再剔除得到的回文子串中的特殊字符(#);

Java 代码实现

 public String longestPalindrome(String s) {

    if (s == null || s.length() <= 1) {
        return s;
    }

    //插入 # 解决偶数对称字符
    s = insertSpecialChar(s, '#');

    final int length = s.length();
    // 最大回文子串长度
    int maxLen = 0;
    int[] longestIndex = new int[2];
    int i = 0, j = 0;
    for (int index = 1; index < length; index++) {
        i = index - 1;
        j = index + 1;
        while ((i >= 0 && j <= length - 1) && (s.charAt(i) == s.charAt(j))) {
            i--;
            j++;
        }

        if (maxLen < (j - i - 1)) {
            maxLen = j - i - 1;
            longestIndex[0] = i + 1;
            longestIndex[1] = j;//substring 方法 endIndex 可以等于length 。取值范围是[startIndex, endIndex)
        }

    }
    final String longestPalindromeStr = s.substring(longestIndex[0], longestIndex[1]);
    return deleteSpecialChar(longestPalindromeStr, '#'));
 }

   // 插入特殊字符
   private String insertSpecialChar(String s, char specialChar) {
        StringBuilder sBuilder = new StringBuilder();
        for (int i = 0; i < s.length(); i++) {
            sBuilder.append(specialChar + String.valueOf(s.charAt(i)));
        }
        sBuilder.append(specialChar);

        return sBuilder.toString();
    }

   //剔除
   private String deleteSpecialChar(String s, char specialChar) {
       StringBuilder stringBuilder = new StringBuilder();
       for (int i = 0; i < s.length(); i++) {
           if (specialChar != s.charAt(i)) {
               stringBuilder.append(String.valueOf(s.charAt(i)));
           }
       }
       return stringBuilder.toString();
    }

这个解法思路比较简单,按照官方说法这就是蛮力解决方案(brute force solution)。效率缺陷就在于检查后面字符的回文子串时有可能前面已经比较过的字符又得重复比较一次。

官方推荐解法1

public String longestPalindrome(String s) {
    int start = 0, end = 0;
    for (int i = 0; i < s.length(); i++) {
        int len1 = expandAroundCenter(s, i, i);
        int len2 = expandAroundCenter(s, i, i + 1);
        int len = Math.max(len1, len2);
        if (len > end - start) {
            start = i - (len - 1) / 2;
            end = i + len / 2;
        }
    }
    return s.substring(start, end + 1);
}

private int expandAroundCenter(String s, int left, int right) {
    int L = left, R = right;
    while (L >= 0 && R < s.length() && s.charAt(L) == s.charAt(R)) {
        L--;
        R++;
    }
    return R - L - 1;
}

思路还是比较清晰,但未使用字符间插入特殊字符,而循环比较i和i 、i和i+1(也就是处理最大回文字符长度为偶数的情况,相邻的两个数相等)。较上一种解法少了插入和剔除特殊字符的操作,但每次遍历都多一次i和i+1的比较,按照正常理解如果字符串太长,此方法应该会耗时更多才对;于是我做了个测试,同样的字符串(长度约为1w),实际测试下来,此解法较上一种执行反而更快。我猜原因应该是:插入和剔除特殊字符操作耗时、插入后字符长度翻倍、比较逻辑简单三个原因导致。有其它见解的同学请留言解惑,谢谢。

官方推荐解法2

  public String longestPalindromeOfficialSn(String s) {
        if (s == null || s.length() <= 1) {
            return s;
        }
         //插入 # 解决偶数对称字符
        final String temp = insertSpecialChar(s, '#');

        final int length = temp.length();

        int centerIndex = 0, rightIndex = 0;
        final int[] p = new int[length];

        for (int i = 0; i < length; i++) {
            // iMirror 为 i 的对称点
            int iMirror = 2 * centerIndex - i;
            int pMirror = (iMirror < 0) ? 0 : p[iMirror];

            p[i] = (rightIndex > i) ? Math.min(pMirror, rightIndex - i) : 0;

            int L = i - 1 - p[i];
            int R = i + 1 + p[i];
            while (L >= 0 && R < length && temp.charAt(L) == temp.charAt(R)) {
                p[i]++;
                L = i - 1 - p[i];
                R = i + 1 + p[i];
            }

            if (i + p[i] > rightIndex) {
                // 右移已算出的回文子串
                rightIndex = i + p[i];
                centerIndex = i;
            }
        }

        // 找出最大回文子串: 数组中最大的数
        int maxLength = 0, index = 0;
        for (int j = 0; j < length; j++) {
            if (maxLength < p[j]) {
                maxLength = p[j];
                index = j;
            }
        }

        String ret = temp.substring(index - maxLength, index + maxLength);
        System.out.println( "index = "+index+" maxLength "+maxLength);
        return s.substring((index - maxLength)/2, (index + maxLength)/2);
    }

此解法最大优势就是利用回文子串的对称性,避免重复比较,提高执行效率;也省去了剔除特殊字符的过程。

要想理解此解法关键要看懂这两句代码

int iMirror = 2 * centerIndex - i;
p[i] = (rightIndex > i) ? Math.min(pMirror, rightIndex - i) : 0;
  1. 变量含义:i 和 iMirror 表示字符串中两个字符索引,centerIndex 是其对称点索引。p[i]表示以i索引的字符为中心的左右对称字符对数。rightIndex 则是以centerIndex点字符的最大回文子串的最右边索引位置。
  2. 由于对称性,iMirror 为 i 的对称点不难得出 iMirror = centerIndex - (i - centerIndex),即代码中的 int iMirror = 2 * centerIndex - i;
  3. 当前点i + 对称点iMirror的p[iMirror] 如果小于rightIndex则可得出p[i] = p[iMirror],看下面代码:
if(i + p[iMirror] < rightIndex){
   p[i] = p[iMirror];
}else{
  p[iMirror] >= rightIndex - i;
  //根据p[centerIndex]的回文子串对称性可知,p[i]>=rightIndex - i; 超过rightIndex为未知情况,所以去最小值p[i] = rightIndex - i;
}

// 所以就得出了下面这句代码。其中i>=rightIndex的情况未比较过的字符,所以默认赋值0
p[i] = (rightIndex > i) ? Math.min(pMirror, rightIndex - i) : 0;

此算法重点在于理解对称性,避免重复比较。欲知详情请查阅参考文章。

参考

其它

说下文章标题中的 DSAA ,其实就是数据结构与算法(Data Structures And Algorithms)的英文字母缩写,这样命名主要是想写一个系列文章来分享和记录我的算法学习过程,与君共勉。代码托管在github,欢迎star。

本文为原创内容,转载请说明出处,首发博客

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容