1. 程式人生 > >雙陣列Trie樹 (Double-array Trie) 及其應用

雙陣列Trie樹 (Double-array Trie) 及其應用

雙陣列Trie樹(Double-array Trie, DAT)是由三個日本人提出的一種Trie樹的高效實現 [1],兼顧了查詢效率與空間儲存。Ansj便是用DAT(雖然作者宣稱是三陣列Trie樹,但本質上還是DAT)構造詞典用作初次分詞,極大地節省了記憶體佔用。本文將簡要地介紹DAT,並實現了基於DAT的前向最大匹配的中文分詞演算法。

1. Trie樹

兩種實現

Trie樹(也稱為字典樹、字首樹)是一種常被用於詞檢索的樹結構,其思想非常簡單:利用詞的共同字首以達到節省空間的目的;基本的實現有array與linked-list兩種。array實現需要為每一個字元開闢一個字母表大小的陣列:

上圖給出四個單詞bachelor, baby, badge, jar的Trie樹array實現示例圖;對應的Java程式碼如下:

class TrieNode {
  public Character value;
  public TrieNode[] next = new TrieNode[65536]; // 65536 = 2^16
}

雖然,array的查詢時間複雜度為\(O(1)\);但是,從圖中可以看出,存在著大量的空間浪費。當然,有人會想到用HashMap來代替陣列,以減少空間浪費:

class TrieNode {
  public Character value;
  public Map<Character, TrieNode> next = new HashMap<Character, TrieNode>();
}

mmseg4j便是以此來實現Trie樹的。但是,HashMap本質上就是一個hash table;存在著一定程度上的空間浪費。由此,容易想到用linked-list實現Trie樹:

雖然linked-list避免了空間浪費,卻增加了查詢時間複雜度,因為公共字首就意味著多次回溯。

Double-array實現

Double-array結合了array查詢效率高、list節省空間的優點,具體是通過兩個陣列basecheck來實現。Trie樹可以等同於一個自動機,狀態為樹節點的編號,邊為字元;那麼goto函式\(g(r,c) = s\)則表示狀態r可以按字元c轉移到狀態s。base陣列便是goto函式array實現,check陣列為驗證轉移的有效性;兩個陣列滿足如下轉移方程

base[r] + c = s
check[s] = r

值得指出的是,代入上述式子中的c為該字元的整數編碼值。那麼,bachelor, baby, badge, jar的DAT如下圖所示:

其中,字元的編碼表為{'#'=1, 'a'=2, 'b'=3, 'c'=4, etc. }。為了對Trie做進一步的壓縮,用tail陣列儲存無公共字首的尾字串,且滿足如下的特點:

tail of string [b1..bh] has no common prefix and the corresponding state is m:
    base[m] < 0;
    p = -base[m], tail[p] = b1, tail[p+1] = b2, ..., tail[p+h-1] = bh;

那麼,用DAT檢索詞badge的過程如下:

// root -> b
base[1] + 'b' = 4 + 3 = 7
// root -> b -> a
base[7] + 'a' = 1 + 2 = 3
// root -> b -> a -> d
base[3] + 'd' = 1 + 5 = 6
// badge#
base[6] = -12
tail[12..14] = 'ge#'

至於如何構造陣列base、check,可參考原論文 [1]及文章 [2].

2. DAT應用

以下程式碼分析基於ansj-5.1.1 版本。

詞典

Ansjcore.dic給出中文詞典的DAT實現:

249952
37  %   65536   -1  3   {q=1}
39  '   65536   -1  4   {en=1}
46  .   65536   -1  5   {nb=1}
...
21360   印   92338   -1  2   {j=24, n=1, ng=2, nr=0, v=32}
24230   度   89338   -1  2   {k=0, ng=2, q=28, v=7, vg=2}
27827   河   142597  -1  2   {n=29, q=0}
...
116568  印度  71557   21360   2   {ns=51}
99384   印度河 65536   116568  3   {ns=0}
116553  振臂一 94926   129740  1   null
116566  捅婁子 65536   116571  3   {v=0}
65333   U   65536   -1  4   {en=1}
...

詞典共有6列,分別為

index   name    base    check   status  {詞性->詞頻}  

其中,index表示字串的id(若為單字元,則為其unicode編碼對應的整數值),name為詞,base、check分別為DAT的base陣列、check陣列,status記錄當前詞的狀態,最後一列表示詞性集合,對應於類org.ansj.domain.AnsjItem中的成員變數termNatures。那麼,根據DAT的轉移方程則有

index['印度'] = 116568 = base['印'] + index['度'] = 92338 + 24230
check['印度'] = 21360 = index['印']
index['印度河'] = 99384 = base['印度'] + index['河'] = 71557 + 27827
check['印度河'] = 116568 = index['印度']

