012-数据结构-树形结构-哈希树[hashtree]、字典树[trietree]、后缀树

一、哈希树概述

1.1.、其他树背景

  二叉排序树,平衡二叉树,红黑树等二叉排序树。在大数据量时树高很深,我们不断向下找寻值时会比较很多次。二叉排序树自身是有顺序结构的,每个结点除最小结点和最大结点外都有前驱和后继,不论是排序还是搜索它的综合性能比较好,但是单独在搜索这一方面二叉排序树的性能就可能没有Hash树快。

1.2、基础理论

1.2.1、质数分辨定理

  什么是质数 : 即只能被 1 和 本身 整除的数。

  为什么用质数:因为N个不同的质数可以 ”辨别“ 的连续整数的数量,与这些质数的乘积相同。

    百度文库解答:https://wenku.baidu.com/view/16b2c7abd1f34693daef3e58.html

  示例、从2起的连续质数,连续10个质数就可以分辨大约M(10) =2*3*5*7*11*13*17*19*23*29= 6464693230 个数,已经超过计算机中常用整数(32bit)的表达范围。连续100个质数就可以分辨大约M(100) = 4.711930 乘以10的219次方。

  而按照目前的CPU水平,100次取余的整数除法操作几乎不算什么难事。在实际应用中,整体的操作速度往往取决于节点将关键字装载内存的次数和时间。一般来说,装载的时间是由关键字的大小和硬件来决定的;在相同类型关键字和相同硬件条件下,实际的整体操作时间就主要取决于装载的次数。他们之间是一个成正比的关系。

1.2.2、余数分辨定理

这个定理可以简单的表述为:n个不同的数可以“分辨”的连续整数的个数不超过他们的最小公倍数。超过这个范围就意味着冲突的概率会增加。定理1是定理2的一个特例。

1.3、基于上述理论分辨算法创建哈希树

  可以选择质数分辨算法来建立一棵哈希树。

  选择从2开始的连续质数来建立一个十层的哈希树。第一层结点为根结点,根结点下有2个结点;第二层的每个结点下有3个结点;依此类推,即每层结点的子节点数目为连续的质数【1,2,3,5,7,11……】。到第十层,每个结点下有29个结点。如下图所示:

    

  原则:求余看对应位置的结点,如果为空则在空处插入一个新节点,如果被逻辑删除了替换值再逻辑恢复,如果有值就继续往下找,继续求余判断。

  示例-插入:有一组元素,按照顺序向hash树中插入元素,取余找位置插入。我们以下图中的 “68” 为例子,68先对2取余得0,但是0位置上有人了,继续对3取余得2,2得位置上也有人了,那就继续对5取余得3,3得位置上没有人则插入 到3的位置。

    

  示例-查询:查询68,我们从2开始取余查找,找对应位置的值是否和它相同,不相同则继续向下取余,直到找到和68相同的数值或者直到为空为止。

1.4、哈希树的应用

  哈希树是一种可以自适应的树。通过给出足够多的不同质数,我们总可以将所有已经出现的关键字进行区别。而质数本身就是无穷无尽的。这种方式使得关键字空间和地址空间不再是压缩对应方式,而是完全可以等价的。

  哈希树可以广泛应用于那些需要对大容量数据进行快速匹配操作的地方。例如:数据库索引系统、短信息中的收条匹配、大量号码路由匹配、信息过滤匹配。程序员可以利用各种代码来实现哈希树结构。它可以为程序员提供一种使用起来更加方便,更加简单的快速数据存储方式。

1.5、优点和缺点

优点:结构简单 注意hash树是单向增加的,即使删除也不会减少hash树的结构 
查找迅速 对于int的数据 最多查找10次 
结构不变 即使删除属于也不会改变结构,

缺点:非排序性 就是数据时没有顺序的

代码地址:地址 中的data-004-tree中 HashTree

参看地址:

  https://blog.csdn.net/xushiyu1996818/article/details/89396909

  https://blog.csdn.net/winter_wu_1998/article/details/79555936

二、字典树

2.1、概述

  Tire树称为字典树,又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。

  Tire树的三个基本性质:
    1) 根节点不包含字符,除根节点外每一个节点都只包含一个字符;
    2) 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串;
    3) 每个节点的所有子节点包含的字符都不相同。
  Tire树的三个基本性质:
  1) 根节点不包含字符,除根节点外每一个节点都只包含一个字符;
  2) 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串;
  3) 每个节点的所有子节点包含的字符都不相同。
  

