后缀树

利用后缀数组构造后缀树 由于蒟蒻azui前段时间忙着准备省选,并在省选中成功闷声滚大粗,博客停更了好久。。 阅读详情

后缀树,就是把一串字符的所有后缀保存并且压缩的字典树。相对于字典树来说,后缀树并不是针对大量字符串的,而是针对一个或几个字符串来解决问题。比如字符串的回文子串,两个字符串的最长公共子串等等,后面应用会说。

一、建立后缀树

比如单词banana,它的所有后缀显示到下面的。1代表从第一个字符为起点,终点不用说都是字符串的末尾。
这里写图片描述

以上面的后缀,我们建立一颗后缀树。如下图,为了方便看到后缀,我没有合并相同的前缀。
这里写图片描述

后缀树是把一个字符串所有后缀压缩并保存的字典树,所以我们把字符串的所有后缀还是按照字典树的规则建立,就成了上图的样子。 注意还是和字典树一样,根节点必须为空。


二、压缩后缀树

下面说下更加节省空间的方案,也就是上面提到的压缩。
这里写图片描述

因为有些后缀串可能是单串,并不和其他的共用同一个前缀。
比如上图中的banana这个后缀串,直接可以用1来表示起点,终点是默认的。
上图的a节点后面有两个节点标记3和5是右边字符数组的下标,对应着a->3-7,a->5-7。因为a是共有的前缀。


三、应用

3.1 判断字符串s1是否是字符串s2的子串

https://blog.youkuaiyun.com/heyuchang666/article/details/49784201

如果在后缀树T中查找子串P,我们需要这样的过程:

(1) 从根结点root出发,遍历所有的根的孩子结点:N1,N2,N3….

(2) 如果所有孩子结点中的关键字的第一个字符都和P的第一个字符不匹配,则没有这个子串,查找结束。

(3) 假如N3结点的关键字K3第一个字符与P的相同,则匹配K3和P。

若 K3.length>=P.length 并且K3.subString(0,P.length-1)=P,则匹配成功,否则匹配失败。

若 K3.length<=P.length 并且K3=P.subString(0, K3.length-1),则将子串P1=P.subString(K3.length, P.length); 即取出P中排除K3之后的子串。然后P1以N3为根结点继续重复(1)~(3)的步骤。直到匹配完P1的所有字符,则匹配成功。否则匹配失败。

查询效率:很显然,在上面的算法中。匹配成功正好比较了P.length次字符。而定位结点的孩子指针,和Trie情况类似,假如字母表数量为d。则查询效率为O(d*m),实际上,d是固定常数,如果使用Hash表直接定位,则d=1.

因此,后缀树查询子串P的时间复杂度为O(m),其中m为P的长度。


3.2 获得字符串s1在字符串s2中重复的次数

比如说banana是s1,an是s2,那么计算an出现的次数实际上就是看an是几个后缀串的前缀。
a节点是保存所有起始为a字母的后缀串,我们看a字母后的n字母的引用计数即可。


3.3 两个字符串S1,S2的最长公共部分

先说下广义后缀树,前面说了后缀树可以存储一个或多个字符串,当存储的字符串数量大于等于2时就叫做广义后缀树。
建立一棵广义后缀树,如下图:

这里写图片描述

$和#是为了区分字符串的。

我们为每个后缀串末尾单独添加一个空间存储区分字符串的符号。那么怎么找s1和s2串最长的公共部分?

遍历每个后缀串,如果其引用计数为1则直接跳过,因为不可能有两个子串存放在这里,当引用计数>1时,往下遍历,直到分叉分别记录子串的符号,如果不同,说明他们是不同字符串的,记录已经匹配的值即可,若相同继续下一次遍历。上图的ana部分,到ana时,子串$结束,然后继续向下,子串anab以#结束,那么匹配了ana。


3.4 最长回文串

把要求的最长回文串的字符串s1和它的反向(逆)字符串s2建立一棵广义后缀树。
这里写图片描述

