多思路深度剖析经典面试题最长回文子串
Posted 程序员小熊
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多思路深度剖析经典面试题最长回文子串相关的知识,希望对你有一定的参考价值。
前言
大家好,我是程序员小熊,来自大厂的程序猿。最长回文子串是面试中常考的题目,尤其是一些互联网大厂,像亚马逊、微软、脸书、字节和腾讯等都考过这道题。
本文提供四种解题思路和三种解法,将本题解法的时间复杂度由 O(n^3) 一步步降为 O(n) ,供大家参考,希望对大家有所帮助。
题目
给你一个字符串 s,找到 s 中最长的回文子串。
解题思路
本题要求的是最长回文子串,必须先明确两个概念。
回文串:从左向右读跟从右向左读都一样的字符串。例如 “level”或者“noon”等等。
子串:原始字符串中任意个连续的字符组成的子序列。
由于题目提示 1 <= s.length <= 1000,因此设计一个 O(n^2) 的算法是合理的。因为 O(n^2) 的算法可以在 1s 内处理大约 10^4 级别的数据;并且从示例1中可以知道,如果字符串存在多个最长回文子串,只需要输出一个即可。
暴力法
以字符串 "abba" 为例子,如下动图所示。
要求字符串的最长回文子串,只需要先找出最长回文子串的起始位置,再求出最长回文子串的长度即可。
因此可以通过两层遍历枚举长度大于等于2且超过当前得到的最长回文子串长度的子串,再加一层判断子串是否是回文串的遍历,就可求出给定字符串的最长回文子串。
Show me the Code
java
1 /* 验证子串 s[left...right]是否为回文子串 */ 2 boolean isPalindrome(char[] charArray, int left, int right) { 3 while (left < right) { 4 if (charArray[left] != charArray[right]) { 5 return false; 6 } 7 left++; 8 right--; 9 } 10 11 return true; 12 } 13 14 String longestPalindrome(String s) { 15 int len = s.length(); 16 /* 长度少于 2 的字符串的最长回文子串是其自身 */ 17 if (len < 2) { 18 return s; 19 } 20 21 int maxLen = 1; // 记录最长回文子串的长度 22 int start = 0; // 记录最长回文子串的起始位置 23 char[] charArray = s.toCharArray(); 24 for (int i = 0; i < len - 1; i++) { 25 for (int j = i + 1; j < len; j++) { 26 /* 回文子串 s[i...j] 的长度超过当前最长回文子串长度 */ 27 if (j - i + 1 > maxLen && isPalindrome(charArray, i, j)) { 28 /* 更新最长回文子串的长度 */ 29 start = i; 30 maxLen = j - i + 1; 31 } 32 } 33 } 34 35 return s.substring(start, start + maxLen); 36 }
复杂度分析
时间复杂度:三层遍历 O(n^3)。
空间复杂度:没有开辟额外空间 O(1)。
动态规划
回文串具有天然状态转移性,一个长度大于 2 的回文串,去掉首尾两头之后,剩余的部分依然是回文串。
情况一:如果一个子串首尾两头的字符不相同,则该子串不是回文串。如下图示。
情况二:如果一个子串首尾两头的字符相同,则去掉首尾两头的字符,继续判断去掉后的子串,直至子串的首尾两头的字符不相同或子串为空。如下动图示。
也就是说一个子串首尾两头的字符相同,去掉首尾两头的字符后剩余的子串是否是回文串决定了原子串是否是回文串。
状态:dp[i][j] 表示子串 s[i...j] 是否为回文子串。
状态转移方程:
边界条件:[i + 1...j - 1]不成立(构成区间)
初始化:单个字符一定是字符串 dp[i][j] = true。
输出:状态为 true 时,记录当前最长回文子串的起始位置和长度,填完表后截取字符串。
举例 以字符串 "abcba" 为例子,如下动图示,表格中的 dp[i][j] 也可参考它左下方的值填写。
Show me the Code
java
1 String longestPalindrome(String s) { 2 int len = s.length(); 3 if (len < 2) { 4 return s; 5 } 6 7 int start = 0; 8 int maxLen = 1; 9 10 /* dp[i][j]:s[i...j] 是否是回文串 */ 11 boolean[][] dp = new boolean[len][len]; 12 for (int i = 0; i < len; i++) { 13 dp[i][i] = true; 14 } 15 16 char[] charArray = s.toCharArray(); 17 for (int j = 1; j < len; j++) { 18 for (int i = 0; i < j; i++) { 19 if (charArray[i] != charArray[j]) { 20 dp[i][j] = false; 21 } else { 22 if (j - i < 3) { 23 dp[i][j] = true; 24 } else { 25 dp[i][j] = dp[i + 1][j - 1]; 26 } 27 } 28 29 /* s[i...j] 是回文串,记录当前最长回文串长度和起始位置 */ 30 if (dp[i][j] && j - i + 1 > maxLen) { 31 maxLen = j - i + 1; 32 start = i; 33 } 34 } 35 } 36 37 return s.substring(start, start + maxLen); 38 }
利用状态转移方程,快速判断一个子串是否是回文串,每一步计算都尽可能地利用之前计算的结果(保存的),空间换时间。
复杂度分析
时间复杂度:O(n^2)。
空间复杂度:O(n^2)。
中心扩散法
回文串的枚举可以从两边开始,也可以从中间开始,所以可以枚举所有可能的回文子串的中心位置。
由于奇/偶数长度的回文子串的中心位置不一样,所以枚举时需要考虑回文子串长度是奇数还是偶数,如下图示。
举例
以奇数长度的字符串 "abcba" 为例子,中心扩散如下动图示,
Show me the Code
C++
1 /* s[left] 和 s[right] 想两断扩散, 求以 s[left] 和 s[right] 为中心的最长回文串*/ 2 string longestPalindromeHelper(string s, int left, int right) { 3 while (left >= 0 && right < s.length() && s[left] == s[right]) { 4 left--; 5 right++; 6 } 7 8 return s.substr(left + 1, right - left - 1); 9 } 10 11 string longestPalindrome(string s) { 12 int len = s.length(); 13 if (len < 2) { 14 return s; 15 } 16 17 string res; 18 for (int i = 0; i < len; i++) { 19 /* 以s[i]作奇数回文子串的的回文中心向两边扩散,得到的最长回文子串*/ 20 string s1 = longestPalindromeHelper(s, i, i); 21 /* 以s[i]、s[i + 1] 分别作偶数回文子串的的回文中心向两边扩散,得到的最长回文子串*/ 22 string s2 = longestPalindromeHelper(s, i, i + 1); 23 res = res.size() > s1.size() ? res : s1; 24 res = res.size() > s2.size() ? res : s2; 25 } 26 27 return res; 28 }
复杂度分析
时间复杂度:O(n^2),枚举中心位置的个数是 2(n - 1),每次向两边扩散检测试服是回文子串。
空间复杂度:O(1)。
Manacher 算法
通过将原始字符串进行预处理(用不在输入字符串中的字符隔开),使得奇/偶数长度的回文串的性质统一表示。该算法专门用于查找最长回文子串,其时间复杂度为 O(n)。
由于绝大多数笔/面试不要求掌握次算法,所以这里就不再介绍了,感兴趣的童鞋,可以查阅相关资料进一步了解。
以上是关于多思路深度剖析经典面试题最长回文子串的主要内容,如果未能解决你的问题,请参考以下文章
LeetCode-面试算法经典-Java实现05-Longest Palindromic Substring(最大回文字符串)