Rust面试宝典第12题:最长回文子串

107 阅读6分钟

题目

回文是一个正读和反读都相同的字符串,比如:"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)。大家可以自行去了解,这里就不再赘述了。

💡 如果想阅读最新的文章,或者有技术问题需要交流和沟通,可搜索并关注微信公众号“希望睿智”。