算法:leetcode_5_最长回文子串

目录

1. 题目介绍

2. 几种方法:

2.1 方法一:动态规划

2.1.1 思路

2.1.1.1 设置重要变量

2.1.1.2 确定返回值

2.1.1.3 二维dp表

2.1.1.4 规律

2.1.2 代码

Java:

 C++:

JS:

2.1.3 提交通过

 2.2 方法二:中心扩散法(双指针)

2.2.1 思路

2.2.2 代码

2.2.2.1 错误示范

2.2.2.2 正确示范

2.2.2.3 多逼逼一句

C++:

JS:


1. 题目介绍

给你一个字符串s, 找到s中最长的回文子串(子字符串 是字符串中连续的 非空 字符序列。)。

示例 :

输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
  • 1 <= s.length <= 1000
  • s 仅由数字和英文字母组成

2. 几种方法:

2.1 方法一:动态规划

2.1.1 思路

首先可以通过暴力(动态规划dp),将每一段子字符串都判断下是否为回文,同时更新最大长度。

2.1.1.1 设置重要变量

可以设置变量1.begin作为结果字符串的开始索引;2.maxLen作为最大回文子串长度。

2.1.1.2 确定返回值

最后返回的最长的回文子字符串为s.substring(begin, begin + maxLen);

例如最长的回文子字符串"aba":begin为1、maxLen为3,

s.subString(1,4):左闭右开--->索引为1到3的位置,即"aba"。

2.1.1.3 二维dp表

i和j可以构成一个len*len的2维dp数组:横轴代表i,纵轴代表j,

坐标(i,j)代表从索引为i到索引为j的子字符串,例如(1,3)代表"aba"。

其中i一定是 <= j的, 即我们只需要关注对角线(橙色)上方(蓝色)区域。

dp数组内的值为true或者false:为true则表示对应坐标范围的子字符串为回文字符串。

dp映射表
01234
010100
101010
200100
300010
400001
2.1.1.4 规律

接下来我们寻找一下规律:

1.由于单个字符构成的字符串一定是回文的,因此对角线值为true,用1表示。

2.当两个字符相同的时候:

        ①如果这个子字符串长度<=3,如"bab",它肯定是回文;

        ②如果子字符串长度>3,如"babab",最左边最右边的"b"都相同,那么这个字符串是否回文会取决于它的中间的字符串"aba"是否为回文,即dp[i+1][j-1]是否为true,对应的dp表格内,它将会依赖于左下方的位置。

3.如果确定是回文,那么我们需要判断,此时的子字符串的长度是否是更长的,如果是则更新最大长度开始索引

4.遍历顺序:由于dp[i][j]是否为true将会依赖于它的左下角dp[i+1][j-1],因此遍历顺序需要从下到上,每一行从左到右进行遍历。

接下来展示完整代码并且附上详细注释

2.1.2 代码

本题用Java写的,其他语言为根据原Java代码用GPT生成,仅供参考。

Java:
public class Leetcode_5_LongestPalindromicSubstring {
    public static String longestPalindrome(String s) {
        int len = s.length();
        int begin = 0; // 结果字符串的开始索引
        int maxLen = 1; // 最大长度 字符串最短为1 因此初始化为1
        boolean[][] dp = new boolean[len][len];
        // 由于dp[i][j]依赖于它的左下角dp[i+1][j-1], 因此遍历顺序为从下到上, 每一行从左到右边
        // 为什么i为len-2, 从倒数第2行开始; j从i+1开始
        // 由dp表所示对角线(橙色部分)可以提前确定为true, 因此无需多余判断
        for (int i = len-2; i>=0; i--) {
            for (int j = i+1; j < len; j++) {
                char a = s.charAt(i);
                char b = s.charAt(j);
                // 两个字符相同--->同时(字符串长度<= 3, 可以直接写成j - i <= 2) 或者两个字符中间夹着的子字符串已经是回文了
                if (a == b && (j - i  + 1 <= 3 || dp[i + 1][j - 1])) {
                    // 标识这个子字符串为回文字符串
                    dp[i][j] = true;
                    // 如果子字符串长度大于了最大长度
                    if (j - i + 1 > maxLen) {
                        // 更新最大长度以及开始索引
                        maxLen = j - i + 1;
                        begin = i;
                    }
                }
            }
        }
        // 返回指定区间的子字符串 substring函数为左闭右开 最终begin为1 maxLen为3 substring(1,1+3)为"aba"
        return s.substring(begin, begin + maxLen);
    }
}
 C++:
