暴力法
困难题,第一下还真没啥好的思路,直接暴力遍历再判断回文应该是不行的,复杂度太高。还是先写下来:
class Solution {
public:
vector<vector<int>> res;
bool isOk(const string &s1, const string &s2){
string s = s1 + s2;
int i = 0, j = s.size()-1;
while(i < j){
if(s[i] != s[j]) return false;
++i;--j;
}
return true;
}
vector<vector<int>> palindromePairs(vector<string>& words) {
for(int i = 0; i < words.size(); ++i){
for(int j = i+1; j < words.size(); ++j){
bool flag = isOk(words[i], words[j]);
if(flag) res.push_back({i,j});
if(flag && words[i].size() == words[j].size() || isOk(words[j], words[i]))
res.push_back({j, i});
}
}
return res;
}
};
emmm…很简单很直接也很拉跨,直接就超时了,不过也在意料之中。
哈希表
遇事不决先瞅瞅标签,字典树,哈希表。。。
先分析一下规律:
- 两个单词a, b能够组成回文对,且长度相同,那么(a, b),(b, a)都可以。
- 难点在于长度不同的时候,比如"lls", 和"sssll"这样的,能够组成回文对的条件是:短的那个是长的那个的逆序后缀(就是长的那个先翻转(“llsss”)过来,然后短的这个是长的那个的前缀(llsss)),然后长的这个除去那部分前缀的剩下部分(llsss)还得是回文串才行。
第二点总结就是:较长的单词逆序后,分割成两部分,一部分得和较短的单词相同,另一部分得是回文串。这两部分谁都可能在前面。
C++、字典树,注释详细 - 回文对 - 力扣(LeetCode):这个的图很形象
代码可以参考:[前缀树]leetcode336:回文对(hard)_algsup-CSDN博客_前缀树 回文
class Solution {
public:
vector<vector<int>> res;
unordered_map<string, int> map;
set<int> set;//用来保存长度,有序
bool isOk(string word, int left, int right)
{
while(left<right)
{
if(word[left++]!=word[right--])
return false;
}
return true;
}
vector<vector<int>> palindromePairs(vector<string>& words) {
for(int i = 0; i < words.size(); ++i){//建立map和set
map[words[i]] = i;
set.insert(words[i].size());
}
for(int i = 0; i < words.size(); ++i){
auto word = words[i];
auto size = word.size();
reverse(word.begin(), word.end());//翻转
//长度相等时,直接翻转后查找就行(注意排除叠词比如"bbb"翻转后还是"bbb"的情况)
if(map.count(word) != 0 && map[word] != i) res.push_back({i, map[word]});
//除了上面的情况,还要判断所有比words短的单词,是否可以和word构成回文对
auto up_bound = set.find(size);//bound是word长度上界
for(auto it = set.begin(); it != up_bound; ++it){
auto len = *it;
if(isOk(word, 0, size-len-1) && map.count(word.substr(size-len)))
res.push_back({i, map[word.substr(size-len)]});
if(isOk(word, len, size-1) && map.count(word.substr(0, len)) )
res.push_back({map[word.substr(0, len)], i});
}
}
return res;
}
};
代码逻辑其实很简单,对于每一个单词,我们先考虑该单词翻转后的单词是不是也在哈希表里面,是的话得到一组长度相同的匹配。然后,我们还要考虑该单词能不能和比它短的那些单词构成回文串,判断方法就是上面说的切成两部分判断。
可以看到该方法其实还是有点暴力的,不过使用了哈希表提升了效率。
字典树
字典树的实现就不用说了,这儿可以将单词逆序插入字典树,这样后面检索的时候就不用进行翻转了,可以节省一小点时间。因为插入字典树是逐个字符插入的,正序插、逆序插并没有区别。
代码重点还是单词切割成两部分判断,其他的地方采用什么数据结构只是为了检索或者判断算法服务的。
class Trie{
public:
struct TrieNode{
int ID = -1;//该单词在原words中的下标
TrieNode* next[26] = {NULL};
};
Trie() : root(new TrieNode){}
void insert(const string &word, int index){
auto p = root;
for(int i = word.size()-1; i >= 0; --i){//这儿逆序插入
int idx = word[i] - 'a';
if(p->next[idx] == NULL) p->next[idx] = new TrieNode;
p = p->next[idx];
}
p->ID = index;
}
int search(const string &word, int l, int r){
auto p = root;
for(int i = l; i <= r; ++i){
int idx = word[i] - 'a';
if(p->next[idx] == NULL) return -1;//没找到
p = p->next[idx];
}
return p->ID;//返回该单词在原words中的下标
}
private:
TrieNode* root;
};
class Solution {
public:
vector<vector<int>> res;
vector<vector<int>> palindromePairs(vector<string>& words) {
int n = words.size();
auto trieTree = new Trie();//构建前缀树并(逆序)插入单词
for(int i = 0; i < n; ++i) trieTree->insert(words[i], i);
for(int i = 0; i < n; ++i){//对于words中每个单词
int m = words[i].size();
//开始取word的子串进行判断
//idx != i是因为 "aaa"这样的字符串查询结果就是idx == i的,两者指向同一字符串
for(int j = 0; j < m+1; ++j){
if(isOk(words[i], j, m-1)){//截取的后一部分是回文
int idx = trieTree->search(words[i], 0, j-1);
if(idx != -1 && idx != i) res.push_back({i, idx});
}
if(j > 0 && isOk(words[i], 0, j-1)){//截取的前一部分是回文
int idx = trieTree->search(words[i], j, m-1);
if(idx != -1 && idx != i) res.push_back({idx, i});
}
}
}
return res;
}
private:
bool isOk(const string& s, int l, int r){//判断是否是回文
while(l < r){
if(s[l] != s[r]) return false;
++l;--r;
}
return true;
}
};
代码虽然看起来有点长,但是主要是字典树的实现代码比较多(但是并不难)。不过上述的代码还有优化的空间,因为单词切割那儿上述代码每种可能的方式都进行了尝试。这样其实是没有必要的(参考哈希表方法里面的处理),因为某些切割毫无意义,原words里根本就没有这个长度的单词。
所以可以再加一个set来保存单词长度。