回文串有一个定义就是正反相同,也就是正着和反着可以重和在一起,那么我们直接看这棵广义后缀树的共同前缀即可,每个banana的子串和ananab的子串重合的部分都是回文串,我们只需要找到最长的即可。比如上面的anana,从后面不同的标记可以看出两个字符串的某个后缀都有这个前缀,能完美重合到一起。即它是回文串。
记录Max,每次找到一个回文串比较即可。


四、代码实现

首先定义一个SuffixTree类,用于封装后缀树,内部定义了两个内部类:Node和ActivePoint,分别封装树的节点和算法中提到的活动点。代码结构如下:

public class SuffixTree {
    private Node root = new Node(new char[0]);// 根节点
    // active point,一个三元组:(active_node,active_edge,active_length)
    // active_node是当前的活动点,用节点代表,active_edge是活动的边,这里用节点来表示,active_length是活动的长度
    private ActivePoint activePoint = new ActivePoint(root, null, 0);
    private int reminder = 0;// remainder,表示还剩多少后缀需要插入

    /**
     * <p>
     * 后缀树的节点,即边
     * </p>
     */
    private class Node {
        public char[] chars;
        public Node child;
        public Node brother;
        public Node suffixNode;

        public Node(char[] chars) {
            this.chars = chars;
        }
    }

    /**
     * <p>
     * 活动点(active point),一个三元组:(active_node,active_edge,active_length)
     * </p>
     */
    private class ActivePoint {
        public Node point;
        public Node index;
        public int length;

        public ActivePoint(Node point, Node index, int length) {
            this.point = point;
            this.index = index;
            this.length = length;
        }
    }
}

说明一下,算法中使用了边来保存字符,但是实现时没必要多维护一个类,直接使用节点(Node)来保存字符即可,效果上没有任何差别。同时树的结构通过子节点与兄弟节点的方式保存,如下结构图所示:

父节点
|
子节点—兄弟节点—兄弟节点123

采用这种方式的原因是因为一个节点的子节点数量是未知的,所以不适合使用一个固定长度的数组来保存节点的全部子节点,使用集合会造成数据结构嵌套数据结构,不适合。同时根节点也是一个普通节点,只是根节点不存在任何字符(字符数组长度==0)。ActivePoint是一个三元组(包含三个属性),分别对应:活动节点(Node),活动边(Node),活动长度。reminder对应算法中的reminder,记录剩余后缀数量。Node表示一个节点,有4个属性:chars表示该节点上的字符,child和brother是子节点和兄弟节点的指针,suffixNode是后缀连接。

介绍完了整体结构,下面来看看具体如何对一个字符串构建后缀树。

注意:在构建后缀树时使用了一个优化手段,算法中提到每次修改#,使得边上的字符自动扩充一位,在实际实现时我们可直接将从插入字符开始到字符串结束所有的字符全部一次性放到边上,省去每次扩充#。该优化点引用如下(原文见参考文章二):

借助后缀树的特性, 我们可以做出一个相当有效的算法. 首先一个重要的特性是: 一朝为叶, 终生为叶. 一个叶节点自诞生以后绝不会有子孙. 更重要的是, 每当我们往树上加入一个新的前缀, 每一条通往叶节点的边都会延长一个字符(新前缀的最后一个字符). 这使得处理通往叶节点的边变得异常简单, 我们完全可以在创建叶节点的时候就把当前字符到文本末的所有字符一股脑塞进去. 是的, 我们不需要知道后面的字符是啥, 但我们知道它们最终都要被加进去. 因此, 一个叶节点诞生的时候, 也正是它可以被我们遗忘的时候. 你可能会担心通往叶节点的边被分割了怎么办, 那也不要紧, 分割之后只是起点变了, 尾部该怎么着还是怎么着。

/**
 * 构建后缀树
 * 
 * @param word
 */
