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