乘风破浪:LeetCode真题_005_Longest Palindromic Substring

一、前言

前面我们已经提到过了一些解题方法,比如递推,逻辑推理,递归等等,其实这些都可以用到动态规划上来。动态规划可以说是比较容易理解但是难以写出代码的。究其原因还是我们的分析没有达到细致入微的程度,下面我们看一个可以使用动态规划解决的问题。

二、Longest Palindromic Substring

2.1 问题理解

2.2 问题分析与解答

    通过题目,我们可以发现在一个大的字符串中寻找一个回文子序列,还要保证序列的长度最大,我们可以想象如果一个序列A[1...n]是回文序列,那么定义一个数据结构来标记所有的回文序列,dp[i][j]==true,代表字符串从下标i到下标j是一个回文序列,且是最长的,那么dp[i+1][j-1]也必定为一个回文序列,并且A[i]==A[j]以此类推直至我们最开始知道的一个字符是一个回文序列,也就是dp[i][i]==true。这样如果我们能找到一个子串使得这样一路下来一直满足,那么我们就找到了回文序列,根据长度我们可以知道最长的子序列。

   于是我们的算法为:

  1. public class Solution {
  2. /**
  3. *
  4. * 题目大意:
  5. * 给定一个字符串S,找出它的最大的回文子串,你可以假设字符串的最大长度是1000,
  6. * 而且存在唯一的最长回文子串
  7. *
  8. * 解题思路:
  9. * 动态规划法,
  10. * 假设dp[ i ][ j ]的值为true,表示字符串s中下标从 i 到 j 的字符组成的子串是回文串。那么可以推出:
  11. * dp[ i ][ j ] = dp[ i + 1][ j - 1] && s[ i ] == s[ j ]。
  12. * 这是一般的情况,由于需要依靠i+1, j -1,所以有可能 i + 1 = j -1, i +1 = (j - 1) -1,因此需
  13. * 要求出基准情况才能套用以上的公式:
  14. *
  15. * a. i + 1 = j -1,即回文长度为1时,dp[ i ][ i ] = true;
  16. * b. i +1 = (j - 1) -1,即回文长度为2时,dp[ i ][ i + 1] = (s[ i ] == s[ i + 1])。
  17. *
  18. * 有了以上分析就可以写出代码了。需要注意的是动态规划需要额外的O(n^2)的空间。
  19. * </pre>
  20. *
  21. * @param s
  22. * @return
  23. */
  24. public String longestPalindrome(String s) {
  25.  
  26. if (s == null || s.length() < 2) {
  27. return s;
  28. }
  29.  
  30. int maxLength = 0;
  31. String longest = null;
  32.  
  33. int length = s.length();
  34. boolean[][] table = new boolean[length][length];
  35.  
  36. // 单个字符都是回文
  37. for (int i = 0; i < length; i++) {
  38. table[i][i] = true;
  39. longest = s.substring(i, i + 1);
  40. maxLength = 1;
  41. }
  42.  
  43. // 判断两个字符是否是回文
  44. for (int i = 0; i < length - 1; i++) {
  45. if (s.charAt(i) == s.charAt(i + 1)) {
  46. table[i][i + 1] = true;
  47. longest = s.substring(i, i + 2);
  48. maxLength = 2;
  49. }
  50. }
  51.  
  52. // 求长度大于2的子串是否是回文串
  53. for (int len = 3; len <= length; len++) {
  54. for (int i = 0, j; (j = i + len - 1) <= length - 1; i++) {
  55. if (s.charAt(i) == s.charAt(j)) {
  56. table[i][j] = table[i + 1][j - 1];
  57. if (table[i][j] && maxLength < len) {
  58. longest = s.substring(i, j + 1);
  59. maxLength = len;
  60. }
  61. } else {
  62. table[i][j] = false;
  63. }
  64. }
  65. }
  66.  
  67. return longest;
  68. }
  69.  
  70. }

     可以看到从长度为1,2开始,将这些作为已知条件,然后进行更高层次的判断,对所有的情况进行遍历之后就得到了我们想要的结果。

   那么还有没有其他比较好的解答方案呢?官网给出了一些解释。

   首先我们可以使用穷举法,我们遍历完所有的可能,按照长度不断地增加然后尝试,每一个子串都进行比较,这样将会是O(n~3)的时间复杂度。这是一种方法。

其次我们可以使用“中心节点法”,这一种方法非常的巧妙,主要是利用了回文的对称性,分为奇数和偶数对称,因此遍历所有的元素,对于每一个元素,分别进行奇扩展和偶扩展,以此来尝试最大的扩展空间,然后将长度返回,等遍历结束就能找到所有的结果。

