示例 1:
输入:words = ["abcd","dcba","lls","s","sssll"]
输出:[[0,1],[1,0],[3,2],[2,4]]
解释:可拼接成的回文串为 ["dcbaabcd","abcddcba","slls","llssssll"]
示例 2:
输入:words = ["bat","tab","cat"]
输出:[[0,1],[1,0]]
解释:可拼接成的回文串为 ["battab","tabbat"]
示例 3:
输入:words = ["a",""] 输出:[[0,1],[1,0]]
提示:
1 <= words.length <= 5000
0 <= words[i].length <= 300
words[i]
由小写英文字母组成
解法1:暴力数组法
我们知道要单词i和单词j能组成回文,那么单词i的第一个字符肯定和单词j的最后一个字符相等。
那么,我们可以把最后一个字符相同的单词放到同一个字符为下标的数组里。在遍历比较的时候,通过单词i取它的首字母为下标的列表里的值list(word[j])进行逐个比较,判断若是回文则直接输出。
代码如下:
class Solution {
public List<List<Integer>> palindromePairs(String[] words) {
int len = words.length;
Map<String, Integer> indexMap = new HashMap<>(len * 4/3);
// char list, contains ends with the character
List[] postfixList = new List[128];
for (int i=0; i<len; i++) {
// save word index
indexMap.put(words[i], i);
int wordLen = words[i].length();
// skip empty ""
if (wordLen > 0) {
char lastChar = words[i].charAt(wordLen-1);
if (postfixList[lastChar] == null) {
postfixList[lastChar] = new ArrayList<>();
}
postfixList[lastChar].add(words[i]);
}
}
List<List<Integer>> result = new ArrayList<>();
boolean containsEmpty = indexMap.containsKey("");
for (int i=0; i<len; i++) {
String word = words[i];
int wordLen = word.length();
// skip empty string
if (wordLen == 0) {
continue;
}
// process postfix list
char firstChar = word.charAt(0);
List<String> posts = postfixList[firstChar];
if (posts != null) {
for(int j=0; j<posts.size(); j++) {
String candidate = posts.get(j);
if(!word.equals(candidate) && isPalindrome(word + candidate)) {
System.out.println("postfix word: "+ words[i]+", candidate:"+candidate);
result.add(Arrays.asList(i, indexMap.get(candidate)));
}
}
}
// process empty string if existed
if (containsEmpty && isPalindrome(word)) {
int emptyIndex = indexMap.get("");
result.add(Arrays.asList(i, emptyIndex));
result.add(Arrays.asList(emptyIndex, i));
}
}
return result;
}
public boolean isPalindrome(String str) {
char[] chars = str.toCharArray();
for (int i=0, j=chars.length-1; i<j; i++,j--) {
if (chars[i] != chars[j]){
return false;
}
}
return true;
}
}
运行结果:跑到第134个测试用例超时。
分析原因发现:测试用例134大部分是以相同字母结尾的,那么就退变成每个单词都要比较所有单词,所以效率低。
解法2:前缀树(字典树)
1)使用前缀树来存储逆序后的单词。
2)把单词word[i]按字母拆分 prefix:string[0,i], postfix [i+1,len-1],有下面两种情况:
i) 假如后半部分为回文,则用前半分去查询前缀树得到对于的翻转串 s' (对应的单词j)。 prefix + palindrome(postfix) + s' 那么组成的新词一定为回文,得到的结果为(i,j);
ii) 假如前半部分为回文,在用后半部分去查询前缀树得到的翻转串s' (对应的单词j)。s'+palindrome(prefix)+postifix, 得到的结果为(j, i)
字典树的参考实现:
java - 一文搞懂字典树 - bigsai - SegmentFault 思否
Java数据结构---Trie(字典树/前缀树)_java 数据结构 trible-优快云博客
代码如下:
class Solution {
public List<List<Integer>> palindromePairs(String[] words) {
// build trie tree with reversed word
int len = words.length;
TrieTree tree = new TrieTree();
for(int i=0; i<len; i++) {
StringBuffer reversed = new StringBuffer(words[i]).reverse();
// full reverse
tree.insert(reversed, i);
}
List<List<Integer>> result = new ArrayList<>();
for (int i = 0; i < len; i++) {
String word = words[i];
for (int j=1; j<=word.length(); j++) {
String prefix = word.substring(0, j);
String postfix = word.substring(j);
if (isPalindrome(prefix)) {
// search word in the reversed trie tree
int wordIndex = tree.search(postfix);
if (wordIndex >= 0 && wordIndex != i) {
result.add(List.of(wordIndex, i));
}
}
if (isPalindrome(postfix)) {
// search word in the reversed trie tree
int wordIndex = tree.search(prefix);
if (wordIndex >= 0 && wordIndex != i) {
result.add(List.of(i, wordIndex));
}
}
}
// process empty string
if (word.length() == 0) {
for (int j=0; j<len; j++) {
if (i != j && isPalindrome(words[j])) {
result.add(List.of(j, i));
}
}
}
}
return result;
}
/**
* double pointer checking palindrome.
*
* @param wordChars
* @param candidate
* @return
*/
public boolean isPalindrome(String str) {
char[] chars = str.toCharArray();
for (int i=0, j=chars.length-1; i<j; i++,j--) {
if (chars[i] != chars[j]){
return false;
}
}
return true;
}
}
class TrieTree {
private TrieNode root;
public TrieTree() {
root = new TrieNode();
}
/**
* build trie tree and save index into end node.
* @param word
* @param index
*/
public void insert(StringBuffer word, int index) {
TrieNode current = root;
int len = word.length();
for (int i=0; i<len; i++) {
char ch = word.charAt(i);
current.children.putIfAbsent(ch, new TrieNode());
current = current.children.get(ch);
}
current.wordIndex = index;
}
/**
* search the word in trie tree.
* @param word
* @return
*/
public int search(String word) {
TrieNode current = root;
for (char ch: word.toCharArray()) {
TrieNode node = current.children.get(ch);
if (node == null) {
return -2;
}
current = node;
}
return current.wordIndex;
}
public boolean isPalindrome(StringBuilder str) {
int len = str.length();
for (int i=0, j=len-1; i<j; i++,j--) {
if (str.charAt(i) != str.charAt(j)){
return false;
}
}
return true;
}
public class TrieNode {
// if this is a word, wordIndex >= 0, or else = -1
int wordIndex = -1;
Map<Character, TrieNode> children = new HashMap<>();
}
}
运行结果如下:
前进了一小步,测试用例从134进步到了135,还是未通过。
分析原因:发现这里给的词都是对称的长度300,在150,151开始变化,前缀树深度很深在150~151才开始分化。
优化:既然前缀树退化成了链表,需要全部遍历才能获取结果。那么使用HashMap是不是应该很快呢?
解法3:HashMap存储逆序字符串
算法还是算法2的步骤,只不过把存储逆序的结构换成了HashMap,这个才更好理解和快速嘛。
代码如下:
class Solution {
public List<List<Integer>> palindromePairs(String[] words) {
// build trie tree with reversed word
int len = words.length;
Map<String,Integer> reversedMap = new HashMap<>(len*4/3);
for(int i=0; i<len; i++) {
StringBuffer reversed = new StringBuffer(words[i]).reverse();
reversedMap.put(reversed.toString(), i);
}
List<List<Integer>> result = new ArrayList<>();
for (int i = 0; i < len; i++) {
String word = words[i];
char[] wordChars = word.toCharArray();
for (int j=1; j<=wordChars.length; j++) {
// prefix is palindrome, search postfix
if (isPalindrome(wordChars, 0, j)) {
// search word in the reversed map
Integer wordIndex = reversedMap.get(word.substring(j));
if (wordIndex != null && wordIndex >= 0 && wordIndex != i) {
result.add(List.of(wordIndex, i));
}
}
// postfix is palindrome, search prefix
if (isPalindrome(wordChars, j, wordChars.length)) {
// search word in the reversed map
Integer wordIndex = reversedMap.get(word.substring(0, j));
if (wordIndex != null && wordIndex >= 0 && wordIndex != i) {
result.add(List.of(i, wordIndex));
}
}
}
// process empty string
if (word.length() == 0) {
for (int j=0; j<len; j++) {
char[] chars = words[j].toCharArray();
if (i != j && isPalindrome(chars, 0, chars.length)) {
result.add(List.of(j, i));
}
}
}
}
return result;
}
/**
* double pointer checking palindrome, not included end; [start, end)
*
* @param wordChars
* @param candidate
* @return
*/
public boolean isPalindrome(char[] wordChars, int start, int end) {
for (int i=start, j=end-1; i<j; i++,j--) {
if (wordChars[i] != wordChars[j]){
return false;
}
}
return true;
}
}
执行结果: