1. 程式人生 > >深入雙陣列Trie(Double-Array Trie)

深入雙陣列Trie(Double-Array Trie)

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow

也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!

                 

什麼是Double Array Trie

  • Double Array Trie是TRIE樹的一種變形,它是在保證TRIE樹檢索速度的前提下,提高空間利用率而提出的一種資料結構,本質上是一個確定有限自動機(deterministic finite automaton,簡稱DFA)。
  • 所謂的DFA就是一個能實現狀態轉移的自動機。對於一個給定的屬於該自動機的狀態和一個屬於該自動機字母表Σ的字元,它都能根據事先給定的轉移函式轉移到下一個狀態。
  • 對於Double Array Trie(以下簡稱DAT),每個節點代表自動機的一個狀態,根據變數的不同,進行狀態轉移,當到達結束狀態或者無法轉移的時候,完成查詢。

什麼是Double Array Trie

  • Double Array Trie是TRIE樹的一種變形,它是在保證TRIE樹檢索速度的前提下,提高空間利用率而提出的一種資料結構,本質上是一個確定有限自動機(deterministic finite automaton,簡稱DFA)。
  • 所謂的DFA就是一個能實現狀態轉移的自動機。對於一個給定的屬於該自動機的狀態和一個屬於該自動機字母表Σ的字元,它都能根據事先給定的轉移函式轉移到下一個狀態。
  • 對於Double Array Trie(以下簡稱DAT),每個節點代表自動機的一個狀態,根據變數的不同,進行狀態轉移,當到達結束狀態或者無法轉移的時候,完成查詢。

DAT結構

DAT定義

  • DAT是採用兩個線性陣列(base[]和check[]),base和check陣列擁有一致的下標,(下標)即DFA中的每一個狀態,也即TRIE樹中所說的節點,base陣列用於確定狀態的轉移,check陣列用於檢驗轉移的正確性。因此,從狀態s輸入c到狀態t的一個轉移必須滿足如下條件:
  base[s] + c == t  check[base[s] + c] == s
  • DAT也可如下描述:
    1. 對於給定的狀態s,如果有n個狀態(字元c1,c2,...,cn)的轉移,需要在base陣列中找到一段空位t1,t2,...,tn,使得t1-c1,t2-c2,...,tn-cn都為base陣列中下標為s的值,注意此處的t1,t2,...,tn不一定在base陣列中連續;
    2. 對於轉移的狀態t1,t2,...,tn,其作為下標時,check[t1],check[t2],...,check[tn]的值都為狀態s;
  • 為了便於理解,這裡有一個256叉樹的圖(準確的說是26叉樹,但是圖畫得不好,暫且我們把它當作256叉樹吧)。
256x_tree.png

  • 圖中最頂端有256的父節點,每個父節點都有256個子節點;那麼無論漢字和字母,都可以分佈在256個子節點上,但是如果詞語只有app和apple以及banana三個詞語,那麼256個父節點顯得有些浪費,實際上只需要2個父節點就可以了。
    • 如果節點的型別為整型,我們把所有的節點進行編號的話,且直接採用詞語的首字母ascii碼來直接作為節點,97,98這兩個父節點會使用到,其餘的父節點是多餘的,使用一個空指標即可。
    • 根據256叉樹的定義,97和98也會有256個子節點,但是詞語的第二個字母顯示,97的下一個節點只有一個p(112)節點,98的下一個節點只有一個a(97)節點,則其餘的節點仍然為空,這樣,由於樹型的演算法的複雜度為On,即最多n次匹配即可完成一次查詢,而我們可以省略不用的節點,降低空間的空閒率。

DAT匹配

基於上述定義,DAT的匹配過程如下:假設當前狀態為s,對於輸入的字元c有:

  t = base[s] + c;  if check[t] = s then       next state = t;  else       fail;  endif

DAT匹配的過程相對簡單,很容易理解。