此外,status的數值具有如下含義:

  • 1對應的詞性為null,name不能單獨成詞,應繼續,比如“振臂一”;
  • 2表示name既可單獨成詞,也可與其他字元組成新詞,比如詞“印度”;
  • 3表示詞結束,name成詞不再繼續,比如詞“捅婁子”;
  • 4表示英文字母(包括全形)+字元',共計105(26*4+1)個字元;
  • 5表示數字(包括全形)+小數點,共有21(10*2+1)個字元.

分詞

正向最大匹配(Forward Maximum Matching, FMM)的分詞思路非常簡單:正向匹配詞典中的詞,取最長匹配者。Scala 2.11 實現FMM如下:

import org.ansj.library.DATDictionary
import scala.collection.mutable.ArrayBuffer

// max-matching algorithm for CWS
def maxMatching(sentence: String): Array[String] = {
  val segmented = ArrayBuffer.empty[String]
  val chars = sentence.toCharArray
  var i = 0
  while (i < chars.length) {
    DATDictionary.status(chars(i)) match {
      // not in core.dic or word-end or last char
      case t if t == 0 || t == 3 || i == chars.length - 1 =>
        i = singleCharWord(chars, i, segmented)
      // word-start
      case t if t == 1 || t == 2 =>
        i = goOnWord(chars, i, segmented)
      // English character or number
      case _ =>
        i = goOnEnNum(chars, i, segmented)
    }
  }
  segmented.toArray
}

// a single character segment
private def singleCharWord(chars: Array[Char], start: Int, arr: ArrayBuffer[String]): Int = {
  arr += chars(start).toString
  start + 1
}

// word segment which is in core.dic
private def goOnWord(chars: Array[Char], start: Int, arr: ArrayBuffer[String]): Int = {
  var nextIndex: Int = chars(start).toInt
  for (j <- start + 1 until chars.length) {
    val preIndex = nextIndex
    nextIndex = DATDictionary.getItem(nextIndex).getBase + chars(j).toInt
    if (DATDictionary.getItem(nextIndex).getCheck != preIndex) {
      arr += chars.subSequence(start, j).toString
      return j
    }
  }
  chars.length
}

// English chars and numbers compose a word
private def goOnEnNum(chars: Array[Char], start: Int, arr: ArrayBuffer[String]): Int = {
  for (j <- start + 1 until chars.length) {
    val status = DATDictionary.status(chars(j))
    if (status != 4 && status != 5) {
      arr += chars.subSequence(start, j).toString
      return j
    }
  }
  chars.length
}

函式goOnWord用到了DAT的轉移方程。直觀感受下FMM的分詞效果:

val sentence = "非農一觸即發,現貨原油撲朔迷離,倫敦金回暖已定"
println(maxMatching(sentence).mkString("/"))
// 非農/一觸即發/,/現貨/原油/撲朔迷離/,/倫敦/金/回暖/已/定

我實現了一個DAT生成演算法,扔在中文分詞專案thulac4j

3. 參考資料

[1] Aoe, J. I., Morimoto, K., & Sato, T. (1992). An efficient implementation of trie structures. Software: Practice and Experience, 22(9), 695-721.
[2] Theppitak Karoonboonyanan, An Implementation of Double-Array Trie.

相關推薦

陣列字典(Double Array Trie)

參考文獻 1.雙陣列字典樹(DATrie)詳解及實現 2.小白詳解Trie樹 3.論文《基於雙陣列Trie樹演算法的字典改進和實現》           DAT的基本內容介紹這裡就不展開說了,從Trie過來的同學應該比較熟悉,Trie對記憶體的消耗比較大,DA

陣列Trie (Double-array Trie) 及其應用

雙陣列Trie樹(Double-array Trie, DAT)是由三個日本人提出的一種Trie樹的高效實現 [1],兼顧了查詢效率與空間儲存。Ansj便是用DAT(雖然作者宣稱是三陣列Trie樹,但本質上還是DAT)構造詞典用作初次分詞,極大地節省了記憶體佔用。本文將簡要地介紹DAT,並實現了基於DAT的前

深入陣列TrieDouble-Array Trie

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

