【经典算法】LeetCode 5: 最长回文子串(Java/C/Python3实现含注释说明,Medium)

简介: 【经典算法】LeetCode 5: 最长回文子串(Java/C/Python3实现含注释说明,Medium)

题目描述

给定一个字符串 `s`,找到其中最长的回文子串。可以假设 `s` 的最大长度为 1000。
示例1:
输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
示例2:
输入: "cbbd"
输出: "bb"

原题:LeetCode 5

思路及实现

方式一:动态规划

思路

Dynamic Programming(DP) 动态规划是一种将问题分解成子问题并分别计算的优化技术。对于回文子串,我们可以使用动态规划来解决。

对于一个子串而言,如果它是回文串,并且长度大于 2,那么将它首尾的两个字母去除之后,它仍然是个回文串。例如对于字符串 “ababa”,如果我们已经知道 “bab” 是回文串,那么 “ababa” 一定是回文串,这是因为它的首尾两个字母都是 “a”。

通过定义一个二维数组 dp[i][j],表示 s 的第 i 个字符到第 j 个字符组成的子串是否为回文字符串。当 i == j 时,表示一个字符,是回文字符串,当 i + 1 == j ,则优先考虑两个字符是否相等来将问题规模缩小,同时考虑前一个子串是否为回文字符串。对于 i + 1 < j 的情况,可以通过判断 s[i] 和 s[j] 是否相等,并判断定义的 dp[i+1][j-1] 是否为回文字符串。如果是回文字符串,则 dp[i][j] 也是回文字符串。

代码实现

Java版本
class Solution {
    public String longestPalindrome(String s) {
        int n = s.length(); // 计算字符串的长度
        boolean[][] dp = new boolean[n][n]; // 创建一个二维数组用于记录子串是否为回文串
        String ans = ""; // 初始化最长回文子串为空字符串
        // 遍历所有长度的子串
        for (int len = 1; len <= n; len++) {
            // 遍历子串的起始位置
            for (int i = 0; i + len - 1 < n; i++) {
                int j = i + len - 1; // 子串的结束位置
                if (len == 1) {
                    dp[i][j] = true; // 单个字符必定是回文串
                } else if (len == 2) {
                    dp[i][j] = (s.charAt(i) == s.charAt(j)); // 只有两个字符时判断是否相等
                } else {
                    dp[i][j] = (s.charAt(i) == s.charAt(j) && dp[i + 1][j - 1]); // 多于两个字符时判断首尾字符是否相等
                }
                if (dp[i][j] && len > ans.length()) { // 如果当前子串是回文串并且长度更长
                    ans = s.substring(i, j + 1); // 更新结果为当前子串
                }
            }
        }
        return ans;
    }
}

说明:

longestPalindrome 方法用于寻找给定字符串中的最长回文子串。使用动态规划的思想,创建一个二维数组 dp,其中 dp[i][j] 表示从索引 i 到索引 j 的子串是否为回文串。

通过遍历所有长度的子串,以及遍历子串的起始位置,判断子串是否为回文串,并根据回文串的长度更新最长回文子串 ans。

当子串的长度为 1 时,即一个字符,必定是回文串。

当子串的长度为 2 时,判断首尾两个字符是否相等。

当子串的长度大于 2 时,判断首尾两个字符是否相等,并根据 dp[i+1][j-1] 的结果判断子串是否为回文串。

如果当前子串是回文串,并且长度比之前记录的最长回文子串长度更长,则更新最长回文子串 ans。

最后,返回最长回文子串。

