首页 文章

大型(10Mb)文本的后缀树占用过多的内存

提问于
浏览
0

我实现了(参见下面的代码)绝对最小的通用后缀树构建算法 . 我写了一个单元测试,它似乎按预期工作(在正确的位置找到正确的子串) . 但这棵树实在太大了 . Question: 我在某处犯了错误,或者这种基本形式的后缀树只能用于非常短的文本吗?

Statistics

我想用它来搜索大量的文本:多个15-20Mb文本文件,或者例如每个40'000字符串~60个字符 .

当我构建40'000字符串2.5Mb后缀树(如果公司名称是一个列表),树需要400Mb . 我可以将其优化大约4倍,但即便如此,每个字符的原始文本也要超过40个字节 . 这是正常的吗?文献中的估计对我来说这很高 .

查看树的每个级别的平均分支因子,它们是:80 40 8 3 2 1 1 1 ...即,只有树的前3-5个级别实际上是分支的 . 我 Build 3-5级并保留长文本后缀节点要好得多 . 低于第5级的所有内容基本上都是链接的字符列表 . 这是预期的表现吗?从直觉上看,公司名称之间共享的6个字符子串并不多 .

在进一步使用15MB文本的实验中,在我添加前10个'000 suffixes (long to short). There'几乎没有重复的子串之后,我耗尽了2Gb的内存,因此树不能重复使用 . 我完全可以看到后缀 array 实际上是多么有用,每个字符需要固定2-4个字节,并且在20Mb文本中每次搜索只需要24个字符串比较 . 对于具有所有唯一字符的字符串,我可以't possibly see how suffix tree with as many vertices as there are unique substrings in text can fit in memory. It'的O(n ^ 2)个节点,但对于英文文本它似乎仍然非常超线性 . 后缀树如何为大文本工作?我读过的论文和问题似乎暗示它应该是可用的,因此我的问题在这里 .

Questions

我在某个地方犯了一个错误,使树比它应该更大吗?

建造后缀树的方式应该与最终的树形状无关?这是正确的吗?

我的不是Ukkonen算法,而只是强力将所有后缀添加到树中以简化代码和数据结构(无后缀链接字段)并稍后将perf与Ukkonen进行比较 . 建造方法应该只影响建筑的速度,树的大小或形状 . 无论哪种方式,构建树都是递增的,因此比最终树更大's no intermediate result that' .

这是代码:

#include <vector>
#include <assert.h>
class Node 
{public:
    char c; // 0 is terminator. terminators have no children
    Node(char _c) { c = _c; }
};

class Terminator 
{
public:
    Terminator(int i, int p ) { id = i; pos = p; }
    bool operator == (const Terminator &that)const { return id == that.id && pos == that.pos; }
    int id; // id of the string
    int pos; //position of this substring in the string
};


class Vertex : public Node 
{
public:
    static size_t s_nCount;
    std::vector< Vertex* > children; // interior tree nodes; char != 0
    std::vector< Terminator >terminators;
    Vertex(char c) :Node(c) { s_nCount++; }
    ~Vertex() { for (Vertex*v : children) delete v; }
    //void* operator new  (size_t count) { return g_GstAlloc.Alloc(count); }
    //void operator delete(void *p) { g_GstAlloc.Free(p); }
    void getDepthCounts(std::vector<unsigned> &depth, size_t nLevel = 0)const
    {
        if (depth.size() <= nLevel)
            depth.resize(nLevel + 1);
        depth[nLevel]++;
        for (Vertex*v : children)
            v->getDepthCounts(depth,nLevel + 1);
    }
    Vertex *getOrCreateChildVertex(char c )
    {
        Vertex *out = getChild(c);
        if (!out)
        {
            out = new Vertex(c);
            children.push_back(out);
        }
        return out;
    }

    void getTerminators(std::vector<Terminator> &out, size_t limit )
    {
        if (out.size() >= limit)
            return;
        out.insert(out.end(), terminators.begin(), terminators.end());;
        for (Vertex* c: children) {
            c->getTerminators(out, limit);
            if (out.size() >= limit)
                break;
        }
    }
    Vertex *getChild(char c) 
    {
        for (Vertex *p : children)
            if (p->c == c)
                return p;
        return nullptr;
    }
    size_t memSize()const
    {
        size_t out = sizeof(*this) + terminators.size() * sizeof(terminators[0]);
        for (Vertex*v : children)
            out += sizeof(v) + v->memSize();
        return out;
    }
};

class Root : public Vertex 
{
public:
    Root():Vertex(0) {  }
    void appendString(const char *str, int id )
    {
        for (volatile size_t len = strlen(str), suffix = len; suffix-- > 0;)
        {
            Vertex* parent = this;
            for (size_t pos = suffix; pos < len; pos++)
            {
                parent = parent->getOrCreateChildVertex(str[pos]);
            }
            parent->terminators.push_back(Terminator(id, (int)suffix));
        }
    }

    void findSubstr(std::vector<Terminator> &out, const char *substr, size_t limit )
    {
        Vertex *parent = this;
        for (size_t len = strlen( substr ), i = 0; i < len; i++)
        {
            parent = parent->getChild(substr[i]);
            if (!parent)
                return;
        }
        parent->getTerminators(out, limit);
    }
};

1 回答

  • 0

    在Bo Persson评论的推动下,我重新阅读了维基百科article on Suffix Trees,最后点击了它 . 我可能仍然误解后缀树,但是这种特殊的内存爆炸是由于我构建了一个扩展的树(让's call it character tree) with a single character at every node. That'的格式不正确) .

    Ukkonen的算法以及所有其他后缀树构建算法在O(n)时间内构建树,其中包含一个提示 . 字符串ABCD .... XYZ将具有带有O(n ^ 2)个节点的字符树,这显然不可能在O(n)时间内构建 .

    正确的后缀树必须具有唯一子串的节点(在wiki页面中,BANANA $是一个节点而不是6个节点加一个终结符) . 它需要固定的内存(例如,第一个字符和长度的索引) .

    Ukkonen的算法洞察力是优化AAAAAA ... AAAAA字符串的情况 . 这样的字符串具有O(n)后缀树节点(以及O(n)“字符树”节点),并且naiive构建需要O(n ^ 2)时间,因为您必须在每个步骤上遵循不断增长的节点串 . 但是,使用Ukkonen的后缀链接添加每个后缀需要进行O(1)操作(它只是在末尾添加一个节点,并且在跟随后缀链接一次之后,它会停止) .

    对于ABCD ... XYZ字符串,正确的后缀树仍将具有O(n)个节点 . 例如 . 后缀FGH ... XYZ只有一个节点 .

    在我的代码中,该后缀将生成21个单独的节点 . 这就是创建O(n ^ 2)内存消耗的原因,基本上我对后缀树节点的误解是子字符串而不是一个字符 .

相关问题