資料結構丨字首樹

vincent1997發表於2019-07-24

字首樹簡介

什麼是字首樹?

字首樹N叉樹的一種特殊形式。通常來說,一個字首樹是用來儲存字串的。字首樹的每一個節點代表一個字串字首)。每一個節點會有多個子節點,通往不同子節點的路徑上有著不同的字元。子節點代表的字串是由節點本身的原始字串,以及通往該子節點路徑上所有的字元組成的。

下面是字首樹的一個例子:

img

在上圖示例中,我們在節點中標記的值是該節點對應表示的字串。例如,我們從根節點開始,選擇第二條路徑 'b',然後選擇它的第一個子節點 'a',接下來繼續選擇子節點 'd',我們最終會到達葉節點 "bad"。節點的值是由從根節點開始,與其經過的路徑中的字元按順序形成的。

值得注意的是,根節點表示空字串

字首樹的一個重要的特性是,節點所有的後代都與該節點相關的字串有著共同的字首。這就是字首樹名稱的由來。

我們再來看這個例子。例如,以節點 "b" 為根的子樹中的節點表示的字串,都具有共同的字首 "b"。反之亦然,具有公共字首 "b" 的字串,全部位於以 "b" 為根的子樹中,並且具有不同字首的字串來自不同的分支。

字首樹有著廣泛的應用,例如自動補全,拼寫檢查等等。我們將在後面的章節中介紹實際應用場景。

如何表示一個字首樹?

在前面的文章中,我們介紹了字首樹的概念。在這篇文章中,我們將討論如何用程式碼表示這個資料結構。

在閱讀一下內容前,請簡要回顧N叉樹的節點結構。

字首樹的特別之處在於字元和子節點之間的對應關係。有許多不同的表示字首樹節點的方法,這裡我們只介紹其中的兩種方法。

方法一 - 陣列

第一種方法是用陣列儲存子節點。

例如,如果我們只儲存含有字母 az 的字串,我們可以在每個節點中宣告一個大小為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 的節點。

下面是一個例子:

search

我們來用虛擬碼總結一下以上策略:

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

搜尋字首

正如我們在字首樹的簡介中提到的,所有節點的後代都與該節點相對應字串的有著共同字首。因此,很容易搜尋以特定字首開頭的任何單詞。

同樣地,我們可以根據給定的字首沿著樹形結構搜尋下去。一旦我們找不到我們想要的子節點,搜尋就以失敗終止。否則,搜尋成功。為了更具體地解釋搜尋的過程,我們提供了下列示例:

search2

我們來用虛擬碼總結一下以上策略:

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

搜尋單詞

你可能還想知道如何搜尋特定的單詞,而不是字首。我們可以將這個詞作為字首,並同樣按照上述同樣的方法進行搜尋。

  1. 如果搜尋失敗,那麼意味著沒有單詞以目標單詞開頭,那麼目標單詞絕對不會存在於字首樹中。
  2. 如果搜尋成功,我們需要檢查目標單詞是否是字首樹中單詞的字首,或者它本身就是一個單詞。為了進一步解決這個問題,你可能需要稍對節點的結構做出修改。

提示:往每個節點中加入布林值可能會有效地幫助你解決這個問題。

實現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 類裡的兩個方法,insertsum

對於方法 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. 輸入只包含小寫字母。
  2. 1 <= 字典單詞數 <=1000
  3. 1 <= 句中詞語數 <= 1000
  4. 1 <= 詞根長度 <= 100
  5. 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);
};

相關文章