`
把酒泯恩仇
  • 浏览: 27171 次
  • 性别: Icon_minigender_1
  • 来自: 广州
最近访客 更多访客>>
文章分类
社区版块
存档分类
最新评论

图解后缀树,翻译了3个小时,你还不懂的话,找我

阅读更多

原文查看:

http://www.ibaiyang.org/2013/01/06/suffix-tree-introduction/

看过非常多的不靠谱suffix tree介绍后,本文是我在网上发现至今最好的一篇,通过三个规则讲述了整棵后缀树的构建过程,图形结合,非常容易理解,并且本文尊重原作者Ukkonen的论文术语,清楚的讲解了出现在suffix tree中的每一个概念,花时3个小时翻译之,共勉,部分有修改和抛弃。

正文如下:

接下来我将通过一个简单的字符串(不包含重复的字符)来试着分析Ukkonen算法,接着来讲述完整的算法框架。

首先,一点简单的事前描述

1. 我们构建的是一个简单的类似搜索字典类(search trie)结构,所以存在一个根节点(root node)。树的边(edges)指向一个新的节点,直到叶节点。

2. 但是,不同于搜索字典类(search trie),边标签(edge label)不是单个字符,相反,每一个边被标记为一对整数[from, to]。这一对整数是输入字符串的索引(index)。这样,每一个边记录了任意长度的子字符(substring),但是只需要O(1)空间复杂度(一对整数索引)。

基本约定

下面我将用一个没有重复字符的字符串来说明如何创建一颗后缀树(suffix tree):

abc

本算法将从字符串的左边向右边一步一步的执行。每一步处理输入字符串的一个字符,并且每一步抑或涉及不止一种的操作,但是所有的操作数和是O(n)时间复杂度的。

好,我们现在将字符串左边的a插入到后缀树,并且将此边标记为[0, #],它的意思是此边代表了从索引0开始,在#索引结束的子字符串(我使用符号#表示当前结束索引,现在的值是1,恰好在a位置后面)。

所以,我们有初始化后的后缀树:

其意思是:

现在我们处理索引2,字符b。我们每步的目的是将所有后缀(suffixes)的结束索引更新当前的索引。我们可以这样做:

1. 拓展存在的a边,使其成为ab;

2. 为b插入一条新边。

然后变成这样:

其意思是:

我们观察到了二点:

  1. 表示ab的边同我们初始化的后缀树:[0, #]。它意味着将会自动改变,我们仅仅更新#,使其成为2即可;
  2. 每一步只需要O(1)的空间复杂度,因为我们只记录了一对整数索引而已。

接下来,我们继续自增#索引,现在我们需要插入字符c了。我们将c插入到后缀树中的每一条边,然后在为后缀c插入一条新边。

它们像下面:

其意思是:

我们注意到:

  1.  在每一步后,恰好都是一颗正确的后缀树;
  2. 总共需要字符串长度的数量的操作;
  3. 所有的操作都是O(1)。

第一次拓展:简单的重复字符串

上面的算法工作的非常正确,接下来我们来看看更加复杂的字符串:

abcabxabcd

步骤1至3:正如之前的例子:

步骤4:我们自增#到索引4。这将自动的更新所有已存在的边:

接着,我们需要将当前步骤的后缀a(suffix a), 插入到根节点。

在此之前,我们引入另外二个变量(不包括之前的变量#):

  1. active point, 其实一个三元组(active_node, active_edge, active_length);
  2. remainder, 一个用来记录还有剩余多少新的后缀需要插入的整数数量。

这二个变量的准确意义将在后面愈来愈清楚,至于现在我们可以这样来解说:

  1. 在abc这个例子中,active point始终都是(root, None, 0), 其作用也就是说,如果有一条新边需要插入,那么都插入到根节点下。
  2. remainder变量在每一步的开始都始终设为1。其意思是,我们必须要插入的后缀数量是1,也就是待插入单个字符本身。

现在这二个变量将有所改变。当我们在root节点插入当前的最后一个字符a时,我们注意到已经存在了一条以a开头的边了,它就是abca。所以,出现这种情况下,我们需要如下做:

  1. 我们不在根节点插入新的节点;
  2. 相反,我们注意到后缀a已经存在了我们的树中,所以我们不管它;
  3. 修改active point为(root, ‘a’, 1)。它的意思是现在active point如今指向从根节点出发的一条以a开头的边上,并且在索引1后面。
  4. 我们增加remainder的值,现在其为2了。

注意:当我们最后一个需要插入的后缀如果已经存在在这颗树种,那么我们什么都不做,只是更新active point和remainder即可。现在这棵树已经不在非常标准的后缀树了,但是它包含所有的后缀,只是最后一个后缀a被隐式的包含了。

步骤5:我们更新当前的索引#为5. 这将自动的进行如下更新:

因为remainder是2,我们需要插入二个当前索引的最后二个后缀:abb。这是因为:

  1. 上一步的后缀a并没有合适的插入,只是被隐式包含在abca边中。所以,它需要被保留,而且,在当前这步,它已经从a变成了ab了。
  2. 我们需要插入新的边b

实际上,这意味着我们需到active point节点(它指向边abcab的索引1,在a的后面位置)插入最后字符b,但是恰好b也已经存在在同一条边了。

所以,我们什么也不做,仅仅:

  1. 更新active point为(root, ‘a’, 2)(同一个节点和边,但是我们指向b的后面,所以active point的长度变成了2,指向b的后面)
  2. 增加remainder为3,因为我们也没有插入。

当然,我们不得不在当前步骤插入abb,但是因为ab已经存在,所以我们更新active point并且我们不插入b。为什么?因为ab在这颗树中,那么它的每一个后缀必定存在这个树中。也许它是隐士被包含的,但是,它一定存在,因为我们是一步一步如此建这颗树的。

步骤6:增加#为6,这棵树自动更新为:

因为remainder是3,所以我们不得不插入abxbxx。active point告诉我们ab在哪里结束,所以我们仅仅只需要跳到此节点,然后插入x。更加准确的说,x如果也不存在,我们需要分隔边abcabx,并且插入一个内部节点:

边的表示还是用一对整数索引表示,所以分隔和插入只需要O(1)时间复杂度。

到目前为止,我们处理了后缀abx,并且remainder减少到了2. 好,我们还要继续插入接下来的后缀bx。但是在我们插入之前,我们需要更新active point。

这里有一个规则,其是在分隔和插入一条边后,叫做Rule1,它将起作用当active node是根节点时,至于其他的情况,我们有Rule3, 后面将会介绍。
这里是Rule1,在从root插入之后:

  1. active_node 依然不变;
  2. active_edge 被设置为下一个新后缀的第一个字符,本例中是b;
  3. active_length 自减。

到现在为止,新active point三元组(root, ‘b’, 1)表示下一步插入将在边bcabx发生,本例中是在b的后面。我们检查x是否已经存在,如果存在,我们将结束当前步骤,什么都不做;如果不存在,我们分隔此边,插入该字符。

它将花费O(1)的时间,更新remainder为1,并且根据Rule1更新active point为(root, ‘x’, 0)。

我们还有其他事需要做,接着我们介绍Rule2:

如果我们分隔一条边并且插入一个新的节点,而且这个新的节点不是在当前步骤中第一个新的节点,我们需要将之前创建的节点指向这个新创建的节点,这条边称为 suffix link。我们将在后面发现其非常有用,这里使用虚线表示 suffix link。

在插入后缀bx后,加上suffix link后:

到这里,我们还有后缀x还没有插入。因为active point(root, ‘x’, 0)中active_length是0,所以,最后一个后缀x直接从root插入,因为这里没有一个边以x作为前缀。

从上图看,之前遗留的三个后缀abx,bx和x已悉数插入。

步骤7:更新#为7,其将自动的添加下一个字符a到所有的叶边(leaf edges). 然后,我们试着插入新的最后字符到active point(root,’x', 0),但是发现字符a已经存在,所以我们什么也不做,只是更新active point为(root, ‘a’, 1)和自增remainder,此时为2。

步骤8:#=8, 我们需要插入ab和b,因为remainder为2。我们插入ab,正如之前的例子,这个也只需要更新update point为(root, ‘a’, 2)即可,并且自增remainder,因为b已经。这是我们发现active point现在处于一条边的终端。我们设此节点为node1,然后active point可以变为(node1, None, 0)。这里我使用node1表示边ab的终结点。

步骤9:#=9, 我们将要理解后缀树中最后一个难点。

第二次拓展:使用suffix link

现在,#已经更新到了字符c,它将会自动的添加到叶边(leaf edges),并且我们跳到active point是否我们能插入字符c。结果被证明c已经存在,所以我们设active point为(node1, ‘c’, 1),自增remainder,不需要做什么。

步骤10:经过几步的自增remainder,现在其值已经是4.所以在步骤10,我们首先需要通过向active point插入d来实现插入abcd(其值追溯到前三步,它们分别插入abc).

将d插入到active point。

这个被标记的active node在图中用红色被标识。

这是最后一个规则Rule3:

在从一个非root节点的active_node分隔一条边后,我们沿着suffix link(如果存在的话),将active_node设定为其指向的节点;如果不存在的话,设定active_node为root根节点。active_edge和active_length保持不变。

所以在应用Rule3后,现在指向(node2, ‘c’, 1),node2在下图被标识为红色:

因为后缀abcd已经被插入,所以自减remainder为3. 接着插入bcd。因为Rule3已经设定active point到了node2,所以我们只需要在active point后插入d即可。

通过插入d,将应用Rule2,我们必须创建suffix link。

我们注意到,suffix link可以让我们重新设定active point,使接下来的插入操作能够在O(1)时间完成。

步骤10还没有完成,因为remainder是2. 我们需要使用Rule3重新设定active point,因为当前的active_node(被标识为红色)没有suffix link,所以我们设定其为root,这样,active point被标记为(root, ‘c’, 1)。

也就是说,下面的插入将发生在从root节点,以c起始的边上。所以,在插入d后:

在自减remainder后,是1,继续应用Rule2,加入新的suffix link从之前被创建的节点。

最后,remainder被设定为1,因为active node是root,所以我们使用Rule1来更新active point(root, ‘d’, 0),这以为着,我们将在根节点加入d.

到此为止,所有的步骤已经完成。

这是最后的一点思考:

  1. 在每一步,我们自增#。这将自动的更新所有的叶节点(leaf nodes)在O(1)的时间内。
  2. 但是,其并没有处理由之前的步骤产生的后缀,只是被隐士的包含了。
  3. remainder告诉我们还有多少额外的插入需要做,并且active point能准确的告诉我们在哪里插入。

总结一下三个规则:
规则1:
从root节点插入之后:

  1. active_node 依然不变;
  2. active_edge 被设置为下一个新后缀的第一个字符,本例中是b;
  3. active_length 自减。

规则2:
如果我们分隔一条边并且插入一个新的节点,而且这个新的节点不是在当前步骤中第一个新的节点,我们需要将之前创建的节点指向这个新创建的节点,这条边称为 suffix link。我们将在后面发现其非常有用,这里使用虚线表示 suffix link。

规则3:
在从一个非root节点的active_node分隔一条边后,我们沿着suffix link(如果存在的话),将active_node设定为其指向的节点;如果不存在的话,设定active_node为root根节点。active_edge和active_length保持不变。

References:
http://stackoverflow.com/questions/9452701/ukkonens-suffix-tree-algorithm-in-plain-english

1
3
分享到:
评论

相关推荐

    关于后缀树的超牛B论文,有详细的图解

    ### 关于后缀树的超牛B论文:详细介绍与应用 #### 后缀树简介 后缀树是一种高效的数据结构,用于处理字符串匹配问题。它主要用于查找一个模式(pattern)是否存在于一个较大的文本中,或者查找一个模式在文本中...

    IT面试--后缀树详解及其代码实现

    构建后缀树的过程主要包括两个步骤:构建初始 Trie 树和压缩 Trie 树。 - **构建初始 Trie 树**:将字符串的所有后缀作为根节点的子节点插入 Trie 树中。 - **压缩 Trie 树**:通过合并单一子节点的路径来减少节点...

    图解红黑树篇(图解).svg

    图解红黑树篇(图解).svg

    629969 图解密码技术 第三版_密码学_www.629969.com_629969_图解密码_图解密码技术_

    哈希函数,如MD5和SHA系列,用于数据完整性验证,而消息认证码(MAC)结合了密钥和哈希函数,确保数据既不被篡改又能验证来源。 破译学则侧重于密码分析与破解,包括密码攻击策略如蛮力攻击、选择明文攻击、选择...

    图解 B+ 树的插入和删除 ( 一看就懂)

    3. 每个节点可以有多于两个子节点,通常与磁盘块大小有关,以便一次性读取多个键值对。 4. 节点中的键按照升序排列,每个键都与一个指向其子节点的指针关联。 ### 插入操作 插入新数据时,B+树会遵循以下步骤: 1....

    数据结构-史上最强图解红黑树

    红黑树是一种自平衡二叉搜索树,在实际应用中,它广泛用于Linux内核、Java的TreeMap和TreeSet数据结构以及C++的STL中。红黑树的设计旨在保持树的平衡,但是与AVL树不同,它追求的不是绝对的平衡,而是通过放弃一定的...

    谁都看得懂的统计学超图解

    资源名称:谁都看得懂的统计学超图解资源目录:.1章? 了解統計學的基礎.何謂統計學?? ?.統計的實施及運用方法?? ?.統計學的歷史 ?.越發活躍的統計學?? ?.統計學所使用的資料種類? ?.統計學人物傳1南丁格爾與...

    SSH框架整合图解(3个方案)

    SSH框架整合图解(3个方案)SSH框架整合图解(3个方案)SSH框架整合图解(3个方案)SSH框架整合图解(3个方案)SSH框架整合图解(3个方案)SSH框架整合图解(3个方案)SSH框架整合图解(3个方案)SSH框架整合图解(3...

    算法图解.pdf,就是个简单的一个pdf

    算法图解.pdf,就是个简单的一个pdf,这里有字数要求啊,哎呀哎呀,算法图解算法图解算法图解算法图解算法图解算法图解算法图解算法图解算法图解算法图解算法图解算法图解算法图解算法图解算法图解算法图解算法图解...

    《图解HTTP》完整彩色版1

    《图解HTTP》是一部深入浅出介绍Web和HTTP协议的著作,由上野宣撰写,于均良翻译成中文。这本书旨在帮助读者理解HTTP协议的基本原理和Web的工作机制,同时结合历史背景,让读者能够更好地领会HTTP的设计理念。 1. ...

    关于b+树的操作详细图解

    在【标题】"关于b+树的操作详细图解"和【描述】中提到的PPT,很可能是对B+树的操作进行了深入且直观的解释。 首先,B+树是一种M阶的树(M>3),这意味着每个节点可以有M个子节点。与二叉树相比,较高的分支因子M能...

    图解HTTP,图解HTTP

    - **注重实践应用**:不仅介绍理论知识,还提供了大量实战案例,使读者能够将所学应用于实际工作中。 #### 四、适合人群 - **Web开发工程师**:无论是前端还是后端开发人员,都能从本书中获得宝贵的HTTP协议知识。 ...

    微信小程序开发图解案例教程-源代码

    微信小程序开发图解案例教程-源代码微信小程序开发图解案例教程-源代码微信小程序开发图解案例教程-源代码微信小程序开发图解案例教程-源代码微信小程序开发图解案例教程-源代码微信小程序开发图解案例教程-源代码...

    图解css3源码

    图解css3源码

    麦克斯韦方程直观 翻译版 图解直观数学

    图解直观数学 麦克斯韦方程直观 翻译版 图解直观数学 麦克斯韦方程直观 翻译版

    linux源代码目录树结构图解

    linux 源代码目录树结构图解,希望能帮助大家更直观的,更好的了解Linux内核的结构。

    利用Arcgis图解建模

    利用Arcgis图解建模实现替代路径规划 Arcgis中的图解建模是一种强大的空间分析技术,可以用于解决各种复杂的地理空间问题。图解建模的应用非常广泛,包括城市规划、交通规划、环境监测、灾害预测等。今天,我们将...

    算法图解-python,算法图解python3

    《算法图解-python,算法图解python3》是一本针对初学者和有一定编程基础的读者设计的算法入门书籍,特别注重使用Python语言进行讲解。这本书旨在帮助读者理解算法的基本概念,提升解决实际问题的能力,同时也为深入...

    激活你大脑的“图解思考”术

    《激活你大脑的"图解思考"术》从"图解思考法"的概念,类型,以及如何绘制图解等入手,帮助读者领域图解思考的内涵,让你在工作和生活中及时合理运用图解思考法。通过阅读《激活你大脑的"图解思考"术》,不仅可以快速...

    图解TCPIP 第5版

    图解TCPIP 第5版图解TCPIP 第5版图解TCPIP 第5版图解TCPIP 第5版

Global site tag (gtag.js) - Google Analytics