本文汇总分析leetcode上与回文相关的题目,包括以下题目:
leetcode5.最长回文子串
给你一个字符串
s
,找到s
中最长的回文子串。
一个简单的思路是遍历字符串s
的所有子串,判断每个子串是否为回文串,并找出长度最长的子串,伪代码如下:
public String longestPalindrome(String s) {
int maxLen = 0;
int start = -1, end = -1;
for (int i = 0; i < s.length(); i++) {
for (int j = i; j < s.length(); j++) {
if (isPalindrome(s, i, j)) {
if (maxLen < j - i + 1) {
maxLen = j - i + 1;
start = i;
end = j;
}
}
}
}
return s.substring(start, end + 1);
}
isPalindrome(s, i, j)
函数用于判断子串s[i~j]
是否为回文串,简单算法如下:
private boolean isPalindrome(String s, int i, int j) {
while (i < j) {
if (s.charAt(i) != s.charAt(j)) {
return false;
}
i++;
j--;
}
return true;
}
isPalindrome
的时间复杂度为
O
(
n
)
O(n)
O(n),再加上遍历字串的时间复杂度为
O
(
n
2
)
O(n^2)
O(n2),整个算法的时间复杂度为
O
(
n
3
)
O(n^3)
O(n3),对于
1
≤
s
.
l
e
n
g
t
h
≤
1000
1 \leq s.length \leq 1000
1≤s.length≤1000的数据规模比较吃力。
实际上,isPalindrome(s, i, j)
还可以通过递推计算得到:
i s P a l i n d r o m e ( s , i , j ) = { t r u e , i = = j s [ i ] = = s [ j ] , i + 1 = = j s [ i ] = = s [ j ] ∧ i s P a l i n d r o m e ( s , i + 1 , j − 1 ) , e l s e isPalindrome(s,i,j) = \begin{cases} true, & i==j \\ s[i]==s[j], & i+1==j \\ s[i]==s[j] \wedge isPalindrome(s,i+1,j-1), & else \end{cases} isPalindrome(s,i,j)=⎩⎪⎨⎪⎧true,s[i]==s[j],s[i]==s[j]∧isPalindrome(s,i+1,j−1),i==ji+1==jelse
isPalindrome
的递推算法似乎与直接通过循环计算没区别,但是配合记忆化搜索后,以上递推算法可以保证在
O
(
n
2
)
O(n^2)
O(n2)内求出所有子串的回文性,完整实现如下:
private Boolean[][] cache;
private boolean isPalindrome(String s, int i, int j) {
if (cache[i][j] != null) {
return cache[i][j];
}
if (i == j) {
return cache[i][j] = true;
}
if (i + 1 == j) {
return cache[i][j] = s.charAt(i) == s.charAt(j);
}
return cache[i][j] = s.charAt(i) == s.charAt(j) && isPalindrome(s, i + 1, j - 1);
}
由于有了缓存,不管isPalindrome
被调用多少次,最多只会占用
n
2
n^2
n2的计算次数,而不是每次调用都是
O
(
n
)
O(n)
O(n),因此加上遍历子串的
O
(
n
2
)
O(n^2)
O(n2)后时间复杂度仍然为
O
(
n
2
)
O(n^2)
O(n2),完整代码如下:
class Solution {
private Boolean[][] cache;
public String longestPalindrome(String s) {
int maxLen = 0;
int start = -1, end = -1;
cache = new Boolean[s.length()][s.length()];
for (int i = 0; i < s.length(); i++) {
for (int j = i; j < s.length(); j++) {
if (isPalindrome(s, i, j)) {
if (maxLen < j - i + 1) {
maxLen = j - i + 1;
start = i;
end = j;
}
}
}
}
return s.substring(start, end + 1);
}
private boolean isPalindrome(String s, int i, int j) {
if (cache[i][j] != null) {
return cache[i][j];
}
if (i == j) {
return cache[i][j] = true;
}
if (i + 1 == j) {
return cache[i][j] = s.charAt(i) == s.charAt(j);
}
return cache[i][j] = s.charAt(i) == s.charAt(j) && isPalindrome(s, i + 1, j - 1);
}
}
leetcode647.回文子串
给你一个字符串
s
,请你统计并返回这个字符串中回文子串的数目。
本题与5.最长回文子串类似,只不过最后求的是所有回文子串的数量,也是通过遍历s
的所有子串进行计数,可以复用isPalindrome
函数,核心代码如下:
for (int i = 0; i < s.length(); ++i) {
for (int j = i; j < s.length(); ++j) {
if (isPalindrome(s, i, j)) {
cnt++;
}
}
}
return cnt;
leetcode131.分割回文串
给你一个字符串
s
,请你将s
分割成一些子串,使每个子串都是回文串,返回s
所有可能的分割方案。
为得到所有合法的分割方案,可使用DFS枚举所有分割,并在枚举过程中记录每次分割得到的子串。假设dfs(index)
表示分割s[index~len-1]
,然后尝试使用i
枚举下一次划分的位置,将s[index~len-1]
划分为s[index~i]
和s[i+1~len-1]
,在s[index~i]
为回文串的条件下,递归执行dfs(i+1)
。
核心代码如下:
class Solution {
public List<List<String>> partition(String s) {
List<List<String>> result = new ArrayList<>();
dfs(s, 0, new LinkedList<>(), result);
return result;
}
/**
* 从s[index]开始分割
* @param s 待划分字符串
* @param index 开始划分的索引
* @param path 保存当前划分方案
* @param result 保存所有划分方案
*/
private void dfs(String s, int index, LinkedList<String> path, List<List<String>> result) {
if (index == s.length()) {
result.add(new ArrayList<>(path));
return;
}
// 划分成s[index~i]和s[i+1~len-1]
for (int i = index; i < s.length(); i++) {
if (isPalindrome(s, index, i)) {
path.addLast(s.substring(index, i + 1));
dfs(s, i + 1, path, result);
path.removeLast();
}
}
}
}
leetcode132.分割回文串II
给你一个字符串
s
,请你将s
分割成一些子串,使每个子串都是回文,返回符合要求的最少分割次数。
本题与131.分割回文串类似,需要求所有划分中最少的分割次数,可以使用动态规划思想解决本题。
设dp(s,index)
表示将s[0~index]
分割成若干回文子串的最小分割次数,则可以使用i
枚举0~index
之间的分割点,将s[0~index]
分割成s[0~i]
和s[i+1~index]
两部分,并保证s[i+1~index]
为回文串,然后递归求s[0~i]
的最小分割次数,最后返回所有分割点的最小分割次数的最小值加1。在判断回文串时,仍然可以复用前面的isPalindrome
函数。
dp
函数的核心代码如下:
// 将s[0~index]分割成若干回文子串的最小分割次数
private int dp(String s, int index) {
// 如果s[0~index]是回文串,则无需分割,直接返回0
if (isPalindrome(s, 0, index)) {
return 0;
}
// 枚举分割点:s[0~i]、s[i+1~index]
int res = index;
for (int i = index - 1; i >= 0; --i) {
if (isPalindrome(s, i + 1, index)) {
res = Math.min(res, dp(s, i) + 1);
}
}
return res;
}
leetcode1745.回文串分割IV
给你一个字符串
s
,如果可以将它分割成三个非空回文子字符串,那么返回true
,否则返回false
。
本题与131.分割回文串类似,只是固定了分割次数为3,因此只需使用双重for循环枚举两个分割位置,再用isPalindrome
函数判断每段是否为回文串即可,核心代码如下:
public boolean checkPartitioning(String s) {
// 将s分割成[0, i)、[i, j)和[j, len)三段
for (int i = 1; i < s.length(); i++) {
if (!isPalindrome(s, 0, i - 1)) {
continue;
}
for (int j = i + 1; j < s.length(); j++) {
if (!isPalindrome(s, i, j - 1)) {
continue;
}
if (isPalindrome(s, j, s.length() - 1)) {
return true;
}
}
}
return false;
}
leetcode1312.让字符串成为回文串的最少插入次数
给你一个字符串
s
,每一次操作你都可以在字符串的任意位置插入任意字符。请你返回让
s
成为回文串的最少操作次数。
本题可用动态规划求解。假设dp(i, j)
表示将s[i~j]
变成回文串的最小插入次数:
- 当
i >= j
时,显然dp(i, j) = 0
- 如果
s[i] == s[j]
,则子问题缩小为dp(i + 1, j - 1)
- 如果
s[i] != s[j]
,有两种方案可以选择:- 在
s[i]
左边插入s[j]
,字符串变成s[j], s[i], ..., s[j]
,子问题缩小为dp(i, j - 1)
- 在
s[j]
右边插入s[i]
,字符串变成s[i], ..., s[j], s[i]
,子问题缩小为dp(i + 1, j)
- 在
综上所述,dp(i, j)
有如下递推关系:
d p ( i , j ) = { 0 , i ≥ j d p ( i + 1 , j − 1 ) , s [ i ] = = s [ j ] 1 + m a x ( d p ( i , j + 1 ) , d p ( i + 1 , j ) ) , e l s e dp(i,j) = \begin{cases} 0, & i \geq j \\ dp(i+1,j-1), & s[i]==s[j] \\ 1+max(dp(i,j+1),dp(i+1,j)), & else \end{cases} dp(i,j)=⎩⎪⎨⎪⎧0,dp(i+1,j−1),1+max(dp(i,j+1),dp(i+1,j)),i≥js[i]==s[j]else
通过使用记忆化搜索,可实现时间复杂度为 O ( n 2 ) O(n^2) O(n2)的算法,完整代码如下:
class Solution {
private Integer[][] cache;
public int minInsertions(String s) {
cache = new Integer[s.length()][s.length()];
return dp(s, 0, s.length() - 1);
}
// 将s[i~j]变成回文串的最小插入次数
private int dp(String s, int i, int j) {
if (i >= j) {
return 0;
}
if (cache[i][j] != null) {
return cache[i][j];
}
if (s.charAt(i) == s.charAt(j)) {
return cache[i][j] = dp(s, i + 1, j - 1);
}
return cache[i][j] = 1 + Math.min(dp(s, i + 1, j), dp(s, i, j - 1));
}
}