public void build(String word) {
    int index = 0;
    char[] chars = word.toCharArray();
    while (index < chars.length) {// 循环建立后缀
        int currenctIndex = index++;// 保存当前的位置
        char w = chars[currenctIndex];// 当前的后缀字符

        this.print();// 打印
        System.out.println();
        System.out.println("当前插入后缀:" + w + "========");

        if (find(w)) {// 查找是否存在保存有当前后缀字符的节点
            reminder++;// 存在,则将reminder+1,activePoint.length+1,然后返回即可
            continue;
        }

        // 不存在的话,如果reminder==0表示之前在该字符之前未剩余有其他带插入的后缀字符,所以直接插入该后缀字符即可
        if (reminder == 0) {
            // 直接在当前活动节点插入一个节点即可
            // 这里插入的节点包含的字符是从当前字符开始该字符串剩余的全部字符,这里是一个优化,
            // 优化参考自:http://blog.youkuaiyun.com/v_july_v/article/details/6897097 (3.6、归纳, 反思, 优化)
            Node node = new Node(Arrays.copyOfRange(chars, currenctIndex, chars.length));
            // 如果当前活动点无子节点,则将新建的节点作为其子节点即可,否则循环遍历子节点(通过兄弟节点进行保存)
            Node child = activePoint.point.child;
            if (null == child) {
                activePoint.point.child = node;
            } else {
                while (null != child.brother) {
                    child = child.brother;
                }
                child.brother = node;
            }
        } else {
            // 如果reminder>0,则说明该字符之前存在剩余字符,需要进行分割,然后插入新的后缀字符
            Node splitNode = activePoint.index;// 待分割的节点即为活动边(active_edge)
            // 创建切分后的节点,放到当前节点的子节点
            // 该节点继承了当前节点的子节点以及后缀节点信息
            Node node = new Node(Arrays.copyOfRange(splitNode.chars, activePoint.length, splitNode.chars.length));// 从活动边长度开始截取剩余字符作为子节点
            node.child = splitNode.child;
            node.suffixNode = splitNode.suffixNode;
            splitNode.child = node;
            splitNode.suffixNode = null;
            // 创建新插入的节点,放到当前节点的子节点(通过子节点的兄弟节点保存)
            Node newNode = new Node(Arrays.copyOfRange(chars, currenctIndex, chars.length));// 插入新的后缀字符
            splitNode.child.brother = newNode;
            splitNode.chars = Arrays.copyOfRange(splitNode.chars, 0, activePoint.length);// 修改当前节点的字符

            // 分割完成之后需根据规则1和规则2进行区分对待
            // 按照规则1进行处理
            if (root == activePoint.point) {// 活动节点是根节点的情况
                // activePoint.point == root
            // 按照规则3进行处理
            } else if (null == activePoint.point.suffixNode) {// 无后缀节点,则活动节点变为root
                activePoint.point = root;
            } else {// 否则活动节点变为当前活动节点的后缀节点
                activePoint.point = activePoint.point.suffixNode;
            }
            // 活动边和活动边长度都重置
            activePoint.index = null;
            activePoint.length = 0;
            // 递归处理剩余的待插入后缀
            innerSplit(chars, currenctIndex, splitNode);
        }
    }
}

在SuffixTree中定义一个build(String word)方法,参数word是待生成后缀树的字符串。首先将字符串转成字符数组,并按照算法步骤逐个插入。find(char w)用于查找指定的后缀是否存在(这里所说的后缀其实就是单个字符,因为单个字符代表的就是以该字符开头的后缀)。

/**
 * 寻找当前活动点的子节点中是否存在包含后缀字符的节点(边)
 * 
 * @param w
 * @return
 */
private boolean find(char w) {
    final Node start = activePoint.point;
    final Node current = activePoint.index;
    boolean exist = false;
    if (null == current) {// current==null 无活动边,则从活动点的子节点开始查找
        // 寻找子节点
        Node child = start.child;
        while (null != child) {
            if (child.chars[0] == w) {// 存在
                activePoint.index = child;
                activePoint.length++;// activePoint.length++
                exist = true;
                break;
            } else {
                child = child.brother;
            }
        }
    } else if (current.chars[activePoint.length] == w) {// 有活动边,则在活动边上查找
        activePoint.length++;
        exist = true;
        if (current.chars.length == activePoint.length) {
            // 如果活动边的长度已达到活动边的最后一个字符,则将活动点置为活动边,同时活动边置为null,长度置为0
            activePoint.point = current;
            activePoint.index = null;
            activePoint.length = 0;
        }
    } else {
        exist = false;
    }
    return exist;
}