#include <string>
#include <vector>
using namespace std;

class Solution {
public:
    static string longestPalindrome(string s) {
        int len = s.size();
        if (len < 2) return s;

        int begin = 0;          // 结果字符串的开始索引
        int maxLen = 1;         // 最大长度,字符串最短为1,因此初始化为1
        vector<vector<bool>> dp(len, vector<bool>(len, false));

        // 由于 dp[i][j] 依赖于左下角 dp[i+1][j-1],因此遍历顺序为从下到上,每一行从左到右
        // 为什么 i 为 len-2,从倒数第2行开始;j 从 i+1 开始
        // 由 dp 表所示对角线(橙色部分)可以提前确定为 true,因此无需多余判断
        for (int i = len - 2; i >= 0; --i) {
            dp[i][i] = true;    // 单个字符一定是回文
            for (int j = i + 1; j < len; ++j) {
                char a = s[i];
                char b = s[j];

                // 两个字符相同 ---> 同时(字符串长度 <= 3,可以直接写成 j - i <= 2)
                // 或者两个字符中间夹着的子字符串已经是回文了
                if (a == b && (j - i + 1 <= 3 || dp[i + 1][j - 1])) {
                    dp[i][j] = true;                     // 标识这个子字符串为回文字符串
                    if (j - i + 1 > maxLen) {            // 如果子字符串长度大于了最大长度
                        maxLen = j - i + 1;              // 更新最大长度以及开始索引
                        begin = i;
                    }
                }
            }
        }

        // 返回指定区间的子字符串
        // substr 函数为左闭区间 [begin, begin + maxLen)
        return s.substr(begin, maxLen);
    }
};
JS:
/**
 * @param {string} s
 * @return {string}
 */
var longestPalindrome = function (s) {
    const len = s.length;
    if (len < 2) return s;

    let begin = 0;          // 结果字符串的开始索引
    let maxLen = 1;         // 最大长度,字符串最短为1,因此初始化为1
    const dp = Array.from({ length: len }, () => Array(len).fill(false));

    // 由于 dp[i][j] 依赖于左下角 dp[i+1][j-1],因此遍历顺序为从下到上,每一行从左到右
    // 为什么 i 为 len-2,从倒数第2行开始;j 从 i+1 开始
    // 由 dp 表所示对角线(橙色部分)可以提前确定为 true,因此无需多余判断
    for (let i = len - 2; i >= 0; --i) {
        dp[i][i] = true;    // 单个字符一定是回文
        for (let j = i + 1; j < len; ++j) {
            const a = s[i];
            const b = s[j];

            // 两个字符相同 ---> 同时(字符串长度 <= 3,可以直接写成 j - i <= 2)
            // 或者两个字符中间夹着的子字符串已经是回文了
            if (a === b && (j - i + 1 <= 3 || dp[i + 1][j - 1])) {
                dp[i][j] = true;                     // 标识这个子字符串为回文字符串
                if (j - i + 1 > maxLen) {            // 如果子字符串长度大于了最大长度
                    maxLen = j - i + 1;              // 更新最大长度以及开始索引
                    begin = i;
                }
            }
        }
    }

    // 返回指定区间的子字符串
    // slice 为左闭右开区间 [begin, begin + maxLen)
    return s.slice(begin, begin + maxLen);
};

2.1.3 提交通过

