最长回文子串-leetCode-005

最长回文子串-leetCode-005_第1张图片

针对这个问题,共有四种解法,分别是暴力法,中心拓展法,动态规划,Manacher 算法

解法一:暴力法

思路:枚举所有可能的子串,然后判断每个子串是否是回文串,最后找出最长的回文子串。

class Solution {
    public String longestPalindrome(String s) {
        int n = s.length();
        if (n == 0) {
            return "";
        }
        String maxPalindrome = s.substring(0, 1); // 初始化为第一个字符
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                if (isPalindrome(s, i, j)) {
                    if (j - i + 1 > maxPalindrome.length()) {
                        maxPalindrome = s.substring(i, j + 1);
                    }
                }
            }
        }
        return maxPalindrome;
    }

    private boolean isPalindrome(String s, int left, int right) {
        while (left < right) {
            if (s.charAt(left) != s.charAt(right)) {
                return false;
            }
            left++;
            right--;
        }
        return true;
    }
}
时间复杂度:
  • 暴力法的时间复杂度是 O(n^3),其中 n 是字符串的长度。这是因为枚举所有子串需要 O(n^2) 的时间,而判断每个子串是否是回文串需要 O(n) 的时间。

  • 这种方法在字符串较长时效率较低,不适用于大规模数据。

解法二:中心拓展法

思路:

  1. 中心扩展法:回文子串的中心可能是一个字符(奇数长度)或两个相同字符(偶数长度)。对于字符串中的每个字符,都检查以它为中心的回文子串长度。

  2. 初始化变量startend 用于记录最长回文子串的起始和结束位置。

  3. 循环遍历:对每个字符进行中心扩展,分别处理奇数和偶数长度的回文子串。

  4. 更新最大回文子串:每次扩展后,比较当前回文子串的长度,更新 startend

  5. 返回结果:根据 startend 提取最长回文子串。

class Solution {
    public String longestPalindrome(String s) {
        // 如果输入的字符串为空或者长度小于1,直接返回空字符串(因为无法构成回文)
        if (s == null || s.length() < 1) {
            return "";
        }
        // 初始化最长回文子串的起始和结束位置(初始时假设第一个字符就是最长回文子串)
        int start = 0, end = 0;
        // 遍历字符串中的每个字符,以每个字符为中心进行扩展查找回文子串
        for (int i = 0; i < s.length(); i++) {
            // 寻找以 i 为中心的奇数长度回文子串的长度
            int len1 = expandAroundCenter(s, i, i);
            // 寻找以 i 和 i+1 为中心的偶数长度回文子串的长度
            int len2 = expandAroundCenter(s, i, i + 1);
            // 取两种情况下的最大回文子串长度
            int len = Math.max(len1, len2);
            // 如果当前找到的回文子串长度大于之前记录的最大长度,则更新起始和结束位置
            if (len > end - start) {
                // 根据回文子串的长度 len 和中心位置 i,计算新的起始位置
                start = i - (len - 1) / 2;
                // 计算新的结束位置
                end = i + len / 2;
            }
        }
        // 根据起始和结束位置,截取最长回文子串并返回
        return s.substring(start, end + 1);
    }

    // 定义一个辅助方法,用于从中心向两边扩展查找回文子串
    private int expandAroundCenter(String s, int left, int right) {
        // 当左右指针未越界且左右字符相等时,继续向外扩展
        while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
            left--;  // 左指针左移
            right++;  // 右指针右移
        }
        // 返回回文子串的长度(结束位置减去开始位置再减1)
        return right - left - 1;
    }
}

时间复杂度: O(n^2)其中 n 是字符串的长度,空间复杂度是 O(1)

它适用于题目中给出的约束条件(字符串长度最多为1000)。

解法三:动态规划

思路解释:

  1. 状态定义

    • 使用一个二维数组 dp[i][j],表示子串 s[i..j] 是否是回文串。

    • dp[i][j] = true 表示子串 s[i..j] 是回文串,false 表示不是。

  2. 状态转移方程

    • 如果 s[i] != s[j],则 dp[i][j] = false(子串两端字符不相等,不是回文串)。

    • 如果 s[i] == s[j]

      • 当子串长度为 2 时(即 j = i + 1),dp[i][j] = true

      • 当子串长度大于 2 时,dp[i][j] = dp[i + 1][j - 1](取决于内部子串是否是回文串)。

  3. 初始化

    • 所有长度为 1 的子串都是回文串,即 dp[i][i] = true

  4. 遍历顺序

    • 按子串长度从 2 到 n 逐步增加,枚举所有可能的子串起始位置 i,计算对应的结束位置 j

  5. 记录最长回文子串

    • 在遍历过程中,记录最长回文子串的长度 maxLen 和起始位置 begin,最后通过 substring 方法提取结果。