查找后缀是否存在是从活动节边开始查找,如果活动边为NULL,则从活动节点的子节点挨个查找,查找是通过比较边上的指定位置(活动长度指定)与查找字符是否相等。这里有个地方需要注意:算法中提到,如果一个活动边已到达结尾(即活动长度==活动边的字符长度),则将活动边晋升为活动节点,并重置活动边和活动长度为NULL和0。如下代码所示:

if (current.chars.length == activePoint.length) {
    // 如果活动边的长度已达到活动边的最后一个字符,则将活动点置为活动边,同时活动边置为null,长度置为0
    activePoint.point = current;
    activePoint.index = null;
    activePoint.length = 0;
}

如果查找到后缀存在,则直接将活动长度+1(在find()方法内部处理的),reminder+1即可。

if (find(w)) {// 查找是否存在保存有当前后缀字符的节点
    reminder++;// 存在,则将reminder+1,activePoint.length+1,然后返回即可
    continue;
}

如果不存在,需区分两种情况,一种是:前面没有堆积未插入的后缀,即reminder==0,另外一种是reminder>0。
对于reminder==0(如算法中举例的前三个字符abc),只需要直接将当前后缀插入到活动节点即可。具体首先新建一个节点,Node node = new Node(Arrays.copyOfRange(chars, currenctIndex, chars.length));,该节点包含从当前字符往后所有的字符,即上面提到的优化点;接着将新建的节点作为活动节点的子节点插入,这里需判断子节点是否存在,不存在,作为子节点,存在则作为子节点的最后一个兄弟节点。

Node child = activePoint.point.child;
if (null == child) {
    activePoint.point.child = node;
} else {
    while (null != child.brother) {
        child = child.brother;
    }
    child.brother = node;
}

如果reminder>0(如算法中举例步骤6,插入第四到第六个字符abx,到达x时就是这种情况),我们需要对当前活动边进行分割操作。

// 如果reminder>0,则说明该字符之前存在剩余字符,需要进行分割,然后插入新的后缀字符
Node splitNode = activePoint.index;// 待分割的节点即为活动边(active_edge)
// 创建切分后的节点,放到当前节点的子节点
// 该节点继承了当前节点的子节点以及后缀节点信息
Node node = new Node(Arrays.copyOfRange(splitNode.chars, activePoint.length, splitNode.chars.length));// 从活动边长度开始截取剩余字符作为子节点
node.child = splitNode.child;
node.suffixNode = splitNode.suffixNode;
splitNode.child = node;
splitNode.suffixNode = null;
// 创建新插入的节点,放到当前节点的子节点(通过子节点的兄弟节点保存)
Node newNode = new Node(Arrays.copyOfRange(chars, currenctIndex, chars.length));// 插入新的后缀字符
splitNode.child.brother = newNode;
splitNode.chars = Arrays.copyOfRange(splitNode.chars, 0, activePoint.length);// 修改当前节点的字符

分割的节点是活动边指向的节点,分割的位置由活动长度指定。具体分割是新建一个节点A,该节点的字符是被分割节点分割之后剩余的字符(’cabx’),同时该节点需继承被分割节点的子节点信息,以及后缀连接信息;再新建一个节点B存放当前要插入的后缀(’x’)。以上两个新建的节点都将作为被分割节点的子节点存在,所以A的兄弟节点指向B,并将被分割节点的字符切去只剩活动长度指定的字符(’ab’)。分割完之后需要根据规则1和规则3重置活动点信息,但是不管活动节点如何设定,活动边和活动边长度必须要重置为NULL和0。