回文字符串都是对称的,有两种对称方式,一是关于字符对称,比如a,aba,cabac,这种回文字符串长度都是奇数;二是关于间隔对称,比如aa,abba,cbaabc,这种回文字符串长度都是偶数,所以要分别检测这两种情况。中心结点法,就是遍历整个字符串,分别设为中心结点,然后第二个遍历是分别对设定的中心向左右扩展,所以复杂度为o(n~2)。比如对于字符串abba,先检测关于字符对称,设定中心为a,发现最长回文为a,再检测关于间隔对称,给定中心为ab之间间隔,发现最长回文为空。然后坐标前移,设定中心为b,发现最长回文为b,再设定中心为bb的间隔,发现最长回文为abba,为目前最长,所以最长回文设为abba,然后坐标前移,继续检测

  1. public String longestPalindrome(String s) {
  2. if (s == null || s.length() < 1) return "";
  3. int start = 0, end = 0;
  4. for (int i = 0; i < s.length(); i++) {
  5. int len1 = expandAroundCenter(s, i, i);
  6. int len2 = expandAroundCenter(s, i, i + 1);
  7. int len = Math.max(len1, len2);
  8. 8 if (len > end - start) {
  9. 9 start = i - (len - 1) / 2;
  10. 10 end = i + len / 2;
  11. 11 }
  12. }
  13. return s.substring(start, end + 1);
  14. }
  15.  
  16. private int expandAroundCenter(String s, int left, int right) {
  17. int L = left, R = right;
  18. while (L >= 0 && R < s.length() && s.charAt(L) == s.charAt(R)) {
  19. L--;
  20. R++;
  21. }
  22. return R - L - 1;
  23. }

2.3 额外扩充:Manacher's Algorithm 马拉车算法

马拉车算法(Manacher‘s Algorithm)是用来查找一个字符串的最长回文子串的线性方法,由一个叫Manacher的人在1975年发明的,这个方法的最大贡献是在于将时间复杂度提升到了线性,这是非常了不起的。对于回文串想必大家都不陌生,就是正读反读都一样的字符串,比如 "bob","level", "noon" 等等,那么如何在一个字符串中找出最长回文子串呢,可以以每一个字符为中心,向两边寻找回文子串,在遍历完整个数组后,就可以找到最长的回文子串。但是这个方法的时间复杂度为O(n*n),并不是很高效,下面我们来看时间复杂度为O(n)的马拉车算法。
    由于回文串的长度可奇可偶,比如"bob"是奇数形式的回文,"noon"就是偶数形式的回文,马拉车算法的第一步是预处理,做法是在每一个字符的左右都加上一个特殊字符,比如加上'#',那么

  1. bob --> #b#o#b#
  2. noon --> #n#o#o#n#

这样做的好处是不论原字符串是奇数还是偶数个,处理之后得到的字符串的个数都是奇数个,这样就不用分情况讨论了,而可以一起搞定。接下来我们还需要和处理后的字符串t等长的数组p,其中p[i]表示以t[i]字符为中心的回文子串的半径,若p[i] = 1,则该回文子串就是t[i]本身,那么我们来看一个简单的例子:

  1. # 1 # 2 # 2 # 1 # 2 # 2 #
  2. 1 2 1 2 5 2 1 6 1 2 3 2 1

为啥我们关心回文子串的半径呢?看上面那个例子,以中间的 '1' 为中心的回文子串 "#2#2#1#2#2#" 的半径是6,而为添加井号的回文子串为 "22122",长度是5,为半径减1。这是个普遍的规律么?我们再看看之前的那个 "#b#o#b#",我们很容易看出来以中间的 'o' 为中心的回文串的半径是4,而 "bob"的长度是3,符合规律。再来看偶数个的情况"noon",添加井号后的回文串为 "#n#o#o#n#",以最中间的 '#' 为中心的回文串的半径是5,而 "noon" 的长度是4,完美符合规律。所以我们只要找到了最大的半径,就知道最长的回文子串的字符个数了。只知道长度无法确定子串,我们还需要知道子串的起始位置。   
     我们还是先来看中间的 '1' 在字符串 "#1#2#2#1#2#2#" 中的位置是7,而半径是6,貌似7-6=1,刚好就是回文子串 "22122" 在原串 "122122" 中的起始位置1。那么我们再来验证下 "bob","o" 在 "#b#o#b#" 中的位置是3,但是半径是4,这一减成负的了,肯定不对。所以我们应该至少把中心位置向后移动一位,才能为0啊,那么我们就需要在前面增加一个字符,这个字符不能是井号,也不能是s中可能出现的字符,所以我们暂且就用美元号吧。这样都不相同的话就不会改变p值了,那么末尾要不要对应的也添加呢,其实不用的,不用加的原因是字符串的结尾标识为'\0',等于默认加过了。那此时 "o" 在 "$#b#o#b#" 中的位置是4,半径是4,一减就是0了,貌似没啥问题。我们再来验证一下那个数字串,中间的 '1' 在字符串 "$#1#2#2#1#2#2#" 中的位置是8,而半径是6,这一减就是2了,而我们需要的是1,所以我们要除以2。之前的 "bob" 因为相减已经是0了,除以2还是0,没有问题。再来验证一下 "noon",中间的 '#' 在字符串 "$#n#o#o#n#" 中的位置是5,半径也是5,相减并除以2还是0,完美。可以任意试试其他的例子,都是符合这个规律的,最长子串的长度是半径减1,起始位置是中心点位置减去半径再除以2。

三、总结

