题目
回文是一个正读和反读都相同的字符串,比如:"aba"是回文,而"abc"不是回文。现给定一个字符串s,找出s中最长的回文子串(可能有多个最长的,找出一个即可)。
示例 1:
输入: "babad"
输出: "bab"("aba" 也是一个有效答案)
示例 2:
输入: "cbbd"
输出: "bb"
解析
最长回文子串是一道比较常见和经典的面试题,有好几种不同的解法。每种解法的时间复杂度和空间复杂度可能都不相同,下面分别进行介绍。
最直接、最简单的方法,就是暴力求解法。暴力求解法会遍历子字符串的起始位置和结束位置,再判断起始位置和结束位置之间的子字符串是否为回文。具体实现,可参考下面的示例代码。
fn is_palindrome(s: &str, mut start: usize, mut end: usize) -> bool {
while start < end {
if s.chars().nth(start).unwrap() != s.chars().nth(end).unwrap() {
return false;
}
start += 1;
end -= 1;
}
true
}
fn get_longest_palindrome(s: &str) -> &str {
let len = s.len();
if len < 2 {
return s;
}
let mut max_len = 1;
let mut start_max = 0;
for i in 0..len {
for j in i + 1..=len {
if j - i + 1 > max_len && is_palindrome(s, i, j - 1) {
max_len = j - i;
start_max = i;
}
}
}
&s[start_max..start_max + max_len]
}
fn main() {
let str_text = "hello hope, bacab";
// 输出:bacab
println!("{}", get_longest_palindrome(str_text));
}
在上面的示例代码中,我们在第一层循环中使用i遍历,作为子字符串的起始位置。在第二层循环中使用j遍历,作为子字符串的结束位置。最后,我们判断i与j之间的子字符串是否为回文。由于判断回文的函数is_palindrome也遍历了字符串,因此,总共有三层循环。暴力求解法的时间复杂度为O(n^3),空间复杂度为O(1)。
接下来,我们使用动态规划法来求解本题。动态规划法的思路是:对于一个字符串,以每个字符为中心点,向两边扩展寻找最长的回文子串;如果相邻两个字符相同,则以这两个字符为中心点,向两边扩展寻找最长的回文子串。具体实现,可参考下面的示例代码。
use std::cmp;
fn get_longest_palindrome(s: &str) -> &str {
let len = s.len();
if len < 2 {
return s;
}
let mut start = 0;
let mut max_len = 1;
let mut palindrome = vec![0u8; len * len];
// 记录所有长度为1的子串为回文
for i in 0..len {
palindrome[i * len + i] = 1;
}
// 处理长度为2的子串
for i in 0..len - 1 {
if s.chars().nth(i) == s.chars().nth(i + 1) {
start = i;
max_len = 2;
palindrome[i * len + i + 1] = 1;
}
}
// 遍历长度为3及以上的子串
for i in 3..=len {
for j in 0..=(len - i) {
let k = j + i - 1;
if s.chars().nth(j) == s.chars().nth(k) && palindrome[(j + 1) * len + k - 1] == 1 {
start = j;
max_len = cmp::max(max_len, i);
palindrome[j * len + k] = 1;
}
}
}
&s[start..start + max_len]
}
fn main() {
let str_text = "hello hope, bacab";
// 输出:bacab
println!("{}", get_longest_palindrome(str_text));
}
在上面的示例代码中,我们使用Rust的向量palindrome保存长度为1到nLen的所有子串是否为回文的标记。若为1,则表示子串为回文;若为0,则表示子串不为回文。在第一个循环中,我们初始化了palindrome,将长度为1和2的所有子串是否为回文的标记进行了赋值。在第二个循环中,我们遍历了长度为3及以上(使用i)的子串,并使用j作为子串的左边界,则k = j + i - 1为子串的右边界。如果左右边界的字符相同,且j+1到k-1的子串为回文,则j到k的子串也为回文。最终,我们记录了最长子串的起始位置和长度,得到了最长子串。
分析上面的代码可以发现,动态规划法的时间复杂度为O(n^2),空间复杂度为O(n^2)。
还有更优的解法吗?我们可以尝试一下中心扩展算法。仔细观察可以发现,回文中心的两侧互为镜像。而回文的中心,可以是一个字符(对应奇数个字符的回文),也可以是两个字符(对应偶数个字符的回文)。中心扩展算法的基本思路是:从给定字符串的中心点开始,向两边扩展,判断以当前中心点为中心的子串是否是回文串;如果是,则继续向两边扩展,直到无法再扩展为止。具体实现,可参考下面的示例代码。
fn expand_around_center(s: &str, mut left: isize, mut right: isize) -> usize {
while left >= 0
&& right < s.len() as isize
&& s.chars().nth(left as usize) == s.chars().nth(right as usize)
{
left -= 1;
right += 1;
}
(right - left - 1) as usize
}
fn get_longest_palindrome(s: &str) -> &str {
let len = s.len();
if len < 2 {
return s;
}
let mut start = 0;
let mut max_len = 1;
for i in 0..len {
// 扩展长度为奇数的子串
let len1 = expand_around_center(s, i as isize, i as isize);
// 扩展长度为偶数的子串
let len2 = expand_around_center(s, i as isize, (i + 1) as isize);
let sub_len = std::cmp::max(len1, len2);
if sub_len > max_len {
start = (i - sub_len / 2) as isize;
max_len = sub_len;
}
}
&s[start as usize..((start as usize) + max_len)]
}
fn main() {
let str_text = "hello hope, bacab";
// 输出:bacab
println!("{}", get_longest_palindrome(str_text));
}
在上面的示例代码中,我们首先封装了expand_around_center函数。该函数接受一个字符串str、一个左指针left和一个右指针right作为输入,并返回以left和right为中心点的子串的长度。在函数内部,我们使用一个while循环向两边扩展子串,直到无法再扩展为止。最后,我们返回扩展后的子串的长度。
接着,我们声明了一个get_longest_palindrome函数。它接受一个字符串作为输入,并返回最长回文子串。在函数内部,我们使用一个循环遍历字符串中的每个字符。对于每个字符,我们分别计算以该字符为中心点、扩展长度为奇数和偶数的子串的长度,并取两个长度的较大值作为最长回文子串的长度。最后,我们返回了最长回文子串。
分析上面的代码可以发现,中心扩展算法的时间复杂度为O(n^2),空间复杂度为O(n)。
总结
通过这道题,我们学习了最长回文子串的三种解法,分别为:暴力求解法、动态规划法和中心扩展算法。实际上,还有一种效率更高的Manacher算法,其时间复杂度为O(n)。大家可以自行去了解,这里就不再赘述了。
💡 如果想阅读最新的文章,或者有技术问题需要交流和沟通,可搜索并关注微信公众号“希望睿智”。