可以看到时间消耗很大,因为这是通过dp暴力求解,后续本文将会继续补充更快的解法。

先睡觉觉咯。

--------------------------------------------------------------------------------------------------------------------------------

 2.2 方法二:中心扩散法(双指针)

2.2.1 思路

方法一动态规划中,判断一个子串是否为回文子串,除了左右两边字符相同,它将会依赖于两个字符夹着的中间的子串。

接下来我们换一种思路,从中间往左右两边扩散。

对于字符串"babad", 长度为5,

我们可以这样思考,对于中间的字符'b', 它肯定是回文的,

接下来我们在判断它的左右两边, 都是'a', 那么'aba'也是回文,

接下来继续扩散, ‘b’ != 'd', 因此在这一轮遍历中, 得到的最长回文子串为'aba'。

设置变量和返回值都与方法一相同, 就不赘述了。

2.2.2 代码

2.2.2.1 错误示范
public static String longestPalindrome_CentralDiffusionMethod(String s) {
        int n = s.length();
        int maxBegin = 0;
        int maxLen = 0;
        for (int i = 0; i < n; i++) {
            // 左指针, 从 i 开始
            int l = i;
            // 右指针,也从 i 开始
            int r = i;
            // 当左右指针在字符串范围内且对应字符相等时,继续向两边扩展
            while (l >= 0 && r < n && s.charAt(l) == s.charAt(r)) {
                l--;
                r++;
            }
            // 因为上面的 while 循环结束时,l 和 r 已经越界了,所以要回退一步
            l++;
            r--;
            // 比较当前找到的回文子串长度和之前记录的最长回文子串长度
            if (maxLen <= r - l + 1) {
                maxLen = r - l + 1;
                maxBegin = l;
            }
        }
        return s.substring(maxBegin, maxBegin + maxLen);
    }

接下来 我们来手动模拟一下:

1. i=0开始遍历, 开始while循环, l和r都没有越界,并且两边字符相等, 此时两边收缩。

   此时 l和r越界了, while循环结束, 此时l和r需要回溯一下, 因为还需要记录这一轮的有效子回文 串。

2.i=1开始遍历, 话不多说, while判断成功,l和r往两边扩散-> l = 0, r = 2;

    然后while循环又判断成功-> l = -1, r = 3, while结束, l和r又回溯, 记录这一轮的回文子串"bab"。

 后续类似, 让我们运行一下逝世

唉怎么不对啊?

仔细琢磨, 刚刚给出的代码如果按照"cbbd"的话, 它只能判断出单个字符的两边是否相等。

如第一个'b'的两边'c'和'b'不同, 第二个'b'的两边'b'和'd'不同。

哦哦哦哦哦!

回文串的中心可能是一个字符(如 "cbd" 的中心是 'b'),也可能是两个字符(如 "cbbd" 的中心是 "bb")。

那么我们在写代码的时候, 两个字符串的也要去处理一下:

我们多整一个参数j, 用于控制中心的字符串是单个还是两个

2.2.2.2 正确示范
public static String longestPalindrome_CentralDiffusionMethod(String s) {
        int n = s.length();
        int maxBegin = 0;
        int maxLen = 0;
        for (int i = 0; i < n; i++) {
            // j = 0 表示中心节点只有 i,即回文串中心是单个字符;
            // j = 1 表示中心节点有两个 i 和 i + 1,即回文串中心是两个字符
            for (int j = 0; j <= 1; j++) {
                // 左指针,初始指向中心位置
                int l = i;
                // 右指针,根据 j 的值确定初始位置
                int r = i + j;
                // 当左右指针在字符串范围内且对应字符相等时,继续向两边扩展
                while (l >= 0 && r < n && s.charAt(l) == s.charAt(r)) {
                    l--;
                    r++;
                }
                // 回溯到回文字符串的上一步起始位置
                // 因为上面的 while 循环结束时,l 和 r 已经越界了,所以要回退一步
                l++;
                r--;
                // 比较当前找到的回文子串长度和之前记录的最长回文子串长度
                // 这里如果不取等号, 那么返回的子串将会是对于这个长度, 第一次记录的子串
                if (maxLen <= r - l + 1) {
                    maxLen = r - l + 1;
                    maxBegin = l;
                }
            }
        }
        return s.substring(maxBegin, maxBegin + maxLen);
    }

 哎哎哎哎哎, 过啦!