C语言版本
char* longestPalindrome(char* s) {
    int n = strlen(s); // 计算字符串的长度
    bool dp[n][n]; // 创建一个二维数组用于记录子串是否为回文串
    memset(dp, false, sizeof(dp)); // 初始化dp数组为false
    char* ans = ""; // 初始化最长回文子串为空字符串
    // 遍历所有长度的子串
    for (int len = 1; len <= n; len++) {
        // 遍历子串的起始位置
        for (int i = 0; i + len - 1 < n; i++) {
            int j = i + len - 1; // 子串的结束位置
            if (len == 1) {
                dp[i][j] = true; // 单个字符必定是回文串
            } else if (len == 2) {
                dp[i][j] = (s[i] == s[j]); // 只有两个字符时判断是否相等
            } else {
                dp[i][j] = (s[i] == s[j] && dp[i + 1][j - 1]); // 多于两个字符时判断首尾字符是否相等
            }
            if (dp[i][j] && len > strlen(ans)) { // 如果当前子串是回文串并且长度更长
                char* sub = (char*)malloc((len + 1) * sizeof(char)); // 分配内存保存当前子串
                strncpy(sub, s + i, len); // 复制当前子串到内存中
                sub[len] = '\0'; // 添加字符串结束标志
                ans = sub; // 更新结果为当前子串
            }
        }
    }
    return ans;
}

说明: (同上)

longestPalindrome 函数用于寻找给定字符串中的最长回文子串。使用动态规划的思想,创建一个二维数组 dp,用于记录子串是否为回文串。

通过遍历所有长度的子串,以及遍历子串的起始位置,判断子串是否为回文串,并根据回文串的长度更新最长回文子串 ans。

子串的判断分三种情况:

当子串的长度为 1 时,即一个字符,必定是回文串。

当子串的长度为 2 时,判断首尾两个字符是否相等。

当子串的长度大于 2 时,判断首尾两个字符是否相等,并根据 dp[i+1][j-1] 的结果判断子串是否为回文串。

如果当前子串是回文串,并且长度比之前记录的最长回文子串长度更长,则更新最长回文子串 ans。

最后,返回最长回文子串。

Python3版本
class Solution:
    def longestPalindrome(self, s: str) -> str:
        n = len(s)
        dp = [[False] * n for _ in range(n)]
        ans = ""
        # 遍历所有长度的子串
        for length in range(1, n + 1):
            # 遍历子串的起始位置
            for i in range(n - length + 1):
                j = i + length - 1  # 子串的结束位置
                if length == 1:
                    dp[i][j] = True  # 单个字符必定是回文串
                elif length == 2:
                    dp[i][j] = (s[i] == s[j])  # 只有两个字符时判断是否相等
                else:
                    dp[i][j] = (s[i] == s[j] and dp[i + 1][j - 1])  # 多于两个字符时判断首尾字符是否相等
                if dp[i][j] and length > len(ans):  # 如果当前子串是回文串并且长度更长
                    ans = s[i:j + 1]  # 更新结果为当前子串
        return ans

说明: (同上)

longestPalindrome 方法用于寻找给定字符串中的最长回文子串。使用动态规划的思想,创建一个二维数组dp,其中dp[i][j]表示从索引i到索引j的子串是否为回文串。通过遍历所有长度的子串,从最短的子串起,依次判断是否为回文串,并根据判断结果更新最长回文子串。最后,返回最长回文子串。

dp[i][j]的判断依据如下:

当子串长度为1时,dp[i][j]为True,因为单个字符必定是回文串;

当子串长度为2时,子串为回文串的条件是s[i]和s[j]相等;

当子串长度大于2时,子串为回文串的条件是首尾字符相等且去除首尾字符的子串也为回文串。

当发现一个更长的回文子串时,将其更新为结果。

复杂度分析

  • 时间复杂度:该算法的时间复杂度为 O(n^2),其中 n 是字符串的长度。双重循环遍历了所有长度为 1 到 n 的子串。
  • 空间复杂度:该算法的空间复杂度为 O(n^2),存储了一个二维数组 dp。

方式二:中心扩展法

思路

中心扩展法的基本思路是从左至右遍历字符串,以当前字符和其相邻字符为中心,向两边扩展,判断是否为回文串。在判断时,可以将回文串的起始和结束位置保存下来。

