【问题标题】:Delete on a very deep tree在非常深的树上删除
【发布时间】:2010-06-14 21:45:28
【问题描述】:

我正在为 10 个字符集构建一个后缀树(不幸的是,没有时间正确实现后缀树)。我希望解析的字符串会很长(最多 1M 个字符)。树的构建没有任何问题,但是,当我在完成后尝试释放内存时遇到了一些问题。

特别是,如果我将构造函数和析构函数设置为这样(其中 CNode.child 是指向由 10 个指向其他 CNode 的指针组成的数组的指针,而 count 是一个简单的无符号整数):

CNode::CNode(){
    count = 0;
    child = new CNode* [10];
    memset(child, 0, sizeof(CNode*) * 10);
}

CNode::~CNode(){
    for (int i=0; i<10; i++)
        delete child[i];
}

尝试删除根节点时出现堆栈溢出。我可能是错的,但我相当肯定这是由于过多的析构函数调用(每个析构函数最多调用 10 个其他析构函数)。我知道这在空间和时间上都是次优的,但是,这应该是解决重复子字符串问题的快速而肮脏的解决方案。

tl;dr:如何释放被一棵非常深的树占用的内存?

感谢您的宝贵时间。

【问题讨论】:

  • 如果您可以使用调试器获取堆栈跟踪,您就可以判断是否真的是您的析构函数导致了堆栈溢出。
  • 只需更改节点指针即可将树转换为线性列表,然后从头到尾进行删除。
  • child = new CNode* [10]() 将创建一个包含 10 个指针的数组,最初设置为 null。不需要任何memset
  • @AndreyT:干杯,我知道有些事情看起来不对劲
  • 为什么CNode 总是有 10 个指向它的孩子的指针?这不应该是动态的吗?这将消除对相同指针的重复 delete 调用。

标签: c++ class tree


【解决方案1】:

一种选择是从一个大缓冲区分配,然后一次性释放该缓冲区。

例如(未经测试):

class CNodeBuffer {
    private:
        std::vector<CNode *> nodes;

    public:
        ~CNodeBuffer() {
            empty();
        }

        CNode *get(...) {
            CNode *node = new CNode(...);
            nodes.push_back(node);
            return node;
        }

        void empty() {
            for(std::vector<CNode *>::iterator *i = nodes.begin(); i != nodes.end(); ++i) {
                delete *i;
            }

            nodes = std::vector<CNode *>();
        }
};

如果指向std::vector 元素的指针是稳定的,您可以让事情变得更简单,只需使用std::vector&lt;CNode&gt;。这需要测试。

【讨论】:

  • 指向向量的指针不稳定。可以改用向量的索引。我通常实现您在上面举例说明的内容并使用索引而不是指针(可能可以围绕它们实现自定义迭代器?)。虽然当需要保留空闲节点列表以支持 put()(或 delete())操作时,它通常会变得有点混乱。
  • @Dummy00001,“放置”或单独删除基本上是一个 noop,或者可以重用结构(例如,召回 ctor)。我想我以前见过这种模式(更概括),但我忘记了它叫什么。自定义迭代器听起来是个好主意,但感觉有点沉重。
【解决方案2】:

您是否为节点本身初始化内存?据我所知,您的代码只为指针分配内存,而不是实际的节点。

就您的问题而言,请尝试以迭代方式而不是递归方式迭代树。递归很糟糕,不幸的是,它只有在纸上而不是在代码中才是好的。

【讨论】:

  • 我愿意,虽然在不同的地方。它在这里似乎不相关。我可能会尝试这种方法,但我希望有一个更优雅的解决方案:)
  • @Kathoz :这确实是最简单的方法。实际上,递归实现总是受到堆栈大小的限制,因此使用递归方法很容易遇到问题。
  • 说递归不好就像说高级语言不好(或其他类似的通用语句)。这可能很糟糕,但这取决于上下文,我不建议任何人完全远离它。也许你的意思是说它在 C++ 中很糟糕,这可能更有效..(即使我不确定我是否同意)
  • @Jakob :同意,我的说法有点过于苛刻,但我们仍然受到堆栈大小的限制,不是吗?递归实现是错误的可能来源(嗯,不是),在树/图的情况下,我当然建议远离递归,支付一些额外的内存来存储必要的标志变量迭代实现。
【解决方案3】:

您是否考虑过增加您的堆栈大小?

在 Visual Studio 中,您可以使用 /FNUMBER 来执行此操作,其中 NUMBER 是堆栈大小(以字节为单位)。您可能还需要指定 /STACK:reserve[,commit]。

【讨论】:

  • OP 说数据集可能有 100 万深……这在现代系统上至少有 4MB 的堆栈。添加到该参数、临时变量等,您将拥有一个非常大的堆栈。
  • @strager:你这么说好像 4 兆字节很多。在嵌入式系统上当然可以,但如果您谈论的是现代 Windows 或 linux 桌面或服务器系统,那么 4 MB 比花生还小。 8 或 16 MB 的堆栈是没有问题的。
  • 我们在我工作的地方使用 8MB 堆栈 PER 线程 (18)...服务器编程确实不同于嵌入式:D
【解决方案4】:

您将执行很多删除操作。这将花费大量时间,因为您将以非常随意的方式访问内存。但是,此时您不再需要树结构。因此,我会通过两次。在第一遍中,为树中的所有节点创建一个std::vector&lt;CNode*&gt;reserve() 足够的空间。现在在树上递归并将所有 CNode* 复制到您的向量中。在第二步中,对它们进行排序 (!)。然后,在第三步中,将它们全部删除。第二步在技术上是可选的,但可能会使第三步更快。如果不是,请尝试以相反的顺序排序。

【讨论】:

    【解决方案5】:

    我认为在这种情况下,广度优先清理可能会有所帮助,方法是将所有回溯信息放入双端队列而不是操作系统提供的堆栈中。尽管如此,它仍然不能愉快地解决在析构函数中发生的问题。

    伪代码:

    void CNode::cleanup()
    {
        std::deque<CNode*> nodes;
        nodes.push_back(this);
        while(!nodes.empty())
        {
            // Get and remove front node from deque.
            // From that node, put all non-null children at end of deque.
            // Delete front node.
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-10-01
      • 1970-01-01
      • 2018-03-08
      • 2023-04-10
      • 2019-01-25
      • 2019-02-23
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多