python利用Trie(字首)實現搜尋引擎中關鍵字輸入提示(學習Hash TrieDouble-array Trie

python利用Trie(字首樹)實現搜尋引擎中關鍵字輸入提示(學習Hash Trie和Double-array Trie)  主要包括兩部分內容:(1)利用python中的dict實現Trie;(2)按照darts-java的方法做python的實現Double-array Trie比較:(1)

B、B-、B+、B*、紅黑、 二叉排序trieDouble Array 字典查詢簡介

B  樹 即二叉搜尋樹:      1.所有非葉子結點至多擁有兩個兒子(Left和Right);       2.所有結點儲存一個關鍵字;       3.非葉子結點的左指標指向小於其關鍵字的子樹,右指標指向大於其關鍵字的子樹;       如:       B樹的

[轉]Trie優化演算法:Double Array Trie 陣列Trie

Trie邏輯結構      Trie是一種常見的資料結夠,可以實現字首匹配(hash是不行的),而且對於詞典搜尋來說也是O(1)的時間複雜度,雖然比不上Hash,但是空間會省不少。       比如下圖表示了包含“pool, prize, preview, prepare,

Trie進階:Double-Array Trie原理及狀態轉移過程詳解

前言:  Trie樹本身就是一個很迷人的資料結構,何況是其改進的方案。  在本部落格中我會從DAT(Double-Array Tire)的原理開始,並結合其原始碼對DAT的狀態轉移過程進行解析。如果因此

Double Array Trie(一)

Trie是一種常見的資料結夠,可以實現字首匹配(hash是不行的),而且對於詞典搜尋來說也是O(1)的時間複雜度,雖然比不上Hash,但是空間會省不少。 Trie樹主要應用在資訊檢索領域,非常高效。今天我們講Double Array Trie,請先把Trie樹忘掉,把資訊檢索忘掉,我們來講一個確

【bzoj4212】神牛的養成計劃 Trie+可持久化Trie

dna sid -- ihe for 物體 ota span xsd 題目描述 Hzwer成功培育出神牛細胞,可最終培育出的生物體卻讓他大失所望...... 後來,他從某同校女神 牛處知道,原來他培育的細胞發生了基因突變,原先決定神牛特征的基因序列都被破壞了,神牛hzw

中文分詞系列(一) 陣列Tire(DART)詳解

雙陣列Tire樹是Tire樹的升級版,Tire取自英文Retrieval中的一部分,即檢索樹,又稱作字典樹或者鍵樹。下面簡單介紹一下Tire樹。 1.1 Tire樹 Trie是一種高效的索引方法,它實際上是一種確定有限自動機(DFA),在樹的結構中,每一個結點對應一個DFA狀態,每一個從父結點指向子結點

[算法學習筆記]左偏的基本操作及其應用

swa merge 完全 span read har 步驟 建議 i++ 簡介 左偏樹是一種可以快速支持合並等操作的堆, 是可並堆中代碼復雜度最低,也最容易理解的一種(註意左偏樹的每一棵子樹都為左偏樹) 性質 左偏樹是一種二叉樹, 除了有二叉樹的左右兒子,還有2個屬性,鍵

陣列TRIE原理

關鍵詞查詢策略可以被大致分為兩類,按照關鍵詞集合是否可變可以將這些演算法分為“動態方法”(允許查詢表被修改)和“靜態方法”(顯然相反)兩種。廣為人知的“動態方法”有:hashing,二叉樹,B+樹,擴充套件hashing,和trie hashing。而“靜態方法”有:完美hashing,稀疏表,以及壓縮tri

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

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

Trie陣列實現

正文組織 1.什麼是Trie樹? 2.如何實現一個Trie樹? 3.三陣列Trie(Tripple-Array Trie) 4.雙陣列Trie(Double-Array Trie) 5.字尾壓縮 6.關鍵詞插入操作 7.關鍵詞刪除操作 8.雙輸出池分配(Double-A

陣列trie的基本構造及簡單優化(DAT沒那麼複雜)

一 基本構造 Trie樹是搜尋樹的一種,來自英文單詞"Retrieval"的簡寫,可以建立有效的資料檢索組織結構,是中文匹配分詞演算法中詞典的一種常見實現。它本質上是一個確定的有限狀態自動機(DFA),每個節點代表自動機的一個狀態。在詞典中這此狀態包括“詞字首”,“已成

CH601字尾陣列Trie

內含字典樹建立及查詢模板 1601 字首統計 0x10「基本資料結構」例題 描述 給定N個字串S1,S2...SN,接下來進行M次詢問,每次詢問給定一個字串T,求S1~SN中有多少個字串是T的字首。輸入字串的總長度不超過10^6,僅包含小寫字母。 輸入格式 第一行兩個整數N,M。接下來N行

Trie(字首/字典及其應用

Trie,又經常叫字首樹,字典樹等等。它有很多變種,如字尾樹,Radix Tree/Trie,PATRICIA tree,以及bitwise版本的crit-bit tree。當然很多名字的意義其實有交叉。   定義 在電腦科學中,trie,又稱字首樹或字典樹,是一種有序樹,用於

字典Trie)模板 陣列表示 + 連結串列表示

 陣列模擬,缺點是並不知道要開多大,可能會出現陣列開小導致wrong answer。 對應題目:hdu 1251  #include <iostream> #include <cstdio> #include <cstring> #de

Trie詳解及其應用

                一、知識簡介        最近在看字串演算法了,其中字典樹、AC自動機和字尾樹的應用是最廣泛的了,下面將會重點介紹下這幾個演算法的應用。      字典樹(Trie)可以儲存一些字串->值的對應關係。基本上,它跟 Java 的 HashMap 功能相同,都是 key-v

自然語言--Trie詳解及其應用

連結:http://blog.csdn.net/hackbuteer1/article/details/7964147 參考連結:https://segmentfault.com/a/1190000005810561 一、知識簡介         最近在看字串演算法了,其