目录
•写在前面
这种类型的题目算是比较常见的,有很多种解决方案,但我想着重说一下马拉车算法,第一因为之前我没有接触了解这个算法,第二就是,这个算法的时间复杂度降到了O(n),我自己在写的时候是使用中心扩展法进行解决的,时间复杂度是O(n^2)。
•题目
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
示例 2:
输入: "cbbd"
输出: "bb"
•解法一 暴力破解
暴力是最直白的一种解法,有时候不失为一种解决问题的方法,不过暴力解法的问题就是要注意时间复杂度的问题,在这里我直接呈现两种暴力破解的方式,一个纯暴力法,一个暴力优化法(就是使用空间弥补时间的优化)。首先第一种就不用说了,对一个字符串的所有子串进行判断,够暴力吧,这里个我就不多说了,直接看下面代码。
public static String longestPalindrome(String s) {
String ans = "";
int max = 0;
int len = s.length();
for (int i = 0; i < len; i++)
for (int j = i + 1; j <= len; j++) {
String test = s.substring(i, j);
boolean flag = true;
int len1 = test.length();
for (int k = 0; k < len1 / 2; k++) {
if (test.charAt(k) != test.charAt(len1 - k - 1)) {
flag = false;
}
}
if (flag && test.length() > max) {
ans = s.substring(i, j);
max = Math.max(max, ans.length());
}
}
return ans;
}
•解法二 暴力优化
但是上面的这种暴力方法时间复杂度是O(n^3),很明显这种解法过不了滴,那么如果只想用暴力过怎么办,其实还是有办法的,我们可以多开一个二维数组,把每次判断是否为回文的子串记录下来,如果包含了不是回文子串的大一号子串,说明这个大一号子串也不是回文。举例来说,一个二维数组boolean的P[i][j]用来储存,i代表子串的起始位置,j代表子串的终止位置,则s.substring(i,j)是回文的话,P[i][j]存true,反之则是false。所以每次到了要判断P[i][j]的时候,你只要判断P[i+1][j-1]是否为true,以及s.charAt(i)是否等于s.charAt(j),两个要同时成立才能为true,注意要单独判断一位子串和两位子串的情况。典型的空间换时间,时间复杂度降到了O(n^2)。
public String longestPalindrome(String s) {
int length = s.length();
boolean[][] P = new boolean[length][length];
int maxLen = 0;
String maxPal = "";
for (int len = 1; len <= length; len++) //遍历所有的长度
for (int start = 0; start < length; start++) {
int end = start + len - 1;
if (end >= length) //下标已经越界,结束本次循环
break;
P[start][end] = (len == 1 || len == 2 || P[start + 1][end - 1])
&& s.charAt(start) == s.charAt(end); //长度为 1 和 2 的单独判断下
if (P[start][end] && len > maxLen) {
maxPal = s.substring(start, end + 1);
}
}
return maxPal;
}
•解法三 最长公共子串(动态规划整体)
这个方法是比较经典的算法了,就是正向的字符串和逆向的字符串进行匹配比较,找到公共子串,同时判断是否为回文,从而记录最大的回文子串。首先先得知道最长公共子串怎么求,其实挺简单的,我们只要利用一个二维数组就可以了,话不多说,先上图,不然讲了和没讲一样,图是偷来的,别介意。
看图就知道S'是S字符串的倒置,我们只要通过两层循环,(我这里暂时把字符串当成数组说,这样方便我讲解,就不用charAt了,直接用[])判断S[i]和S[j]是否相等,如果相等,则二维数组arr[i][j] = arr[i-1][j-1] + 1,你仔细想一下就知道为什么了,你看图会发现arr[i][j]和arr[i-1][j-1]的关系,这样arr[i][j]里面记录的就是这两个相等子串的长度,也就是找到公共子串的算法,那接下来我们怎么判断是否是回文呢?我们想想S = "caba" ,S = "abac",最长公共子串是 "aba",再看一个例子,S="abc435cba"最长公共子串是 "abc"和 "cba"
,但很明显这两个字符串都不是回文串。所以我们求出最长公共子串后,并不一定是回文串,我们还需要判断该字符串倒置前的下标和当前的字符串下标是不是匹配。比如 S="caba",S'="abac" ,S’ 中 aba 的下标是 0 1 2 ,倒置前是 3 2 1,和 S 中 aba 的下标符合,所以 aba 就是我们需要找的。当然我们不需要每个字符都判断,我们只需要判断末尾字符就可以。是不是很简单,那就直接上代码。时间复杂度是O(n^2)哦。当然啦,可以使用动态规划,将这个空间复杂度降低一个level.
public static String longestPalindromeS3(String s) {
if(s.equals(""))
return "";
String origin = s;
String reverse = new StringBuilder(s).reverse().toString();
int length = s.length();
int[][] arr = new int[length][length];
int maxLen = 0;
int maxEnd = 0;
for(int i = 0; i < length; i++)
for(int j = 0; j < length; j++){
if(origin.charAt(i) == reverse.charAt(j)){
if(i == 0 || j == 0){
arr[i][j] = 1;
}else{
arr[i][j] = arr[i - 1][j - 1] + 1;
}
}
if(arr[i][j] > maxLen){
int beforeRev = length - 1 - j;
if(beforeRev + arr[i][j] - 1 == i){ // 用于判断下标是否对应,从而判断是否是回文
maxLen = arr[i][j];
maxEnd = i;
}
}
}
return s.substring(maxEnd - maxLen + 1, maxLen + 1);
}
•解法四 中心扩展法
中心扩展法的思想不难,我们只要分别考虑字符串的奇数偶数的情况,奇数的话,就由一个点向两边展开,偶数的话,两个点同时展开。我们知道回文串一定是对称的,所以我们可以每次循环选择一个中心,进行左右扩展,判断左右字符是否相等即可。思想很简单,我就直接上代码了,看具体实现。当然啦,最好封装成一个方法,别向我这样写的好臃肿。
public static String longestPalindromeS1(String s) {
int max = 1; //记录最大回文字符串的长度
int max1 = 1; //记录偶数最大回文字符串的长度
int left = 0;
int right = 0;
int left1 = 0;
int right1 = 0;
int length = s.length();
for(int i = 0; i < length; i++){
int len = 1;
int leftIndex = i - 1;
int rightIndex = i + 1;
while(leftIndex >= 0 && rightIndex < length){
if(s.charAt(leftIndex) == s.charAt(rightIndex)){
len += 2;
leftIndex--;
rightIndex++;
}else break;
}
if(max < len){
max = len;
left = leftIndex + 1;
right = rightIndex;
}
int len1 = 0;
int leftIndex1 = i;
int rightIndex1 = i + 1;
while(leftIndex1 >= 0 && rightIndex1 < length){
if(s.charAt(leftIndex1) == s.charAt(rightIndex1)){
len1 += 2;
leftIndex1--;
rightIndex1++;
}else break;
}
if(max1 < len1){
max1 = len1;
left1 = leftIndex1 + 1;
right1 = rightIndex1;
}
}
if(max1 < max) return s.substring(left, right);
else if(s.equals("")) return "";
else if(max1 == 1 && max == 1) return s.substring(0,1);
else return s.substring(left1, right1);
}
•解法五 马拉车算法
真的要重点介绍一下马拉车,我们上面的算法的时间复杂度都是O(n^2),而使用了这个算法,将时间复杂度降到了O(n),不得不感叹一下。首先,马拉车算法 Manacher‘s Algorithm 是用来查找一个字符串的最长回文子串的线性方法,这个方法的最大贡献是在于将时间复杂度提升到了线性。
第一步,就是处理字符串的奇数偶数引起的分类问题,在马拉车算法里,我们对字符串的首尾以及间隔进行填充(注意填充的时候要用字符串中不会出现的字符),这样保证了我们在使用中心扩展法的时候,解决了奇数偶数的问题,形象一点看下图。
首先我们用一个数组 P 保存从中心扩展的最大个数,而它刚好也是去掉 "#" 的原字符串的总长度。例如下图中下标是 6 的地方,可以看到 P[ 6 ] 等于 5,所以它是从左边扩展 5 个字符,相应的右边也是扩展 5 个字符,也就是 "#c#b#c#b#c#"。而去掉 # 恢复到原来的字符串,变成 "cbcbc",它的长度刚好也就是 5。
只要我们有了P这个数组我们就可以使用P数组求得原来数组的下标,用 P 的下标 i 减去 P [ i ],再除以 2,就是原字符串的开头下标了。但是我们怎么求得这个数组P呢?算法的最核心就在这,充分利用回文字符串的对称性,我们用 C 表示回文串的中心,用 R 表示回文串的右边半径。所以 R = C + P[ i ]。C 和 R 所对应的回文串是当前循环中 R 最靠右的回文串。让我们考虑求 P [ i ] 的时候,如下图。用 i_mirror 表示当前需要求的第 i 个字符关于 C 对应的下标。
我们现在要求 P [ i ],如果是用中心扩展法,那就向两边扩展比对就行了。但是我们其实可以利用回文串 C 的对称性。i 关于 C 的对称点是 i_mirror,P [ i_mirror ] = 3,所以 P [ i ] 也等于 3。但是有三种情况直接赋值会出现问题,哪三种情况呢,1、i加上值超过R。2、P[i_mirror ]遇到了原字符串的左边界。3、i到了R。这三种情况均使用中心扩展法,解决就可以了,就这样一步一步的求出每个 P [ i ],当求出的 P [ i ] 的右边界大于当前的 R 时,我们就需要更新 C 和 R 为当前的回文串了。因为我们必须保证 i 在 R 里面,所以一旦有更右边的 R 就要更新 R。
此时的 P [ i ] 求出来将会是 3,P [ i ] 对应的右边界将是 10 + 3 = 13,所以大于当前的 R,我们需要把 C 更新成 i 的值,也就是 10,R 更新成 13。继续下边的循环。思路就这样,我们直接上代码。
public static String preProcess(String s){
int n = s.length();
if(n == 0){
return "^$";
}
String ret = "^";
for(int i = 0; i < n; i++){
ret += "#" + s.charAt(i);
}
ret += "#$";
return ret;
}
public static String longestPalindrome(String s) {
String T = preProcess(s);
int n = T.length();
int[] P = new int[n];
int C = 0, R = 0;
for(int i = 1; i < n - 1; i++){
int i_mirror = 2 * C - i;
if(R > i){
P[i] = Math.min(R - i, P[i_mirror]); //防止超出R
}else{
P[i] = 0 ;//等于R的情况
}
//碰到之前讲的三种情况时候,需要利用中心扩展法
while(T.charAt(i + 1 + P[i]) == T.charAt(i - 1 - P[i])){
P[i]++;
}
//判断是否需要更新R
if(i + P[i] > R){
C = i;
R = i + P[i];
}
}
//找出P的最大值
int maxLen = 0;
int centerIndex = 0;
for(int i = 1; i < n - 1; i++){
if(P[i] > maxLen){
maxLen = P[i];
centerIndex = i;
}
}
int start = (centerIndex - maxLen) / 2;//最开始讲的求原字符串下标
return s.substring(start, start + maxLen);
}
•结束
学无止境呀,马拉车算法可以说是充分利用了回文串的特性,但也限制了它在其他地方的使用,不过我们有了这种思想,说不定对付以后的问题,将变的更加简单。