class Solution {
    public String longestPalindrome(String s) {
        int n = s.length();
        if (n == 0) {
            return "";
        }
        // 初始化一个二维数组 dp,dp[i][j] 表示子串 s[i..j] 是否是回文串
        boolean[][] dp = new boolean[n][n];
        int maxLen = 1; // 最长回文子串的长度,初始为1(单个字符)
        int begin = 0;  // 最长回文子串的起始位置

        // 所有长度为1的子串都是回文串
        for (int i = 0; i < n; i++) {
            dp[i][i] = true;
        }

        // 枚举子串长度,从2到n
        for (int L = 2; L <= n; L++) {
            // 枚举子串的起始位置 i
            for (int i = 0; i < n; i++) {
                // 子串的结束位置 j = i + L - 1
                int j = i + L - 1;
                if (j >= n) {
                    break; // 如果结束位置超出字符串范围,跳出循环
                }

                // 如果子串两端字符不相等,则不是回文串
                if (s.charAt(i) != s.charAt(j)) {
                    dp[i][j] = false;
                } else {
                    // 如果子串两端字符相等,则需要判断内部子串是否是回文串
                    // 当子串长度为2时,内部没有字符,直接是回文串
                    if (L == 2) {
                        dp[i][j] = true;
                    } else {
                        // 内部子串是否是回文串取决于 dp[i+1][j-1]
                        dp[i][j] = dp[i + 1][j - 1];
                    }
                }

                // 如果当前子串是回文串且长度大于 maxLen,更新 maxLen 和 begin
                if (dp[i][j] && L > maxLen) {
                    maxLen = L;
                    begin = i;
                }
            }
        }
        // 返回最长回文子串
        return s.substring(begin, begin + maxLen);
    }
}

时间复杂度: O(n^2)

空间复杂度是 O(n^2)

其中 n 是字符串的长度。它适用于题目中给出的约束条件(字符串长度最多为1000)。 

 解法四:Manacher 算法

核心思想:Manacher 算法通过巧妙地将所有可能的回文子串统一为奇数长度的形式,利用对称性和已知回文信息来避免重复计算,从而实现高效的最长回文子串查找。

class Solution {
    public String longestPalindrome(String s) {
        // 如果输入字符串为空或者长度为0,直接返回空字符串
        if (s == null || s.length() == 0) {
            return "";
        }
        // 在字符串中插入特殊字符 '#',将所有可能的回文子串统一为奇数长度的形式
        String transformed = "#" + String.join("#", s.split("")) + "#";
        int n = transformed.length(); // 获取预处理后字符串的长度
        int[] p = new int[n]; // p[i] 表示以 i 为中心的回文半径
        int center = 0, right = 0; // center 是当前回文右边界的中心,right 是当前回文右边界
        int maxLen = 0, maxCenter = 0; // 记录最长回文半径和其中心位置

        // 遍历预处理后的字符串
        for (int i = 0; i < n; i++) {
            // 计算当前位置 i 的镜像位置 mirror
            int mirror = 2 * center - i;

            // 如果当前位置 i 在当前回文右边界内,则利用镜像位置 mirror 的信息初始化 p[i]
            if (i < right) {
                p[i] = Math.min(right - i, p[mirror]);
            }

            // 尝试扩展回文边界,直到无法扩展为止
            while (i - p[i] - 1 >= 0 && i + p[i] + 1 < n && transformed.charAt(i - p[i] - 1) == transformed.charAt(i + p[i] + 1)) {
                p[i]++; // 扩展成功,回文半径增加
            }

            // 如果当前位置 i 的回文右边界超过了之前的 right,更新 center 和 right
            if (i + p[i] > right) {
                center = i;
                right = i + p[i];
            }

            // 更新最长回文半径及其对应的中心位置
            if (p[i] > maxLen) {
                maxLen = p[i];
                maxCenter = i;
            }
        }

        // 计算最长回文子串在原始字符串中的起始位置和长度
        int start = (maxCenter - maxLen) / 2;
        int length = maxLen;

        // 提取并返回最长回文子串
        return s.substring(start, start + length);
    }
}

注释说明:

  1. 字符串预处理

    • 在原始字符串中插入特殊字符 #,使得所有可能的回文子串都转换为奇数长度的形式。例如,字符串 "abc" 转换为 "#a#b#c#"

  2. 变量初始化

    • p[i] 表示以位置 i 为中心的回文半径。

    • centerright 分别表示当前回文右边界的中心位置和右边界位置。

    • maxLenmaxCenter 用于记录最长回文半径及其对应的中心位置。

  3. 主循环

    • 遍历预处理后的字符串,计算每个位置 i 的回文半径 p[i]

    • 利用镜像位置 mirror 的信息初始化 p[i],以避免重复计算。

    • 尝试扩展回文边界,更新 p[i] 的值。

    • 如果当前回文右边界超过之前的 right,更新 centerright

  4. 结果提取

    • 根据最长回文半径 maxLen 和中心位置 maxCenter,计算原始字符串中的起始位置和子串长度。

    • 使用 substring 方法提取并返回最长回文子串。

总结:

Manacher 算法通过巧妙的预处理和镜像对称的性质,将最长回文子串问题的时间复杂度降低到 O(n),非常高效。虽然实现上稍显复杂,但它是最长回文子串问题的最优解之一

你可能感兴趣的:(最长回文子串-leetCode-005)