1. 程式人生 > >Trie樹介紹及實現(傳統&雙陣列)

Trie樹介紹及實現(傳統&雙陣列)

Trie樹,又叫字典樹、字首樹(Prefix Tree)、單詞查詢樹 或 鍵樹,是一種樹形結構。典型應用是用於統計和排序大量的字串(但不僅限於字串),所以經常被搜尋引擎系統用於文字詞頻統計。它的優點是最大限度地減少無謂的字串比較, 查詢效率比較高 。

Trie的核心思想是空間換時間,利用字串的公共字首來降低查詢時間的開銷以達到提高效率的目的。

它有3個基本性質:

  1. 根節點不包含字元,除根節點外每一個節點都只包含一個字元。
  2. 從根節點到某一節點,路徑上經過的字元連線起來,為該節點對應的字串。
  3. 每個節點的所有子節點包含的字元都不相同。

其結構大致如下:

Trie樹的傳統實現

傳統的實現方式中,每個節點都包含著一個指標陣列,用於指向子節點:

structNode {
boolendOfWord;//是否為單詞的結尾
charch;
structNode* child[MAX_NUM];//26-tree->a, b ,c, .....z
};

因為可能有多個字串擁有相同的字首,所以用一個 bool 的欄位來表示該字母是否為一個字串的結尾。插入(Insert)、刪除( Delete)和查詢(Find)都非常簡單,用一個一重迴圈即可,即第i 次迴圈找到前i 個字母所對應的子樹,然後進行相應的操作。其實現如下:

#defineMAX_NUM 26

