指针或索引

Pointers or Indexes?

本文关键字:索引 指针      更新时间:2023-10-16

我有一个类似网络的数据结构,由连接在一起的节点组成。节点的数量将发生变化,这些节点将不按特定顺序存储在std::vector<Node>中,其中Node是一个适当的类。

我想跟踪节点之间的链接。同样,这些链接的数量会发生变化,我正在考虑再次使用std::vector<Link>Link类必须包含有关它所连接的两个节点的信息,以及其他链接功能。

Link是否应包含

  1. 指向两个节点的两个指针
  2. 两个整数,用作std::vector<Node>
  3. 或者我应该采用不同的系统(为什么?)

第一种方法虽然可能更好,但有问题,因为每次我从网络中添加或删除节点时都必须重新生成指针,但另一方面,这将使我免于将节点存储在随机访问容器中。

这通常很难回答。有各种性能和易用性的权衡。

使用指针可以为某些操作提供更方便的用法。例如

link.first->value

与。

nodes[link.first].value

使用指针可以提供比索引更好或更差的性能。这取决于各种因素。您需要进行测量,以确定在您的情况下哪个更好。

如果可以保证只有一定数量的节点,则使用索引可以节省空间。然后,您可以为索引使用较小的数据类型,而对于指针,无论您有多少节点,都需要使用完整的指针大小。使用较小的数据类型可以在单个缓存行中容纳更多的链接,从而提高性能。

使用索引复制网络数据结构会更容易,因为您不必重新创建链接指针。

具有指向std::vector的元素的指针可能容易出错,因为矢量可能会在插入后将元素移动到内存中的另一个位置。

使用索引可以进行边界检查,这样可以更容易地发现一些错误。

使用索引使序列化更加简单。

尽管如此,我经常发现指数是总体上的最佳选择。使用方便的方法可以克服索引在语法上的许多不便,并且可以在指针具有更好性能的某些操作期间将索引切换到指针。

指定要使用或创建的类的接口。编写单元测试。做最简单的事情来完成单元测试。

所以它取决于类的接口。例如,如果Link不导出有关节点的信息,那么选择什么方法并不重要。另一方面,如果您选择指针,请考虑std::shared_ptr。

我会向Node类添加一个(或多个)link指针,然后手动维护链接。这将使您不必使用额外的容器。

如果你正在寻找更结构化的东西,你可以尝试使用Boost Intrusive。这以一种更普遍的方式有效地做了同样的事情。

如果使用,则可以完全避免Link

struct Node
{
  std::vector<Node*> parents;
  std::vector<Node*> children;
};

通过这种方法,

  1. 您可以避免创建另一个类
  2. 您的内存需求减少了
  3. 您必须进行较少的指针遍历才能遍历Nodes的网络

下行。你必须确保:

  1. 创建或删除链接时,必须更新两个对象
  2. 删除Node时,必须从其parentschildren中删除指向它的指针

您可以将其设为std::vector<Node *>而不是std::vector<Node>,并使用new分配节点。

然后:

  • 您可以将指向节点的指针存储在Link类中,而不用担心它们会成为无效的

  • 您仍然可以在节点向量中随机访问它们。

不利的一面是,当它们从节点列表中删除时,您需要记住delete

我对类图结构中的向量的个人经验提出了这些不变量。

不要将数据存储在向量中,因为其他类都有指针/引用

你有一个类似图表的数据结构。如果代码不是性能关键型的(这与性能敏感型不同!),则不应考虑缓存压缩数据结构。

  • 如果你不知道你的图有多大,并且你已经在一个向量中获得了Node数据,那么一旦你的向量调用vector::reallocate(),所有迭代器和指针都将无效,这意味着你必须以某种方式重新生成整个数据结构,也许你必须创建所有数据结构的副本,并使用dfs或类似方法来调整指针。如果您想要移除某个向量中间的数据,也会发生同样的事情。

  • 如果你知道你的数据会有多大,你就会坚定地保持这种状态,否则一旦你重新考虑,你就会头疼不已。

不要使用共享指针来跟踪需要释放的内容

如果你有一个类似图形的数据结构,并且你在性能关键路径上进行删除,那么每当你的算法决定他不再需要数据时,调用delete是不明智的。一种可能性是将数据保留在堆上(如果它是性能关键型的,请考虑使用池分配器)标记在性能关键部分不再需要的对象(如果你真的需要节省空间,可以考虑使用指针标记),或者在之后使用一些简单的标记和扫描算法来查找不再需要的项目(是的,图算法就是sutter所说的垃圾收集比智能指针更快的情况之一)。

请注意,对象的延迟销毁意味着您失去了Node类中所有类似RAII的功能。