给定一个字符串,输出最长的回文子串。回文串指的是正的读和反的读是同样的字符串,例如 "aba","ccbbcc"。java
暴力求解,列举全部的子串,判断是否为回文串,保存最长的回文串。算法
public boolean isPalindromic(String s) { int len = s.length(); for (int i = 0; i < len / 2; i++) { if (s.charAt(i) != s.charAt(len - i - 1)) { return false; } } return true; } // 暴力解法 public 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); if (isPalindromic(test) && test.length() > max) { ans = s.substring(i, j); max = Math.max(max, ans.length()); } } return ans; }
时间复杂度:两层 for 循环 O(n²),for 循环里边判断是否为回文,O(n),因此时间复杂度为 O(n³)。c#
空间复杂度:O(1),常数个变量。segmentfault
根据回文串的定义,正着和反着读同样,那咱们是否是把原来的字符串倒置了,而后找最长的公共子串就能够了。例如,S = " caba",S' = " abac",最长公共子串是 "aba",因此原字符串的最长回文串就是 "aba"。数组
关于求最长公共子串(不是公共子序列),有不少方法,这里用动态规划的方法,能够先阅读下边的连接。函数
https://blog.csdn.net/u010397...优化
https://www.kancloud.cn/diges...spa
总体思想就是,申请一个二维的数组初始化为 0,而后判断对应的字符是否相等,相等的话.net
arr [ i ][ j ] = arr [ i - 1 ][ j - 1] + 1 。 3d
当 i = 0 或者 j = 0 的时候单独分析,字符相等的话 arr [ i ][ j ] 就赋为 1 。
arr [ i ][ j ] 保存的就是公共子串的长度。
public String longestPalindrome(String s) { if (s.equals("")) return ""; String origin = s; String reverse = new StringBuffer(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) { maxLen = arr[i][j]; maxEnd = i; //以 i 位置结尾的字符 } } } return s.substring(maxEnd - maxLen + 1, maxEnd + 1); }
再看一个例子,S = "abc435cba",S’ = "abc534cba" ,最长公共子串是 "abc" 和 "cba" ,但很明显这两个字符串都不是回文串。
因此咱们求出最长公共子串后,并不必定是回文串,咱们还须要判断该字符串倒置前的下标和当前的字符串下标是否是匹配。
好比 S = " caba ",S' = " abac " ,S’ 中 aba 的下标是 0 1 2 ,倒置前是 3 2 1,和 S 中 aba 的下标符合,因此 aba 就是咱们须要找的。固然咱们不须要每一个字符都判断,咱们只须要判断末尾字符就能够。
首先 i ,j 始终指向子串的末尾字符。因此 j 指向的红色的 a 倒置前的下标是 beforeRev = length - 1 - j = 4 - 1 - 2 = 1,对应的是字符串首位的下标,咱们还须要加上字符串的长度才是末尾字符的下标,也就是 beforeRev + arr[ i ] [ j ] - 1 = 1 + 3 - 1 = 3,由于 arr[ i ] [ j ] 保存的就是当前子串的长度,也就是图中的数字 3 。此时再和它与 i 比较,若是相等,则说明它是咱们要找的回文串。
以前的 S = "abc435cba",S' = "abc534cba" ,能够看一下图示,为何不符合。
当前 j 指向的 c ,倒置前的下标是 beforeRev = length - 1 - j = 9 - 1 - 2 = 6,对应的末尾下标是 beforeRev + arr[ i ] [ j ] - 1 = 6 + 3 - 1 = 8 ,而此时 i = 2 ,因此当前的子串不是回文串。
代码的话,在上边的基础上,保存 maxLen 前判断一下下标匹不匹配就能够了。
public String longestPalindrome(String s) { if (s.equals("")) return ""; String origin = s; String reverse = new StringBuffer(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, maxEnd + 1); }
时间复杂度:两层循环,O(n²)。
空间复杂度:一个二维数组,O(n²)。
空间复杂度其实能够再优化一下。
咱们分析一下循环,i = 0 ,j = 0,1,2 ... 8 更新一列,而后 i = 1 ,再更新一列,而更新的时候咱们其实只须要上一列的信息,更新第 3 列的时候,第 1 列的信息是没有用的。因此咱们只须要一个一维数组就能够了。可是更新 arr [ i ] 的时候咱们须要 arr [ i - 1 ] 的信息,假设 a [ 3 ] = a [ 2 ] + 1,更新 a [ 4 ] 的时候, 咱们须要 a [ 3 ] 的信息,可是 a [ 3 ] 在以前已经被更新了,因此 j 不能从 0 到 8 ,应该倒过来,a [ 8 ] = a [ 7 ] + 1,a [ 7 ] = a [ 6 ] + 1 , 这样更新 a [ 8 ] 的时候用 a [ 7 ] ,用完后才去更新 a [ 7 ],保证了不会出错。
public String longestPalindrome(String s) { if (s.equals("")) return ""; String origin = s; String reverse = new StringBuffer(s).reverse().toString(); int length = s.length(); int[] arr = new int[length]; int maxLen = 0; int maxEnd = 0; for (int i = 0; i < length; i++) /**************修改的地方***************************/ for (int j = length - 1; j >= 0; j--) { /**************************************************/ if (origin.charAt(i) == reverse.charAt(j)) { if (i == 0 || j == 0) { arr[j] = 1; } else { arr[j] = arr[j - 1] + 1; } /**************修改的地方***************************/ //以前二维数组,每次用的是不一样的列,因此不用置 0 。 } else { arr[j] = 0; } /**************************************************/ if (arr[j] > maxLen) { int beforeRev = length - 1 - j; if (beforeRev + arr[j] - 1 == i) { maxLen = arr[j]; maxEnd = i; } } } return s.substring(maxEnd - maxLen + 1, maxEnd + 1); }
时间复杂度:O(n²)。
空间复杂度:降为 O(n)。
解法一的暴力解法时间复杂度过高,在 leetCode 上并不能 AC 。咱们能够考虑,去掉一些暴力解法中重复的判断。咱们能够基于下边的发现,进行改进。
首先定义 P(i,j)。
$$P(i,j)=\begin{cases}true& \text{s[i,j]是回文串} \\\\false& \text{s[i,j]不是回文串}\end{cases}$$
接下来
$$P(i,j)=(P(i+1,j-1)\&\&S[i]==S[j])$$
因此若是咱们想知道 P(i,j)的状况,不须要调用判断回文串的函数了,只须要知道 P(i + 1,j - 1)的状况就能够了,这样时间复杂度就少了 O(n)。所以咱们能够用动态规划的方法,空间换时间,把已经求出的 P(i,j)存储起来。
若是 $$S[i+1,j-1]$$ 是回文串,那么只要 S [ i ] == S [ j ] ,就能够肯定 S [ i , j ] 也是回文串了。
求 长度为 1 和长度为 2 的 P ( i , j ) 时不能用上边的公式,由于咱们代入公式后会遇到 $$P[i][j]$$ 中 i > j 的状况,好比求 $$P[1][2]$$ 的话,咱们须要知道 $$P[1+1][2-1]=P[2][1]$$ ,而 $$P[2][1]$$ 表明着 $$S[2,1]$$ 是否是回文串,显然是不对的,因此咱们须要单独判断。
因此咱们先初始化长度是 1 的回文串的 P [ i , j ],这样利用上边提出的公式 $$P(i,j)=(P(i+1,j-1)\&\&S[i]==S[j])$$,而后两边向外各扩充一个字符,长度为 3 的,为 5 的,全部奇数长度的就都求出来了。
同理,初始化长度是 2 的回文串 P [ i , i + 1 ],利用公式,长度为 4 的,6 的全部偶数长度的就都求出来了。
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; }
时间复杂度:两层循环,O(n²)。
空间复杂度:用二维数组 P 保存每一个子串的状况,O(n²)。
咱们分析下每次循环用到的 P(i,j),看一看能不能向解法二同样优化一下空间复杂度。
当咱们求长度为 6 和 5 的子串的状况时,其实只用到了 4 , 3 长度的状况,而长度为 1 和 2 的子串状况其实已经不须要了。可是因为咱们并非用 P 数组的下标进行的循环,暂时没有想到优化的方法。
以后看到了另外一种动态规划的思路
https://leetcode.com/problems... 。
公式仍是这个不变
首先定义 P(i,j)。
$$P(i,j)=\begin{cases}true& \text{s[i,j]是回文串}\\\\false& \text{s[i,j]不是回文串}\end{cases}$$
接下来
$$P(i,j)=(P(i+1,j-1)\&\&S[i]==S[j])$$
递推公式中咱们能够看到,咱们首先知道了 i +1 才会知道 i ,因此咱们只须要倒着遍历就好了。
public String longestPalindrome(String s) { int n = s.length(); String res = ""; boolean[][] dp = new boolean[n][n]; for (int i = n - 1; i >= 0; i--) { for (int j = i; j < n; j++) { dp[i][j] = s.charAt(i) == s.charAt(j) && (j - i < 2 || dp[i + 1][j - 1]); //j - i 表明长度减去 1 if (dp[i][j] && j - i + 1 > res.length()) { res = s.substring(i, j + 1); } } } return res; }
时间复杂度和空间复杂和以前都没有变化,咱们来看看可不能够优化空间复杂度。
当求第 i 行的时候咱们只须要第 i + 1 行的信息,而且 j 的话须要 j - 1 的信息,因此和以前同样 j 也须要倒叙。
public String longestPalindrome7(String s) { int n = s.length(); String res = ""; boolean[] P = new boolean[n]; for (int i = n - 1; i >= 0; i--) { for (int j = n - 1; j >= i; j--) { P[j] = s.charAt(i) == s.charAt(j) && (j - i < 3 || P[j - 1]); if (P[j] && j - i + 1 > res.length()) { res = s.substring(i, j + 1); } } } return res; }
时间复杂度:不变,O(n²)。
空间复杂度:降为 O(n ) 。
咱们知道回文串必定是对称的,因此咱们能够每次循环选择一个中心,进行左右扩展,判断左右字符是否相等便可。
因为存在奇数的字符串和偶数的字符串,因此咱们须要从一个字符开始扩展,或者从两个字符之间开始扩展,因此总共有 n + n - 1 个中心。
public String longestPalindrome(String s) { if (s == null || s.length() < 1) return ""; int start = 0, 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) { int L = left, R = right; while (L >= 0 && R < s.length() && s.charAt(L) == s.charAt(R)) { L--; R++; } return R - L - 1; }
时间复杂度:O(n²)。
空间复杂度:O(1)。
马拉车算法 Manacher‘s Algorithm 是用来查找一个字符串的最长回文子串的线性方法,由一个叫Manacher的人在1975年发明的,这个方法的最大贡献是在于将时间复杂度提高到了线性。
主要参考了下边连接进行讲解。
https://segmentfault.com/a/11...
https://blog.crimx.com/2017/0...
http://ju.outofmemory.cn/entr...
https://articles.leetcode.com...
首先咱们解决下奇数和偶数的问题,在每一个字符间插入"#",而且为了使得扩展的过程当中,到边界后自动结束,在两端分别插入 "^" 和 "$",两个不可能在字符串中出现的字符,这样中心扩展的时候,判断两端字符是否相等的时候,若是到了边界就必定会不相等,从而出了循环。通过处理,字符串的长度永远都是奇数了。
首先咱们用一个数组 P 保存从中心扩展的最大个数,而它恰好也是去掉 "#" 的原字符串的总长度。例以下图中下标是 6 的地方。能够看到 P[ 6 ] 等于 5,因此它是从左边扩展 5 个字符,相应的右边也是扩展 5 个字符,也就是 "#c#b#c#b#c#"。而去掉 # 恢复到原来的字符串,变成 "cbcbc",它的长度恰好也就是 5。
用 P 的下标 i 减去 P [ i ],再除以 2 ,就是原字符串的开头下标了。
例如咱们找到 P[ i ] 的最大值为 5 ,也就是回文串的最大长度是 5 ,对应的下标是 6 ,因此原字符串的开头下标是 (6 - 5 )/ 2 = 0 。因此咱们只须要返回原字符串的第 0 到 第 (5 - 1)位就能够了。
接下来是算法的关键了,它充分利用了回文串的对称性。
咱们用 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 。
可是有三种状况将会形成直接赋值为 P [ i_mirror ] 是不正确的,下边一一讨论。
当咱们要求 P [ i ] 的时候,P [ mirror ] = 7,而此时 P [ i ] 并不等于 7 ,为何呢,由于咱们从 i 开始日后数 7 个,等于 22 ,已经超过了最右的 R ,此时不能利用对称性了,但咱们必定能够扩展到 R 的,因此 P [ i ] 至少等于 R - i = 20 - 15 = 5,会不会更大呢,咱们只须要比较 T [ R+1 ] 和 T [ R+1 ]关于 i 的对称点就好了,就像中心扩展法同样一个个扩展。
此时P [ i_mirror ] = 1,可是 P [ i ] 赋值成 1 是不正确的,出现这种状况的缘由是 P [ i_mirror ] 在扩展的时候首先是 "#" == "#" ,以后遇到了 "^"和另外一个字符比较,也就是到了边界,才终止循环的。而 P [ i ] 并无遇到边界,因此咱们能够继续经过中心扩展法一步一步向两边扩展就好了。
此时咱们先把 P [ i ] 赋值为 0 ,而后经过中心扩展法一步一步扩展就好了。
就这样一步一步的求出每一个 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 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 String longestPalindrome2(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); }
时间复杂度:for 循环里边套了一层 while 循环,难道不是 O ( n² )?不!实际上是 O ( n )。不严谨的想一下,由于 while 循环访问 R 右边的数字用来扩展,也就是那些还未求出的节点,而后不断扩展,而期间访问的节点下次就不会再进入 while 了,能够利用对称获得本身的解,因此每一个节点访问都是常数次,因此是 O ( n )。
空间复杂度:O(n)。
时间复杂度从三次方降到了一次,美妙!这里两次用到了动态规划去求解,初步认识了动态规划,就是将以前求的值保存起来,方便后边的计算,使得一些多余的计算消失了。而且在动态规划中,经过观察数组的利用状况,从而下降了空间复杂度。而 Manacher 算法对回文串对称性的充分利用,不得不让人叹服,本身加油啦!