2.2、应用场景

  典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。

  它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。 

  Tire树的应用:

  1) 串的快速检索

  给出N个单词组成的熟词表,以及一篇全用小写英文书写的文章,请你按最早出现的顺序写出所有不在熟词表中的生词。

在这道题中,我们可以用数组枚举,用哈希,用字典树,先把熟词建一棵树,然后读入文章进行比较,这种方法效率是比较高的。

  2) “串”排序

  给定N个互不相同的仅由一个单词构成的英文名,让你将他们按字典序从小到大输出。用字典树进行排序,采用数组的方式创建字典树,这棵树的每个结点的所有儿子很显然地按照其字母大小排序。对这棵树进行先序遍历即可。

  3) 最长公共前缀

  对所有串建立字典树,对于两个串的最长公共前缀的长度即他们所在的结点的公共祖先个数,于是,问题就转化为求公共祖先的问题。

  4)作为其他数据结构和算法的辅助结构:如后缀树,AC自动机等。

代码地址:地址 中的data-004-tree中 TrieTree

参看地址:

  https://www.cnblogs.com/xujian2014/p/5614724.html

三、后缀树

3.1、后缀树的性质

    • 存储所有 n(n-1)/2 个后缀需要 O(n) 的空间,n 为的文本(Text)的长度;
    • 构建后缀树需要 O(dn) 的时间,d 为字符集的长度(alphabet);
    • 对模式(Pattern)的查询需要 O(dm) 时间,m 为 Pattern 的长度;

3.2、字典树→压缩字典树→后缀树

  根据集合 {bear, bell, bid, bull, buy, sell, stock, stop} 所构建的 Trie 树。

    

  观察上面这颗 Trie,对于关键词 "bear",字符 "a" 和 "r" 所在的节点没有其他子节点,所以可以考虑将这两个节点合并,如下图所示。

    

  这样,我们就得到了一棵压缩过的 Trie,称为压缩字典树(Compressed Trie)

  后缀树(Suffix Tree)则首先是一棵 Compressed Trie,其次,后缀树中存储的关键词为所有的后缀。这样,实际上我们也就得到了构建后缀树的抽象过程:

  1. 根据文本 Text 生成所有后缀的集合;
  2. 将每个后缀作为一个单独的关键词,构建一棵 Compressed Trie。

示例、后缀树

  对于文本 "banana\0",其中 "\0" 作为文本结束符号。下面是该文本所对应的所有后缀。

banana\0
anana\0
nana\0
ana\0
na\0
a\0
\0

  将每个后缀作为一个关键词,构建一棵 Trie

    

  然后,将独立的节点合并,形成 Compressed Trie。 

    

  则上面这棵树就是文本 "banana\0" 所对应的后缀树。

3.3、显式后缀树(Explicit Suffix Tree)隐式后缀树(Implicit Suffix Tree)

1、隐式后缀树

  隐式后缀树(implicit suffix tree):后缀可能终止于叶子结点,也可能隐藏在内部结点中。如果输入串中最后一个字符不同于其他字符,那么所有的后缀都终止于叶子结点,不会有后缀隐藏在内部结点中。这就是为什么ukk算法中最后一个字符必须是特殊字符的原因。

  使用上述banana说明

banana
anana
nana
ana
na
a

  后缀 "ana" 、”na“和 "a" 已经分别包含在后缀 "anana" 、”nana“和 "anana" 的前缀中,这样构造出来的后缀树称为隐式后缀树(Implicit Suffix Tree)。

    

2、显示后缀树

  如果不希望这样的上述情形发生,可以在每个后缀的结尾加上一个特殊字符,比如 "$" 或 "#" 等,这样我们就可以使得后缀保持唯一性。

banana$
anana$
nana$
ana$
na$
a$
$

    

