Longest Palindromic Substring

       今天闲来无事忽然想到刷LeetCode, 这一刷就刷出了一些心得,于是就想把这些心得总结一下,打开博客惭愧感油然而生,上一次更新算法日志居然是去年,那时还雄心勃勃的立下誓言每周更新一篇,现在看看我的战果吧...... 怪不得自己始终没什么长进。

        废话少说,转入正题,算法方面目前我还是初学者,实在没有什么独特的见解,因此完全就是编辑整理别人的东西,以便自己回过头来学习的时候能够一下抓住重点,不必把当时学习算法时的思考过程重新再来一遍,就像现在再来看一年前的那篇算法日志,又领悟到了很多东西,这就是所谓的“温故而知新”吧!

       关于这道题的解法,LeetCode上的Solution其实总结得非常好,

                https://leetcode.com/problems/longest-palindromic-substring/solution/

          总体来说有四种做法,一是Brute Force, 这是没有算法思想,纯粹为了解题而解题的做法,不过有时从中也可以领悟到一些东西,有利于形成一定的算法思想;二是Dynamic Programming, 动态规划更加侧重于考察算法思想,看你能不能推导出递推公式,有了公式,算法其实是水到渠成的;三是利用Longest Common SubString问题,稍加优化进行求解;四是Manacher's Algorithm。后两种今天没有时间学,所以先总结前两种。每次刷LeetCode, 总是试图以一个个算法题为中心,从中扩展出很多知识点,所以对于我来说刷LeetCode学算法比单纯的看算法书效率要高得多,为什么没有在学校里时间相对充裕的情况下发现这样一个高效率的学习算法的方式?不必嗟叹了,种一颗树最好的时间是十年前或是现在,迟到总比不到好。

       发现关于这个算法的两个好资源, 一是皓神的:

       https://github.com/haoel/leetcode/blob/master/algorithms/cpp/longestPalindromicSubstring/longestPalindromicSubstring.cpp

       另一个来源是LeetCode上某网友,http://windliang.cc/2018/08/05/leetCode-5-Longest-Palindromic-Substring/,图文并茂,很适合初学者。我下面的所有代码均是从这三个资源中整理所得。

       (1) 暴力破解法 (Brute Force),

       既然是要求回文子串,那么肯定要先搞清楚如何判断一个字符串是否是回文串,       

public boolean isPalindromic(String s) {
	int len = s.length();
	for (int i = 0; i < len / 2; i++) {
		 if (s.charAt(i) != s.charAt(len - i - 1)) {
			 return false;
		 }
	}
	return true;
}

       算法思想是一目了然的,就是根据回文串的概念,遍历字符串,这里更多考察的是关于数组下标操作的基本功了。知道了回文串怎么判断,剩下的问题就是怎么得到一个字符串的所有子串, LeetCode上这样说,“Assume that n is the length of the input string, there are a total of n(n-1)/2 ​​such substrings”,但是这个n(n-1)/2不包含单个字符(因为根据回文串定义,单个字符也可以算回文),举个例子,有字符串“abc", 则它就有三个子串,“ab, bc, abc",  其实单个"a", "b", "c"也应该算子串,应该如何获得字符串s的所有子串呢?贴一下代码,

for (int i = 0; i < n; i++)
    for (int j = i+1, j <= n; j++)
         s.substring(i,j)
 

public String substring(int beginIndex, int endIndex)
- beginIndex 起始索引(包括)
- endIndex 结束索引 (不包括)

        为什么要加上这一段呢,算法和编码基础都实在太差,我是担心自己连暴力破解法都搞不出来,凡事都从基础开始吧!

       分析一下时间与空间复杂度,Brute Force法找子串要花费^{}O(n2), 找到子串后再判断是否回文,要花费O(n), 所以总得时间复杂度是O(n3), 空间复杂度为O(1)

       (2) 动态规划(Dynamic Programming)

       没有系统得学过动态规划,了解了之后发现动态规划更多地是考察推导数学公式的能力,而并非考察编码能力,这是用动态规划求解的思想LeetCode的Solution已经说得非常清楚了,下面贴一段皓神的代码:

string longestPalindrome_dp_way(string s) {

    string longest;

    int n = s.size();
    if (n<=1) return s;
    
    //Construct a matrix, and consdier matrix[i][j] as s[i] -> s[j] is Palindrome or not.

    //using char or int could cause the `Memory Limit Error`
    //vector< vector > matrix (n, vector(n));

    //using bool type could cause the `Time Limit Error`
    vector< vector > matrix (n, vector(n));

    // Dynamic Programming 
    //   1) if i == j, then matrix[i][j] = true;
    //   2) if i != j, then matrix[i][j] = (s[i]==s[j] && matrix[i+1][j-1])
    for (int i=n-1; i>=0; i--){
        for (int j=i; j 3, then, check s[i]==s[j] && matrix[i+1][j-1]
            if ( i==j || (s[i]==s[j] && (j-i<2 || matrix[i+1][j-1]) ) )  {
                matrix[i][j] = true;
                if (longest.size() < j-i+1){
                    longest = s.substr(i, j-i+1);
                }
            }
        }
    }

    return longest;
}

         他这段代码写得是中规中矩,注释也加得恰到好处,基本上看Solution还不能充分理解动态规划算法思想的童鞋,看他的解法这时会有恍然大悟的感觉(我的情况是这样),这里还是要说明一下,由于计算matrix[i][j]的时候必须知道matrix[i+1][j-1]的值,所以为了算法简捷,采用从后往前遍历。算法写出来之后,动态规划解法的时间与空间复杂度是一目了然的,都是O(n2).

       LeetCode上说可以进一步优化空间复杂度,但是他没有给出具体算法思想,这样网友在以下这个网址给出了解答

       http://windliang.cc/2018/08/05/leetCode-5-Longest-Palindromic-Substring/  , 其实对于这个算法他的分析更加详尽,但不大符合我的思考方式,所以我仍然觉得有必要重新总结一下,先贴出代码:

public String longestPalindrome7(String s) {
		int n = s.length();
		String res = "";
		boolean[] P = new boolean[n];
		for (int i = n - 1; i >= 0; i--) {
			for (int j = n - 1; j >= i; j--) {
				P[j] = s.charAt(i) == s.charAt(j) && (j - i < 3 || P[j - 1]);
				if (P[j] && j - i + 1 > res.length()) {
					res = s.substring(i, j + 1);
				}
			}
		}
		return res;
	}

        这样把两维的辅助空间降成了一维,空间复杂度降成了O(n), 想出这种解法需要对基本动态规划解法极其了解, 遗憾的是我看作者的博客没有很清晰的整理出他的思路,也许有些东西是只可意会, 不可言传的,这段代码我试着在LeetCode中提交,用时85ms, 动态规划的求解方法基本保持在这个数量级。 

       (3)  Expand Around Center

        LeetCode上提出了Expand Around Center的解法,这种解法不属于基础解法的范畴,针对这个问题效率非常高,仍然是先贴出代码,

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);
        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;
}

      因为考虑到奇数串和偶数串的问题,所以要分别对 Si (奇数串)和Si,S(i+1 ) 扩散中心,有了思想,写出算法不难, 同样要注意下标的处理,算法的时间复杂度O(n2), 空间复杂度O(1), 实测它的运行时间是18ms, 难道空间复杂度的降低也可以使算法变快?从代码提交结果看似乎是的,DP算法 空间复杂度O(n2), 用时108ms, DP优化算法 空间复杂度O(n),用时81ms, 扩散中心算法,空间复杂度O(1), 用时18ms.    

你可能感兴趣的:(算法)