字首樹簡介
什麼是字首樹?
字首樹
是N叉樹
的一種特殊形式。通常來說,一個字首樹是用來儲存字串
的。字首樹的每一個節點代表一個字串
(字首
)。每一個節點會有多個子節點,通往不同子節點的路徑上有著不同的字元。子節點代表的字串是由節點本身的原始字串
,以及通往該子節點路徑上所有的字元
組成的。
下面是字首樹的一個例子:
在上圖示例中,我們在節點中標記的值是該節點對應表示的字串。例如,我們從根節點開始,選擇第二條路徑 'b',然後選擇它的第一個子節點 'a',接下來繼續選擇子節點 'd',我們最終會到達葉節點 "bad"。節點的值是由從根節點開始,與其經過的路徑中的字元按順序形成的。
值得注意的是,根節點表示空字串
。
字首樹的一個重要的特性是,節點所有的後代都與該節點相關的字串有著共同的字首。這就是字首樹
名稱的由來。
我們再來看這個例子。例如,以節點 "b" 為根的子樹中的節點表示的字串,都具有共同的字首 "b"。反之亦然,具有公共字首 "b" 的字串,全部位於以 "b" 為根的子樹中,並且具有不同字首的字串來自不同的分支。
字首樹有著廣泛的應用,例如自動補全,拼寫檢查等等。我們將在後面的章節中介紹實際應用場景。
如何表示一個字首樹?
在前面的文章中,我們介紹了字首樹的概念。在這篇文章中,我們將討論如何用程式碼表示這個資料結構。
在閱讀一下內容前,請簡要回顧N叉樹的節點結構。
字首樹的特別之處在於字元和子節點之間的對應關係。有許多不同的表示字首樹節點的方法,這裡我們只介紹其中的兩種方法。
方法一 - 陣列
第一種方法是用陣列
儲存子節點。
例如,如果我們只儲存含有字母 a
到 z
的字串,我們可以在每個節點中宣告一個大小為26的陣列來儲存其子節點。對於特定字元 c
,我們可以使用 c - 'a'
作為索引來查詢陣列中相應的子節點。
// change this value to adapt to different cases
#define N 26
struct TrieNode {
TrieNode* children[N];
// you might need some extra values according to different cases
};
/** Usage:
* Initialization: TrieNode root = new TrieNode();
* Return a specific child node with char c: (root->children)[c - 'a']
*/
訪問子節點十分快捷
。訪問一個特定的子節點比較容易
,因為在大多數情況下,我們很容易將一個字元轉換為索引。但並非所有的子節點都需要這樣的操作,所以這可能會導致空間的浪費
。
方法二 - Map
第二種方法是使用 Hashmap
來儲存子節點。
我們可以在每個節點中宣告一個Hashmap。Hashmap的鍵是字元,值是相對應的子節點。
struct TrieNode {
unordered_map<char, TrieNode*> children;
// you might need some extra values according to different cases
};
/** Usage:
* Initialization: TrieNode root = new TrieNode();
* Return a specific child node with char c: (root->children)[c]
*/
通過相應的字元來訪問特定的子節點更為容易
。但它可能比使用陣列稍慢一些
。但是,由於我們只儲存我們需要的子節點,因此節省了空間
。這個方法也更加靈活
,因為我們不受到固定長度和固定範圍的限制。
補充
我們已經提到過如何表示字首樹中的子節點。除此之外,我們也需要用到一些其他的值。
例如,我們知道,字首樹的每個節點表示一個字串,但並不是所有由字首樹表示的字串都是有意義的。如果我們只想在字首樹中儲存單詞,那麼我們可能需要在每個節點中宣告一個布林值(Boolean)作為標誌,來表明該節點所表示的字串是否為一個單詞。
基本操作
Insertion in Trie
我們已經在另一張卡片中討論了 (如何在二叉搜尋樹中實現插入操作)。
提問:
你還記得如何在二叉搜尋樹中插入一個新的節點嗎?
當我們在二叉搜尋樹中插入目標值時,在每個節點中,我們都需要根據 節點值
和 目標值
之間的關係,來確定目標值需要去往哪個子節點。同樣地,當我們向字首樹中插入一個目標值時,我們也需要根據插入的 目標值
來決定我們的路徑。
更具體地說,如果我們在字首樹中插入一個字串 S
,我們要從根節點開始。 我們將根據 S[0]
(S中的第一個字元),選擇一個子節點或新增一個新的子節點。然後到達第二個節點,並根據 S[1]
做出選擇。 再到第三個節點,以此類推。 最後,我們依次遍歷 S 中的所有字元併到達末尾。 末端節點將是表示字串 S 的節點。
下面是一個例子:
我們來用虛擬碼總結一下以上策略:
1. Initialize: cur = root
2. for each char c in target string S:
3. if cur does not have a child c:
4. cur.children[c] = new Trie node
5. cur = cur.children[c]
6. cur is the node which represents the string S
通常情況情況下,你需要自己構建字首樹。構建字首樹實際上就是多次呼叫插入函式。但請記住在插入字串之前要 初始化根節點
。
Search in Trie
搜尋字首
正如我們在字首樹的簡介中提到的,所有節點的後代都與該節點相對應字串的有著共同字首。因此,很容易搜尋以特定字首開頭的任何單詞。
同樣地,我們可以根據給定的字首沿著樹形結構搜尋下去。一旦我們找不到我們想要的子節點,搜尋就以失敗終止。否則,搜尋成功。為了更具體地解釋搜尋的過程,我們提供了下列示例:
我們來用虛擬碼總結一下以上策略:
1. Initialize: cur = root
2. for each char c in target string S:
3. if cur does not have a child c:
4. search fails
5. cur = cur.children[c]
6. search successes
搜尋單詞
你可能還想知道如何搜尋特定的單詞,而不是字首。我們可以將這個詞作為字首,並同樣按照上述同樣的方法進行搜尋。
- 如果搜尋失敗,那麼意味著沒有單詞以目標單詞開頭,那麼目標單詞絕對不會存在於字首樹中。
- 如果搜尋成功,我們需要檢查目標單詞是否是字首樹中單詞的字首,或者它本身就是一個單詞。為了進一步解決這個問題,你可能需要稍對節點的結構做出修改。
提示:往每個節點中加入布林值可能會有效地幫助你解決這個問題。
實現Trie(字首樹)
實現一個 Trie (字首樹),包含 insert
, search
, 和 startsWith
這三個操作。
示例:
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple"); // 返回 true
trie.search("app"); // 返回 false
trie.startsWith("app"); // 返回 true
trie.insert("app");
trie.search("app"); // 返回 true
說明:
- 你可以假設所有的輸入都是由小寫字母
a-z
構成的。 - 保證所有輸入均為非空字串。
遞迴解法
#include <iostream>
#include <vector>
#include <map>
using namespace std;
/// Trie Recursive version
class Trie{
private:
struct Node{
map<char, int> next;
bool end = false;
};
vector<Node> trie;
public:
Trie(){
trie.clear();
trie.push_back(Node());
}
/** Inserts a word into the trie. */
void insert(const string& word){
insert(0, word, 0);
}
/** Returns if the word is in the trie. */
bool search(const string& word){
return search(0, word, 0);
}
/** Returns if there is any word in the trie that starts with the given prefix. */
bool startsWith(const string& prefix) {
return startsWith(0, prefix, 0);
}
private:
void insert(int treeID, const string& word, int index){
if(index == word.size()) {
trie[treeID].end = true;
return;
}
if(trie[treeID].next.find(word[index]) == trie[treeID].next.end()){
trie[treeID].next[word[index]] = trie.size();
trie.push_back(Node());
}
insert(trie[treeID].next[word[index]], word, index + 1);
}
bool search(int treeID, const string& word, int index){
if(index == word.size())
return trie[treeID].end;
if(trie[treeID].next.find(word[index]) == trie[treeID].next.end())
return false;
return search(trie[treeID].next[word[index]], word, index + 1);
}
bool startsWith(int treeID, const string& prefix, int index){
if(index == prefix.size())
return true;
if(trie[treeID].next.find(prefix[index]) == trie[treeID].next.end())
return false;
return startsWith(trie[treeID].next[prefix[index]], prefix, index + 1);
}
};
void printBool(bool res){
cout << (res ? "True" : "False") << endl;
}
int main() {
Trie trie1;
trie1.insert("ab");
printBool(trie1.search("a")); // false
printBool(trie1.startsWith("a")); // true;
cout << endl;
// ---
Trie trie2;
trie2.insert("a");
printBool(trie2.search("a")); // true
printBool(trie2.startsWith("a")); // true;
return 0;
}
非遞迴解法
#include <iostream>
#include <vector>
#include <map>
using namespace std;
/// Trie Recursive version
class Trie{
private:
struct Node{
map<char, int> next;
bool end = false;
};
vector<Node> trie;
public:
Trie(){
trie.clear();
trie.push_back(Node());
}
void insert(const string& word){
int treeID = 0;
for(char c: word){
//若未找到該節點
if(trie[treeID].next.find(c) == trie[treeID].next.end()){
trie[treeID].next[c] = trie.size();
trie.push_back(Node());
}
treeID = trie[treeID].next[c];
}
trie[treeID].end = true;
}
bool search(const string& word){
int treeID = 0;
for(char c: word){
if(trie[treeID].next.find(c)==trie[treeID].next.end())
return false;
treeID = trie[treeID].next[c];
}
return trie[treeID].end;
}
bool startsWith(const string& prefix){
int treeID = 0;
for(char c: prefix){
if(trie[treeID].next.find(c)==trie[treeID].next.end())
return false;
treeID = trie[treeID].next[c];
}
return true;
}
};
void printBool(bool res){
cout << (res? "True" : "False") << endl;
}
int main() {
Trie trie1;
trie1.insert("ab");
printBool(trie1.search("a")); // false
printBool(trie1.startsWith("a")); // true;
cout << endl;
// ---
Trie trie2;
trie2.insert("a");
printBool(trie2.search("a")); // true
printBool(trie2.startsWith("a")); // true;
return 0;
}
實際應用I
Map Sum Pairs
實現一個 MapSum 類裡的兩個方法,insert
和 sum
。
對於方法 insert
,你將得到一對(字串,整數)的鍵值對。字串表示鍵,整數表示值。如果鍵已經存在,那麼原來的鍵值對將被替代成新的鍵值對。
對於方法 sum
,你將得到一個表示字首的字串,你需要返回所有以該字首開頭的鍵的值的總和。
示例 1:
輸入: insert("apple", 3), 輸出: Null
輸入: sum("ap"), 輸出: 3
輸入: insert("app", 2), 輸出: Null
輸入: sum("ap"), 輸出: 5
參考https://www.cnblogs.com/grandyang/p/7616525.html
#include <iostream>
#include <bits/stdc++.h>
using namespace std;
//這道題讓我們實現一個MapSum類,裡面有兩個方法,insert和sum,其中inser就是插入一個鍵值對,而sum方法比較特別,是在找一個字首,需要將所有有此字首的單詞的值累加起來返回。看到這種玩字首的題,照理來說是要用字首樹來做的。但是博主一般想偷懶,不想新寫一個結構或類,於是就使用map來代替字首樹啦。博主開始想到的方法是建立字首和一個pair之間的對映,這裡的pair的第一個值表示該詞的值,第二個值表示將該詞作為字首的所有詞的累加值,那麼我們的sum函式就異常的簡單了,直接將pair中的兩個值相加即可。關鍵就是要在insert中把資料結構建好,構建的方法也不難,首先我們suppose原本這個key是有值的,我們更新的時候只需要加上它的差值即可,就算key不存在預設就是0,算差值也沒問題。然後我們將first值更新為val,然後就是遍歷其所有的字首了,給每個字首的second都加上diff即可,參見程式碼如下:
class MapSum{
private:
unordered_map<string, pair<int, int>> m;
public:
MapSum(){}
void insert(string key, int val){
//diff的作用防止重複插入
int diff = val - m[key].first, n = key.size();
m[key].first = val;
for(int i=n-1; i>0; --i)
m[key.substr(0, i)].second += diff;
}
int sum(string prefix){
return m[prefix].first + m[prefix].second;
}
};
//下面這種方法是論壇上投票最高的方法,感覺很叼,用的是帶排序的map,insert就是把單詞加入map。在map裡會按照字母順序自動排序,然後在sum函式裡,我們根據prefix來用二分查詢快速定位到第一個不小於prefix的位置,然後向後遍歷,向後遍歷的都是以prefix為字首的單詞,如果我們發現某個單詞不是以prefix為字首了,直接break;否則就累加其val值,參見程式碼如下:
class MapSum{
private:
map<string, int> m;
public:
MapSum(){}
void insert(string key, int val){
m[key] = val;
}
int sum(string prefix){
int res = 0, n = prefix.size();
for(auto it = m.lower_bound(prefix); it != m.end(); ++it){
if(it->first.substr(0, n) != prefix) break;
res += it->second;
}
return res;
}
};
單詞替換
在英語中,我們有一個叫做 詞根
(root)的概念,它可以跟著其他一些片語成另一個較長的單詞——我們稱這個詞為 繼承詞
(successor)。例如,詞根an
,跟隨著單詞 other
(其他),可以形成新的單詞 another
(另一個)。
現在,給定一個由許多詞根組成的詞典和一個句子。你需要將句子中的所有繼承詞
用詞根
替換掉。如果繼承詞
有許多可以形成它的詞根
,則用最短的詞根替換它。
你需要輸出替換之後的句子。
示例 1:
輸入: dict(詞典) = ["cat", "bat", "rat"]
sentence(句子) = "the cattle was rattled by the battery"
輸出: "the cat was rat by the bat"
注:
- 輸入只包含小寫字母。
- 1 <= 字典單詞數 <=1000
- 1 <= 句中詞語數 <= 1000
- 1 <= 詞根長度 <= 100
- 1 <= 句中詞語長度 <= 1000
參考https://www.cnblogs.com/grandyang/p/7423420.html
#include <iostream>
#include <vector>
#include <sstream>
using namespace std;
//這道題最好的解法其實是用字首樹(Trie / Prefix Tree)來做,關於字首樹使用之前有一道很好的入門題Implement Trie (Prefix Tree)。瞭解了字首樹的原理機制,那麼我們就可以發現這道題其實很適合字首樹的特點。我們要做的就是把所有的字首都放到字首樹裡面,而且在字首的最後一個結點的地方將標示isWord設為true,表示從根節點到當前結點是一個字首,然後我們在遍歷單詞中的每一個字母,我們都在字首樹查詢,如果當前字母對應的結點的表示isWord是true,我們就返回這個字首,如果當前字母對應的結點在字首樹中不存在,我們就返回原單詞,這樣就能完美的解決問題了。所以啊,以後遇到了有關字首或者類似的問題,一定不要忘了字首樹這個神器喲~
class Solution{
public:
class TrieNode{
public:
bool isWord;
TrieNode *child[26];
// TrieNode(){};
TrieNode(){
isWord = false;
for(auto &a : child) a = NULL;
};
};
string replaceWords(vector<string>& dict, string sentence){
string res = "", t="";
istringstream is(sentence);
TrieNode* root = new TrieNode();
for(string word: dict){
insert(root, word);
}
while(is >> t){
if(!res.empty()) res += " ";
res += findPrefix(root, t);
}
return res;
}
void insert(TrieNode* node, string word){
for(char c: word){
if(!node->child[c-'a']) node->child[c-'a'] = new TrieNode();
node = node->child[c-'a'];
}
node->isWord = true;
}
string findPrefix(TrieNode* node, string word){
string cur = "";
for(char c: word){
if(!node->child[c-'a']) break;
cur.push_back(c);
node = node->child[c - 'a'];
if(node->isWord) return cur;
}
return word;
}
};
新增與搜尋單詞 - 資料結構設計
設計一個支援以下兩種操作的資料結構:
void addWord(word)
bool search(word)
search(word) 可以搜尋文字或正規表示式字串,字串只包含字母 .
或 a-z
。 .
可以表示任何一個字母。
示例:
addWord("bad")
addWord("dad")
addWord("mad")
search("pad") -> false
search("bad") -> true
search(".ad") -> true
search("b..") -> true
說明:
你可以假設所有單詞都是由小寫字母 a-z
組成的。
#include <iostream>
#include <vector>
using namespace std;
class WordDictionary{
private:
struct TrieNode{
bool isWord;
vector<TrieNode*> children;
TrieNode(): isWord(false), children(26, nullptr){}
~TrieNode(){
for(TrieNode* child: children)
if(child) delete child;
}
};
TrieNode* trieRoot;
bool myFind(string &str, TrieNode* nowPtr, int nowIndex){
int strSize = str.size();
if(nowPtr == NULL){
return false;
}
if(nowIndex >= strSize){
if(nowPtr->isWord){
return true;
}
return false;
}
else if(str[nowIndex] != '.'){
if(nowPtr->children[str[nowIndex] - 'a'] != NULL){
return myFind(str, nowPtr->children[str[nowIndex] - 'a'], nowIndex+1);
}
return false;
}
else{
for(int i=0; i<26; ++i){
if(nowPtr->children[i] != NULL && myFind(str, nowPtr->children[i], nowIndex+1 )){
return true;
}
}
}
return false;
}
public:
WordDictionary(){
trieRoot = new TrieNode();
}
void addWord(string word){
TrieNode * ptr = trieRoot;
for(auto ch : word){
if(ptr->children[ch - 'a'] == NULL){
ptr->children[ch - 'a'] = new TrieNode();
}
ptr = ptr->children[ch - 'a'];
}
ptr->isWord = true;
}
bool search(string word){
return myFind(word, trieRoot, 0);
}
};
實際應用II
陣列中兩個樹的最大異或值
給定一個非空陣列,陣列中元素為 a0, a1, a2, … , an-1,其中 0 ≤ ai < 2^31 。
找到 ai 和aj 最大的異或 (XOR) 運算結果,其中0 ≤ i, j < n 。
你能在O(n)的時間解決這個問題嗎?
示例:
輸入: [3, 10, 5, 25, 2, 8]
輸出: 28
解釋: 最大的結果是 5 ^ 25 = 28.
//https://blog.csdn.net/weijiechenlun133/article/details/70135937
class SolutionA
{
public:
int findMaximumXOR(vector<int> &nums)
{
if (nums.size() < 2) return 0;
int maxNum = 0;
int flag = 0;
for(int i = 31; i>=0; --i){
set<int> hash;
flag |= (1 << i);
for(int x:nums)
hash.insert(flag & x);
int tmp = maxNum | (1<<i);
for(int x:hash){
if(hash.find(x^tmp)!=hash.end()){
maxNum = tmp;
break;
}
}
}
return maxNum;
}
};
struct Node{
Node* next[2];
Node(){
next[0] = nullptr;
next[1] = nullptr;
}
};
class SolutionB{
public:
void buildTrieTree(Node* root, int x){
for(int i = 31; i>=0; --i){
int flag = (x & (1<<i) )? 1:0;
if(root->next[flag] == nullptr){
root->next[flag] = new Node();
}
root = root->next[flag];
}
}
int findMaxXorInTire(Node* root, int x){
int result = 0;
for(int i = 31; i>=0; --i){
int flag = (x & (1<<i) )? 0:1;
if(root->next[flag] != nullptr){
result |= (1<<i); //result = result | (1<<i)
root = root->next[flag];
}
else
root = root->next[1-flag];
}
return result;
}
int findMaximumXOR(vector<int>& nums){
if(nums.size()<2) return 0;
Node head;
for(int x : nums)
buildTrieTree(&head, x);
int maxNum = 0;
for(int x: nums){
int m = findMaxXorInTire(&head, x);
maxNum = max(maxNum, m);
}
return maxNum;
}
};
單詞搜尋II
給定一個二維網格 board 和一個字典中的單詞列表 words,找出所有同時在二維網格和字典中出現的單詞。
單詞必須按照字母順序,通過相鄰的單元格內的字母構成,其中“相鄰”單元格是那些水平相鄰或垂直相鄰的單元格。同一個單元格內的字母在一個單詞中不允許被重複使用。
示例:
輸入:
words = ["oath","pea","eat","rain"] and board =
[
['o','a','a','n'],
['e','t','a','e'],
['i','h','k','r'],
['i','f','l','v']
]
輸出: ["eat","oath"]
說明:
你可以假設所有輸入都由小寫字母 a-z
組成。
提示:
- 你需要優化回溯演算法以通過更大資料量的測試。你能否早點停止回溯?
- 如果當前單詞不存在於所有單詞的字首中,則可以立即停止回溯。什麼樣的資料結構可以有效地執行這樣的操作?雜湊表是否可行?為什麼? 字首樹如何?如果你想學習如何實現一個基本的字首樹,請先檢視這個問題: 實現Trie(字首樹)。
參考:https://blog.csdn.net/qq_41855420/article/details/88064909
#include <iostream>
#include <vector>
using namespace std;
//字首樹的程式表示
class TrieNode {
public:
bool isWord;//當前節點為結尾是否是字串
vector<TrieNode*> children;
TrieNode() : isWord(false), children(26, nullptr) {}
~TrieNode() {
for (TrieNode* child : children)
if (child) delete child;
}
};
class Solution {
private:
TrieNode * trieRoot;//構建的單詞字首樹
//在樹中插入一個單詞的方法實現
void addWord(string word) {
TrieNode *ptr = trieRoot;//掃描這棵樹,將word插入
//將word的字元逐個插入
for (auto ch : word) {
if (ptr->children[ch - 'a'] == NULL) {
ptr->children[ch - 'a'] = new TrieNode();
}
ptr = ptr->children[ch - 'a'];
}
ptr->isWord = true;//標記為單詞
}
public:
int rowSize;//board的行數
int colSize;//board的列數
vector<vector<bool>> boardFlag;//標記board[row][col]是否已使用
//以board[row][col]為中心點,四個方向進行嘗試搜尋
void dfs(vector<vector<char>>& board, vector<string> &result, string &tempRes, TrieNode * nowRoot, int row, int col) {
if (nowRoot == NULL) {
return;
}
if (nowRoot->isWord) {//如果這個單詞成功找到
result.push_back(tempRes);//放入結果
nowRoot->isWord = false;//將這個單詞標記為公共字尾 防止重複
}
string tempResAdd;
//上方測試
//如果上方未出界,沒有被使用,且nowRoot->children中存在相等的節點
if (row - 1 >= 0 && !boardFlag[row - 1][col] && nowRoot->children[board[row - 1][col] - 'a'] != NULL) {
boardFlag[row - 1][col] = true;//標記使用
tempResAdd = tempRes + char(board[row - 1][col]);
dfs(board, result, tempResAdd, nowRoot->children[board[row - 1][col] - 'a'], row - 1, col);
boardFlag[row - 1][col] = false;//取消標記
}
//下方測試
//如果下方未出界,沒有被使用,且nowRoot->children中存在相等的節點
if (row + 1 < rowSize && !boardFlag[row + 1][col] && nowRoot->children[board[row + 1][col] - 'a'] != NULL) {
boardFlag[row + 1][col] = true;//標記使用
tempResAdd = tempRes + char(board[row + 1][col]);
dfs(board, result, tempResAdd, nowRoot->children[board[row + 1][col] - 'a'], row + 1, col);
boardFlag[row + 1][col] = false;//取消標記
}
//左方測試
//如果左方未出界,沒有被使用,且nowRoot->children中存在相等的節點
if (col - 1 >= 0 && !boardFlag[row][col - 1] && nowRoot->children[board[row][col - 1] - 'a'] != NULL) {
boardFlag[row][col - 1] = true;//標記使用
tempResAdd = tempRes + char(board[row][col - 1]);
dfs(board, result, tempResAdd, nowRoot->children[board[row][col - 1] - 'a'], row, col - 1);
boardFlag[row][col - 1] = false;//取消標記
}
//右方測試
//如果右方未出界,沒有被使用,且nowRoot->children中存在相等的節點
if (col + 1 < colSize && !boardFlag[row][col + 1] && nowRoot->children[board[row][col + 1] - 'a'] != NULL) {
boardFlag[row][col + 1] = true;//標記使用
tempResAdd = tempRes + char(board[row][col + 1]);
dfs(board, result, tempResAdd, nowRoot->children[board[row][col + 1] - 'a'], row, col + 1);
boardFlag[row][col + 1] = false;//取消標記
}
}
vector<string> findWords(vector<vector<char>>& board, vector<string>& words) {
rowSize = board.size();
if (rowSize == 0) {
return {};
}
colSize = board[0].size();
boardFlag = vector<vector<bool>>(rowSize, vector<bool>(colSize, false));//構建標記容器
trieRoot = new TrieNode();//單詞字尾樹
//將單詞都放入字首樹中
for (auto word : words) {
addWord(word);
}
vector<string> result;//用於儲存結果
string tempRes;
for (int row = 0; row < rowSize; ++row) {
for (int col = 0; col < colSize; ++col) {
if (trieRoot->children[board[row][col] - 'a'] != NULL) {//搜尋
tempRes = "";
tempRes += char(board[row][col]);
boardFlag[row][col] = true;//標記使用
dfs(board, result, tempRes, trieRoot->children[board[row][col] - 'a'], row, col);
boardFlag[row][col] = false;//取消使用
}
}
}
return result;
}
};
迴文對
給定一組唯一的單詞, 找出所有不同 的索引對(i, j)
,使得列表中的兩個單詞, words[i] + words[j]
,可拼接成迴文串。
示例 1:
輸入: ["abcd","dcba","lls","s","sssll"]
輸出: [[0,1],[1,0],[3,2],[2,4]]
解釋: 可拼接成的迴文串為 ["dcbaabcd","abcddcba","slls","llssssll"]
示例 2:
輸入: ["bat","tab","cat"]
輸出: [[0,1],[1,0]]
解釋: 可拼接成的迴文串為 ["battab","tabbat"]
大多數解法都是基於hash表,看著很複雜,我找到一個可讀性比較高的版本,之後還得拿出來溫習。
#include <iostream>
#include <vector>
#include <bits/stdc++.h>
#include <string>
using namespace std;
class Solution{
public:
bool isPalindrome(string& s, int start, int end){
while(start < end)
if(s[start++] != s[end--])
return false;
return true;
}
vector<vector<int>> palindromePairs(vector<string> words){
vector<vector<int>> ans;
unordered_map<string, int> dict;
int len = words.size();
for(int i=0; i<len; i++)
dict[words[i]] = i;
for(int i=0; i<len; i++){
string cur = words[i];
int clen = cur.size();
for(int j=0; j<=clen; j++){
//找字尾
if(isPalindrome(cur, j, clen - 1)){
string suffix = cur.substr(0, j);
reverse(suffix.begin(), suffix.end());
if(dict.find(suffix)!=dict.end() && i!=dict[suffix])
ans.push_back({i, dict[suffix]});
}
//找字首
if(j>0 && isPalindrome(cur, 0, j-1)){
string prefix = cur.substr(j);
reverse(prefix.begin(), prefix.end());
if(dict.find(prefix) != dict.end() && i!=dict[prefix])
ans.push_back({dict[prefix], i});
}
}
}
return ans;
}
};
int main(){
vector<string> a = {"lls", "s", "sssll"};
Solution s = Solution();
vector<vector<int>> v = s.palindromePairs(a);
};