Ukkonen的简明英语后缀树算法

时间:2012-02-26 11:30:10

标签: string algorithm language-agnostic suffix-tree

此时我感觉有点厚。我花了几天时间试图完全用后缀树构建我的头,但由于我没有数学背景,因为他们开始过度使用数学符号系统时,许多解释都没有。最接近我发现的一个很好的解释是 Fast String Searching With Suffix Trees ,但是他掩盖了各种观点,算法的某些方面仍然不清楚。

对于除了我之外的许多其他人来说,在Stack Overflow上对此算法的逐步解释将是非常宝贵的,我敢肯定。

供参考,这是Ukkonen关于算法的论文:http://www.cs.helsinki.fi/u/ukkonen/SuffixT1withFigs.pdf

我的基本理解,到目前为止:

  • 我需要遍历给定字符串T的每个前缀P
  • 我需要遍历前缀P中的每个后缀S并将其添加到树
  • 要向树中添加后缀S,我需要遍历S中的每个字符,迭代包括沿着现有分支向下走,该分支以S中相同的字符集C开头,并可能将边缘分成后代当我在后缀中找到不同的字符时,或者如果没有匹配的边缘可以向下走,则为节点。如果找不到匹配的边缘向下走C,则会为C创建新的叶边。

基本算法似乎是O(n 2 ),正如我们在大多数解释中所指出的那样,因为我们需要遍历所有前缀,然后我们需要逐步执行每个前缀每个前缀的后缀。由于他使用了后缀指针技术,Ukkonen的算法显然是独一无二的,尽管我认为 是我无法理解的。

我也无法理解:

  • 确切地指定,使用和更改“活动点”的时间和方式
  • 算法的标准化方面正在发生什么
  • 为什么我看到的实现需要“修复”他们正在使用的边界变量

以下是已完成的 C#源代码。它不仅工作正常,而且支持自动规范化,并提供更好看的输出文本图表。源代码和示例输出位于:

  

https://gist.github.com/2373868


更新2017-11-04

多年以后,我发现了后缀树的新用途,并在 JavaScript 中实现了该算法。要点如下。它应该没有错误。从同一位置将其转储到js文件npm install chalk中,然后使用node.js运行以查看一些彩色输出。在同一个Gist中有一个精简版本,没有任何调试代码。

  

https://gist.github.com/axefrog/c347bf0f5e0723cbd09b1aaed6ec6fc6

5 个答案:

答案 0 :(得分:122)

我尝试使用jogojapan的答案中给出的方法实现后缀树,但由于规则使用的措辞,它在某些情况下没有用。此外,我已经提到没有人设法使用这种方法实现一个绝对正确的后缀树。下面我将写一个"概述" jogojapan的答案,对规则进行了一些修改。我还将描述忘记创建 重要 后缀链接的情况。

