给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: “babad”
输出: “bab”
注意: “aba” 也是一个有效答案。
示例 2:
输入: “cbbd”
输出: “bb”
暴力解法
在记录最长回文子串的时候,可以只记录“当前子串的起始位置”和“子串长度”,不必做截取。这一步我们放在后面的方法中实现。
说明:暴力解法时间复杂度高,但是思路清晰、编写简单。由于编写正确性的可能性很大,可以使用暴力匹配算法检验我们编写的其它算法是否正确。优化的解法在很多时候,是基于“暴力解法”,以空间换时间得到的,因此思考清楚暴力解法,分析其缺点,很多时候能为我们打开思路。
public class longestPalindrome5 {
public static void main(String[] args) {
String s = "babaadadadfcda";
String res = longestPalindrome(s);
System.out.println(res);
}
private static String longestPalindrome(String s){
int n = s.length();
if(n<2) return s;
int max = 1;
int start = 0;
// s.charAt(i) 每次都会检查数组下标越界,因此先转换成字符数组
char[] num = s.toCharArray();
// 枚举所有长度大于 1 的子串 charArray[i..j]
for(int i=0;i<n-1;i++){
for(int j=i+1;j<n;j++){
if(j-i+1>max && validHelper(num,i,j)){
max = j-i+1;
start = i;
}
}
}
return s.substring(start,start+max);
}
//验证子串 s[left..right] 是否为回文串
private static boolean validHelper(char[] num, int left, int right) {
while(left<right){
if(num[left]!=num[right]){
return false;
}
left++;
right--;
}
return true;
}
}
时间复杂度:O(N^3)
动态规划 思考
第 1 步:定义状态
dp[i][j] 表示子串 s[i, j] 是否为回文子串。
第 2 步:思考状态转移方程
这一步在做分类讨论(根据头尾字符是否相等),根据上面的分析得到:
dp[i][j] = (s[i] == s[j]) and dp[i + 1][j - 1]
分析这个状态转移方程:
(1)“动态规划”事实上是在填一张二维表格,i 和 j 的关系是 i <= j ,因此,只需要填这张表的上半部分;
(2)看到 dp[i + 1][j - 1] 就得考虑边界情况。
边界条件是:表达式 [i + 1, j - 1] 不构成区间,即长度严格小于 2,即 j - 1 - (i + 1) + 1 < 2 ,整理得 j - i < 3。
这个结论很显然:当子串 s[i, j] 的长度等于 2 或者等于 3 的时候,我其实只需要判断一下头尾两个字符是否相等就可以直接下结论了。
如果子串 s[i + 1, j - 1] 只有 1 个字符,即去掉两头,剩下中间部分只有 11 个字符,当然是回文;
如果子串 s[i + 1, j - 1] 为空串,那么子串 s[i, j] 一定是回文子串。
因此,在 s[i] == s[j] 成立和 j - i < 3 的前提下,直接可以下结论,dp[i][j] = true,否则才执行状态转移。
(这一段看晕的朋友,直接看代码吧。我写晕了,车轱辘话来回说。)
第 3 步:考虑初始化
初始化的时候,单个字符一定是回文串,因此把对角线先初始化为 1,即 dp[i][i] = 1 。
事实上,初始化的部分都可以省去。因为只有一个字符的时候一定是回文,dp[i][i] 根本不会被其它状态值所参考。
第 4 步:考虑输出
只要一得到 dp[i][j] = true,就记录子串的长度和起始位置,没有必要截取,因为截取字符串也要消耗性能,记录此时的回文子串的“起始位置”和“回文长度”即可。
第 5 步:考虑状态是否可以压缩
因为在填表的过程中,只参考了左下方的数值。事实上可以压缩,但会增加一些判断语句,增加代码编写和理解的难度,丢失可读性。在这里不做状态压缩。
下面是编码的时候要注意的事项:总是先得到小子串的回文判定,然后大子串才能参考小子串的判断结果。
思路是:
1、在子串右边界 j 逐渐扩大的过程中,枚举左边界可能出现的位置;
2、左边界枚举的时候可以从小到大,也可以从大到小。
这两版代码的差别仅在内层循环,希望大家能够自己动手,画一下表格,思考为什么这两种代码都是可行的,相信会对“动态规划”作为一种“表格法”有一个更好的理解。
class Solution {
public String longestPalindrome(String s) {
int n=s.length();
if(s==null) return null;
if(n<2) return s;//预先判断
boolean[][] dp=new boolean[n][n];
for(int i=0;i<n;i++){// base case
dp[i][i]=true;
}
int max=1; //记录最长长度
int start=0;//记录最长长度开始位置
for(int j=1;j<n;j++){ // 注意循环开始情况 为j 里套着 i
for(int i=0;i<j;i++){
if(s.charAt(i)==s.charAt(j)){
if(j-i<3) {//不用再判断了 剩一个或者不剩都算回文
dp[i][j]=true;
}
else{
dp[i][j]=dp[i+1][j-1];
}
}else{
dp[i][j]=false;
}
if(dp[i][j]){//不substring 只是记录就可
int cur=j-i+1;
if(cur>max){
max=cur;
start=i;
}
}
}
}
return s.substring(start,start+max);
}
}
中心扩散法
暴力法采用双指针两边夹,验证是否是回文子串。
除了枚举字符串的左右边界以外,比较容易想到的是枚举可能出现的回文子串的“中心位置”,从“中心位置”尝试尽可能扩散出去,得到一个回文串。
因此中心扩散法的思路是:遍历每一个索引,以这个索引为中心,利用“回文串”中心对称的特点,往两边扩散,看最多能扩散多远。
枚举“中心位置”时间复杂度为 O(N)O(N),从“中心位置”扩散得到“回文子串”的时间复杂度为 O(N)O(N),因此时间复杂度可以降到 O(N^2)O(N
2
)。
在这里要注意一个细节:回文串在长度为奇数和偶数的时候,“回文中心”的形式是不一样的。
奇数回文串的“中心”是一个具体的字符,例如:回文串 “aba” 的中心是字符 “b”;
偶数回文串的“中心”是位于中间的两个字符的“空隙”,例如:回文串串 “abba” 的中心是两个 “b” 中间的那个“空隙”。
们可以设计一个方法,兼容以上两种情况:
1、如果传入重合的索引编码,进行中心扩散,此时得到的回文子串的长度是奇数;
2、如果传入相邻的索引编码,进行中心扩散,此时得到的回文子串的长度是偶数。
具体编码细节在以下的代码的注释中体现。
class Solution {
public String longestPalindrome(String s) {
int n=s.length();
if(n<2) return s;
int max=1;//最少一个字母
String res=s.substring(0,1);//先设第一个字母为res
for(int i=0;i<n;i++){// 遍历一遍数
String oddstr=centerSpread(s, i, i);
String evenstr=centerSpread(s,i,i+1);
String maxstr=(oddstr.length()>evenstr.length()) ?oddstr:evenstr;
if(maxstr.length()>max){
max=maxstr.length();
res=maxstr;
}
}
return res;
}
public String centerSpread(String s, int i,int j){
while(i>=0 && j<s.length()){
if(s.charAt(i)== s.charAt(j)){
i--;
j++;
}
else{
break;
}
}
return s.substring(i+1,j);
}
}
5.22 重写三种方法
package dailyLeetcode;
public class longestPalindrome5 {
public static void main(String[] args) {
String s = "babaadadadfcda";
// String res = longestPalindrome(s);// 暴力
// String res = longestPalindrome_2(s);// 动态规划
String res = longestPalindrome_3(s);// 中心扩散
System.out.println(res);
}
private static String longestPalindrome_3(String s) {
int n = s.length();
if(n<2) return s;
int max = 1;
String res = s.substring(0,1);
for(int i=0;i<n;i++){
String oddStr = centerHelper(s,i,i);
String evenStr = centerHelper(s,i,i+1);
String maxStr = (oddStr.length()>evenStr.length())?oddStr:evenStr;
if(maxStr.length()>max){
max = maxStr.length();
res = maxStr;
}
}
return res;
}
private static String centerHelper(String s, int i, int j) {
while(i>=0 && j<s.length()){
if(s.charAt(i) == s.charAt(j)){
i--;
j++;
}
else{
break;
}
}
return s.substring(i+1,j);
}
private static String longestPalindrome_2(String s) {
int n = s.length();
if(n<2) return s;
int max = 1;
int start = 0;
boolean[][] dp = new boolean[n][n];
char[] str = s.toCharArray();
for(int i=0;i<n;i++){
dp[i][i] = true;
}
for(int j = 1;j<n;j++){
for(int i=0;i<j;i++){// 注意了 这里取得事i到j的范围
if(str[i]!=str[j]){
dp[i][j] = false;
}
else {// 如果相等的时候 不能写两个else if
if(j-i<3)
{
dp[i][j] = true;
}else{
dp[i][j] = dp[i+1][j-1];
}
}
if(dp[i][j] && j-i+1>max ){
max = j-i+1;
start = i;
}
}
}
return s.substring(start,start+max);
}
private static String longestPalindrome(String s){
int n = s.length();
if(n<2) return s;
int max = 1;
int start = 0;
char[] num = s.toCharArray();
for(int i=0;i<n-1;i++){
for(int j=i+1;j<n;j++){
if(j-i+1>max && validHelper(num,i,j)){
max = j-i+1;
start = i;
}
}
}
return s.substring(start,start+max);
}
private static boolean validHelper(char[] num, int left, int right) {
while(left<right){
if(num[left]!=num[right]){
return false;
}
left++;
right--;
}
return true;
}
}
lee516 最长回文子序列
可不连续,输出长度
class Solution {
public int longestPalindromeSubseq(String s) {
int n=s.length();
if(n<2) return n;
int[][] dp=new int[n][n];
for(int i=0;i<n;i++){
dp[i][i]=1;
}
for(int i=n-1;i>=0;i--){//注意方向
for(int j=i+1;j<n;j++){//注意方向
if(s.charAt(i)==s.charAt(j)){
dp[i][j]=dp[i+1][j-1]+2;//注意这里要+2 求长度
}
else{
dp[i][j]=Math.max(dp[i+1][j], dp[i][j-1]);
}
}
}
return dp[0][n-1];
}
}
有个大神方法,二分查找发
public class Solution {
public int lengthOfLIS(int[] nums) {
int len = nums.length;
if (len <= 1) {
return len;
}
// tail 数组的定义:长度为 i + 1 的上升子序列的末尾最小是几
int[] tail = new int[len];
// 遍历第 1 个数,直接放在有序数组 tail 的开头
tail[0] = nums[0];
// end 表示有序数组 tail 的最后一个已经赋值元素的索引
int end = 0;
for (int i = 1; i < len; i++) {
// 【逻辑 1】比 tail 数组实际有效的末尾的那个元素还大
if (nums[i] > tail[end]) {
// 直接添加在那个元素的后面,所以 end 先加 1
end++;
tail[end] = nums[i];
} else {
// 使用二分查找法,在有序数组 tail 中
// 找到第 1 个大于等于 nums[i] 的元素,尝试让那个元素更小
int left = 0;
int right = end;
while (left < right) {
// 选左中位数不是偶然,而是有原因的,原因请见 LeetCode 第 35 题题解
// int mid = left + (right - left) / 2;
int mid = left + ((right - left) >>> 1);
if (tail[mid] < nums[i]) {
// 中位数肯定不是要找的数,把它写在分支的前面
left = mid + 1;
} else {
right = mid;
}
}
// 走到这里是因为 【逻辑 1】 的反面,因此一定能找到第 1 个大于等于 nums[i] 的元素
// 因此,无需再单独判断
tail[left] = nums[i];
}
// 调试方法
// printArray(nums[i], tail);
}
// 此时 end 是有序数组 tail 最后一个元素的索引
// 题目要求返回的是长度,因此 +1 后返回
end++;
return end;
}
// 调试方法,以观察是否运行正确
private void printArray(int num, int[] tail) {
System.out.print("当前数字:" + num);
System.out.print("\t当前 tail 数组:");
int len = tail.length;
for (int i = 0; i < len; i++) {
if (tail[i] == 0) {
break;
}
System.out.print(tail[i] + ", ");
}
System.out.println();
}
public static void main(String[] args) {
int[] nums = new int[]{3, 5, 6, 2, 5, 4, 19, 5, 6, 7, 12};
Solution solution = new Solution8();
int lengthOfLIS = solution8.lengthOfLIS(nums);
System.out.println("最长上升子序列的长度:" + lengthOfLIS);
}
}
作者:liweiwei1419
链接:https://leetcode-cn.com/problems/longest-increasing-subsequence/solution/dong-tai-gui-hua-er-fen-cha-zhao-tan-xin-suan-fa-p/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。