题目地址:
https://leetcode.com/problems/palindrome-pairs/
给定一个长 n n n的字符串数组 A A A,题目保证字符串各不相同。返回所有的数对 ( i , j ) (i,j) (i,j),使得 A [ i ] + A [ j ] A[i]+A[j] A[i]+A[j]是个回文串。
我们可以枚举
A
[
i
]
+
A
[
j
]
A[i]+A[j]
A[i]+A[j]里较长的那个字符串(其实就是遍历
A
A
A的时候枚举每个
A
[
i
]
A[i]
A[i]作为拼接里的较长的字符串,看存不存在较短的串与之拼接为回文串),如果
s
=
A
[
i
]
s=A[i]
s=A[i]作为一个较长串,存在另一个串
t
t
t与之拼接可以是回文串,我们需要枚举两种情况:
第
1
1
1种情况:
s
+
t
s+t
s+t是回文串,那么说明存在分界点
k
k
k使得
s
[
k
:
l
s
−
1
]
s[k:l_s-1]
s[k:ls−1]是回文串,并且
s
[
0
:
k
−
1
]
s[0:k-1]
s[0:k−1]与
t
t
t镜像对称。前者可以暴力判断,后者可以用字符串哈希的方式加速,具体做法是,用“左高右低”的方式,将每个字符串的哈希值和其下标存入一个HashMap里以供查询,然后遍历
s
s
s的时候,从左到右遍历,用右高左低的方式求
s
s
s前缀的哈希值,这样就可以直接在哈希表里查是否有和
s
s
s前缀镜像对称的字符串了。
注意,
k
k
k是可以取
l
A
l_A
lA的,此时
s
[
k
:
l
A
−
1
]
s[k:l_A-1]
s[k:lA−1]是空串,相当于
s
s
s整个作为左半边,右边接一个
s
s
s的镜像作为回文串,这是可以的,需要特判一下。
第
2
2
2种情况,
t
+
s
t+s
t+s是回文串,那么说明存在分界点
k
k
k使得
s
[
0
:
k
]
s[0:k]
s[0:k]是回文串,并且
s
[
k
+
1
:
l
s
−
1
]
s[k+1:l_s-1]
s[k+1:ls−1]与
t
t
t镜像对称,这时要计算
s
[
k
+
1
:
l
s
−
1
]
s[k+1:l_s-1]
s[k+1:ls−1]的哈希值,由于我们已经存了每个字符串的左高右低的哈希值,这里从右向左遍历
s
s
s的时候,就要算右高左低的哈希值,原理是类似的。这里也需要注意
k
k
k是可以取
−
1
-1
−1的,对应的是
s
s
s整个作为右半部分,左半部分的
t
t
t作为
s
s
s的镜像接在前面得到一个回文串。
还需要注意的是,如果
s
s
s和
t
t
t长度相同,
s
+
t
s+t
s+t是回文串的话,那么会在枚举
s
s
s在前和
t
t
t在后的时候都会枚举到
s
+
t
s+t
s+t,这会使得答案里有重复。所以在枚举到
t
t
t的时候我们可以直接略过与之长度相等的字符串。代码如下:
import java.util.*;
public class Solution {
public List<List<Integer>> palindromePairs(String[] words) {
List<List<Integer>> res = new ArrayList<>();
// 将每个字符串的“左高右低”的哈希值及其下标存入一个哈希表里以供查询
Map<Long, Integer> map = new HashMap<>();
for (int i = 0; i < words.length; i++) {
map.put(hash(words[i]), i);
}
long P = 131L;
// 枚举长的
for (int i = 0; i < words.length; i++) {
String s = words[i];
// 初始hash为0,表示空串的哈希值
long pow = 1, hash = 0;
// 长在前,枚举分界点k,并且考虑s[k : ]是回文串的情况,找s[0 : k - 1]镜像
for (int k = 0; k <= s.length(); k++) {
// 这里计算哈希值的方式是从左到右、左低右高
if (k >= 1) {
hash += s.charAt(k - 1) * pow;
pow *= P;
}
if (isPalin(s, k, s.length() - 1)) {
int idx = map.getOrDefault(hash, -1);
if (idx != -1 && idx != i) {
res.add(Arrays.asList(i, idx));
}
}
}
hash = 0;
// 长在后,枚举分界点k,并且考虑s[0 : k-1]是回文串的情况,找s[k : ]的镜像
for (int k = s.length(); k >= 0; k--) {
// 这里计算哈希值的方式是从右到左、左低右高
if (k < s.length()) {
hash = hash * P + s.charAt(k);
}
if (isPalin(s, 0, k - 1)) {
int idx = map.getOrDefault(hash, -1);
if (idx != -1 && idx != i) {
if (words[idx].length() == s.length()) {
continue;
}
res.add(Arrays.asList(idx, i));
}
}
}
}
return res;
}
private boolean isPalin(String s, int l, int r) {
while (l < r) {
if (s.charAt(l) != s.charAt(r)) {
return false;
}
l++;
r--;
}
return true;
}
private long hash(String s) {
long hash = 0, P = 131L;
for (int i = 0; i < s.length(); i++) {
hash = hash * P + s.charAt(i);
}
return hash;
}
}
时间复杂度 O ( n l 2 ) O(nl^2) O(nl2), l l l是最长字符串长度,空间 O ( n ) O(n) O(n)。
不用字符串哈希的话会更好写一些,直接枚举 s s s的分界点 k k k,形成两个字符串 a = s [ 0 : k − 1 ] a=s[0:k-1] a=s[0:k−1]还有 b = s [ k : ] b=s[k:] b=s[k:],那么 s s s在前相当于 b b b回文并且存在 a a a的镜像, s s s在后相当于 a a a回文并且存在 b b b的镜像,所以可以把每个字符串翻转后存入一个HashMap。代码如下:
import java.util.*;
public class Solution {
public List<List<Integer>> palindromePairs(String[] words) {
List<List<Integer>> res = new ArrayList<>();
Map<String, Integer> map = new HashMap<>();
for (int i = 0; i < words.length; i++) {
map.put(new StringBuilder(words[i]).reverse().toString(), i);
}
for (int i = 0; i < words.length; i++) {
String s = words[i];
for (int k = 0; k <= s.length(); k++) {
String left = s.substring(0, k), right = s.substring(k);
if (isPalin(right) && map.containsKey(left) && map.get(left) != i) {
res.add(Arrays.asList(i, map.get(left)));
}
if (isPalin(left) && map.containsKey(right) && map.get(right) != i && words[map.get(right)].length() != s.length()) {
res.add(Arrays.asList(map.get(right), i));
}
}
}
return res;
}
private boolean isPalin(String s) {
for (int i = 0, j = s.length() - 1; i < j; i++, j--) {
if (s.charAt(i) != s.charAt(j)) {
return false;
}
}
return true;
}
}
时空复杂度一样。