// 分割完成之后需根据规则1和规则3进行区分对待
// 按照规则1进行处理
if (root == activePoint.point) {// 活动节点是根节点的情况
    // activePoint.point == root
// 按照规则3进行处理
} else if (null == activePoint.point.suffixNode) {// 无后缀节点,则活动节点变为root
    activePoint.point = root;
} else {// 否则活动节点变为当前活动节点的后缀节点
    activePoint.point = activePoint.point.suffixNode;
}
// 活动边和活动边长度都重置
activePoint.index = null;
activePoint.length = 0;

到这里我们只是插入了后缀’abx’,由于reminder==2,还需要插入’bx’和’x’,所以引入一个递归方法:innerSplit(char[] chars, int currenctIndex, Node prefixNode),用于插入’bx’和’x’。方法有三个参数:chars是构建后缀树的字符串的字符数组,currenctIndex是我们当前插入后缀的位置(for循环的位置),prefixNode是前一次进行分割的节点。所以此处分割完之后需调用innerSplit()方法处理剩余后缀。

// 递归处理剩余的待插入后缀
innerSplit(chars, currenctIndex, splitNode);

第三个参数传入splitNode,即将当前被分割的节点传入方法。下面看看innerSplit()如何递归如何剩余后缀。

/**
 * 处理剩余的待插入后缀
 * @param chars 构建后缀树的全部字符
 * @param currenctIndex 当前已处理到的字符位置
 * @param prefixNode 前继节点,即已经进行分割的节点,用于标识后缀节点
 */
private void innerSplit(char[] chars, int currenctIndex, Node prefixNode) {
    // 此处计算剩余待插入的后缀的开始位置,例如我们需要插入三个后缀(abx,bx,x),已处理了abx,则还剩余ba和x,则下面计算的位置就是b的位置
    int start = currenctIndex - reminder + 1;

    this.print();// 打印
    System.out.println();
    System.out.println("当前插入后缀:" + String.copyValueOf(chars, start, currenctIndex - start + 1) + "========");

    // dealStart表示本次插入我们需要进行查找的开始字符位置,因为由于规则2,可能出现通过后缀节点直接找到活动节点的情况
    // 如通过ab节点的后缀节点,直接找到节点b,那么此时的activePoint(node[b], null, 0),我们需要从node[b]开始查找x,dealStart的位置就是x的位置
    int dealStart = start + activePoint.point.chars.length + activePoint.length;
    // 从dealStart开始查找所有后缀字符是否都存在(相对与活动点)
    for (int index = dealStart; index <= currenctIndex; index++) {
        char w = chars[index];
        if (find(w)) {// 存在,则查找下一个,activePoint.length+1,这里不增加reminder
            continue;
        }
        Node splitNode = null;// 被分割的节点
        if(null==activePoint.index){// 如果activePoint.index==null,说明没有找到活动边,那么只需要在活动节点下插入一个节点即可
            // --此处代码build()方法插入节点部分--
        }else{
            // 开始分割,分割部分同上面的分割
            // --此处代码build()方法分割部分--

            // 规则2,连接后缀节点
            prefixNode.suffixNode = splitNode;
        }
        // --
        reminder--;

        // 分割完成之后需根据规则1和规则3进行区分对待
        // --代码同build()代码部分--

        if(reminder > 0){// 如果reminder==0则不需要继续递归插入后缀
            innerSplit(chars, currenctIndex, splitNode);
        }
    }
}

上面代码为了消减篇幅省去了与build()方法重复的代码(完整代码会放到Github上),其实基本逻辑是一样的,只是所处理的方式略有不同,所以没法放到一起。

这里说一下start和dealStart这两个变量的用处。start是本次需要插入的后缀的开始位置,如’bx’,则start就是b的位置,通过reminder获得;dealStart是下面的for循环开始的位置,也就是需要查找后缀的位置,dealStart的出现是由于规则2的存在,如果没有规则2,那么dealStart就是start了,由于规则2活动节点会从root直接跳到一个节点,而无需进行查找,所以如果发生了跳转,比如字符串时’abcabxabcd’,待插入后缀是’bcd’时,这时就是根据规则2直接跳到一个节点b,所以要从’c’开始找,就是因为节点b为我们省了一个字符,所以说后缀连接是用于优化的。prefixNode.suffixNode = splitNode;就是将之前被分割的节点的后缀连接指向当前被分割的节点。

