【算法】最长回文子串

文章目录

  • 题目
  • 方法一:中心扩展法
    • 解题思路
    • 代码实现
    • 复杂度
  • 方法二:动态规划
    • 解题思路
    • 代码实现
    • 复杂度
  • 方法三:Manacher算法
    • 解题思路
    • 代码实现
    • 复杂度
  • 总结


题目

给你一个字符串 s,找到 s 中最长的回文子串。如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。

示例 1

输入:s = “babad”
输出:“bab”
解释:“aba” 同样是符合题意的答案。

示例 2

输入:s = “cbbd”
输出:“bb”


方法一:中心扩展法

解题思路

  • 遍历字符串s中的每个字符,以每个字符为中心向两侧扩展,找到以当前字符为中心的最长回文子串。
  • 同时,考虑回文串长度为偶数的情况,以当前字符和下一个字符为中心向两侧扩展,找到以这两个字符为中心的最长回文子串。
  • 在扩展过程中,记录最长回文子串的起始和结束位置。
  • 最终返回最长回文子串,可以通过substring方法截取子串。

代码实现

public class Solution {
    public String longestPalindrome(String s) {
        if (s == null || s.length() == 0) {
            return "";
        }
        int start = 0;
        int 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);
    }

    // 中心扩展法,返回以left和right为中心的回文子串的长度
    private int expandAroundCenter(String s, int left, int right) {
        while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
            left--;
            right++;
        }
        // 返回回文子串的长度
        return right - left - 1;
    }
}

复杂度

时间复杂度:O(n^2),其中n为字符串s的长度
时间复杂度:O(1),因为只使用了常数个变量


方法二:动态规划

解题思路

  1. 使用动态规划来解决问题,定义一个二维布尔型数组 dp,其中 dp[i][j] 表示字符串 s 中从索引 i 到 j 的子串是否为回文串。
  2. 初始化 dp 数组,当 i 和 j 相等时,表示单个字符是回文串,即 dp[i][i] = true;当s.charAt(i) == s.charAt(j)j - i <= 2 时,表示长度为 2 的子串是回文串,即 dp[i][j] = true
  3. 遍历 dp 数组,从长度为 3 的子串开始,依次更新 dp 数组。对于 dp[i][j],如果s.charAt(i) == s.charAt(j)dp[i+1][j-1] 为 true,则表示从索引 i 到 j 的子串是回文串,即 dp[i][j] = true
  4. 在遍历过程中记录最长的回文子串,返回最长回文子串。

代码实现

public String longestPalindrome(String s) {
    int len = s.length();
    boolean[][] dp = new boolean[len][len];
    String longestPalindrome = "";
    int maxLength = 0;

    // 初始化长度为1的回文子串
    for (int i = 0; i < len; i++) {
        dp[i][i] = true;
        maxLength = 1;
        longestPalindrome = s.substring(i, i + 1);
    }

    // 初始化长度为2的回文子串
    for (int i = 0; i < len - 1; i++) {
        if (s.charAt(i) == s.charAt(i + 1)) {
            dp[i][i + 1] = true;
            maxLength = 2;
            longestPalindrome = s.substring(i, i + 2);
        }
    }

    // 动态规划遍历和更新
    for (int l = 3; l <= len; l++) {
        for (int i = 0; i <= len - l; i++) {
            int j = i + l - 1;
            if (s.charAt(i) == s.charAt(j) && dp[i + 1][j - 1]) {
                dp[i][j] = true;
                if (l > maxLength) {
                    maxLength = l;
                    longestPalindrome = s.substring(i, j + 1);
                }
            }
        }
    }

    return longestPalindrome;
}

复杂度

时间复杂度:O(n^2),其中 n 是字符串 s 的长度,因为要遍历二维数组 dp
时间复杂度:O(n^2),需要使用二维数组 dp 来记录状态信息


方法三:Manacher算法

Manacher 算法是一种高效的解决回文子串问题的算法,其时间复杂度为 O(n),其中 n 是字符串的长度。以下是 Manacher 算法的解题思路、代码实现以及复杂度分析

