leetcode学习笔记05
开始之前
两周前去考了新Toeic的Listening&writing , 今天可以查成绩了. 一看814. 挺意外, 原本想着600多就差不多了.
这个世界还是挺公平的, 想得到什么, 就得付出相应的代价. 每天上下班在电车上背单词,虽然辛苦, 功夫没白下.
也希望自己可以慢慢的把leetcode 的专题能慢慢的补起来.
想想自己也是懒, 题都做到300了, 才想起来整理.
问题
Given a string s, return the longest palindromic substring in s.
Example 1:
Input: s = “babad”
Output: “bab”
Note: “aba” is also a valid answer.
Example 2:
Input: s = “cbbd”
Output: “bb”
Example 3:
Input: s = “a”
Output: “a”
Example 4:
Input: s = “ac”
Output: “a”
Constraints:
- 1 <= s.length <= 1000
- -s consist of only digits and English letters (lower-case and/or upper-case),
思考
最长回文串问题.
leetcode上第一个让我觉得我长了个假脑子的问题.
方法1
首先当然是暴力破解.
对字符串s进行遍历. 每次遍历时, 分为两种情况
- 以当前字符为中心, 比如 bab 中, 以a为中心
- 以当前字符和后一个字符的中间为字符, 比如baab 中, 以两个a中间为中心
上面两种情况分别对应着奇数长度的回文字符和偶数长度的回文字符.
然后就是在每次遍历时记录下长度和起始/终止位置, 如果发现更长的回文就进行更新.
最后返回结果.
时间复杂度
O
(
n
2
)
O(n^2)
O(n2).
空间复杂度
O
(
1
)
O(1)
O(1).
class Solution {
public String longestPalindrome(String s) {
// 字符串长度, 最大回文长度
int len = s.length(), maxlen = 0;
// 最大回文的起始位置, 终止位置
int res_left = 0, res_right = 0;
for(int i = 0; i < len; i++){
// 奇数长度的回文
int left = i - 1, right = i + 1;
// 当起始位置,终止位置都存在, 并且对应的字符相等的时候
while(left >= 0 && right < len && s.charAt(left) == s.charAt(right)){
// 如果当前长度大于最大回文长度,那么更新
if(right - left + 1 > maxlen){
maxlen = right - left + 1;
res_left = left;
res_right = right;
}
// 检索范围继续扩大
left--;
right++;
}
// 偶数长度的回文
left = i;
right = i + 1;
// 当起始位置,终止位置都存在, 并且对应的字符相等的时候
while(left >= 0 && right < len && s.charAt(left) == s.charAt(right)){
// 如果当前长度大于最大回文长度,那么更新
if(right - left + 1 > maxlen){
maxlen = right - left + 1;
res_left = left;
res_right = right;
}
// 检索范围继续扩大
left--;
right++;
}
}
return s.substring(res_left, res_right + 1);
}
}
另一种写法
class Solution {
public String longestPalindrome(String s) {
if(s.length() < 2)
return s;
// 字符串长度, 最长回文的中间位置, 最长回文的长度
int n = s.length(), pos = 0, max = 0;
for(int i = 0; i < n; i++){
int len = Math.max(getMax(s, i, i), getMax(s, i, i + 1));
if(len > max){
pos = i;
max = len;
}
}
// java中int的自动向下取整
// 无论回文字符串的长度是奇数还是偶数,都能得到正确的结果
return s.substring(pos - (max - 1)/2, pos + max/2 + 1);
}
private int getMax(String s, int i, int j){
int res = 0;
while(i >= 0 && j < s.length() && s.charAt(i) == s.charAt(j)){
res = j - i + 1;
i--;
j++;
}
return res;
}
}
方法2
MANACHER’S ALGORITHM, 中文名马拉车算法. 这个翻译我是服气的.
1.构造字符数组arr
回文有奇数长度和偶数长度两种可能, 那么通过添加#构造的字符数组, 可以保证最后得到最长回文肯定是字符长度的.
另外,其实不一定要添加#, 任意在原始字符中肯定不存在的合法字符都可以.
在最原始的算法中, 最两端的添加的字符其实是和中间添加的字符是不一样的.其实都可以得到正确的结果. 条条大路通罗马.
原字符串s b a b a d
字符数组arr # b # a # b # a # d #
原始算法版本 $ b # a # b # a # d %
2.构造辅助数组P
对新字符构造一个辅助数组P , P[ i ]表示以第 i 位字符为中心的回文的半径(不包括第 i 位字符)
arr # b # a # b # a # d #
P 0 1 0 3 0 3 0 1 0 1 0
3.由 M a x ( P [ i ] ) Max(P[i]) Max(P[i])求得最后的结果
这里根据 M a x ( P [ i ] ) Max(P[i]) Max(P[i])的奇偶性, 有两种情况
A>奇数
index : 0 1 2 3 4
原字符串s : b a b a d
index : 0 1 2 3 4 5 6 7 8 9 10
new : # b # a # b # a # d #
P : 0 1 0 3 0 3 0 1 0 1 0
有两个最大的
P
[
i
]
P[i]
P[i], 我们取第一个, 于是
P
[
i
]
m
a
x
=
3
,
i
m
a
x
=
3
P[i]_{max}=3, i_{max} = 3
P[i]max=3,imax=3, 一通操作得到在原字符串中对应回文的开始位置为0, 结束位置为2.
好吧开玩笑, 具体的计算公式为为
i
s
t
a
r
t
=
(
i
−
P
[
i
]
)
2
,
i
e
n
d
=
(
i
+
P
[
i
]
)
2
−
1
i_{start} =\frac{(i-P[i]) }{2} , i_{end} =\frac{(i+P[i])} {2} - 1
istart=2(i−P[i]),iend=2(i+P[i])−1.
你要是问我这个公式是怎么得出来的, 我只能说, 天知道. 反正我不知道.
B>偶数
index : 0 1 2 3 4 5
原字符串s : b a b b a d
index : 0 1 2 3 4 5 6 7 8 9 10 11 12
new : # b # a # b # b # a # d #
P : 0 1 0 3 0 1 4 1 0 1 0 1 0
P
[
i
]
m
a
x
=
4
,
i
m
a
x
=
6
P[i]_{max}=4, i_{max} = 6
P[i]max=4,imax=6, 一通操作,得到原字符串中对应回文的开始位置和结束位置分别为1 和 4.
特别的巧, 计算公式和上面奇数的竟然一样.
4.灵魂
算法的灵魂其实是在第二步,如何在线性时间内构建辅助数组P.
定义center,有
center + p[center] : 到目前为止子回文串能达到的最右边的位置
center : 回文串的中心
p[center] : 回文串的半径
当我们已知center时, 对于center后面的位置 i , 有如下两种情况
- 当 i > c e n t e r + p [ c e n t e r ] i > center + p[center] i>center+p[center]时, 即 i 在已知回文的范围以外, 所以没有取巧的办法,只能老老实实的用方法1(中心扩展)来求解P[i]
- 当
i
≤
c
e
n
t
e
r
+
p
[
c
e
n
t
e
r
]
i \le center + p[center]
i≤center+p[center]时, 参照下图, 存在与i 对应的 j ,并且
i
+
j
=
2
∗
c
e
n
t
e
r
=
>
j
=
2
∗
c
e
n
t
e
r
−
i
i + j = 2 * center => j = 2 * center - i
i+j=2∗center=>j=2∗center−i
这时有两种情况
a. 当 i + P [ j ] < c e n t e r + P [ c e n t e r ] i +P[ j ] < center + P[center] i+P[j]<center+P[center]时, P [ i ] P[ i ] P[i]的初始值就是 P [ j ] P[ j ] P[j],
b. 当 i + P [ j ] > c e n t e r + P [ c e n t e r ] i +P[ j ] > center + P[center] i+P[j]>center+P[center]时, P [ i ] P[ i ] P[i]的初始值就是 c e n t e r + P [ c e n t e r ] − i center + P[center] - i center+P[center]−i
也就是对于dp[ i ]的初始值是P[ j ] 和 center + P[center] - i中比较小的那一个.得到初始值之后,再用中心扩展算法来计算最终的P[ i ]. 这样就可以尽可能的利用已经得到的P的数据, 来避免重复计算.
时间复杂度
O
(
n
)
O(n)
O(n).
空间复杂度
O
(
n
)
O(n)
O(n).
class Solution {
public String longestPalindrome(String s) {
int n = s.length();
if(n < 2)
return s;
//预处理
int len = 2*n + 1;
char[] arr = new char[len];
arr[0] = '#';
for(int i = 0; i < n; i++){
arr[2*i + 1] = s.charAt(i);
arr[2*i + 2] = '#';
}
int center = 0, centerRes = 0;
int[] dp = new int[len];
for(int i = 1; i < len; i++){
if(i < center + dp[center])
dp[i] = Math.min(dp[2*center - i], center + dp[center] - i);
// 中心拓展算法, 求最大半径
int left = i - dp[i] - 1, right = i + dp[i] + 1;
while(left >= 0 && right < len){
if(arr[left--] == arr[right++])
dp[i]++;
else
break;
}
// 更新回文串能到达的最右位置
if(i + dp[i] > center + dp[center] ){
center = i;
}
// 更新最长回文串的中心位置
if(dp[center] > dp[centerRes]){
centerRes = center;
}
}
return s.substring((centerRes - dp[centerRes])/2, (centerRes + dp[centerRes])/2);
}
}