持久性adjcacency图索引实现

时间:2016-05-22 05:44:31

标签: c++ graph

我有一个基于对象的邻接列表图,其中包含存储在nodes中的edgesvector

class Graph
{
    struct NodePrivate 
    {
        QVector<int> m_FromEdges, m_ToEdges;
    };

    struct EdgePrivate
    {
        int m_iFrom, m_iFromIndex, m_iTo, m_iToIndex;
    };

    //...        
private:
    QVector<NodePrivate> m_Nodes;
    QVector<EdgePrivate> m_Edges;
};

为了确保图元素在删除时的连续性(和恒定速度),我通过将最后一个元素与要删除的元素进行交换来进行删除。

现在,当图形的用户访问元素时,他通过NodeEdge类来访问元素,这些类实际上只是图形索引的包装器(和int)。 / p>

class Item
{
    //...
private:
    int m_Index = -1; //or QSharedPointer<int>, see below
    const Graph *m_Graph = nullptr;
};

class Node : public Item {};
class Edge : public Item {};

删除nodeedge这些索引可能会失效。我希望这些是坚持不懈的,并且已经尝试过(成功)两种策略,但我不喜欢它们中的任何一种:

1)通过注册它们并分别在构造函数和析构函数中注销它们来跟踪NodeEdge类型的所有对象。然后,只要相关索引发生更改,它们就会用于更新内部索引。最大的缺点是相当多的不必要的注册临时工。

2)另一种选择是通过使索引动态化(std::shared_ptr<int>)来使用智能指针方法。然后通过更新所有对象来更新索引,但是以动态内存为代价。

有没有其他选择来实现这一点或改进这两种设计?

1 个答案:

答案 0 :(得分:1)

首先,我必须承认,我不认为这个问题可以完美解决。如果您真的想要定期对图表进行大量的小改动,那么您应该切换到将所有内容存储在链表而不是数组中。此外,您可以放弃并明确说明,所有NodeEdge句柄都会失效,就像std::vector::iterator - 在向std::vector添加元素时失效

一般性讨论

在您的情况下,顶点和邻接列表存储在数组中。此外,您还拥有NodeEdge个助手,它们允许用户随时指向真实节点和边缘。我将它们称为句柄(它们就像没有任何迭代功能的C ++迭代器)。我看到两种不同的方法可以在更改后维护句柄。

第一种方法是将直接指针(或索引)存储到每个句柄中的物理对象,就像现在这样做。在这种情况下,每当移动对象时,您都必须更改对象的所有句柄。这就是为什么你绝对必须注册你在某处给出的所有句柄。这正是你建议的第一个解决方案,它导致了重做&#34;句柄:无论是否实际移动任何对象,创建,删除和复制句柄都会变得昂贵。

第二种方法是将指针存储到Handle中的某些中间事物。然后确保在对象的生命周期中永远不会更改此内容,即使对象移动也是如此。显然,您在句柄中指向的东西必须与边缘节点的实际物理索引不同,因为它们会发生变化。在这种方法中,每次取消引用句柄时都必须为间接访问付费,因此句柄访问会变得稍微重一些。

您建议的第二种解决方案是遵循第二种方法。中间事物(由句柄指向)被动态分配int - 包裹在shared_ptr中,每个对象一个永不移动int。您必须至少从创建的每个对象的单独动态分配(+ deallocation)中受到影响,同样来自参考计数器更新。可以轻松删除引用计数器:在unique_ptrNodePrivate对象中存储EdgePrivate - s,在NodeEdge对象中存储原始指针。

新方法

第二种方法之后的另一种解决方案是使用ID作为指向句柄的中间事物。无论何时创建节点,都要为其分配一个新的节点ID,边缘也相同。从零开始顺序分配ID。现在,您可以维护物理索引与这些ID之间的双向对应关系,并在更改时在 O(1)时更新它。

struct NodePrivate 
{
    QVector<int> m_FromEdges, m_ToEdges;
    int id;   //getting ID by physical index
};
struct EdgePrivate
{
    int m_iFrom, m_iFromIndex, m_iTo, m_iToIndex;
    int id;   //getting ID by physical index
};

private:
QVector<NodePrivate> m_Nodes;
QVector<EdgePrivate> m_Edges;
QVector<int> m_NodeById;  //getting physical index by ID
QVector<int> m_EdgeById;  //getting physical index by ID

请注意,这些新的m_NodeByIdm_EdgeById向量会在创建对象时增长,但在删除对象时不会缩小。因此,您将在这些数组中包含空单元格,只有在删除图形时才会释放这些单元格。因此,只有在确定图形生命周期内创建的节点和边缘总量相对较小时,才能使用此解决方案,因为每个对象占用4个字节的内存。

提高内存消耗

您可能已经注意到刚刚提出的新解决方案与基于shared_ptr的解决方案之间的相似性。实际上,如果我们不区分C指针和数组索引,那么它们是相同的,除了:在您的解决方案int中 - s在堆中分配,但在提议的解决方案中int - s分配在pool allocator

对于无空闲池分配器的一个众所周知的改进是称为&#39; free lists&#39;的技术,我们可以将其应用于上述解决方案。我们允许重用它们,而不是总是为创建的对象分配新的ID。为了实现这一点,我们存储了一堆免费ID,当一个对象被删除时,我们将它的ID添加到这个堆栈。创建新对象时,我们从堆栈中获取ID。如果stack为空,那么我们分配一个新的ID。

struct EdgePrivate
{
    int m_iFrom, m_iFromIndex, m_iTo, m_iToIndex;
    int id;   //getting ID by physical index
};
private:
QVector<EdgePrivate> m_Edges;
QVector<int> m_EdgeById;    //getting physical index by ID
QVector<int> m_FreeEdgeIds; //freelist: stack of IDs to be reused

此改进确保内存消耗与您同时存在的最大对象数(而不是创建的对象总数)成比例。但是,它当然会进一步增加每个对象的内存开销。它可以节省您的malloc /免费成本,但是在经过多次操作后,您可能会遇到大型图形内存碎片问题。