数据结构-----Trie树
Trie樹
Trie樹,又稱字典樹,前綴樹,單詞查找樹。是字符串算法中一個比較基礎的結構。在字符串查找方面有著線性時間的查找速度,是因為查找時間與Trie中的數據總量無關,只與待查找的字符串的長度有關。
字典樹可以應用在多數字符串查找問題上,
比如說,給定一個非常大的文本,文本中每一行是一個單詞,然后查詢文本中是否包含某個單詞,或者詢問某個單詞出現的次數。
再比如Trie + KMP算法就構成了AC自動機,可以實現多模式匹配問題。
當然首先,需要學會如何構建一棵Trie樹。
Trie樹的思想是利用詞的公共前綴進行存儲,這也正是樹結構天生自帶的優勢,兩個詞具有公共前綴就意味著具有相同的父節點,而公共前綴就是從根節點到父節點這條路徑所表示的內容。
Trie樹的構建有array和linked-list兩種,本文只介紹利用數組構建的方法。
Trie樹節點
要構建一棵Trie樹,首先應該解決的問題是,如何定義樹節點。
假設Trie樹只存儲英文單詞,只由26個小寫英文字母組成,那么從一個節點出發,就有26種可能,也就是每個節點都有26個孩子節點(如果某個節點表示的是字符a,那么緊接著它的字符可能是a,b,c,d,e,…,z中的任何一個,因為英文單詞有很多種)。
注:但是因為開辟的大小是事先規定好的,所以能夠存儲的范圍非常有限。如果想要存儲中文字符,可以使用《雙數組Trie數》。
如圖,每個節點表示一個英文字母,同時每個節點又有26個孩子節點,這些孩子節點分別表示從a到z的英文字母。這樣,當從根節點沿著一條路徑走下來后,將所有經過的節點表示的字母連接起來,就是一個完整的英文單詞。
另外需要注意的是,構造Trie樹時是每次插入一個完整的單詞,所以只有當沿著某條路徑走到特定節點后,連接起來的單詞才是完整的單詞,所以在每個節點中需要有一個bool型變量記錄從根節點到當前結點這條路徑表示的單詞是否存在。
這樣就可以為每一個樹節點都開辟一個數組來存儲孩子節點指針,像這樣:
const size_t LETTER_SIZE = 26; class TrieNode { public:TrieNode(const char& pAlpha = '\0'):m_pAlpha(pAlpha),m_pExist(false),m_pString(""){m_pNext = new TrieNode*[LETTER_SIZE];for(size_t i = 0; i < LETTER_SIZE; ++i)m_pNext[i] = NULL;}char m_pAlpha;bool m_pExist; //記錄從根節點到當前結點所構成的字符串是詞典中的單詞string m_pString; //記錄從根節點到當前結點所構成的字符串(單詞),只有當m_pExist為true時該變量才有實際意義TrieNode **m_pNext; //存儲孩子節點指針的數組 }在實際應用中,還可以根據需要為TrieNode增加多個成員變量,比如說
size_t m_pNextSize; //記錄next數組中有多少個非空指針,即有多少個孩子不是NULL size_t m_pCount; //在統計某個單詞出現次數時使用定義中將m_pNext數組中的每一元素都設置成NULL,表示這個節點沒有孩子節點,也就是沒有其它英文字母在它的后面。而且為了簡便,可以認為表示a到z的孩子節點在next數組中是按順序存儲的,這樣因為小寫字母a到z的ASCII碼是從97開始的,所以在m_pNext數組內想要確定哪個單詞存在,就可以直接判斷m_pNext[pAlpha - 97]是否是NULL即可。pAlpha可以是從a到z的任意字符。
像這樣:
注:對于上面的第二張圖,根節點的左孩子節點表示字符a,它的26個孩子節點中只有表示字符b,d,f的節點存在。所以很顯然利用數組表示的Trie會存在大量的空間浪費。
另外可能也注意到了Trie樹的根節點root,它不表示任何字符,只用來分出不同的字符。
對于Trie樹,構建操作主要就是將詞典中的單詞拆成一個個字符,每個字符申請一個節點,后一個字符作為前一個字符的孩子。當申請完一個單詞的所有節點后,需要把最后一個節點的m_pExist設置成true,表示從根到目前節點所表示的字符串在詞典中存在。
插入函數
在Trie樹的類中,使用insert()函數實現上述對每個單詞處理然后添加的操作
class Trie { public:Trie();~Trie();void insert(const string& pKey); bool exist(const string& pKey); private:TrieNode *m_pRoot; }插入函數的思路如下:
1.先判斷是否已經申請了前綴部分的節點,因為具有公共前綴的單詞只有一份前綴。例如上面的”abc”,”ada”和”af”,三者具有相同的前綴’a’,所以在添加”abc”后添加”ada”時,就不需要再為第一個字符’a’申請節點了,直接移動到字符’a’,為表示字符’a’的節點申請表示字符’d’的孩子。”af”也是如此。
2.不斷遍歷,如果存在當前要申請的節點,則不用再次申請內存,直接移動到那個位置。
3.為最后一個節點的exist變量賦值為true。
代碼實現如下:
void Trie::insert(const string& pKey) {TrieNode* pNode = m_pRoot; //從根節點開始尋找是否已經申請了某些節點for(size_t i = 0; i < pKey.length(); ++i){char pAlpha = pKey.at(i);//如果當前結點沒有表示pAlpha的孩子節點時,申請節點if(pNode->m_pNext[pAlpha - 97] == NULL){TrieNode *ppNode = new TrieNode(pAlpha);pNode->m_pNext[pAlpha - 97] = ppNode;}//移動到表示pAlpha的節點,繼續申請pNode = pNode->m_pNext[pAlpha - 97];}//將最后一個節點的m_pExist變量設置為true,表示單詞存在pNode->m_pExist = true;pNode->m_pString = pKey; }判斷某個字符串是否出現在詞典中
insert函數是用于構造Trie樹,而exist函數則是為了解決給定文本文件,每行為一個單詞,然后給定一個單詞問是否在其中出現的問題。首先需要將文本文件讀入,將每一行的單詞使用insert函數插入到Trie樹中,然后進行查詢。因為使用的是數組,而某個字符是否存在只需要判斷當前結點是否有表示這個字符的孩子節點即可,所以查詢的速度非常快,也只有要查詢的字符串的長度有關。
bool Trie::exist(const string& pKey) {TrieNode *pNode = m_pRoot;for(size_t i = 0; i < pKey.length(); ++i){char pAlpha = pKey.at(i);if(pNode->m_pNext[pAlpha - 97] != NULL){pNode = pNode->m_pNext[pAlpha - 97];}else{return false;}}return true; }問題
由于每個節點的next數組不可能都有元素存在,而更可能的情況是每個節點的next數組的元素都比較少,造成了大量的空間浪費,要想解決這一問題,可以考慮用鏈表將孩子節點連接起來,但是在查詢的過程中就會比較耗時。
另一種解決辦法是采用雙數組Trie樹,僅用兩個數組描述的字典樹,同時可以處理各種字符,比較實用。
注:
圖一來自于http://www.cnblogs.com/en-heng/p/6265256.html
總結
以上是生活随笔為你收集整理的数据结构-----Trie树的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 0/1背包问题-----回溯法求解
- 下一篇: wchar_t*和string相互转换