structNode {
boolendOfWord;//是否為單詞的結尾
charch;
structNode* child[MAX_NUM];//26-tree->a, b ,c, .....z
}; structNode* ROOT;//tree root structNode*createNewNode(charch){ // create a new node structNode *new_node = (structNode*)malloc(sizeof(structNode)); new_node->ch = ch; new_node->endOfWord == false; inti; for(i =0; i < MAX_NUM; i++) new_node->child[i] = NULL; returnnew_node; } voidinitialization(){ //intiazation: creat an empty tree, with only a ROOT
ROOT = createNewNode(' '); } intcharToindex(charch){//a "char" maps to an index<br> returnch -'a'; } intfind(constcharchars[],intlen){ structNode* ptr = ROOT; inti =0; while(i < len) { if(ptr->child[charToindex(chars[i])] ==NULL) { break; } ptr = ptr->child[charToindex(chars[i])]; i++; } return(i == len) && (ptr->endOfWord ==true); } voidinsert(constcharchars[],intlen){ structNode* ptr = ROOT; inti; for(i =0; i < len; i++) { if(ptr->child[charToindex(chars[i])] ==NULL) { ptr->child[charToindex(chars[i])] = createNewNode(chars[i]); } ptr = ptr->child[charToindex(chars[i])]; } ptr->endOfWord = true; }

trie樹的檢索,插入,刪除都很快,但是它佔用了很大的記憶體空間,而且空間的複雜度是基於節點的個數和字元的個數。如果是純單詞,而且兼顧大小寫的話,每個節點就要分配52*4的記憶體空間,耗費很大。

Trie樹的雙陣列實現

importjava.util.ArrayList;
importjava.util.HashMap;
importjava.util.Map;
importjava.util.Arrays;


publicclassDoubleArrayTrie{

finalcharEND_CHAR ='#';
finalintDEFAULT_LEN =1024;
intBase[] =newint[DEFAULT_LEN];
intCheck[] =newint[DEFAULT_LEN];
charTail[] =newchar[DEFAULT_LEN];
intPos =1;//TAIL陣列下一個可用下標
//用於將字元轉換為索引
 Map<Character ,Integer> CharMap = newHashMap<Character,Integer>();
//用於將索引轉換為字元
 ArrayList<Character> CharList = newArrayList<Character>();

publicDoubleArrayTrie()
 {
//一系列初始化
 Base[1] =1;

 CharMap.put(END_CHAR,1);
 CharList.add(END_CHAR);
 CharList.add(END_CHAR);
for(inti=0;i<26;++i)//a對應2,z對應27
 {
 CharMap.put((char)('a'+i),CharMap.size()+1);
 CharList.add((char)('a'+i));
 }

 }
privatevoidExtend_Array()
 {
 Base = Arrays.copyOf(Base, Base.length*2);
 Check = Arrays.copyOf(Check, Check.length*2);
 }

privatevoidExtend_Tail()
 {
 Tail = Arrays.copyOf(Tail, Tail.length*2);
 }

privateintGetCharCode(charc)
 {
if(!CharMap.containsKey(c))
 {
 CharMap.put(c,CharMap.size()+1);
 CharList.add(c);
 }
returnCharMap.get(c);
 }
privateintCopyToTailArray(String s,intp)
 {
int_Pos = Pos;
while(s.length()-p+1> Tail.length-Pos)
 {
 Extend_Tail();
 }
for(inti=p; i<s.length();++i)
 {
 Tail[_Pos] = s.charAt(i);
 _Pos++;
 }
return_Pos;
 }

privateintx_check(Integer []set)//獲得一個可行的最小base,set中的每一個下標都要是空閒的
 {
for(inti=1; ; ++i)
 {
booleanflag =true;
for(intj=0;j<set.length;++j)
 {
intcur_p = i+set[j];
if(cur_p>= Base.length) Extend_Array();
if(Base[cur_p]!=0|| Check[cur_p]!=0)//必須是空閒未使用的
 {
 flag = false;
break;
 }
 }
if(flag)returni;
 }
 }

privateArrayList<Integer>GetChildList(intp)//p為父狀態
 {
 ArrayList<Integer> ret = newArrayList<Integer>();
for(inti=1; i<=CharMap.size();++i)
 {
if(Base[p]+i >= Check.length)break;
if(Check[Base[p]+i] == p)
 {
 ret.add(i);
 }
 }
returnret;
 }

privatebooleanTailContainString(intstart,String s2)
 {
for(inti=0;i<s2.length();++i)
 {
if(s2.charAt(i) != Tail[i+start])returnfalse;
 }

returntrue;
 }
privatebooleanTailMatchString(intstart,String s2)
 {
 s2 += END_CHAR;
for(inti=0;i<s2.length();++i)
 {
if(s2.charAt(i) != Tail[i+start])returnfalse;
 }
returntrue;
 }


publicvoidInsert(String s)throwsException
 {
 s += END_CHAR;

intpre_p =1;
intcur_p;
for(inti=0; i<s.length(); ++i)
 {
//獲取狀態位置
 cur_p = Base[pre_p]+GetCharCode(s.charAt(i));
//如果長度超過現有,拓展陣列
if(cur_p >= Base.length) Extend_Array();

//空閒狀態
if(Base[cur_p] ==0&& Check[cur_p] ==0)
 {
 Base[cur_p] = -Pos;//pos是TAIL陣列的下標
 Check[cur_p] = pre_p;//CHECK中為對應的父狀態
 Pos = CopyToTailArray(s,i+1);//將尾串直接儲存到TAIL陣列,並更新pos
break;
 }else
//已存在狀態
if(Base[cur_p] >0&& Check[cur_p] == pre_p)
 {
 pre_p = cur_p;//更新pre_p,切換到下一個狀態
continue;
 }else
//衝突 1:遇到 Base[cur_p]小於0的,即遇到一個被壓縮存到Tail中的字串
if(Base[cur_p] <0&& Check[cur_p] == pre_p)
 {
inthead = -Base[cur_p];//head為TAIL陣列的下標

if(s.charAt(i+1)== END_CHAR && Tail[head]==END_CHAR)//插入重複字串
 {
break;
 }

//公共字母的情況,因為上一個判斷已經排除了結束符,所以一定是2個都不是結束符
if(Tail[head] == s.charAt(i+1))
 {
//因為和TAIL陣列中的尾串字母重複,則這兩個字母需要提取出來共用一個狀態,需要一個新的base
intavail_base = x_check(newInteger[]{GetCharCode(s.charAt(i+1))});
 Base[cur_p] = avail_base;//更新當前狀態的base

//修改CHECK陣列和BASE陣列
 Check[avail_base+GetCharCode(s.charAt(i+1))] = cur_p;
//論文中是將陣列的字串左移,這邊是將下標右移,節省了幾步操作
 Base[avail_base+GetCharCode(s.charAt(i+1))] = -(head+1);
 pre_p = cur_p;
continue;
 }
else
 {
//2個字母不相同的情況,可能有一個為結束符。同時需要將這個兩個不同的字元給提取出來,
//分配到兩個不同的狀態,也就是不同的index,需要新的base
intavail_base ;
 avail_base = x_check(newInteger[]{GetCharCode(s.charAt(i+1)),GetCharCode(Tail[head])});

 Base[cur_p] = avail_base;//更新base

//修改新的CHECK陣列的值為cur_p(父狀態)
 Check[avail_base+GetCharCode(Tail[head])] = cur_p;
 Check[avail_base+GetCharCode(s.charAt(i+1))] = cur_p;

//Tail 為END_FLAG 的情況
if(Tail[head] == END_CHAR)
 Base[avail_base+GetCharCode(Tail[head])] = 0;
else//修改為TAIL陣列在剩餘尾串的下標
 Base[avail_base+GetCharCode(Tail[head])] = -(head+1);
if(s.charAt(i+1) == END_CHAR)
 Base[avail_base+GetCharCode(s.charAt(i+1))] =0;
else
 Base[avail_base+GetCharCode(s.charAt(i+1))] = -Pos;

 Pos = CopyToTailArray(s,i+2);//插入串的剩餘部分插入到TAIL陣列中
break;
 }
 }else
//衝突2:當前結點已經被佔用,需要調整pre的base,然後將之前的資料遷移到新的base
if(Check[cur_p] != pre_p)
 {
 ArrayList<Integer> list = GetChildList(pre_p);//獲取所有子狀態
intorigin_base = Base[pre_p];//儲存原來的base
 list.add(GetCharCode(s.charAt(i)));
//新base
intavail_base = x_check(list.toArray(newInteger[list.size()]));
 list.remove(list.size()-1);
//更新base
 Base[pre_p] = avail_base;
for(intj=0; j<list.size(); ++j)
 {
//遷移資料
inttmp1 = origin_base + list.get(j);
inttmp2 = avail_base + list.get(j);
 Base[tmp2] = Base[tmp1];
 Check[tmp2] = Check[tmp1];

//有後續
if(Base[tmp1] >0)
 {
 ArrayList<Integer> subsequence = GetChildList(tmp1);
for(intk=0; k<subsequence.size(); ++k)
 {
 Check[Base[tmp1]+subsequence.get(k)] = tmp2;
 }
 }
//將之前的陣列槽置為空閒
 Base[tmp1] = 0;
 Check[tmp1] = 0;
 }

//更新新的cur_p
 cur_p = Base[pre_p]+GetCharCode(s.charAt(i));

if(s.charAt(i) == END_CHAR)
 Base[cur_p] = 0;
else
 Base[cur_p] = -Pos;
 Check[cur_p] = pre_p;
 Pos = CopyToTailArray(s,i+1);//同樣,將插入串剩餘的部分插入TAIL陣列
break;
 }
 }
 }

publicbooleanExists(String word)
 {
intpre_p =1;
intcur_p =0;

for(inti=0;i<word.length();++i)
 {
 cur_p = Base[pre_p]+GetCharCode(word.charAt(i));
if(Check[cur_p] != pre_p)returnfalse;
if(Base[cur_p] <0)
 {
if(TailMatchString(-Base[cur_p],word.substring(i+1)))
returntrue;
returnfalse;
 }
 pre_p = cur_p;
 }
if(Check[Base[cur_p]+GetCharCode(END_CHAR)] == cur_p)
returntrue;
returnfalse;
 }


classFindStruct
 {
intp;
 String prefix="";
 }
privateFindStructFind(String word)
 {
intpre_p =1;
intcur_p =0;
 FindStruct fs = newFindStruct();
for(inti=0;i<word.length();++i)
 {
// BUG
 fs.prefix += word.charAt(i);
 cur_p = Base[pre_p]+GetCharCode(word.charAt(i));
if(Check[cur_p] != pre_p)
 {
 fs.p = -1;
returnfs;
 }
if(Base[cur_p] <0)
 {
if(TailContainString(-Base[cur_p],word.substring(i+1)))
 {
 fs.p = cur_p;
returnfs;
 }
 fs.p = -1;
returnfs;
 }
 pre_p = cur_p;
 }
 fs.p = cur_p;
returnfs;
 }

publicArrayList<String>GetAllChildWord(intindex)
 {
 ArrayList<String> result = newArrayList<String>();
if(Base[index] ==0)
 {
 result.add("");
returnresult;
 }
if(Base[index] <0)
 {
 String r="";
for(inti=-Base[index];Tail[i]!=END_CHAR;++i)
 {
 r+= Tail[i];
 }
 result.add(r);
returnresult;
 }
for(inti=1;i<=CharMap.size();++i)
 {
if(Check[Base[index]+i] == index)
 {
for(String s:GetAllChildWord(Base[index]+i))
 {
 result.add(CharList.get(i)+s);
 }
//result.addAll(GetAllChildWord(Base[index]+i));
 }
 }
returnresult;
 }

publicArrayList<String>FindAllWords(String word)
 {
 ArrayList<String> result = newArrayList<String>();
 String prefix = "";
 FindStruct fs = Find(word);
intp = fs.p;
if(p == -1)returnresult;
if(Base[p]<0)
 {
 String r="";
for(inti=-Base[p];Tail[i]!=END_CHAR;++i)
 {
 r+= Tail[i];
 }
 result.add(fs.prefix+r);
returnresult;
 }

if(Base[p] >0)
 {
 ArrayList<String> r = GetAllChildWord(p);
for(inti=0;i<r.size();++i)
 {
 r.set(i, fs.prefix+r.get(i));
 }
returnr;
 }

returnresult;
 }

}

Trie樹的應用

  1. 字串檢索,詞頻統計,搜尋引擎的熱門查詢

    事先將已知的一些字串(字典)的有關資訊儲存到trie樹裡,查詢另外一些未知字串是否出現過或者出現頻率。舉例:

    1. 有一個1G大小的一個檔案,裡面每一行是一個詞,詞的大小不超過16位元組,記憶體限制大小是1M。返回頻數最高的100個詞。
    2. 給出N 個單片語成的熟詞表,以及一篇全用小寫英文書寫的文章,請你按最早出現的順序寫出所有不在熟詞表中的生詞。
    3. 給出一個詞典,其中的單詞為不良單詞。單詞均為小寫字母。再給出一段文字,文字的每一行也由小寫字母構成。判斷文字中是否含有任何不良單詞。例如,若rob是不良單詞,那麼文字problem含有不良單詞。
    4. 1000萬字符串,其中有些是重複的,需要把重複的全部去掉,保留沒有重複的字串
    5. 尋找熱門查詢:搜尋引擎會通過日誌檔案把使用者每次檢索使用的所有檢索串都記錄下來,每個查詢串的長度為1-255位元組。假設目前有一千萬個記錄,這些查詢串的重複讀比較高,雖然總數是1千萬,但是如果去除重複和,不超過3百萬個。一個查詢串的重複度越高,說明查詢它的使用者越多,也就越熱門。請你統計最熱門的10個查詢串,要求使用的記憶體不能超過1G。
  2. 字串最長公共字首

    Trie樹利用多個字串的公共字首來節省儲存空間,反之,當我們把大量字串儲存到一棵trie樹上時,我們可以快速得到某些字串的公共字首。舉例:

    1. 給出N 個小寫英文字母串,以及Q 個詢問,即詢問某兩個串的最長公共字首的長度是多少. 解決方案:首先對所有的串建立其對應的字母樹。此時發現,對於兩個串的最長公共字首的長度即它們所在結點的公共祖先個數,於是,問題就轉化為了離線 (Offline)的最近公共祖先(Least Common Ancestor,簡稱LCA)問題。 而最近公共祖先問題同樣是一個經典問題,可以用下面幾種方法:

      1. 利用並查集(Disjoint Set),可以採用採用經典的Tarjan 演算法;
      2. 求出字母樹的尤拉序列(Euler Sequence )後,就可以轉為經典的最小值查詢(Range Minimum Query,簡稱RMQ)問題了;
  3. 排序

    Trie樹是一棵多叉樹,只要先序遍歷整棵樹,輸出相應的字串便是按字典序排序的結果。舉例: 給你N 個互不相同的僅由一個單詞構成的英文名,讓你將它們按字典序從小到大排序輸出。

  4. 作為其他資料結構和演算法的輔助結構如字尾樹,AC自動機等。

參考: