算法实战(五)最长回文子串

一.前言

  今天开始第五题,求最长回文子串。不知不觉已经坚持到第五天了,往往在这个时候最容易大易,所以我们不能松懈,坚持就能走向成功。

二.题目

  题目:给定一个字符串 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:我不知道这样解释能不能看明白,要是还不明白的小伙伴可以在纸上画一画,就很清晰了)。这就是今天这个题的解法,大家有什么不明白的,或者有其他的高见,欢迎一起探讨交流,谢谢!

posted @ 2019-08-22 23:43  litterCoder  阅读(433)  评论(0编辑  收藏  举报