2.2.2.3 多逼逼一句

对于示例 1:

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

对于最长的回文子串, 可能有多个, 我们当前代码打印出来的结果是"aba",

那么如果同样长度的子串, 我们需要保留最开始记录的子串, 怎么改?

很简单, 在判断是否更新的时候, 把等号去掉就行

取等号的话, 每次出现同样的最长长度,都要更新一遍了。 

// 比较当前找到的回文子串长度和之前记录的最长回文子串长度
// 这里如果不取等号, 那么返回的子串将会是对于这个长度, 第一次记录的子串
if (maxLen <= r - l + 1) {
    maxLen = r - l + 1;
    maxBegin = l;
}

本题用Java写的,其他语言为根据原Java代码用GPT生成,仅供参考。

其他语言代码:

C++:
#include <string>

std::string longestPalindrome_CentralDiffusionMethod_true(const std::string& s) {
    int n = s.length();
    int maxBegin = 0;
    int maxLen = 0;
    for (int i = 0; i < n; i++) {
        // j = 0 表示中心节点只有 i,即回文串中心是单个字符;
        // j = 1 表示中心节点有两个 i 和 i + 1,即回文串中心是两个字符
        for (int j = 0; j <= 1; j++) {
            // 左指针,初始指向中心位置
            int l = i;
            // 右指针,根据 j 的值确定初始位置
            int r = i + j;
            // 当左右指针在字符串范围内且对应字符相等时,继续向两边扩展
            while (l >= 0 && r < n && s[l] == s[r]) {
                l--;
                r++;
            }
            // 回溯到回文字符串的上一步起始位置
            // 因为上面的 while 循环结束时,l 和 r 已经越界了,所以要回退一步
            l++;
            r--;
            // 比较当前找到的回文子串长度和之前记录的最长回文子串长度
            // 这里如果不取等号, 那么返回的子串将会是对于这个长度, 第一次记录的子串
            if (maxLen <= r - l + 1) {
                maxLen = r - l + 1;
                maxBegin = l;
            }
        }
    }
    return s.substr(maxBegin, maxLen);
}
JS:
function longestPalindrome_CentralDiffusionMethod_true(s) {
    let n = s.length;
    let maxBegin = 0;
    let maxLen = 0;
    for (let i = 0; i < n; i++) {
        // j = 0 表示中心节点只有 i,即回文串中心是单个字符;
        // j = 1 表示中心节点有两个 i 和 i + 1,即回文串中心是两个字符
        for (let j = 0; j <= 1; j++) {
            // 左指针,初始指向中心位置
            let l = i;
            // 右指针,根据 j 的值确定初始位置
            let r = i + j;
            // 当左右指针在字符串范围内且对应字符相等时,继续向两边扩展
            while (l >= 0 && r < n && s[l] === s[r]) {
                l--;
                r++;
            }
            // 回溯到回文字符串的上一步起始位置
            // 因为上面的 while 循环结束时,l 和 r 已经越界了,所以要回退一步
            l++;
            r--;
            // 比较当前找到的回文子串长度和之前记录的最长回文子串长度
            // 这里如果不取等号, 那么返回的子串将会是对于这个长度, 第一次记录的子串
            if (maxLen <= r - l + 1) {
                maxLen = r - l + 1;
                maxBegin = l;
            }
        }
    }
    return s.substring(maxBegin, maxBegin + maxLen);
}

宝宝们觉得我的屎山代码有用的话,可以点个赞么么哒!

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值