DAT構造

  • 首先我們需要了解一下DAT的記憶體管理
    • 在DAT的構造過程當中,一般有兩種構造方法:
      1. 已知所有詞語,靜態構造雙陣列;
        • 此方法構建時,是將所有詞語全部放入到記憶體,對詞語中所有的父節點和其下的子節點分別進行排序(一般為ASCII碼排序),找出最初的父節點數目和有多少個不同的子節點數,方便對記憶體進行分配。這樣的優點是找到放置子節點空間完全能夠容納子節點,以後不需要進行擴充,相對複雜度較低,且構建速度相對很快。缺點是以後新增詞語不太靈活,每次需要重新構建;
      2. 動態輸入詞語,動態構造雙陣列。
        • 當n條詞語準備構建雙陣列時,先以新增一條詞語cat為例,雙陣列中base[1024]陣列根節點為0(預設值,當然根據“個人愛好”可以隨意指定),下標為0,為詞語cat的首字元"c“(99)找一個合適的位置,比如位置100,即:
        •    base[0] = 100;
        • 此處那麼在base[100]的位置下,加上字元"c"的ascii值得到下一個狀態(t)的位置(199),然後在一個合適的位置(空閒的位置)197,使得
        •    base[199]+'a'=197;
        • 那麼狀態(t),即base[199]的值可以通過上述公式得到仍然為100
        • 值得注意是,在狀態(f),目前即字元"t",結束時,其value值可以做如下處理,如果狀態(f)結束,沒有子節點,則
        •    base(f)=-1 * f;
        • 如果狀態(t)結束,仍然有多個子節點,那麼其base陣列標記為
        •    base(f)=-1 * base(f);
        • 當輸入第二條詞語camera時,仍然按照上述方式進行,當進行到字元a時,字元a位置的下標為197,檢查check[base[197]+'m']是否為空位。
          • 如果為空位,則base[197]的值仍然可以為100;
          • 否則需要重新尋找兩個空位位置Ψ(base[197]+'m'),λ(base[197]+'t'),使得 base[base[197]+'m']=-1(-1標記為空位狀態,"m"為camera的第三個字元)和 base[base[197]+'t']=-1("t"為cat的第三個字元),即第二級節點a後面的兩個新節點能有位置存放新的偏移量,並使得 check[base[197]+'m']=197和check[base[197]+'t']=197即可,那麼base[197]的值需重新指向到新的位置(Ψ+λ-'m'-'t')/2。
        • 接著繼續重新構建下面新兩個節點的DAT結構,且第一個節點的結構構造完成後,需要清除原來的構建。
        • 動態構造雙陣列能夠很方便的動態插入詞語,不需要重新構造整個TRIE樹,但是實現的邏輯相對複雜一些。
    • 若初始狀態申請的陣列大小不足時,需要進行擴充,並將原來的陣列拷貝到新增大的陣列上,且原來的陣列一般需要進行記憶體釋放,如下圖:
dat.png


  • DAT構造中,check陣列需要指向父節點,即base陣列中父節點的下標即可,這裡有一副簡圖,描述了構造DAT雙陣列的方式:





trie.png

DAT改進方案

  • DAT相對普通TRIE樹來說,提高了空間的利用率,但是空間利用率還不是最好的。
  • 比如單詞elephant(8個字元),如果所有詞語當中只有一個以e開頭的詞語,一般我們實現trie的結構寫成
   struct BC_st   {          int base;       int check;   };
  • 那麼整個詞語在DAT中佔用的空間是4*2*8=64位元組。其實儲存沒有必要浪費那麼多空間,在DAT結構裡面,完全可以以7個位元組來儲存lephant,查詢的時候lephant實現位元組查詢就可以了。
  • DAT是一個樹型的結構,不斷髮散的結構,如果在對面實現一個同樣的結構,相對來說,會減少一半的空間體積,如圖所示:
reversedat.png


  • 當然上述的結構已經有人實現過,實現比較複雜。
  • 另外,在DAT雙陣列當中,有很多的空閒空間未得到充分利用,可以通過連結串列將未使用的空間串聯起來,更加合理的利用,提高資料密集程度。

參考連線

           

給我老師的人工智慧教程打call!http://blog.csdn.net/jiangjunshow

這裡寫圖片描述