解题思路

  1. Manacher 算法的核心思想是利用回文串的对称性,避免重复的比较,从而实现线性时间复杂度。
  2. 首先,将原始字符串进行预处理,将每个字符之间插入一个特殊的字符(如 “#”),以处理偶数长度的回文串。
  3. 定义一个辅助数组 P,用来记录以每个字符为中心的回文串的半径长度(包括中心字符)。
  4. 初始化辅助数组 P,将每个元素初始化为 1,因为每个字符本身都是一个回文串。
  5. 从左到右遍历字符串,利用中心扩展法找到以每个字符为中心的回文串的半径长度,并更新辅助数组 P。
  6. 在遍历过程中记录当前找到的最长回文串,并记录其结束位置和长度,以便最后返回结果。

代码实现

public class Solution {
    public String longestPalindrome(String s) {
        // 预处理字符串,在字符之间插入特殊字符 "#"
        String t = preprocess(s);

        // Manacher 算法核心部分
        int n = t.length();
        int[] dp = new int[n]; // 动态规划数组,记录每个字符作为回文中心时的回文半径长度
        int center = 0; // 当前回文串的中心
        int right = 0; // 当前回文串的右边界

        for (int i = 0; i < n; i++) {
            // 如果当前字符 i 在当前回文串的右边界内,可以利用回文对称性直接计算 dp[i]
            if (i < right) {
                int j = 2 * center - i; // i 关于 center 的对称点 j
                dp[i] = Math.min(right - i, dp[j]);
            }

            // 尝试将当前字符 i 向两侧扩展,更新 dp[i]
            int left = i - (dp[i] + 1); // 当前回文串的左边界
            int rightBound = i + (dp[i] + 1); // 当前回文串的右边界
            while (left >= 0 && rightBound < n && t.charAt(left) == t.charAt(rightBound)) {
                dp[i]++;
                left--;
                rightBound++;
            }

            // 更新当前回文串的中心和右边界
            if (i + dp[i] > right) {
                center = i;
                right = i + dp[i];
            }
        }

        // 找出最长回文子串
        int maxLen = 0; // 最长回文子串的长度
        int centerIndex = 0; // 最长回文子串的中心索引
        for (int i = 0; i < n; i++) {
            if (dp[i] > maxLen) {
                maxLen = dp[i];
                centerIndex = i;
            }
        }

        // 提取最长回文子串
        StringBuilder result = new StringBuilder();
        for (int i = centerIndex - maxLen; i <= centerIndex + maxLen; i++) {
            if (t.charAt(i) != '#') {
                result.append(t.charAt(i));
            }
        }

        // 去除预处理时插入的特殊字符 "#"
        return result.toString();
    }

    // 预处理字符串,在字符之间插入特殊字符 "#"
    private String preprocess(String s) {
        StringBuilder sb = new StringBuilder();
        for (char c : s.toCharArray()) {
            sb.append("#");
            sb.append(c);
        }
        sb.append("#");
        return sb.toString();
    }
}


复杂度

时间复杂度: O(n),其中 n 是输入字符串的长度
空间复杂度: O(n),因为需要使用额外的动态规划数组来保存每个字符


总结

  1. 中心扩展法:遍历字符串的每个字符,将其作为回文串的中心,向两侧扩展,判断扩展的字符是否相同,记录扩展的长度,最后找到最长回文子串。时间复杂度为O(n^2),空间复杂度为O(1)。

  2. 动态规划法:利用回文串的动态规划性质,通过动态规划数组记录每个字符作为回文中心时的回文半径长度,避免重复计算,从而找到最长回文子串。时间复杂度为O(n^2),空间复杂度为O(n)。

  3. Manacher算法:利用回文串的对称性,通过动态规划的方式避免重复计算,从而在线性时间内找到最长回文子串。时间复杂度为O(n),空间复杂度为O(n)。

综上所述,中心扩展法是简单但效率较低的方法,动态规划法通过优化中心扩展法减少了重复计算,而Manacher算法则是最优的方法,具有线性时间复杂度。在实际应用中,可以根据具体情况选择合适的方法来解决寻找最长回文子串的问题

你可能感兴趣的:(随便写写,算法,leetcode,数据结构,java)