剩余的部分就是查找,分割了,同build()的代码一样,所以省去了。最后,如果本次处理完reminder依旧>0,那么就需要进行递归调用该方法了。

到这里构建后缀树就完成了。下面介绍后缀树的一个应用:查找子串。具体看代码:

/**
 * 查找给定字符串是否是其子串
 * 
 * @param word
 * @return
 */
public boolean select(String word) {
    char[] chars = word.toCharArray();
    int index = 0;// 查找到的节点的匹配的位置
    // 查找从根节点开始,遍历子节点
    Node start = root;
    for (int i = 0; i < chars.length; i++) {
        if (start.chars.length < index + 1) {// 如果当前节点已匹配完,则从子节点开始,同时需重置index==0
            index = 0;
            start = start.child;
            while (null != start) {
                // 比较当前节点指定位置(index)的字符是否与待查找字符一致
                // 由于是遍历子节点,所以如果不匹配换个子节点继续
                if (start.chars[index] == chars[i]) {
                    index++;
                    break;
                } else {
                    start = start.brother;
                }
            }
            if (null == start) {// 子节点遍历完都无匹配则返回false
                return false;
            }
        } else if (start.chars[index] == chars[i]) {
            // 如果当前查找到的节点的还有可比较字符,则进行比较,如果不同则直接返回false
            index++;
        } else {
            return false;
        }
    }
    return true;
}

由于每个节点包含的字符数不确定,所以需要一个额外的索引记录当前匹配节点中字符的位置。查找的主要思想是先从根节点的字节点开始,挨个找,找不到则不是子串,找到一个节点后从节点上的所有字符挨个匹配,匹配不上则没有,匹配完了,就从这个节点的子节点继续找。

完整代码看这里Github


后缀自动机构造后缀树 http://www.elijahqi.win/archives/3945 今天研究一天怎么构造始终想不明白 看到这篇blog之后顿悟 为了帮助其他人避免像蒟蒻我一样 想不明白感到难过 于是随便写一写 要是有错误 qwq麻烦指出 (emm退役之后不知道还会不会上blog) https://www.cnblogs.com/hehe54321/p/8694839.html 看上面两张... 阅读详情

相关推荐

OneOS远程升级能力实操演示 | OTA升级

OneOS远程升级能力实操演示,差分算法提升效率,OTA升级,OTA差分算法,全量升级、压缩升级、差分升级,有线网络连接,bootloader,升级平台,在线差分包制作,触摸屏以及GUI图形操作

OneOS是中国移动针对物联网领域推出的轻量级操作系统 2011

课堂反转——数据结构之后缀树

后缀树基本原理及构造函数的实现

Strelitzia7的博客 1388

ROS2实战:手把手教你用walking机器人仿真实现医院多楼层自动配送(含实时地图切换)

本文详细介绍了如何利用ROS2和walking机器人仿真平台实现医院多楼层自动配送系统,重点讲解了动态地图切换技术和多楼层导航的实现方法。通过实时加载地图和优化电梯过渡策略,解决了医院复杂环境下的自主配送难题,为医疗物流自动化提供了实用解决方案。

weixin_29090917的博客 360

后缀树(一)定义及构造