3.4、使用Ukkonen算法构建后缀树

  上述是使用一种分析的方式来构建一个后缀树,但是如果字符串长度较大,上述方式可能不太可能,故ukk算法可以不用罗列枚举的解决上述问题

  Ukkonen算法(简称ukk算法)是一个online算法,它与mcc算法的一个显著区别是每次只对S的一个前缀生成隐式后缀树(implicit suffix tree),然后考虑S的下一个字符S[i+1]并将S[0...i+1]的所有后缀加入到上一个阶段中生成的隐式后缀树中,形成一个新的隐式后缀树。最后用一个特殊字符将隐式后缀树自动转换成真实的后缀树。这样ukk的一个最大优点就是不需要事先知道输入字串的全部内容,只需使用增量方式生成后缀树。和mcc算法类似,也是采用压缩存储Trie,以达到节省空间的目的。通过使用implicit extensions和suffix link两大技巧,时间复杂度可以达到线性。

  在 1995 年,Esko Ukkonen 发表了论文《On-line construction of suffix trees》,描述了在线性时间内构建后缀树的方法。

  Suffix Tree 与 Trie 的不同在于,边(Edge)不再只代表单个字符,而是通过一对整数 [from, to] 来表示。其中 from 和 to 所指向的是 Text 中的位置,这样每个边可以表示任意的长度,而且仅需两个指针,耗费 O(1) 的空间。

