转自:http://blog./2010/07/15/double-array-trie/
Trie在ACM中已经十分普及,也是一种非常有效的索引结构,好处就不多说了。 它的本质就是一个确定的有限状态自动机(DFA),关于它的实现也是有好几种,ACM中用的最多也是最容易实现的就是多路查找树。 但是Trie最大的缺点就是占用空间过大,很容易爆内存,当然在ACM里对Trie树也有相应的优化,如限定高度,对分支较少的节点使用非随机访问的结构(减少宽度),但这些都是牺牲部分查找效率换取的。 这里介绍一种实现,Double-Array Trie(双数组字典树),其实它就是双数组,跟树结构没啥关系。他能在一定程度上减少内存的浪费。 两个数组,一个是base[],另一个是check[]。设数组下标为i ,如果base[i], check[i]均为0,表示该位置为空。如果base[i]为负值,表示该状态为终止态(即词语)。check[i]表示该状态的前一状态。 定义 1. 对于输入字符 c, 从状态 s 转移到状态 t, 双数组字典树满足如下条件:
从定义1中,我们能得到查找算法,对于给定的状态 s 和输入字符 c :
插入的操作,假设以某字符开头的 base 值为i,第二个字符的字符序列码依次为c1, c2, c3…cn,则肯定要满足base[i+c1], check[i+c1], base[i+c2], check[i+c2], base[i+c3], check[i+c3]…base[i+cn],check[i+cn]均为0。 我们知道双数组的实现方法是当状态有新转移时才分配空间给新状态,或可以表述为只分配需要转移的状态的空间。当遇到无法满足上述条件时再进行调整,使得其 base 值满足上述条件,这种调整只影响当前节点下一层节点的重分配,因为所有节点的地址分配是靠 base 数组指定的起始下标所决定的。 我们先得到重分配算法:
如:有两个单词ac和da,先插入单词ac,这时的 base 数组 假设,Tire里有n个节点,字符集大小为m,则DATrie的空间大小是n+cm,c是依赖于Trie稀疏程度的一个系数。而多路查找树的空间大小是nm。 实际编码过程中,DATrie代码难度大过多路查找树,主要是状态的表示不如树结构那样的清晰,下标很容易搞混掉。 关于优化:
base[i], check[i]均为0,表示该位置为空。我们可以把这部分给利用起来,全为0的标记所包含的信息实在太少了。我们利用base和check数组组成一个双向链表。 定义 2. 设 r1, r2, … ,rcm 为空闲地址有序序列,则我们的双向链表可定义为 :
由于我们把base[0]作为根节点,所以初始化时就可以把base[0]排除在链表之外,而check[0]可以被作为链表的头节点。 设节点的状态转移集为P = {c1, c2, …, cp},依靠链表我们能得到新的空地址查找算法:
优化后的空地址查找算法时间复杂度为O(cm2),而重分配算法的时间复杂度为O(m2),则总的时间复杂度为O(cm2 + m2) = O(cm2)。 重分配或删除节点后,原先的地址被作废,可以重新加入链表,这样如果遇到原状态转移集的子集时,就可以派上用场了。
当有节点删除时,我们不仅可以把它加回到链表中,还可以重新为最大非空节点的状态重新确定base值,因为删除可能导致它的前面有空间容纳下它的状态转移集。这样我们可能又得以删除一些空值状态,使得数组长度有希望被压缩。 这个思想借鉴于后缀树,我们可以将没有分支的后缀单独存放,但这个结构肯定独立于DATrie,所以在这就不详述了。详情见[Aoe1989]。 整体而言,在ACM中,DATrie略高的编码复杂度和过低的插入效率,应用面不会太广。但现实问题中,词库大小一般比较稳定,在离线算法也有很大的优化余地,此时DATrie的空间优势就会比较明显。毕竟Trie高效的检索效率这一优点是值得研究探讨的。 以下是只加入空地址查找优化后的DATrie代码,对于字符集表的映射结构也是个需要持续讨论的问题,在这个代码里只支持英文字母。
本文是对《An Implementation of Double-Array Trie》的整理和翻译。
|
|