后缀树(Suffix tree): :一个具有m个词的字符串S的后缀树T,就是一个包含一个根节点的有向树,该树恰好带有m个叶子,这些叶子被赋予从1到m的标号。每一个内部节点,除了根节点以外,都至少有两个子节点,而且每条边都用$的一个非空子串来标识。出自同一节点的任意两条边的标识不会以相同的词开始。后缀树的关键特征是:对于任何叶子i,从根节点到该叶子所经历的边的所有标识串联起来后恰好拼出S的从i...

)梦想之深邃( 2416

SpringBoot项目整合Knife4j接口文档

写满接口信息的文档,每条接口包括:请求参数,响应参数,接口地址,接口名称,请求类型,请求格式,备注。

诨号无敌鸭的博客 1017

DRV2605L触觉驱动芯片实战指南:从自动校准到音频转触觉应用

触觉反馈技术是现代人机交互的核心组成部分,它通过精确控制振动马达来模拟物理触感,广泛应用于智能手机、游戏手柄和汽车触摸屏等领域。其原理在于将电信号转化为机械振动,关键在于如何高效、精准地驱动不同类型的马达(如ERM和LRA)。这项技术的核心价值在于提升交互的真实感和沉浸感。在工程实践中,集成驱动芯片如DRV2605L通过内置智能反馈引擎和自动校准功能,解决了传统驱动方式响应慢、效果不一致的难题。它特别适用于需要高质量触觉反馈的应用场景,例如消费电子、游戏外设和车载信息娱乐系统。其中,自动校准能自适应优化驱动

weixin_42631411的博客 341

数据结构与算法面试分享(二十一):后缀树(Suffix Tree)

上述字符串匹配算法(朴素的字符串匹配算法, KMP 算法, Boyer-Moore算法)均是通过对模式(Pattern)字符串进行预处理的方式来加快搜索速度。对 Pattern 进行预处理的最优复杂度为 O(m),其中 m 为 Pattern 字符串的长度。那么,有没有对文本(Text)进行预处理的算法呢?本文即将介绍一种对 Text 进行预处理的字符串匹配算法:后缀树(Suffix Tree)。后缀树的性质存储所有 n(n-1)/2 个后缀需要 O(n) 的空间,n 为的文本(Text)的长度;

之乎者也·的博客 1811

【超简单】一文入门在Ubuntu系统使用GitHub

Github是基于git的代码仓库,对于开发项目的科研者或者工程师,代码仓库都是不可多得的版本控制与多端编程工具,今天这篇文章带你轻松Github入门,简单的项目本文的操作足矣。

a13337649640的博客 5236

Suffix Tree (后缀树)、Suffix Array (后缀数组)、LZW树详细解读

以下是 后缀树(Suffix Tree)、后缀数组(Suffix Array) 以及 LZW树 的详细介绍,包括定义、原理、优缺点和应用场景。

欢迎来到我的博客!这里是一个专注于计算机技术的分享平台,涵盖编程开发、算法研究、系统架构、软件工程等多个领域。无论你是初学者还是资深开发者,都能在这里找到有价值的内容。 1728

网络无线AP信号走场测试软件(Ekahau 使用说明)

每次开启 Ekahau 软件需要重复此过程,才可以正常使用。 双击“Ekahau Pro activation.reg”,如下图所示; 点击“Yes”按钮 ,进行注册表合并; 点击“OK”按钮,确认; 运行“Ekahau Pro” 点击“Close”关闭提示; 点击“OK”,关闭提示; ...

Reyn_观极 6412

机器人控制基础:伺服驱动器的泄放电阻

摘要:伺服驱动器泄放电阻用于消耗电机减速或制动时产生的再生电能,防止母线电压过高损坏元件。其核心作用是将多余电能转化为热能,规避硬件损坏和停机风险。选型需关注功率、阻值和封装三大参数,功率应匹配再生能量,阻值需符合驱动器要求,封装应满足散热需求。可通过动态监测母线电压或静态检测电阻阻值来判断其是否正常工作。

start_up_go的博客 381

树:后缀树

在 trie 数据结构中,给定字符串中所有字符串的每个字母表一个接一个地遍历,每个节点代表一个字母表。如果两个或多个单词以相同的子字符串开头,则相同的子字符串由相同的节点链表示。这是一个压缩的 trie,包括字符串的所有后缀。个后缀中的每一个节点一个,没有后缀是另一个后缀的前缀。下面是我们之前用于进行 try 的相同朋友名称列表的后缀树的表示。后缀树的空间使用量远远大于序列集合的实际大小,无论它是线性的。无论哪种方式,都已经发表了开发基于磁盘的后缀树的实际工作,这些后缀树可以扩展到(几)GB/小时。

新华编程特战队 1212

游戏客户端开发工程师部分笔试题解析

你可以选择任意连续K个宝石合并成一个新的宝石,新宝石保持在原队列的位置且它的能量值是这K个宝石能量值之和。给定一个幸运值N=0、一堆包含能量值的宝石,你的任务是找到将所有宝石合并成一个宝石的最大幸运值。2.在一个二维网格中,有一个起点S、一个终点E和一个必须经过的中间点M。C++ 的多态性正是通过基类指针或引用调用派生类覆盖的虚函数实现的,这是运行时多态的核心机制。光线追踪带来的抗锯齿效果是其渲染方法的自然结果,而不是一个独立的算法。宝石具体能量值:[10, 7, 2, 6, 5, 11, 8];

博客 3027

后缀树/后缀数组

字典树:https://blog.youkuaiyun.com/hebtu666/article/details/83141560 后缀树:后缀树,就是把一串字符的所有后缀保存并且压缩的字典树。   相对于字典树来说,后缀树并不是针对大量字符串的,而是针对一个或几个字符串来解决问题。比如字符串的回文子串,两个字符串的最长公共子串等等。 比如单词banana,它的所有后缀显示到下面的。0代表从第一个字符...

少说,多做。 4435

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

从Trie树说到后缀树

参考了july的文章。 什么是Trie树 Trie树,即字典树,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。 它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。 Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。 它有3

imzoer的专栏 2984

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

前缀树和后缀树

今天主要看的是树中的两个比较重要的数据结构——前缀树和后缀树。在此之前,先来看两个问题。(参考博客:从Trie树(字典树)谈到后缀树)

xy的博客 1万+

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

【数据结构课设】:后缀树

概述 后缀树,就是把一串字符的所有后缀保存并且压缩的字典树。相对于字典树来说,后缀树并不是针对大量字符串的,而是针对一个或几个字符串来解决问题。比如字符串的回文子串,两个字符串的最长公共子串等等。 性质:一个字符串构造了一棵树,树中保存了该字符串所有的后缀。 操作:就是建立和应用。 (1)建立后缀树 比如单词banana,它的所有后缀显示到下面的。0代表从第一个字符为起点,终点不用说都是字符串的末尾。 以上面的后缀,我们建立一颗后缀树。如下图,为了方便看到后缀,我没有合并相同的前缀。 前面简介

capsnever的博客 2542

后缀树&后缀数组&LCP

后缀树: 字符串匹配算法一般都分为两个步骤,一预处理,二匹配。 KMP和AC自动机都是对模式串进行预处理,后缀树和后缀数组则是对文本串进行预处理。 后缀树的性质: 存储所有 n(n-1)/2 个后缀需要 O(n) 的空间,n 为的文本(Text)的长度; 构建后缀树需要 O(dn) 的时间,d 为字符集的长度(alphabet); 对模式(Pattern)的查询需要 O(dm) 时间,...

CSer 2299

后缀树的实现形式

————《高级数据结构》 上述后缀树T是根据字符串S的所有后缀构建的。有时候这个S可以是要给字符串集合,所以后缀树T是根据所欲字符串的后缀构建的。这种后缀树也叫做广义后缀树。 一种很自然的构建方式可以给每个字符串加两两不同的结尾符,这些结尾符没有在任何字符串中出现过。然后,我们将这些添加了结尾符的字符串首尾相接作为一个整体来构建其后缀树。由于任何一个后缀都不是其他后缀的前缀,并且任意连个字符串的结...

qq_36172410的博客 542

后缀树及后缀数组

后缀树: 后缀树是一种数据结构,它支持有效的字符串匹配和查询。 一个具有m个词的字符串S的后缀树T,就是一个包含一个根节点的有向树,该树恰好带有m个叶子,这些叶子被赋予从1到m的标号。 每一个内部节点,除了根节点以外,都至少有两个子节点,而且每条边都用S的一个非空子串来标识。出自同一节点的任意两条边的标识不会以相同的词开始。后缀

E.G的专栏 2471
上一篇: 字典树-Trie树
奋起直追CDS
博客等级 码龄11年 57粉丝 94原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值