一.前言
今天开始第五题,求最长回文子串。不知不觉已经坚持到第五天了,往往在这个时候最容易大易,所以我们不能松懈,坚持就能走向成功。
二.题目
题目:给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例1:输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
三.解题思路
首先我们要明白什么是回文字符串,回文是一个正读和反读都相同的字符串,例如,“aba” 是回文,而“abc” 不是。
1)暴力破解法:首先看到这道题目,我最开始的思路就是用暴力破解法,首先拿到字符串的所有字串,然后判断字串是不是回文,最终找出最长的回文字串。但是拿到所有的子串所需要的时间复杂度是O(n^2),而判断一个字符串是不是回文字符串所需要的时间复杂度是O(n),所以暴力法的时间复杂度是O(n^3),不用想都知道leetCode上面肯定通过不了,于是我做了一些修改,去除了大部分不需要的判断。话不多说,代码如下:
1 class Solution { 2 public String longestPalindrome(String s) { 3 //如果为空,直接返回 4 if (s == null || s.length() == 0){ 5 return s; 6 } 7 //定义开始指针,用来指向回文的开始位置 8 int begin = 0; 9 //定义结束指针,用来指向回文的开始位置 10 int end = 0; 11 //外层循环,遍历每一个字符 12 for(int i = 0; i < s.length(); i++){ 13 Character ch = s.charAt(i); 14 //内层循环,寻找回文 15 for(int j = s.length() - 1; j >= 0; ){ 16 //从字符串的末尾开始寻找与头字符匹配的字符串 17 int index = s.lastIndexOf(ch, j); 18 //如果没有,则这个字符串不可能是回文的,直接返回 19 if(index == -1){ 20 break; 21 } 22 //判断头到index之间是不是回文的,如果是回文,计算差值是否大于全局变量头尾位置的差值 23 if(checkStr(s, i, index) && index - i > end - begin){ 24 //大于则直接更新,并跳出本次循环 25 begin = i; 26 end = index; 27 break; 28 } 29 //如果本次头与index直接的距离已经小于全局头尾距离了,没必要往下找了,直接跳出 30 if (index - i < end - begin){ 31 break; 32 } 33 //更新j的值,继续找j之前对应的字符位置 34 j = index - 1; 35 } 36 } 37 return s.substring(begin, end + 1); 38 } 39 40 //判断一个字符串在【i,j】上是不是回文的 41 public boolean checkStr(String s, int i, int j){ 42 while(i <= j){ 43 if(s.charAt(i) != s.charAt(j)){ 44 return false; 45 } 46 i++; 47 j--; 48 } 49 return true; 50 } 51 }
2)上面那种方法可以通过测试,但是耗费的时间还是太长了。官方的解法应该是要使用动态规划,但是本人是一个算法渣渣,还不会这种算法,容我先去学习一天,明天补上,谢谢大家
动态规划的学习篇章已经补上了:https://www.cnblogs.com/litterCoder/p/11415837.htm,如果大家和我一样对dp不是很熟系的可以先学习一下,接下来我们就按照这篇文章中的解题步骤来进行解题。解一个dp的题,我认为首先是要定义好一个状态,然后再根据状态来验证是否满足最优子结构和子问题重叠的性质,都满足后进而推出状态转移方程。
1.状态:F(i,j):表示以第i个字符开头,以第j个字符结尾的字符的回文长度,如果是回文字符,则就是i到j之间的长度,如果不是回文字符串则是0.
2.定性:最优子结构:我们知道了F(i,j)的长度,如果不为0的话,我们就可以推出F(i+1,j-1)的长度,能够通过子问题的解得到原问题的解,满足最优子结构。
重叠子问题:我们计算F(2,6)的时候,需要用到F(3,5)的解,计算F(3,5)的时候,需要使用到F(4,4)的解,如果不将F(3,5)的解保存下来,F(4,4)会被重复计算,满足重叠子问题。(重叠子问题不是dp解法的必要条件,但是有了重叠子问题这个性质,我们使用dp解这个题才有了优势)
3.状态转移方程:状态转移方程的情况比较多,从这两种不同的情况来看
1)当第i个字符和第j个字符相同时, F(i,j)= 1 (j - i = 0时,一个字符串就是长度为1的回文字符串)
2 (j - i = 1 ,两个相邻并且相同的字符串就是长度为2的回文字符串)
F(i+1, j - 1)+ 2 ,(当j - i > 1 并且F(i+1, j - 1)不为0时)
0 (j - i 》1 并且F(i+1, j - 1)为0时,子串的都不是回文字符串,该字符串也不是回文字符串)
2)当第i个字符和第j个字符不相同时,F(i,j) = 0;
4.辅助空间,使用一个二维数组来保存解
代码如下:
1 class Solution { 2 public String longestPalindrome(String s) { 3 //过滤特殊情况 4 if (s == null || s.length() == 0){ 5 return s; 6 } 7 //用来保存最大回文字串的开头位置和结尾位置 8 int begin = 0; 9 int end = 0; 10 //二位数组保存解 11 int[][] arr = new int[s.length()][s.length()]; 12 for (int j = 0; j < s.length(); j++){ 13 for (int i = 0; i <= j; i ++){ 14 if (s.charAt(i) == s.charAt(j)){ 15 if (j - i == 0){ 16 arr[i][j] = 1; 17 }else if (j - i == 1){ 18 arr[i][j] = 2; 19 }else{ 20 arr[i][j] = arr[i + 1][j - 1] == 0 ? 0 :arr[i + 1][j - 1] + 2; 21 } 22 }else { 23 arr[i][j] = 0; 24 } 25 //将每一次的结果与begin和end的差值进行比较,如果大于,则进行替换,这样计算完成后,保存的就是最大值了 26 if (arr[i][j] > end - begin + 1){ 27 begin = i; 28 end = j; 29 } 30 } 31 } 32 return s.substring(begin, end + 1); 33 } 34 }
在我们分析完状态转移方程后,编写代码就是一件比较简单的事了,在我个人看来,比较难理解的地方是两次for循环本身的条件,为什么内层循环是到i 《= j 的判断条件,这个又要和我们的状态扯上关系。状态F(i,j)是要求字符串以i开头以j结尾时回文字串的长度,而我们在计算它的值时,需要用到 i 到 j 中间的子串的解,所以我们要先计算出这些解,才能将j往后移动。(PS:我不知道这样解释能不能看明白,要是还不明白的小伙伴可以在纸上画一画,就很清晰了)。这就是今天这个题的解法,大家有什么不明白的,或者有其他的高见,欢迎一起探讨交流,谢谢!