Trie vs. 后缀树 vs. 后缀数组

Posted

技术标签:

【中文标题】Trie vs. 后缀树 vs. 后缀数组【英文标题】:Trie vs. suffix tree vs. suffix array 【发布时间】:2011-01-30 01:30:45 【问题描述】:

哪种结构能提供最好的性能结果; trie(前缀树)、后缀树还是后缀数组?还有其他类似的结构吗?这些结构有哪些好的 Java 实现?

编辑:在这种情况下,我想在大型名称字典和大量自然语言文本之间进行字符串匹配,以便识别文本上字典的名称。

【问题讨论】:

哪些操作的最佳性能? 【参考方案1】:

trie 是发现的第一个此类数据结构。

后缀树是对 trie 的改进(它具有允许线性错误搜索的后缀链接,后缀树修剪了 trie 的不必要分支,因此它不需要太多空间)。

后缀数组是基于后缀树的精简数据结构(没有后缀链接(错误匹配慢),但模式匹配非常快)。

trie 不适用于现实世界,因为它占用了太多空间。

后缀树比 trie 更轻、更快,用于索引 DNA 或优化一些大型网络搜索引擎。

在某些模式搜索中,suffix 数组比 suffix 树慢,但占用的空间更少,并且比 Suffix 树使用更广泛。

在同一系列的数据结构中:

还有其他实现,CST 是后缀树的实现,使用后缀数组和一些额外的数据结构来获得一些后缀树搜索功能。

FCST 更进一步,它实现了一个带有后缀数组的采样后缀树。

DFCST 是 FCST 的动态版本。

扩展:

两个重要的因素是空间使用和操作执行时间。您可能认为这与现代机器无关,但索引一个人的 DNA 需要 40 GB 的内存(使用未压缩和未优化的后缀树)。在这么多数据上构建其中一个索引可能需要数天时间。想象一下谷歌,它有很多可搜索的数据,他们需要一个包含所有网络数据的大型索引,而且他们不会在每次有人构建网页时都更改它。他们为此提供了某种形式的缓存。然而,主要指数可能是静态的。每隔几周左右,他们就会收集所有新的网站和数据并建立一个新的索引,在新的完成后替换旧的。我不知道他们使用哪种算法来索引,但它可能是一个在分区数据库上具有后缀树属性的后缀数组。

CST 使用 8 GB,但后缀树操作速度大大降低。

后缀阵列可以在大约 700 兆到 2 兆的情况下执行相同的操作。但是,您不会在带有后缀数组的 DNA 中发现遗传错误(意思是:使用通配符搜索模式要慢得多)。

FCST(完全压缩的后缀树)可以创建 800 到 1.5 千兆的后缀树。向 CST 方向的速度下降幅度很小。

DFCST 使用的空间比 FCST 多 20%,并且速度比 FCST 的静态实现慢(但是动态索引非常重要)。

后缀树的可行(空间方面)实现并不多,因为很难使操作速度提升补偿数据结构 RAM 空间成本。

这就是说,后缀树对于带有错误的模式匹配有非常有趣的搜索结果。 aho corasick 没有那么快(尽管对于某些操作来说几乎一样快,而不是错误匹配),并且 boyer moore 被抛在了尘土中。

【讨论】:

线性错误搜索是一种错误搜索,它在线性时间内返回所有可能的错误匹配。例如,某个文本在某处具有“House”、“Housa”、“Hotse”等词。一个恒定的错误匹配将在一个操作中返回所有错误。线性错误匹配返回 COUNT(matches) 中的所有错误(匹配项)。在这种情况下 2. 有些人可能会将其解释为对文本大小的线性搜索(文本扫描以查找错误),因此成本将等于文本大小。几乎所有错误搜索算法都是这种情况,但后缀树并非如此。 后缀数组的优点是使用的空间比后缀树少。但是我们怎么知道呢?是否有任何数学证明或我们基于实际实验? 它基于数学证明和实际实验。 2007年Sadakane提出的压缩后缀树使用了nHk + 6n + o(n)的空间,其中nHk是后缀数组。因此,您可以获得相对空间使用的观点,其中 SA 实际上是 ST 实现的一部分。有几个实现也证明了真实的空间使用。你可以看到他的建议或我的建议。还有这里的Sadakane:Kunihiko Sadakane.Theor。比较。 Sys., 41(4):589–607, 2007. Mine:动态完全压缩后缀树的实现。米格尔·菲格雷多hdl.handle.net/10362/4328【参考方案2】:

您打算进行哪些操作? libdivsufsort 曾经是 C 语言中最好的后缀数组实现。

【讨论】:

实现后缀数组构造效率的技术是什么? 及时提问。 AmpLab 刚刚发布了一个并行版本,有深入的解释,amplab.cs.berkeley.edu/publication/…【参考方案3】:

使用后缀树,您可以在 O(n+m+k) 时间内编写将字典与文本匹配的内容,其中 n 是字典中的字母,m 是文本中的字母,k 是匹配的数量.为此,尝试要慢得多。我不确定 Suffix Array 是什么,所以我无法对此发表评论。

也就是说,编写代码并非易事,而且我碰巧不知道任何提供必要功能的 Java 库。

