乘风破浪: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。这样如果我们能找到一个子串使得这样一路下来一直满足,那么我们就找到了回文序列,根据长度我们可以知道最长的子序列。

   于是我们的算法为:

public class Solution {
/**
*
* 题目大意:
* 给定一个字符串S,找出它的最大的回文子串,你可以假设字符串的最大长度是1000,
* 而且存在唯一的最长回文子串
*
* 解题思路:
* 动态规划法,
* 假设dp[ i ][ j ]的值为true,表示字符串s中下标从 i 到 j 的字符组成的子串是回文串。那么可以推出:
* dp[ i ][ j ] = dp[ i + 1][ j - 1] && s[ i ] == s[ j ]。
* 这是一般的情况,由于需要依靠i+1, j -1,所以有可能 i + 1 = j -1, i +1 = (j - 1) -1,因此需
* 要求出基准情况才能套用以上的公式:
*
* a. i + 1 = j -1,即回文长度为1时,dp[ i ][ i ] = true;
* b. i +1 = (j - 1) -1,即回文长度为2时,dp[ i ][ i + 1] = (s[ i ] == s[ i + 1])。
*
* 有了以上分析就可以写出代码了。需要注意的是动态规划需要额外的O(n^2)的空间。
* </pre>
*
* @param s
* @return
*/
public String longestPalindrome(String s) { if (s == null || s.length() < 2) {
return s;
} int maxLength = 0;
String longest = null; int length = s.length();
boolean[][] table = new boolean[length][length]; // 单个字符都是回文
for (int i = 0; i < length; i++) {
table[i][i] = true;
longest = s.substring(i, i + 1);
maxLength = 1;
} // 判断两个字符是否是回文
for (int i = 0; i < length - 1; i++) {
if (s.charAt(i) == s.charAt(i + 1)) {
table[i][i + 1] = true;
longest = s.substring(i, i + 2);
maxLength = 2;
}
} // 求长度大于2的子串是否是回文串
for (int len = 3; len <= length; len++) {
for (int i = 0, j; (j = i + len - 1) <= length - 1; i++) {
if (s.charAt(i) == s.charAt(j)) {
table[i][j] = table[i + 1][j - 1];
if (table[i][j] && maxLength < len) {
longest = s.substring(i, j + 1);
maxLength = len;
}
} else {
table[i][j] = false;
}
}
} return longest;
} }

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

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

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

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

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

 public String longestPalindrome(String s) {
if (s == null || s.length() < 1) return "";
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);
8 if (len > end - start) {
9 start = i - (len - 1) / 2;
10 end = i + len / 2;
11 }
}
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;
}

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

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

 bob    -->    #b#o#b#
noon --> #n#o#o#n#

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

 # 1 # 2 # 2 # 1 # 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. linux mint 18.2 安装wireshark

    Method 1: Via PPA Step 1: Add the official PPA sudo add-apt-repository ppa:wireshark-dev/stable Step ...

  2. git提交代码到远程仓库

    1.仓库初始化 git init 2.连接仓库 git remote add origin 仓库地址 3.查看状态 git status 4.将文件添加到暂存区 git add 状态里的新文件 5.将 ...

  3. Innosetup中在安装界面左下角添加超链接

    在程序的安装界面左下角加上超链接,如下图: 1. 新建一个标签,这里使用的控件是TNewStaticText ,完整的方法是 //该方法传入两个参数: //1. ParentForm:将这个URLLa ...

  4. 在spring中常被忽视的注解 @Primary

    在spring 中使用注解,常使用@Autowired, 默认是根据类型Type来自动注入的. 但有些特殊情况,对同一个接口,可能会有几种不同的实现类,而默认只会采取其中一种的情况下 @Primary ...

  5. 转:Java 基本数据类型

    Java 基本数据类型 转:http://www.runoob.com/java/java-basic-datatypes.html 变量就是申请内存来存储值.也就是说,当创建变量的时候,需要在内存中 ...

  6. 仿淘宝头像上传功能(三)——兼容 IE6 浏览器。

    前两篇目录: 仿淘宝头像上传功能(一)——前端篇. 仿淘宝头像上传功能(二)——程序篇. 仿淘宝头像上传功能(三)——兼容 IE6 浏览器 之前的这两篇虽然实现了功能,但不兼容低版本浏览器,而且有些浏 ...

  7. del

    Before writing it I spent a fair amount of time on reviewing our chat-log. How silly. asljdfajsodfss ...

  8. [C#]LockBits使用笔记

    昨天想基于一张图片做个手机锁屏来着,原图如下:主要是嫌白底太丑了,一开始是想画图工具直接油漆桶伺候,然而一浇上去就发现问题了,变成了这样:看来得手工处理一下把底色统一了,原图分辨率挺高的,SetPix ...

  9. 八、window搭建spark + IDEA开发环境

    本文将简单搭建一个spark的开发环境,如下: 1)操作系统:window os 2)IDEA开发工具以及scala插件(IDEA和插件版本要对应): 2-1)IDEA2018.2.1:https:/ ...

  10. Spring Boot学习笔记(二)全局捕获异常处理

    非常简单只需要创建自己的异常处理类,加上两个注解,就可以了