一、暴力for循环 (时间复杂度O(n^3))
由于太过“暴力”,就暂不多介绍。
二、中心扩散法 (时间复杂度O(n^2))
中心思想:
1. 选取一个元素或两个(必须相同)元素作为回文串的对称中心。
2. 然后再往两边同时扩散(往两边遍历)。
3. 直到遇到左边元素(i--)不等于右边元素(i++),或者任意一边没有元素了,就停止扩散。
4. 比较当前遍历的回文字符串与之前最大长度回文字符串长度,若大于则更新最长回文字符串。
注意:由于回文串长度可能是奇数长度,也可能是偶数长度,其选取对称中心也不相同。
1. 偶数长度回文字符串:
图 1 图 2
function longestPalindrome(str) {
const strArr = str.split('');
let middleArr = []; // 用来装扩散时的回文子串,这里我叫其 *暂存数组*
let maxStr = strArr[0]; // 记录最长回文子串,初始默认为一个长度的回文串。
// 用于向左右扩散的左右指针
let left = 0, right = 0;
// 寻找最大奇数长度回文字符串
for (let i = 0; i < strArr.length; i++) {
// 左右设置两个指针的初始值,此时中心对称点即为下标i。
left = i - 1;
right = i + 1;
middleArr = [strArr[i]];
// 循环条件:当中心点左边元素等于右边元素,并且左右元素均存在。
while (strArr[left] === strArr[right] && strArr[left] && strArr[right]) {
middleArr.unshift(strArr[left]); // 如果左右元素都符合条件,则左边元素装到暂存数组头部
middleArr.push(strArr[right]); // 右边元素装到暂存数组尾部
left--; // 左指针左移,右指针右移
right++;
}
// 当前轮扩散结束后,与之前最大回文字符串作比较。
if (middleArr.length > maxStr.length) {
maxStr = middleArr.join('');
}
}
// 寻找最大偶数长度回文字符串 (基本原理同上)
for (let i = 0; i < strArr.length - 1; i++) {
left = i; // 注意: 因为是偶数长度回文字符串,所以其对称中心为两个相同的元素。
right = i + 1;
middleArr = [];
while (strArr[left] === strArr[right] && strArr[left] && strArr[right]) {
middleArr.unshift(strArr[left]);
middleArr.push(strArr[right]);
left--;
right++;
}
if (middleArr.length > maxStr.length) {
maxStr = middleArr.join('');
}
}
return maxStr;
}
着重于思路,代码可优化。
三、动态规划 (时间复杂度O(n^2))
1.状态转移方程:dp[ i ][ j ] = ( s[ i ] == s[ j ] ) and ( j - i < 3 or dp[ i + 1][ j - 1] )
估计之前没有接触过动态规划的同志(me too), 有点懵逼。在理解这个状态转移方程之前,先概述一下动态规划的思想:
当前状态需要依赖之前的状态(比如这里,当前字符串是否为回文,需要依赖他内部的字符串是否为回文),以空间换时间。
到这里还是很懵逼,稳住,文章后面还有~~~
2. 用动态规划求回文字符串的思路 (用例:'babab')
图 3 图4
i : 当前字符串的子串的开始下标。
j : 当前字符串的子串的结束下标。
s: 当前字符串。
dp[ i ][ j ]: 以i为开始下标,j为结束下标的字符串是否为回文字符串(状态)。
图3解释:
- 我们用dp[ i ][ j ](二维数组,先不要想其空间结构)来表示下标从 i => j 的字符串是否为回文字符串,是,则其值为TRUE, 反之, 为FALSE。
- 相当于在填一个二维表,由于回文字符串是对称的,所以我们只需要考虑 i <= j 的情况,即 i 始终为回文字符串头下标, j 始终为回文字符串尾下标。
- 如图3中, dp[ 0 ][ 1 ]表示下标 0 到 1 的字符串,即 ‘ba’,这当然不是一个回文字符串,所以为FALSE。
状态依赖(dp[ i + 1][ j - 1]):
- 我们可以想象一下,字符串 ‘babab’ 是否为回文字符串,是不是要求其中间 ‘aba’ 必须为回文字符串,‘bab’ 是否为回文字符串,又要求 ‘a’ 为回文字符串。所以前面dp[ i + 1][ j - 1]表示的就是当前字符串是否为回文,需要依赖它中间的前一个字符串的状态dp[ i + 1][ j - 1],若为TRUE,则dp[ i ][ j ]也为TRUE,反之为FALSE,当然前提是s[ i ] == s[ j ]。
提示:需要仔细研究一下图3。
上面介绍了一些状态方程的核心概念,下面结合代码来分析:
function reverseStr(str) {
const strArr = str.split('');
// 用来装各个字符串的回文状态(true或者false, 后面会将其变成一个二维,如status[i][j]),
let statusArr = [];
let middleArr = [];
// 记录最长回文字符串长度,初始为1
let maxLen = 1;
// 记录最长回文字符串开始下标
let startIndex = 0;
// 初始化对角线(i = j, 即长度为1的字符串,当然为回文字符串,所以填true)
for (let i = 0; i < strArr.length; i++) {
middleArr[i] = true;
statusArr[i] = middleArr;
middleArr = [];
}
// 纵向填表,在图3中从上到下填表,填完一列,再到下一列。
for (let j = 1; j < strArr.length; j++) {
for (let i = 0; i < j; i++) {
// 当s[i] != s[j], 以i为头,j为尾的字符串 肯定不为回文字符串
if (strArr[i] !== strArr[j]) {
statusArr[i][j] = false;
} else {
// 当第i个元素等于第j个元素时s[i] == s[j]:
// 当字符串长度小于等于3(即:len = j - i + 1), 则直接为回文字符串
if (j - i < 3) {
statusArr[i][j] = true;
} else {
// 否则该字符串是否为回文,需要依赖它中间的前一个字符串(即下标i+1 到 j-1)
statusArr[i][j] = statusArr[i+1][j-1];
}
}
// 记录每次最大长度的回文字符串的头部下标startIndex,以及其长度maxLen
if (statusArr[i][j] && j - i + 1 > maxLen) {
maxLen = j - i + 1;
startIndex = i;
}
}
}
return str.substring(startIndex, maxLen + startIndex);
}
填完表后,我们可以人为直接在表中看出哪些为回文字符串,并且可以找出最长的回文字符串。
但在代码中,我们是每找到一个回文字符串,就将其与最大字符串比较,替换或丢弃。
这里动态规划解法其实就是利用二维数组来代替两层for循环,减小时间复杂度( O(n^3) => O(n^2) )。
四、Manacher算法 时间复杂度O(n))
看到这个时间复杂度为O(n),就知道这是一个“神仙算法”,其原理就是结合前面的中心扩散与动态规划。
因能力有限,无法分享更多~~
大家如果需要,请查找其他资料,笔者这里推荐一个:
好了,本章分享到此结束。
如有错误,欢迎指正,后面会继续修改~~