double-array trie 译文+心得

本文翻译并解读了《An Efficient Implementation of Trie Structures》,介绍了double-array trie的数据结构,这是一种结合了数组快速访问和链表紧凑性的数据结构。在插入、查找和删除操作上,double-array trie表现优秀,尤其在空间效率和查找速度上优于传统trie树。文章通过实例详细解释了double-array trie的构建、插入、查找和删除操作,并对比了与原始trie和list-structured trie的优缺点。
摘要由CSDN通过智能技术生成

自己对树是情有独钟,故在元旦放假的时候,翻译了an efficient implementation of trie structures。作者及来源就不叙述了。英文水平有限,但是尽量还原此文章的灵魂。如果有什么不对的地方,敬请各位给予指点。(http://blog.csdn.net/zzran/article/details/8461985-源码).学习的时候要有trie的基础。

概论

    下面将呈现一种新的内部数组结构,它便是double-array。double-array继承了数组访问快速的特性和链表结构紧密的特点。对于double-array的插入,查找和删除将会通过实例来给出解析。虽然插入的过程很慢,但是还是很实用的,对于查找和删除,由于double-array继承了链表的特性,所以很速度。在操作大量关键的时候,我们把double-array和list形式(也就是原始trie的链表的形式)进行比较,会得到如下结果:double-array占用的空间比trie以链表的形式存储节省了百分之十七的空间,同时double-array的遍历,也就是查找的速度会比链表的形式快3.1到5.1倍。

简介

     在很多检索的应用中,有必要采用trie树的形式来检索单词。例如:编译器的单词分析器,拼写检查器,参考书目的检索, 在自然语言处理中的形态字典,等等。看到这里,是不是觉得trie是一个很强大的数据结果。对于trie树,例如它的节点是下面这样的几个struct形式: struct node {char data, struct node next[26]},这是最常见的trie节点形式。它是array-structured。对于next数组的索引index是由一个单词中data所存储字母的下一个字母来决定的。next[index]所指示或者是一个新的trie节点,或者是一个NULL值。图一给出了一个这样形式的trie树,它是基于关键字数组K = {baby, bachelor, badge, jar}建立的。trie树的检索,插入,删除都很快,但是它占用了很大的内存空间,而且空间的复杂度是基于节点的个数和字符的个数。如果是纯单词,而且兼顾大小写的话,每个节点就要分配52*4的内存空间,耗费很大。一种很多人都知道压缩空间的策略就是列出从每个节点引申出来的边,并以空值结尾。图二基于list-structured形式的trie。这种链表的形式通过对于数组形式中NULL值的压缩来节省空间,也就说指向子节点的指针是以链表的形式来存放而不是数组的形式。但是如果从每个节点引申出很多边的话,检索的速度会很慢。

    接下来这篇文章会讲解它trie树的结构压缩到两个一位数组BASE和CHECK中去,这种结构叫做double-array。在double-array中,通过数组BASE,非空子节点的位置被映射到CHECK中去,同时,原来array-trie中,每个节点的非空子节点的位置不会被映射到CHECK的相同位置中。trie树中的每条边都可以在double-array中以O(1)的时间检索到,也就是说,在最坏的情况下,检索一个长度为k的单词只要O(k)的时间。对于拥有大量关键字的结合,trie树种将会有很多节点,所以要用double-array的形式来达到减少空间分配的目的。为了能够让trie存储大量的关键字,double-array尽可能的根据需要存储关键字的前缀来区分不同的单词,除去前缀剩下的部分会被存储到TAIL的一维数组中,以便更进一步的区分不同的单词。

图一



图2


trie树的构建

     关于trie的叙述如下,从根节点到叶子节点形成的每条路径提取出来的字母组成的单词代表了在关键字集合中的一个关键字,或者说,这个提取出来的关键字可以在关键字集合中找到。所有的这些路径代表的单词加起来,就是关键字的结合。为了不混淆像“the”和“then”这样的单词,在每个单词的 后面多加一个结束符号:“#”。接下来的这些定义将会被用到插入,查找,删除的步骤中去,所以不求能够先理解这些,只要能够记得怎么用,在插入,查找,删除的过程中,就会逐渐明白这些定义的用意。K代表关键字集合。trie树是由弧和节点主城的。如果一条弧上标记着a,(注意,在这里a是代表子母集合中的某一个字母,而不是真正的a),那么这个弧可以这样定义 g(n,a)=m,其中n,m是trie树种节点的标号。解释一下n,m。我们用的是两个一位数组来存储的trie,所以n,m就是这两个一位数组中的索引。代表了trie的两个节点对于K中的一个关键字key,他的节点m满足关系g(n,a) = m,如果字母a是一个能够有效的将自己所属的关键字和其他关键字区分的字母的话,那么m就是一个独立的节点。怎么理解这个呢,看下图:



    还记得之前说过的吗,double-array尽量存储关键字的前缀来压缩空间,但是还要能够把这些拥有公共前缀的关键字区分开,就要求这些关键字要有自己的特色,特色就是独立于其他关键字的节点。看上图中的字母c,d,b,先说c吧,g(3,c) = 5,这条边中m=5是一个节点,这个节点能够把bachelo这个单词和其他单词区分开,那么m=5就是一个独立的节点。在这里,话题要进行一个转移,我们想一下原始的trie,就是上面的那个struct形式的那个,它会把每个单词的每个字母都以一个节点罗列出来,刚才我们找到了独立节点(不包括独立节点中的字母),那么从原始trie中对应的这个节点开始到单词最后一个字母所占的节点结束,这些字母所组成的字符串叫做独立节点m的字母串。也就是后面的内容都是由m衔接出来的。一棵树,它是由K集合中所有关键字的独立节点,和独立节点之前的节点,以及这些节点之间的边所组成的,那么我们就叫这棵树为reduced trie。

对于图三,就是一个棵reduced trie。TAIL是用来存储独立节点之后的string的。对TAIL中的符号?现在不要在意,等到我们分析插入和删除的时间就会用到,标记为?,其实相应的内容不是?,而是一些由插入和删除造成的无效字符。

reduced trie 和double-array,还有TAIL之间的关系如下

     1,如果在reduced trie中有一条这样的边:g(n, a) = m, then BASE[n] + a = m, and CHECK[m] = n.(在这个等式BASE[n] + a = m中,a代表的是一个字母,但是在相加的过程中会把它替换成一个整数,因为这个字母代表的是一条边,定义如下:“#”= 1, “a”=2,..."z"= 27)。在实际的编码中没有这么做,因为前面的定义只涉及到27个字符,实际应用中会涉及到更多的字符。


    2,如果m是一个独立的节点,那么string str[m] = b1b2...bn。then (a), BASE[m] < 0, (b), let p = - BASE[m], TAIL[p]= b1, TAIL[p + 1]= b2, TAIL[p + h + 1]= bn.

在整个论文中,这两个关系式是很重要的,所以请先用机械的方式把这两个关系记录下来,不要试图去理解,在检索,插入,和删除的过程中就会明白这样定义的目的和巧妙之处了。

trie树的检索

    先从检索说起吧,建设我们已经将K={bachelor, jar, badge, baby}中的关键字都处理过,放到reduced trie和TAIL中去了。以bachelor为例子作为检索吧。还是以下图为例。



    step1:把trie树的根节点放在BASE数组中第一个位置,然后从第一个位置开始,字母‘b’代表的弧的值为3,在上面定义过,从上面的关系1中可以得到:BASE[n] + 'b' = BASE[1]  + 'b' = BASE[1] + 3 = 4 + 3 = 7; 然后看CHECK[7]的值是1.

  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值