通过对回文字符串的理解,我们可以更加清晰地明白动态规划以及其他方法解决问题的常用技巧,对我们以后理解和分析问题提供了帮助。

参考文献:https://www.cnblogs.com/grandyang/p/4475985.html

乘风破浪:LeetCode真题_005_Longest Palindromic Substring的更多相关文章

  1. LeetCode算法题5----Longest Palindromic Substring

    #5. Longest Palindromic Substring Given a string S, find the longest palindromic substring in S. You ...

  2. 乘风破浪:LeetCode真题_003_Longest Substring Without Repeating Characters

    乘风破浪:LeetCode真题_003_Longest Substring Without Repeating Characters 一.前言 在算法之中出现最多的就是字符串方面的问题了,关于字符串的 ...

  3. 乘风破浪:LeetCode真题_032_Longest Valid Parentheses

    乘风破浪:LeetCode真题_032_Longest Valid Parentheses 一.前言 这也是非常有意思的一个题目,我们之前已经遇到过两个这种括号的题目了,基本上都要用到堆栈来解决,这次 ...

  4. 乘风破浪:LeetCode真题_030_Substring with Concatenation of All Words

    乘风破浪:LeetCode真题_030_Substring with Concatenation of All Words 一.前言    这次我们还是找字符串的索引,不过,需要将另一个字符串列表中的 ...

  5. 乘风破浪:LeetCode真题_028_Implement strStr()

    乘风破浪:LeetCode真题_028_Implement strStr() 一.前言     这次是字符串匹配问题,找到最开始匹配的位置,并返回. 二.Implement strStr() 2.1 ...

  6. 乘风破浪:LeetCode真题_014_Longest Common Prefix

    乘风破浪:LeetCode真题_014_Longest Common Prefix 一.前言 如何输出最长的共同前缀呢,在给定的字符串中,我们可以通过笨办法去遍历,直到其中某一个字符不相等了,这样就得 ...

  7. 乘风破浪:LeetCode真题_010_Regular Expression Matching

    乘风破浪:LeetCode真题_010_Regular Expression Matching 一.前言 关于正则表达式我们使用得非常多,但是如果让我们自己写一个,却是有非常大的困难的,我们可能想到状 ...

  8. 乘风破浪:LeetCode真题_041_First Missing Positive

    乘风破浪:LeetCode真题_041_First Missing Positive 一.前言 这次的题目之所以说是难,其实还是在于对于某些空间和时间的限制. 二.First Missing Posi ...

  9. 乘风破浪:LeetCode真题_040_Combination Sum II

    乘风破浪:LeetCode真题_040_Combination Sum II 一.前言 这次和上次的区别是元素不能重复使用了,这也简单,每一次去掉使用过的元素即可. 二.Combination Sum ...

随机推荐

  1. PHP之string之addslashes()函数使用

    addslashes (PHP 4, PHP 5, PHP 7) addslashes - Quote string with slashes addslashes - 使用反斜线引用字符串 Desc ...

  2. 我爱Markdown (1)

    作为一个程序员,用Word, Excel等写技术文档实在是不那么方便.而我,作为一个Unix/Linux程序员,写技术文档还是喜欢用Wiki等在线写作工具.虽然Wiki已经很酷了,但跟Markdown ...

  3. 剑指offer(11-20)编程题

    二进制中1的个数 数值的整数次方 调整数组顺序使奇数位于偶数前面 链表中倒数第k个结点 反转链表 合并两个排序的链表 树的子结构 二叉树的镜像 顺时针打印矩阵 包含min函数的栈 11.输入一个整数, ...

  4. MYSQL5.7 sql_mode=only_full_group_by

    错误提示: .... which is not functionally dependent on columns in GROUP BY clause; this is incompatible w ...

  5. 你不知道的https工作原理

      HTTPS其实是有两部分组成:HTTP + SSL / TLS,也就是在HTTP上又加了一层处理加密信息的模块.服务端和客户端的信息传输都会通过TLS进行加密,所以传输的数据都是加密后的数据 1. ...

  6. JetBrains 产品激活码

    激活码 K03CHKJCFT-eyJsaWNlbnNlSWQiOiJLMDNDSEtKQ0ZUIiwibGljZW5zZWVOYW1lIjoibnNzIDEwMDEiLCJhc3NpZ25lZU5hb ...

  7. ListView和BaseAdapter

    参考资料:http://www.runoob.com/w3cnote/android-tutorial-listview.html <LinearLayout xmlns:android=&qu ...

  8. 开源代码ViewPageIndicator的使用

    1. 导入Android studio 使用SlidingMenu的方式导入Android studio不行,不知道为何,过会懂了再写上 2. 代码 activity_main.xml <?xm ...

  9. [转]NancyFx/Nancy

    本文转自:https://github.com/NancyFx/Nancy/wiki/Documentation Getting Started Introduction Exploring the ...

  10. GIT使用log命令显示中文乱码

    背静: 公司项目使用GIT进行代码同步. 问题: 之前代码提交后,有中文备注,但是在使用git log查看代码历史记录的时候发现显示乱码,如下: 后查询相关资料,现将解决办法总结如下: 1.运行Git ...