开始在leetcode上做题啦,以后见到有难度的/值得记录的/自己看了解析之后才能做出来的题都尽量以博客的形式做总结,加油加油?
leetcode第5题 给出字符串,要求我们从中求出最长回文子串
回文串定义
首先呢,先明确一下什么是回文串:回文串就是正序读倒序读都一样的字符串,也就是他是轴对称的(这个轴可以是字符串之间的空隙 也可以是字符本身)
判断一个字符串是否是回文串
这个任务是简单的是简单的,用双向指针的方式可以判断,可是怎么求出最长的回文子串,让我们往下看
最长回文串
方法1:暴力求解
比较暴力的方法,找到这个字符串所有的子串并进行逐一判断…
这种方法的时间是
O
(
n
3
)
O(n^3)
O(n3) ,其中子串的个数
O
(
n
2
)
O(n^2)
O(n2),每次判断回文的时间开销为
O
(
n
)
O(n)
O(n),这样看来还是比较慢的
方法2:马拉车算法
那么现在开始介绍神奇的马拉车算法
在介绍马拉车算法前,我们先来考虑下面的这个判断是否回文串的算法:
对于字符串s,我们以迭代元素为中心向两边进行拓展,判断一个字符串是不是回文子串,具体看下图
这样跟之前双向指针判断是不是回文串的方式其实差不多,只是之前是从两边向中间搜索,现在是从中间向两边搜索
那么为什么要做这样的改动呢? 回想一下一开始我们怎么描述回文串的,我们说,回文串是中心对称的,就像照镜子一样
那么我们来看下面的例子:
可以看到,在这个字符串中,以第3个元素b为中心,能构成的回文串为"aba",是原字符串的第2-4位。
以第5个元素d为中心,能构成的回文串为"abadaba",在已知这两个回文子串的情况下,我们能推导出什么有用的信息呢?
让我们来看看第7个元素b,我们会发现,他是6-8位回文的,而且回文串跟是"aba"跟我们前面提到的是一样的!
在这里我们是利用回文串的性质——镜面对称,不需要通过双向计算的方式,就可以直接推导出后面的回文子串。
同理我们可以推导以第6个元素为中心的回文串长度和 以第4个元素为中心的回文串长度相同
不过这种情况其实还是有点特殊了,那我们换一个例子再接着看:
让我们把这个字符串改变一下,假如这个字符串是下面这样
cabadabad
通过前面的迭代我们知道了第3位为中心的回文,和第5位为中心的回文,如下
那么根据我们刚刚讲的,就可以推导出第7位为中心的回文如下
但是我们观察得到,第7位的回文完整的应该是像下面这样的:
也就是,我们需要在之前推导得到的基础上再继续计算才能得到答案。
那么现在我们思考,什么情况下可以直接从推导得到答案而不用继续计算下去,什么时候需要继续计算呢。
让我们再review刚刚的例子,在已知第3位为中心的回文,和第5位为中心的回文情况下(即下图),
像我们刚刚讲的那样,第7位的回文是需要继续计算下去的。再观察第6位,第6位回文可以通过第4位回文直接得到,而不用继续计算下去。
观察对比这两个回文串的位置我们就可以看出端倪了。第5位回文串给到的信息只在第8位为止,第9位之后的都是未知信息。而以第七位为中心的回文串正好在这个已知信息和未知信息的边缘,因此需要继续计算下去
不过在实际编码的时候还有一点是需要注意的,在这里我们讨论的回文串都是以字母为对称中心的(也就是回文串为奇数),但是假如回文串是以字母之间的空隙(也就是这个回文串是偶数的)为中心那就不好处理了,因此在实际编码中我们会对原字符串进行处理,往两个字母之间的空隙加一个特殊字符"*"
比如说,原字符是这样的:
abds
那么加入了特殊字符之后是这样的
a*b*d*s*
加入特殊符号是不影响原字符串的判断的,而且把字母对称的情况和空隙对称的情况统一了,方便处理
马拉车算法将这个问题的时间复杂度降到了
O
(
n
)
O(n)
O(n),一开始不是太懂为什么是
O
(
n
)
O(n)
O(n),参考了其他博客:虽然代码本身还是两层循环,但是在循环的过程中,要么是在扩展右边界,要么就可以直接的出结论,因此算法时间可以认为到
O
(
n
)
O(n)
O(n).(不过感觉这个算法波动比较大,只能说是小于
O
(
n
2
)
O(n^2)
O(n2),但要说是不是
O
(
n
)
O(n)
O(n)我觉得我不太能确定)
最后用伪代码的形式总结一下处理的方法
- 对原字符串进行预处理,往空隙加入特殊字符"*",创建一个数组来记录以该字符为中心的回文串大小
- 遍历字符串,对于处理后的字符串中的每一个字符,我们先看看他是否在左右边界之内,如果是的话,就可以用已有的信息对称得到目前的回文
- 如果当前的回文不在左右边界之内了,那么需要继续拓展,并更新右边界
- 最后去掉特殊符号就可以得到最长回文串了
代码
C++代码如下:
class Solution {
public:
string longestPalindrome(string s) {
if (s.length() == 0)return s;
const int amount = 2000;//s最长长度为amount
int plen[amount];
int rightBound = 0;//回文串的右边界
int longestCenter = 0;//最长回文串对应的中心下标
int longestLen = 1;
int centreIndex = 0;//rightBound对应的中心下表
string ss = prepocess(s);//processString 往中间加#
int len = ss.length();
plen[0] = 1;
for (int i = 1; i < len; i++)
{
if (i < rightBound && (2 * centreIndex - i) >= 0)//在右边界之内(不等于右边界) 且镜面在字符串范围内
{
int leftBound = 2 * centreIndex - rightBound;
if ((2 * centreIndex - i) - plen[2 * centreIndex - i] / 2 > leftBound)//两个左边界比较
plen[i] = plen[2 * centreIndex - i];
else {
plen[i] = (2 * centreIndex - i) - leftBound;
plen[i] = getPlen(ss, i, plen[i] / 2);
}
int rightIBound = i + plen[i] / 2;
if (rightIBound >= rightBound) {
plen[i] = getPlen(ss, i, plen[i] / 2);
}
}
else // 没办法利用以有信息
{
plen[i] = getPlen(ss, i, 0);
}
//更新右边界
if (i + (plen[i] / 2) > rightBound)
{
rightBound = i + plen[i] / 2;
centreIndex = i;
//cout << "更新右边界为" << rightBound << endl;
}
//更新最长回文串对应的下标
if (plen[i] > longestLen)
{
longestCenter = i;
longestLen = plen[i];
}
//范围为 [longestCenter - longestLen / 2, longestCenter + longestLen / 2];
string answer(longestLen, '0');
int j = 0;
for (int i = longestCenter - longestLen / 2; i <= longestCenter + longestLen / 2; i++) {
if(i< ss.length() && ss[i] != '#')
answer[j++] = ss[i];
}
//cout << "最终answer 的长度为" << j << endl;
return answer.substr(0, j);
}
//startLen是指左/右边界的【范围】(指长度)有多大了
int getPlen(string s, int center, int startLen)//get回文串长度
{
int len = s.length();
int i, j;
i = center - startLen - 1;
j = center + startLen + 1;
for (; i >= 0 && j < len; i--, j++) {
if (i < 0 || j >= len) break;
if (s[i] == s[j]) {
startLen++;
}
else break;
}
return startLen * 2 + 1;
}
string prepocess(string s)
{
string ss(s.length() * 2, 's');
int len = s.length();
char addition = '#';
for (int i = 0; i < len; i++)
{
//往原字符串中间的空隙加字符
ss[i * 2] = s[i];
ss[i * 2 + 1] = addition;
}
return ss;
}
};
方法三:KMP
等看了KMP之后再补充…