【讨论】:

是的,我一直在阅读 Suffix Arrays。结果表明它们具有与 Suffix 树相同的渐近速度,但空间效率更高。它们绝对是一种选择。【参考方案4】:

编辑:在这种情况下,我想在一个大的名称字典和一个大的集合之间进行字符串匹配 自然语言文本,以便在文本上识别字典的名称。

这听起来像是Aho-Corasick algorithm 的应用程序:从字典中构造一个自动机(以线性时间),然后可用于查找多个文本中任何字典单词的所有出现(也以线性时间)。

(these lecture notes 中的描述,链接自***页面的“外部链接”部分,比页面本身的描述更容易阅读。)

【讨论】:

【参考方案5】:

我更喜欢 Suffix Auto Machine。 您可以通过我的网站找到更多详细信息: http://www.fogsail.net/2019/03/06/20190306/

enter image description here

首先,如果你使用正常的构造,它需要 O(n^2) 来遍历所有的后缀

我们使用 radix-sort 按第一个字符对后缀数组进行排序。

但是,如果我们对第一个字符进行排序,我们可以使用该信息。

细节以图片显示(忽略中文)

我们按照first-key对数组进行排序,结果用红色矩形表示

????????,???????,....−->????????,???,???,?>?,??>?,?>?>?,?>

enter image description here

#include <iostream>
#include <cstdio>
#include <vector>
#include <queue>
#include <cstring>
#include <algorithm>

using namespace std;
const int maxn = 1001 * 100 + 10;

struct suffixArray 
    int str[maxn], sa[maxn], rank[maxn], lcp[maxn];
    int c[maxn], t1[maxn], t2[maxn];
    int n;

    void init()  n = 0; memset(sa, 0, sizeof(sa)); 

    void buildSa(int Rdx) 
        int i, *x = t1, *y = t2;
        for(i = 0; i < Rdx; i++) c[i] = 0;
        for(i = 0; i < n; i++) c[x[i] = str[i]]++;
        for(i = 1; i < Rdx; i++) c[i] += c[i-1];
        for(i = n-1; i >= 0; i--) sa[--c[x[i]]] = i;

        for(int offset = 1; offset <= n; offset <<= 1) 
            int p = 0;
            for(i = n-offset; i < n; i++) y[p++] = i;
            for(i = 0; i < n; i++) if(sa[i] >= offset) y[p++] = sa[i] - offset;

            // radix sort
            for(i = 0; i < Rdx; i++) c[i] = 0;
            for(i = 0; i < n; i++) c[x[y[i]]]++;
            for(i = 1; i < Rdx; i++) c[i] += c[i-1];
            for(i = n-1; i >= 0; i--)  sa[--c[x[y[i]]]] = y[i]; y[i] = 0; 

            // rebuild x and y
            swap(x, y); x[sa[0]] = 0; p = 1;
            for(i = 1; i < n; i++)
                x[sa[i]] = y[sa[i-1]] == y[sa[i]] && y[sa[i-1]+offset] == y[sa[i]+offset] ? p-1 : p++;
            if(p >= n) break;
            Rdx = p;
        
    

【讨论】:

【参考方案6】:

This 的诱导排序算法(称为 sais)的实现有一个用于构造后缀数组的 Java 版本。

【讨论】:

【参考方案7】:

Trie 与后缀树

这两种数据结构都确保非常快速的查找,搜索时间与查询词的长度成正比,复杂度时间 O(m) 其中 m 是查询词的长度。

这意味着如果我们的查询词有 10 个字符,所以我们最多需要 10 个步骤才能找到它。

Trie :一棵用于存储字符串的树,其中每个公共前缀都有一个节点。字符串存储在额外的叶节点中。

suffix tree:与给定字符串的后缀相对应的 trie 的紧凑表示,其中具有一个子节点的所有节点都与其父节点合并。

def 来自: 算法与数据结构词典

一般 Trie 用于索引字典单词(lexicon)或任何字符串集 例如 D=abcd, abcdd, bxcdf,.....,zzzz

用于索引文本的后缀树,在我们文本的所有后缀上使用相同的数据结构“Trie” T=abcdabcg T的所有后缀 = abcdabcg , abcdabc , abcdab, abcda, abcd, abc , ab, a

现在它看起来像一组字符串。 我们在这组字符串(所有 T 的后缀)上构建一个 Trie。

两种数据结构的构造都是线性的,在时间和空间上都需要O(n)。

在字典(一组字符串)的情况下:n =所有单词的字符之和。 在文本中:n = 文本长度。

后缀数组:是一种表示压缩空间中后缀树的技术,它是一个字符串后缀的所有起始位置的数组。

搜索时间比后缀树慢。

有关更多信息,请访问 wikipedia,有一篇很好的文章讨论了这个主题。

【讨论】:

以上是关于Trie vs. 后缀树 vs. 后缀数组的主要内容,如果未能解决你的问题,请参考以下文章

trie上构建后缀数组

loj6198谢特 后缀数组+并查集+Trie

最长公共后缀-前缀

后缀数组与后缀树

(后缀数组/Trie)HDU 6138-Fleet of the Eternal Throne

ACM算法目录