从每一个位置出发,向两边扩散即可。遇到不是回文的时候结束。举个例子,str=acdbbdaa 我们需要寻找从第一个 b(位置为 3)出发最长回文串为多少。怎么寻找?

首先往左寻找与当期位置相同的字符,直到遇到不相等为止。

然后往右寻找与当期位置相同的字符,直到遇到不相等为止。

最后左右双向扩散,直到左和右不相等。如下图所示:

每个位置向两边扩散都会出现一个窗口大小(len)。如果 len>maxLen(用来表示最长回文串的长度)。则更新 maxLen 的值。

因为我们最后要返回的是具体子串,而不是长度,因此,还需要记录一下 maxLen 时的起始位置(maxStart),即此时还要 maxStart=len。

代码实现

Java版本
class Solution {
    public String longestPalindrome(String s) {
        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); // 根据起始位置和结束位置返回最长回文子串
    }
    
    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; // 返回回文串的长度
    }
}

说明:

longestPalindrome 方法用于寻找给定字符串中的最长回文子串。根据中心扩展法的思想,遍历字符串中的每个字符,以当前字符为中心向两边扩展,同时以当前字符和下一个字符为中心向两边扩展,获取回文串的长度。通过比较回文串的长度,不断更新最长回文串的起始位置和结束位置。最后,根据起始位置和结束位置从原字符串中截取出最长回文子串并返回。

expandAroundCenter 方法用于以指定的左右位置为中心向两边扩展,判断是否为回文串。通过比较左右位置的字符是否相等,并同时向左和向右移动位置来不断扩展回文串的长度。最后,返回回文串的长度。

C语言版本
char* longestPalindrome(char* s) {
    int len = strlen(s);
    int start = 0;
    int end = 0;
    for (int i = 0; i < len; i++) {
        int len1 = expandAroundCenter(s, i, i);
        int len2 = expandAroundCenter(s, i, i + 1);
        int len = len1 > len2 ? len1 : len2;
        if (len > end - start) {
            start = i - (len - 1) / 2; // 更新回文串起始位置
            end = i + len / 2; // 更新回文串结束位置
        }
    }
    char *longest_palindrome = malloc((end - start + 2) * sizeof(char));
    strncpy(longest_palindrome, s + start, end - start + 1); // 复制回文串至新分配的内存
    longest_palindrome[end - start + 1] = '\0';
    return longest_palindrome;
}
int expandAroundCenter(char *s, int left, int right) {
    while (left >= 0 && right < strlen(s) && s[left] == s[right]) {
        left--; // 向左扩展
        right++; // 向右扩展
    }
    return right - left - 1; // 返回回文串的长度
}

说明:

longestPalindrome 函数用于寻找给定字符串中的最长回文子串。通过遍历字符串并以每个字符为中心依次判断以该字符或相邻两个字符为中心的回文串长度。通过比较回文串的长度,不断更新最长回文串的起始位置和结束位置。最后,将最长回文串从原字符串复制到新分配的内存中并返回。

expandAroundCenter 函数用于在给定字符串中以指定的左右位置为中心向两边扩展,判断是否为回文串。通过比较左右位置的字符是否相等,并同时向左和向右移动位置来不断扩展回文串的长度。最后,返回回文串的长度。

Python3版本
class Solution:
    def longestPalindrome(self, s: str) -> str:
        start = 0
        end = 0
        for i in range(len(s)):
            len1 = self.expandAroundCenter(s, i, i)
            len2 = self.expandAroundCenter(s, i, i + 1)
            length = max(len1, len2)
            if length > end - start:
                start = i - (length - 1) // 2 # 更新回文串起始位置
                end = i + length // 2 # 更新回文串结束位置
        return s[start: end + 1] # 根据起始位置和结束位置返回最长回文子串
    def expandAroundCenter(self, s: str, left: int, right: int) -> int:
        while left >= 0 and right < len(s) and s[left] == s[right]:
            left -= 1 # 向左扩展
            right += 1 # 向右扩展
        return right - left - 1 # 返回回文串的长度
}