使用其他变量

  1. 活动点 - 三元组(active_node; active_edge; active_length),显示我们必须从哪里开始插入新后缀。
  2. 余下 - 显示我们必须明确添加 的后缀数量。例如,如果我们的单词是' abcaabca',其余= 3,则表示我们必须处理3个最后的后缀: bca ca 即可。
  3. 让我们使用内部节点的概念 - 除了 root leafs 之外的所有节点都是内部节点

    观察1

    当我们需要插入的最终后缀已经存在于树中时,树本身根本没有改变(我们只更新active pointremainder)。

    观察2

    如果在某个时刻active_length大于或等于当前边缘的长度(edge_length),我们会将active point向下移动,直到edge_length严格大于active_length {1}}。

    现在,让我们重新定义规则:

    规则1

      

    如果从活动节点 = root 插入后,活动长度大于0,则:

         
        
    1. 活动节点未更改
    2.   
    3. 有效长度递减
    4.   
    5. 活动边缘向右移动(到我们必须插入的下一个后缀的第一个字符)
    6.   

    规则2

      

    如果我们创建新的内部节点 内部节点创建插件,这不是第一个 SUCH 内部节点在当前步骤,然后我们通过后缀链接将 SUCH 节点与 THIS 连接起来 EM>

    Rule 2的定义与jogojapan不同,因为我们不仅考虑新创建的内部节点,还考虑内部节点,我们从插入。

    规则3

      

    活动节点(不是 root 节点)插入后,我们必须遵循后缀链接并将活动节点设置为它指向的节点。如果没有后缀链接,请将活动节点设置为 root 节点。无论哪种方式,活动边活动长度保持不变。

    Rule 3的定义中,我们还考虑了叶节点的插入(不仅是分裂节点)。

    最后,观察3:

    当我们想要添加到树的符号已经位于边缘时,我们根据Observation 1仅更新active pointremainder,保持树不变。 但是如果内部节点标记为需要后缀链接,我们必须通过后缀将该节点与我们当前的active node连接起来链接。

    如果我们在这种情况下添加后缀链接,并且如果我们不这样做,那么让我们看一下 cdddcdc 的后缀树示例。

    1. 如果我们 DON' 通过后缀链接连接节点:

      • 在添加最后一个字母 c
      • 之前

      • 添加最后一个字母 c

    2. 如果我们 DO 通过后缀链接连接节点:

      • 在添加最后一个字母 c
      • 之前

      • 添加最后一个字母 c

    3. 似乎没有显着差异:在第二种情况下,还有两个后缀链接。但是这些后缀链接正确,其中一个 - 从蓝色节点到红色节点 - 非常重要用于活动点。问题是,如果我们不在此处添加后缀链接,稍后,当我们向树中添加一些新字母时,由于Rule 3,我们可能会省略向树中添加一些节点,因为根据对它来说,如果没有后缀链接,那么我们必须将active_node放到根目录。

      当我们将最后一个字母添加到树中时,红色节点已经已经存在,然后我们从蓝色节点进行插入(边缘标记' c&#39 ; )。由于蓝色节点有插入,我们将其标记为需要后缀链接。然后,依靠活动点方法,active node被设置为红色节点。但是我们不能从红色节点插入插入内容,因为字母' 已经位于边缘。这是否意味着蓝色节点必须没有后缀链接?不,我们必须通过后缀链接将蓝色节点与红色节点连接起来。为什么这是正确的?因为有效点方法可以保证我们到达正确的位置,即下一个我们必须处理较短后缀的插入位置。

      最后,这是我对后缀树的实现:

      1. Java
      2. C++
      3. 希望这个"概述"结合jogojapan的详细答案将有助于某人实现自己的后缀树。

答案 1 :(得分:9)

感谢 @jogojapan 的精心解释的教程,我在Python中实现了算法。

@jogojapan提到的一些小问题比我预期的要强得多复杂,需要非常小心对待。我花了几天的时间来实现足够强大(我想)。问题和解决方案如下:

  1. Remainder > 0结束事实证明,这种情况也可能在展开步骤中发生,而不仅仅是整个算法的结束。当发生这种情况时,我们可以保留余数,actnode,actedge和actlength 不变,结束当前的展开步骤,并通过保持折叠或展开来开始另一个步骤,具体取决于原始中的下一个字符。 string是否在当前路径上。

  2. 跳过节点:当我们按照后缀链接时,更新活动点,然后发现其active_length组件与新的active_node不兼容。我们必须前进到正确的位置进行拆分或插入叶子。这个过程可能不那么简单因为在移动期间actlength和actted一直在变化,当你必须回到根节点时,由于这些举动,actedge actlength 可能错误。我们需要额外的变量来保存这些信息。

    enter image description here

  3. @managonov

    以某种方式指出了另外两个问题
    1. 拆分可能会退化尝试拆分边缘时,有时您会发现拆分操作在节点上是正确的。在这种情况下,我们只需要为该节点添加一个新的叶子,将其作为标准的边缘分割操作,这意味着如果有任何后缀链接,则应相应地进行维护。

    2. 隐藏的后缀链接 问题1 问题2 会产生另一种特殊情况。有时我们需要跳过几个节点到正确的点进行拆分,如果我们通过比较余数字符串和路径标签来移动,我们可能超过正确的点。那种情况下,后缀链接将被无意中忽略,如果有的话。在向前移动时,记住正确的点可以避免这种情况。如果拆分节点已存在,或者在展开步骤中发生问题1 ,则应保留后缀链接。

    3. 最后,我在 Python 中的实现如下:

        

      提示: 它在上面的代码中包含一个天真的树打印函数,这在调试时非常重要。它为我节省了很多   时间,便于查找特殊情况。

答案 2 :(得分:6)

我的直觉如下:

在主循环的k次迭代之后,您构建了一个后缀树,其中包含以前k个字符开头的完整字符串的所有后缀。

在开始时,这意味着后缀树包含一个表示整个字符串的根节点(这是从0开始的唯一后缀)。

在len(字符串)迭代之后,你有一个包含所有后缀的后缀树。

在循环期间,键是活动点。我的猜测是,这代表后缀树中最深的一点,它对应于字符串前k个字符的正确后缀。 (我认为正确意味着后缀不能是整个字符串。)

例如,假设您已经看过字符'abcabc'。活动点将表示树中与后缀'abc'对应的点。

活动点由(origin,first,last)表示。 这意味着您当前位于树中的点,从节点原点开始,然后以字符串[first:last] <[p]中的字符输入

添加新角色时,您会查看活动点是否仍在现有树中。如果是,那么你就完成了。 否则,您需要在活动点的后缀树中添加一个新节点,回退到下一个最短匹配,然后再次检查。

注1: 后缀指针为每个节点提供了下一个最短匹配的链接。

注2: 添加新节点和回退时,为新节点添加新的后缀指针。 此后缀指针的目标将是缩短活动点的节点。 此节点将已存在,或者在此回退循环的下一次迭代中创建。

注3:典型化部分只是节省了检查活动点的时间。 例如,假设您总是使用origin = 0,并且只是首先和最后一次更改。 要检查活动点,每次沿着所有中间节点都必须遵循后缀树。 通过仅记录距离最后一个节点的距离来缓存跟踪此路径的结果是有意义的。

你能举出“修复”边界变量的代码示例吗?

健康警告:我也发现这个算法特别难以理解,所以请认识到这种直觉可能在所有重要细节中都是错误的......

答案 3 :(得分:5)

@jogojapan you brought awesome explanation and visualisation. But as @makagonov mentioned it's missing some rules regarding setting suffix links. It's visible in nice way when going step by step on http://brenden.github.io/ukkonen-animation/通过单词&#39; aabaaabb&#39;传递不同的$ _SESSION变量。当您从步骤10转到步骤11时,从节点5到节点2没有后缀链接,但活动点突然移动到那里。

@makagonov因为我住在Java世界,我也试着按照你的实现来掌握ST建设工作流程但是因为以下原因我很难:

  • 将边缘与节点相结合
  • 使用索引指针而不是引用
  • 打破陈述;
  • 继续陈述;

所以我最终在Java中实现了这样的实现,我希望以更清晰的方式反映所有步骤,并减少其他Java人员的学习时间:

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

public class ST {

  public class Node {
    private final int id;
    private final Map<Character, Edge> edges;
    private Node slink;

    public Node(final int id) {
        this.id = id;
        this.edges = new HashMap<>();
    }

    public void setSlink(final Node slink) {
        this.slink = slink;
    }

    public Map<Character, Edge> getEdges() {
        return this.edges;
    }

    public Node getSlink() {
        return this.slink;
    }

    public String toString(final String word) {
        return new StringBuilder()
                .append("{")
                .append("\"id\"")
                .append(":")
                .append(this.id)
                .append(",")
                .append("\"slink\"")
                .append(":")
                .append(this.slink != null ? this.slink.id : null)
                .append(",")
                .append("\"edges\"")
                .append(":")
                .append(edgesToString(word))
                .append("}")
                .toString();
    }

    private StringBuilder edgesToString(final String word) {
        final StringBuilder edgesStringBuilder = new StringBuilder();
        edgesStringBuilder.append("{");
        for(final Map.Entry<Character, Edge> entry : this.edges.entrySet()) {
            edgesStringBuilder.append("\"")
                    .append(entry.getKey())
                    .append("\"")
                    .append(":")
                    .append(entry.getValue().toString(word))
                    .append(",");
        }
        if(!this.edges.isEmpty()) {
            edgesStringBuilder.deleteCharAt(edgesStringBuilder.length() - 1);
        }
        edgesStringBuilder.append("}");
        return edgesStringBuilder;
    }

    public boolean contains(final String word, final String suffix) {
        return !suffix.isEmpty()
                && this.edges.containsKey(suffix.charAt(0))
                && this.edges.get(suffix.charAt(0)).contains(word, suffix);
    }
  }

  public class Edge {
    private final int from;
    private final int to;
    private final Node next;

    public Edge(final int from, final int to, final Node next) {
        this.from = from;
        this.to = to;
        this.next = next;
    }

    public int getFrom() {
        return this.from;
    }

    public int getTo() {
        return this.to;
    }

    public Node getNext() {
        return this.next;
    }

    public int getLength() {
        return this.to - this.from;
    }

    public String toString(final String word) {
        return new StringBuilder()
                .append("{")
                .append("\"content\"")
                .append(":")
                .append("\"")
                .append(word.substring(this.from, this.to))
                .append("\"")
                .append(",")
                .append("\"next\"")
                .append(":")
                .append(this.next != null ? this.next.toString(word) : null)
                .append("}")
                .toString();
    }

    public boolean contains(final String word, final String suffix) {
        if(this.next == null) {
            return word.substring(this.from, this.to).equals(suffix);
        }
        return suffix.startsWith(word.substring(this.from,
                this.to)) && this.next.contains(word, suffix.substring(this.to - this.from));
    }
  }

  public class ActivePoint {
    private final Node activeNode;
    private final Character activeEdgeFirstCharacter;
    private final int activeLength;

    public ActivePoint(final Node activeNode,
                       final Character activeEdgeFirstCharacter,
                       final int activeLength) {
        this.activeNode = activeNode;
        this.activeEdgeFirstCharacter = activeEdgeFirstCharacter;
        this.activeLength = activeLength;
    }

    private Edge getActiveEdge() {
        return this.activeNode.getEdges().get(this.activeEdgeFirstCharacter);
    }

    public boolean pointsToActiveNode() {
        return this.activeLength == 0;
    }

    public boolean activeNodeIs(final Node node) {
        return this.activeNode == node;
    }

    public boolean activeNodeHasEdgeStartingWith(final char character) {
        return this.activeNode.getEdges().containsKey(character);
    }

    public boolean activeNodeHasSlink() {
        return this.activeNode.getSlink() != null;
    }

    public boolean pointsToOnActiveEdge(final String word, final char character) {
        return word.charAt(this.getActiveEdge().getFrom() + this.activeLength) == character;
    }

    public boolean pointsToTheEndOfActiveEdge() {
        return this.getActiveEdge().getLength() == this.activeLength;
    }

    public boolean pointsAfterTheEndOfActiveEdge() {
        return this.getActiveEdge().getLength() < this.activeLength;
    }

    public ActivePoint moveToEdgeStartingWithAndByOne(final char character) {
        return new ActivePoint(this.activeNode, character, 1);
    }

    public ActivePoint moveToNextNodeOfActiveEdge() {
        return new ActivePoint(this.getActiveEdge().getNext(), null, 0);
    }

    public ActivePoint moveToSlink() {
        return new ActivePoint(this.activeNode.getSlink(),
                this.activeEdgeFirstCharacter,
                this.activeLength);
    }

    public ActivePoint moveTo(final Node node) {
        return new ActivePoint(node, this.activeEdgeFirstCharacter, this.activeLength);
    }

    public ActivePoint moveByOneCharacter() {
        return new ActivePoint(this.activeNode,
                this.activeEdgeFirstCharacter,
                this.activeLength + 1);
    }

    public ActivePoint moveToEdgeStartingWithAndByActiveLengthMinusOne(final Node node,
                                                                       final char character) {
        return new ActivePoint(node, character, this.activeLength - 1);
    }

    public ActivePoint moveToNextNodeOfActiveEdge(final String word, final int index) {
        return new ActivePoint(this.getActiveEdge().getNext(),
                word.charAt(index - this.activeLength + this.getActiveEdge().getLength()),
                this.activeLength - this.getActiveEdge().getLength());
    }

    public void addEdgeToActiveNode(final char character, final Edge edge) {
        this.activeNode.getEdges().put(character, edge);
    }

    public void splitActiveEdge(final String word,
                                final Node nodeToAdd,
                                final int index,
                                final char character) {
        final Edge activeEdgeToSplit = this.getActiveEdge();
        final Edge splittedEdge = new Edge(activeEdgeToSplit.getFrom(),
                activeEdgeToSplit.getFrom() + this.activeLength,
                nodeToAdd);
        nodeToAdd.getEdges().put(word.charAt(activeEdgeToSplit.getFrom() + this.activeLength),
                new Edge(activeEdgeToSplit.getFrom() + this.activeLength,
                        activeEdgeToSplit.getTo(),
                        activeEdgeToSplit.getNext()));
        nodeToAdd.getEdges().put(character, new Edge(index, word.length(), null));
        this.activeNode.getEdges().put(this.activeEdgeFirstCharacter, splittedEdge);
    }

    public Node setSlinkTo(final Node previouslyAddedNodeOrAddedEdgeNode,
                           final Node node) {
        if(previouslyAddedNodeOrAddedEdgeNode != null) {
            previouslyAddedNodeOrAddedEdgeNode.setSlink(node);
        }
        return node;
    }

    public Node setSlinkToActiveNode(final Node previouslyAddedNodeOrAddedEdgeNode) {
        return setSlinkTo(previouslyAddedNodeOrAddedEdgeNode, this.activeNode);
    }
  }

  private static int idGenerator;

  private final String word;
  private final Node root;
  private ActivePoint activePoint;
  private int remainder;

  public ST(final String word) {
    this.word = word;
    this.root = new Node(idGenerator++);
    this.activePoint = new ActivePoint(this.root, null, 0);
    this.remainder = 0;
    build();
  }

  private void build() {
    for(int i = 0; i < this.word.length(); i++) {
        add(i, this.word.charAt(i));
    }
  }

  private void add(final int index, final char character) {
    this.remainder++;
    boolean characterFoundInTheTree = false;
    Node previouslyAddedNodeOrAddedEdgeNode = null;
    while(!characterFoundInTheTree && this.remainder > 0) {
        if(this.activePoint.pointsToActiveNode()) {
            if(this.activePoint.activeNodeHasEdgeStartingWith(character)) {
                activeNodeHasEdgeStartingWithCharacter(character, previouslyAddedNodeOrAddedEdgeNode);
                characterFoundInTheTree = true;
            }
            else {
                if(this.activePoint.activeNodeIs(this.root)) {
                    rootNodeHasNotEdgeStartingWithCharacter(index, character);
                }
                else {
                    previouslyAddedNodeOrAddedEdgeNode = internalNodeHasNotEdgeStartingWithCharacter(index,
                            character, previouslyAddedNodeOrAddedEdgeNode);
                }
            }
        }
        else {
            if(this.activePoint.pointsToOnActiveEdge(this.word, character)) {
                activeEdgeHasCharacter();
                characterFoundInTheTree = true;
            }
            else {
                if(this.activePoint.activeNodeIs(this.root)) {
                    previouslyAddedNodeOrAddedEdgeNode = edgeFromRootNodeHasNotCharacter(index,
                            character,
                            previouslyAddedNodeOrAddedEdgeNode);
                }
                else {
                    previouslyAddedNodeOrAddedEdgeNode = edgeFromInternalNodeHasNotCharacter(index,
                            character,
                            previouslyAddedNodeOrAddedEdgeNode);
                }
            }
        }
    }
  }

  private void activeNodeHasEdgeStartingWithCharacter(final char character,
                                                    final Node previouslyAddedNodeOrAddedEdgeNode) {
    this.activePoint.setSlinkToActiveNode(previouslyAddedNodeOrAddedEdgeNode);
    this.activePoint = this.activePoint.moveToEdgeStartingWithAndByOne(character);
    if(this.activePoint.pointsToTheEndOfActiveEdge()) {
        this.activePoint = this.activePoint.moveToNextNodeOfActiveEdge();
    }
  }

  private void rootNodeHasNotEdgeStartingWithCharacter(final int index, final char character) {
    this.activePoint.addEdgeToActiveNode(character, new Edge(index, this.word.length(), null));
    this.activePoint = this.activePoint.moveTo(this.root);
    this.remainder--;
    assert this.remainder == 0;
  }

  private Node internalNodeHasNotEdgeStartingWithCharacter(final int index,
                                                         final char character,
                                                         Node previouslyAddedNodeOrAddedEdgeNode) {
    this.activePoint.addEdgeToActiveNode(character, new Edge(index, this.word.length(), null));
    previouslyAddedNodeOrAddedEdgeNode = this.activePoint.setSlinkToActiveNode(previouslyAddedNodeOrAddedEdgeNode);
    if(this.activePoint.activeNodeHasSlink()) {
        this.activePoint = this.activePoint.moveToSlink();
    }
    else {
        this.activePoint = this.activePoint.moveTo(this.root);
    }
    this.remainder--;
    return previouslyAddedNodeOrAddedEdgeNode;
  }

  private void activeEdgeHasCharacter() {
    this.activePoint = this.activePoint.moveByOneCharacter();
    if(this.activePoint.pointsToTheEndOfActiveEdge()) {
        this.activePoint = this.activePoint.moveToNextNodeOfActiveEdge();
    }
  }

  private Node edgeFromRootNodeHasNotCharacter(final int index,
                                             final char character,
                                             Node previouslyAddedNodeOrAddedEdgeNode) {
    final Node newNode = new Node(idGenerator++);
    this.activePoint.splitActiveEdge(this.word, newNode, index, character);
    previouslyAddedNodeOrAddedEdgeNode = this.activePoint.setSlinkTo(previouslyAddedNodeOrAddedEdgeNode, newNode);
    this.activePoint = this.activePoint.moveToEdgeStartingWithAndByActiveLengthMinusOne(this.root,
            this.word.charAt(index - this.remainder + 2));
    this.activePoint = walkDown(index);
    this.remainder--;
    return previouslyAddedNodeOrAddedEdgeNode;
  }

  private Node edgeFromInternalNodeHasNotCharacter(final int index,
                                                 final char character,
                                                 Node previouslyAddedNodeOrAddedEdgeNode) {
    final Node newNode = new Node(idGenerator++);
    this.activePoint.splitActiveEdge(this.word, newNode, index, character);
    previouslyAddedNodeOrAddedEdgeNode = this.activePoint.setSlinkTo(previouslyAddedNodeOrAddedEdgeNode, newNode);
    if(this.activePoint.activeNodeHasSlink()) {
        this.activePoint = this.activePoint.moveToSlink();
    }
    else {
        this.activePoint = this.activePoint.moveTo(this.root);
    }
    this.activePoint = walkDown(index);
    this.remainder--;
    return previouslyAddedNodeOrAddedEdgeNode;
  }

  private ActivePoint walkDown(final int index) {
    while(!this.activePoint.pointsToActiveNode()
            && (this.activePoint.pointsToTheEndOfActiveEdge() || this.activePoint.pointsAfterTheEndOfActiveEdge())) {
        if(this.activePoint.pointsAfterTheEndOfActiveEdge()) {
            this.activePoint = this.activePoint.moveToNextNodeOfActiveEdge(this.word, index);
        }
        else {
            this.activePoint = this.activePoint.moveToNextNodeOfActiveEdge();
        }
    }
    return this.activePoint;
  }

  public String toString(final String word) {
    return this.root.toString(word);
  }

  public boolean contains(final String suffix) {
    return this.root.contains(this.word, suffix);
  }

  public static void main(final String[] args) {
    final String[] words = {
            "abcabcabc$",
            "abc$",
            "abcabxabcd$",
            "abcabxabda$",
            "abcabxad$",
            "aabaaabb$",
            "aababcabcd$",
            "ababcabcd$",
            "abccba$",
            "mississipi$",
            "abacabadabacabae$",
            "abcabcd$",
            "00132220$"
    };
    Arrays.stream(words).forEach(word -> {
        System.out.println("Building suffix tree for word: " + word);
        final ST suffixTree = new ST(word);
        System.out.println("Suffix tree: " + suffixTree.toString(word));
        for(int i = 0; i < word.length() - 1; i++) {
            assert suffixTree.contains(word.substring(i)) : word.substring(i);
        }
    });
  }
}

答案 4 :(得分:3)

您好我已尝试在ruby中实现上述解释,请查看。 它似乎工作正常。

实现的唯一区别是,我试图使用边缘对象而不是仅仅使用符号。

它也出现在https://gist.github.com/suchitpuri/9304856

    require 'pry'


class Edge
    attr_accessor :data , :edges , :suffix_link
    def initialize data
        @data = data
        @edges = []
        @suffix_link = nil
    end

    def find_edge element
        self.edges.each do |edge|
            return edge if edge.data.start_with? element
        end
        return nil
    end
end

class SuffixTrees
    attr_accessor :root , :active_point , :remainder , :pending_prefixes , :last_split_edge , :remainder

    def initialize
        @root = Edge.new nil
        @active_point = { active_node: @root , active_edge: nil , active_length: 0}
        @remainder = 0
        @pending_prefixes = []
        @last_split_edge = nil
        @remainder = 1
    end

    def build string
        string.split("").each_with_index do |element , index|


            add_to_edges @root , element        

            update_pending_prefix element                           
            add_pending_elements_to_tree element
            active_length = @active_point[:active_length]

            # if(@active_point[:active_edge] && @active_point[:active_edge].data && @active_point[:active_edge].data[0..active_length-1] ==  @active_point[:active_edge].data[active_length..@active_point[:active_edge].data.length-1])
            #   @active_point[:active_edge].data = @active_point[:active_edge].data[0..active_length-1]
            #   @active_point[:active_edge].edges << Edge.new(@active_point[:active_edge].data)
            # end

            if(@active_point[:active_edge] && @active_point[:active_edge].data && @active_point[:active_edge].data.length == @active_point[:active_length]  )
                @active_point[:active_node] =  @active_point[:active_edge]
                @active_point[:active_edge] = @active_point[:active_node].find_edge(element[0])
                @active_point[:active_length] = 0
            end
        end
    end

    def add_pending_elements_to_tree element

        to_be_deleted = []
        update_active_length = false
        # binding.pry
        if( @active_point[:active_node].find_edge(element[0]) != nil)
            @active_point[:active_length] = @active_point[:active_length] + 1               
            @active_point[:active_edge] = @active_point[:active_node].find_edge(element[0]) if @active_point[:active_edge] == nil
            @remainder = @remainder + 1
            return
        end



        @pending_prefixes.each_with_index do |pending_prefix , index|

            # binding.pry           

            if @active_point[:active_edge] == nil and @active_point[:active_node].find_edge(element[0]) == nil

                @active_point[:active_node].edges << Edge.new(element)

            else

                @active_point[:active_edge] = node.find_edge(element[0]) if @active_point[:active_edge]  == nil

                data = @active_point[:active_edge].data
                data = data.split("")               

                location = @active_point[:active_length]


                # binding.pry
                if(data[0..location].join == pending_prefix or @active_point[:active_node].find_edge(element) != nil )                  


                else #tree split    
                    split_edge data , index , element
                end

            end
        end 
    end



    def update_pending_prefix element
        if @active_point[:active_edge] == nil
            @pending_prefixes = [element]
            return

        end

        @pending_prefixes = []

        length = @active_point[:active_edge].data.length
        data = @active_point[:active_edge].data
        @remainder.times do |ctr|
                @pending_prefixes << data[-(ctr+1)..data.length-1]
        end

        @pending_prefixes.reverse!

    end

    def split_edge data , index , element
        location = @active_point[:active_length]
        old_edges = []
        internal_node = (@active_point[:active_edge].edges != nil)

        if (internal_node)
            old_edges = @active_point[:active_edge].edges 
            @active_point[:active_edge].edges = []
        end

        @active_point[:active_edge].data = data[0..location-1].join                 
        @active_point[:active_edge].edges << Edge.new(data[location..data.size].join)


        if internal_node
            @active_point[:active_edge].edges << Edge.new(element)
        else
            @active_point[:active_edge].edges << Edge.new(data.last)        
        end

        if internal_node
            @active_point[:active_edge].edges[0].edges = old_edges
        end


        #setup the suffix link
        if @last_split_edge != nil and @last_split_edge.data.end_with?@active_point[:active_edge].data 

            @last_split_edge.suffix_link = @active_point[:active_edge] 
        end

        @last_split_edge = @active_point[:active_edge]

        update_active_point index

    end


    def update_active_point index
        if(@active_point[:active_node] == @root)
            @active_point[:active_length] = @active_point[:active_length] - 1
            @remainder = @remainder - 1
            @active_point[:active_edge] = @active_point[:active_node].find_edge(@pending_prefixes.first[index+1])
        else
            if @active_point[:active_node].suffix_link != nil
                @active_point[:active_node] = @active_point[:active_node].suffix_link               
            else
                @active_point[:active_node] = @root
            end 
            @active_point[:active_edge] = @active_point[:active_node].find_edge(@active_point[:active_edge].data[0])
            @remainder = @remainder - 1     
        end
    end

    def add_to_edges root , element     
        return if root == nil
        root.data = root.data + element if(root.data and root.edges.size == 0)
        root.edges.each do |edge|
            add_to_edges edge , element
        end
    end
end

suffix_tree = SuffixTrees.new
suffix_tree.build("abcabxabcd")
binding.pry