示例一、分析构建text=abc后缀树

  不用使用上述。使用ukk,可以理解为这个单词从左至右进入树

  1、录入a:第 1 个字符是 "a",创建一条边从根节点(root)到叶节点,以 [0, #] 作为标签代表其在 Text 中的位置从 0 开始。使用 "#" 表示末尾,可以认为 "#" 在 "a" 的右侧,位置从 0 开始,则当前位置 "#" 在 1 位。

        其代表的后缀意义如下。      

  2、录入b:开始处理第 2 个字符 "b"。涉及的操作包括:

  • 扩展已经存在的边 "a" 至 "ab";
  • 插入一条新边以表示 "b";

        其代表的后缀意义如下。      

  这里,我们观察到了两点:

  • "ab" 边的表示 [0, #] 与之前是相同的,当 "#" 位置由 1 挪至 2 时,[0, #] 所代表的意义自动地发生了改变。
  • 每条边的空间复杂度为 O(1),即只消耗两个指针,而与边所代表的字符数量无关;

  3、录入c:再处理第 3 个字符 "c",重复同样的操作,"#" 位置向后挪至第 3 位:

        其代表的后缀意义如下。      

  小结:

    操作步骤的数量与 Text 中的字符的数量一样多;

    每个步骤的工作量是 O(1),因为已存在的边都是依据 "#" 的挪动而自动更改的,仅需为最后一个字符添加一条新边,所以时间复杂度为 O(1)。则,对于一个长度为 n 的 Text,共需要 O(n) 的时间构建后缀树。

示例二、分析abcabxabcd

  abc开始的,接着重复ab ,紧跟着x,再接着重复abc,紧跟着d

  步骤1到3:经过前三步后,我们拥有和前面那个例子一样的树:

    

  步骤4:我们移动#到位置4。所有已经存在的边隐含地修改如下:

    

  如之前步骤所言,我们还需要在根节点插入当前步骤的最后一个后缀a。

  我们在插入最后一个后缀a之前,我们引入除#之外的两个或者两个更多的变量,当然这些变量一直都存在,只是我们目前为止没有使用它们而已:

  • 活动点(active point),它是一个三元组(active_node,active_edge, active_ length)
  • 剩余后缀数(remainder),它是一个整数,表名我们还需要插入多少个新的后缀。
  两个变量的含义
    在那个简单的abc例子里,活动点总是(root,’0x’,0),也就是说,active_node是根节点,active_edge总是被指定为空字符’0x’,active_ length是0。
      这么做的结果是我们在每一步插入的那一条新边是作为新创建的边插入到根节点。不久我们就会明白为什么需要三元组表示这些信息。     在每步开始时remainder 总是设置为1。这样做的意义是,在每一步结束后我们不得不主动插入的后缀数目为1(总是最后一个字符)。

  现在将会发生变化了,当我们给根节点插入当前最后一个字符a的时候,我们注意到已经存在一条以a开始的边:abca。在这种情况下我们做如下工作:

  • 我们不会在根节点插入一条新边[4,#]。相反,我们只是注意到后缀a已经在我们的树里。它会终止在更长的边的中间位置,对此我们并不疑惑,我们还是保留它们原来的样子。
  • 我们设置活动点为(root,’a’,1)。这意味着活动点现在是在根节点的以a开始的向外的边的中间某个位置,具体地指这条边的位置1之后。我们注意到这条边只是由它的首个字符a来声明的。这就足够了,因为以一个特定的字符开始的只有一条边(通读整个文档之后可以确定这是真的)。
  • 我们还增加了remainder, 那么在下一步骤开始的时候,remainder为2。

    注意:当发现我们需要插入的最终后缀已经存在在这棵树里的时候,这棵树不会发生任何改变(我们只是修改了活动节点和remainder)。这棵树就不再精确的表示当前位置的后缀树了,但是它已经包含了所有的后缀了(因为最终的后缀a也隐含地包含在里面了)。因此,除了修改变量外(所有这些变量都是定长的,因此空间复杂度是 O(1)),在这一步里没有做其他工作。

  步骤5:我们修改#当前的位置为5。这将自动地更新这棵树如下:

    

  而且由于remainder为2 ,我们需要在目前位置需要插入两个最终后缀:ab和b。这主要是因为:

  • 在上一步中的后缀a就从来没有真正地插入树中(只是用变量表示而已)。因此它一直保留着,然而由于我们已经向前走了一步,它现在由a变为ab。
  • 还有,我们需要插入新的最终边b。

  实际上,我们只需要修改活动点(它现在指向的是边abcab中a之后),而且插入当前的最后一个字符b, 不过:同时它也证明b也 已经出现在同一条边里。(But: Again, it turns out that b is also already present on that same edge.)

  因此,我们再次不修改这棵树,我们只是:

  • 修改活动点为(root,’a’,2)(是与前面相同的节点和边,只不过现在我们指向到b之后)。
  • 增加remainder为3,因为我们仍然不能插入前一步里的最终边,同时我们也不能插入当前的最终边。

  为了清晰地说明:我们不得不在当前这一步插入ab和b,但是因为ab已经找到,我们只是修改了活动点,甚至都没试图插入b。为什么?因为如果ab处于这棵树里,那么它的每个后缀(包括b)也一定在这棵树里。也许仅仅是隐含性的,不过它一定在这棵树里,因为这是我们迄今为止建立这棵树所采用的方法。

  步骤6:我们继续增加#,这棵树自动修改如下:

    

  由于remainder是3 ,我们不得不插入abx,bx和x。活动点告诉我们ab在哪结束,因此我们仅仅需要跳过这儿,然后插入x。x确实还不在这棵树里,因此我们拆分边abcabx,插入一个内部节点:

    

  这条边表示的仍然是指向文本内部的指针,因此拆分和插入内部节点的时间复杂度为O(1)。

  这时我们处理了abx,并且把remainder减为2。现在我们需要插入下一个保留的后缀bx。但是在我们做这些之前,我们需要修改活动节点。拆分并插入一条边遵循的规则称作规则1,如下,而且它适用于活动节点是根节点的情况(针对下面后续的其他情况,我们将要了解规则3)。规则1如下: 
  向根节点插入后,

  • active_node 保留为根节点
  • active_edge 为我们需要插入的新后缀的第一个字符,也就是 b。
  • active_length 减1

  因此,新的活动节点三元组为(root,’b’,1)表明下一个插入在bcabx边,第一个字符之后,也就是 b之后。我们可以确定插入点的时间复杂度为 O(1),并且检查x是否已经出现在b之后。如果它出现,我们将结束当前的步骤,保持一切为原样。然而如果x没有出现,那么我们拆分这条边而插入它:

    

  再此说明,它的时间复杂度为 O(1),而且我们按照规则1所示把remainder修改为1,活动节点修改为(root,’x’,0)。

  不过还有一件事情需要我们必须做。我们称它为规则2:

  如果我们拆分一条边并插入新的节点,如果它不是在当前这一步里创建的第一个节点的话,我们通过特殊的指针,即后缀连接(suffix link),把 以前插入的节点和新增的节点连接起来。后面我们将会知道这么做是有用的。这儿我们要明白:后缀连接用虚线边表示:

    

  我们仍然需要插入当前步骤的最终后缀x。因为活动节点的active_length已经减为0了,因此直接插入到根节点上。由于根节点上没有以x开始的边,所以我们插入了新边:

      

  正如我们所能看到的那样,在当前这一步里插入了所有剩余的后缀。

  步骤7: 我们设置#=7,这将像往常一样自动添加下一个字符a到所有的叶子边上。然后我们试图插入新的最终字符到活动节点(根节点),然后发现已经在这棵树里了。因此我们结束当前的步骤,不插入任何边,并且修改活动点为(root,’a’,1)。

  步骤8:设置#=8,我们像往常一样添加字符b,这仅仅意味着我们修改活动点为(root,’a’,2) ,增加remainder,其他事情都不需要做。因为b已经出现在这棵树里。然而我们(在 O(1)时间复杂度里)注意到活动节点现在是一条边的结尾(However, we notice (in O(1) time) that the active point is now at the end of an edge. )。我们通过重置活动节点为(node1,’\0x’,0)来反映这个。这儿,我们用node1来指ab边结束的哪个内部节点。 

  步骤9:接着设置#=9,我们需要插入’c’,这将有助于我们理解的最后一条技巧。

    第二次扩展:使用后缀连接(suffix link)

    像往常一样,#的修改自动给每条是叶子的边添加了c,而且我们转到活动点看是否可以插入’c’。活动点显示’c’已经存在在那条边里,因此我们设置活动点为(node1,’c’,1),且增加剩余后缀数,不做任何其他事情。 
    现在 设置#=10进入步骤10,剩余后缀数是4 ,因此我们首先需要在活动点插入d而实现插入abcd(这条边从第三步骤开始就一直保留着)。

    试图在活动点插入d将引起时间复杂度为O(1)的边分割:

      

  分割起始的活动点在上图中标记为红色。 最后一条规则即规则3如下:

    分割从不是根节点的活动点开始的边之后,我们应当紧跟着从活动点开始的后缀连接,如果存在一条这样的连接,那么重置活动节点使它指向这个节点。如果不存在这样后缀连接,那么我们设置活动节点为根节点,活动边和活动长度保持不变。

  因此活动节点现在是(node2,’c’,1),这里node2如下图所示标记为红色:

      

    由于abcd的插入已经完成,我们把剩余后缀数减为3,而且考虑当前步骤的下一个剩余后缀bcd。规则3已经设置活动点为右边的节点和边,因此插入bcd可以简单地向活动点插入剩余后缀的最后一个字符d来完成。要做到这个将引起另一个边分割,根据规则2 ,我们必须创建一条从以前已插入的节点开始的到新建节点的后缀连接:

    

  我们注意到:后缀连接使我们重置了活动点,因为我们能在O(1)复杂度下插入下一个剩余后缀。看看上面的图就可确定标签为ab的真正节点连接到节点b(它的后缀),而节点abc则连接到bc节点。

  当前步骤仍然没有结束。现在剩余后缀数是2,我们需要遵循规则3再次重置活动节点。由于当前的活动节点(上图中红色标记的)已经没有后缀连接,我们重置活动节点位根节点。活动节点现在是(root,’c’,1)。

  因此下一个插入发生在根节点的一条边上,以c开始的这条边的标签为:cabxabcd,位于第一个字符之后,即c之后。这将产生另一个分割:

      

    另外,由于这涉及到新的内部节点的创建,我们遵循规则2,设置一条新的从前面已创建的内部节点开始的后缀连接:

      

    (为了制作这些小图,我使用了Graphviz Dot软件。新的后缀连接使得Dot软件爱你重新布局了已经存在的边,因此仔细地检查并确定上图中插入的唯一的东西就是一条新的后缀连接。)创建了这条连接,剩余后缀树可设置为1 ,另外由于活动节点是根节点,我们根据规则1修改活动点位(root,’d’,0)。这意味着这一步的最后一个插入是向根节点插入单独的d:

      

  这是最后一步,至此我们已经完成了后缀树的建立。虽然工作已经完成,但还有许多最后要注意的地方:

  • 在每一步里,我们向前移动#一个位置。这自动在时间复杂度O(1)内修改了所有的叶子结点。
  • 不过,后缀树没有处理 a) 前一步骤保留下来的任何后缀 b)和当前步骤的最后一个字符。
  • 剩余后缀树告诉我们我们需要做多少个后续的插入。这些插入把一对一对应为在当前位置#结束的字符串的最后的后缀。我们认为是一个接着一个,然后再对它们进行插入。重要的是:每条插入都在O(1)的时间复杂度内完成,因为活动点告诉我们确切的位置,然后我们只需要在活动点增加一个单独的字符。为什 么?因为其他字符都隐含地包含了(否则活动点将是其他地方)。
  • 在做了每个这样的插入之后,我们把剩余后缀数减少,并且如果存在后缀的边,就添加一条后缀连接。如果不存在,(根据规则3)我们把活动节点设置为根节点。如果我们已经处在根节点,那么我们根据规则1修改活动节点。在任何情况下,它只花费O(1)的时间复杂度。
  • 在任意插入期间,我们发现我们需要插入的字符已经存在,那么我们不作任何事情而结束当前步骤,甚至在剩余后缀树大于0的情况下。理由是保留的任何插入都是我们试图插入的边的后缀。因此它们所有都隐藏在当前的树里。事实是剩余后缀树大于0确保我们后续对剩余后缀的处理。
  • 如果在算法结束时剩余后缀数大于0意味着什么呢?将是这中情况:结束的文本是以前出现在某个地方的这个文本的子字符串。在这种 情况下,我们必须给这个字符串结尾添加一个额外以前没有出现过的字符。在这样的文档里,通常使用美元符号$作为解决这个问题的 符号。为什么会发生这种事情呢?—>如果后来我们使用完整的后缀树搜寻后缀,那么我们只有在后缀结束于叶子时才接受搜寻匹配。 否则我们会得到许多假的匹配,因为后缀树立简单地包含了不是猪字符串的真正后缀的许多这样的字符串。在结束的时候强制剩余后 缀数为0是确保所有的后缀都结束在叶子节点的重要方法。然而,如果玩么想用这棵树来寻找通常的子字符串,而不仅仅是主字符串的 后缀,那么根据下面OP的评论的建议,最后一步确实不是必需的。
  • 那么,整个算法的复杂性如何呢?如果文本是长度为n的字符组成,那么显然需要n步(或者如果我们增加了没有符号,那么就是n+1 步)。在每个步骤里,我们要么(除了修改变量外)什么都不做,要门我们插入剩余的后缀,每一步都花费O(1)时间复杂度。由于剩余后缀数表明了我们在以前的步骤里不做任何事情的次数,而且现在我们每做一次插入就对剩余后缀数递减,我们做这样的事情 总的次数准确地说是n(或者n+1)。因此,整体的复杂度是O(n)。
  • 然而,有一处小的地方我没有正确地说明: 可能发生这样的情况,我们添加了一条后缀连接,修改活动点,然后发现活动点的活动长度与新的活动节点不能一起正常工作。例如,看看下面这种情况: 

    

    (短划线指的是这棵树的剩余部分,虚线指的是后缀连接。)

    现在,假设活动节点是(red,’d’,3),因此它指向def边的f之后的位置。现在假设我们做了必须的修改,而且现在依据规则3续接了后缀连接并修改了活动节点。新的活动节点是(green,’d’,3)。然而从绿色节点出发的d边是de,因此这条边只有2个字符。为了找到正确的活动点,很明显我们需要添加一个到蓝色节点的边,然后重置活动节点为(blue,’f’,1)。

    在特别糟的情况下,活动长度可以是剩余后缀数那么大,它甚至可以与n一样大。再在找正确的活动节点的时候,这种情况可能刚好发生,我们不仅仅需要跳过一个内部节点长度,不过也许很长,最坏的情况是高达n。由于在每一步里 剩余后缀的插入通常是O(n),续接了后缀之后的对活动节点的后续调整也是O(n)的复杂度 ,这是否意味着这个算法具有隐藏的O(n 2)的复杂度?

    不是这样的,理由是如果我们确实需要调整活动节点(例如,如上图所示从绿色节点调整到蓝色节点),那么这就给我们引入了一个拥有自己的后缀连接的新节点,而且活动长度将缩减。当我们沿着后缀连接这个链向下走,我们就要插入剩余的后缀,且只是缩减活动长度,使用这种方法我们可以调整的活动点的数目不可能超过任何给定时刻的活动长度。由于活动长度从来不会超过剩余后缀数,而后缀剩余数不仅仅在每个单一步骤里是O(n),而且对整个处理过程进行的剩余后缀递增的总数也是O(n),因此调整活动节点的数目也是以O(n)为界的。

资料:

Ukkonen’s paper on the algorithm 
Fast String Searching With Suffix Trees 
Ukkonen’s suffix tree algorithm

参看原文地址:

  https://blog.csdn.net/SunnyYoona/article/details/43986299

  https://blog.csdn.net/ljsspace/article/details/6596509

  https://blog.csdn.net/aiphis/article/details/48489709


代码地址:地址 中的data-004-tree中 suffixtree

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


  

 

posted @ 2019-05-22 09:26  bjlhx15  阅读(1798)  评论(0编辑  收藏  举报
Copyright ©2011~2020 JD-李宏旭