说明:

longestPalindrome 函数用于寻找给定字符串中的最长回文子串。通过遍历字符串并以每个字符为中心或相邻两个字符为中心依次判断以该中心为起点的回文串长度。通过比较回文串的长度,不断更新最长回文串的起始位置和结束位置。最后,根据起始位置和结束位置从原字符串中截取出最长回文子串并返回。

expandAroundCenter 函数用于在给定字符串中以指定的左右位置为中心向两边扩展,判断是否为回文串。通过比较左右位置的字符是否相等,并同时向左和向右移动位置来不断扩展回文串的长度。最后,返回回文串的长度。

复杂度分析

  • 时间复杂度:该算法的时间复杂度为 O(n^2),其中 n 是字符串的长度。在中心扩展法中,每个字符仅遍历一次。
  • 空间复杂度:该算法的空间复杂度为 O(1),只使用了常量级的额外空间。

总结

方式 备注 优点 缺点 时间复杂度 空间复杂度
动态规划法 通过动态规划计算回文子串 算法稳定可靠 需要额外的二维数组存储状态 O(n^2) O(n^2)
中心扩展法 通过扩展中心位置计算回文子串 具有较高效率 对空间的使用较低 O(n^2) O(1)

相似题目

(表格形式,列举出)

相似题目 难度 链接
两个数组的交集 简单 leetcode-349
相关文章
|
1月前
|
存储 人工智能 算法
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
这篇文章详细介绍了Dijkstra和Floyd算法,这两种算法分别用于解决单源和多源最短路径问题,并且提供了Java语言的实现代码。
69 3
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
|
27天前
|
Java 测试技术 程序员
💡Java 零基础 | 深入理解注释的重要性与应用
【10月更文挑战第10天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
24 5
|
1月前
|
算法 搜索推荐 Java
java 后端 使用 Graphics2D 制作海报,画echarts图,带工具类,各种细节:如头像切割成圆形,文字换行算法(完美实验success),解决画上文字、图片后不清晰问题
这篇文章介绍了如何使用Java后端技术,结合Graphics2D和Echarts等工具,生成包含个性化信息和图表的海报,并提供了详细的代码实现和GitHub项目链接。
104 0
java 后端 使用 Graphics2D 制作海报,画echarts图,带工具类,各种细节:如头像切割成圆形,文字换行算法(完美实验success),解决画上文字、图片后不清晰问题
|
1月前
|
算法 Java Linux
java制作海报一:java使用Graphics2D 在图片上写字,文字换行算法详解
这篇文章介绍了如何在Java中使用Graphics2D在图片上绘制文字,并实现自动换行的功能。
92 0
|
1月前
|
算法 Java 测试技术
数据结构 —— Java自定义代码实现顺序表,包含测试用例以及ArrayList的使用以及相关算法题
文章详细介绍了如何用Java自定义实现一个顺序表类,包括插入、删除、获取数据元素、求数据个数等功能,并对顺序表进行了测试,最后还提及了Java中自带的顺序表实现类ArrayList。
19 0
|
8天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
17天前
|
安全 Java
java 中 i++ 到底是否线程安全?
本文通过实例探讨了 `i++` 在多线程环境下的线程安全性问题。首先,使用 100 个线程分别执行 10000 次 `i++` 操作,发现最终结果小于预期的 1000000,证明 `i++` 是线程不安全的。接着,介绍了两种解决方法:使用 `synchronized` 关键字加锁和使用 `AtomicInteger` 类。其中,`AtomicInteger` 通过 `CAS` 操作实现了高效的线程安全。最后,通过分析字节码和源码,解释了 `i++` 为何线程不安全以及 `AtomicInteger` 如何保证线程安全。
java 中 i++ 到底是否线程安全?
|
4天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
22 9
|
7天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
4天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin