【发布时间】:2010-07-29 09:44:08
【问题描述】:
我无法在网上找到太多关于他们的信息。它们是什么以及它们通常在什么时候使用?
谢谢。
【问题讨论】:
我无法在网上找到太多关于他们的信息。它们是什么以及它们通常在什么时候使用?
谢谢。
【问题讨论】:
侵入式列表是指向下一个列表节点的指针存储在与节点数据相同的结构中的列表。这通常是一件坏事,因为它将数据与特定的列表实现联系起来。大多数类库(例如,C++ 标准库)使用非侵入式列表,其中数据对列表(或其他容器)实现一无所知。
【讨论】:
std::list<T> 可以实现为struct _Node { _Node* prev,next; T object }。模板与强大的价值语义相结合,使这些非侵入式列表变得高效。如您所见,没有单独的缓存行或链接块。
void*)并且您失去了引用的局部性,或者您为每个元素类型重写了列表节点类型。一个很好的例子是 qsort 与 std::sort,其中 C++ 通常在整数上比 C 高出 600%。是的,C 中的 intsort() 可能与 C++ 一样快,但无法对浮点数进行排序。
std::sort 优势在于内联。 qsort 接收到作用于 void* 的函数的类型擦除指针,而 std::sort (由于模板)知道其参数的类型,更重要的是,在排序中使用的函数(如果它在同一个 TU 中),所以如果它是足够小(而且通常是这样)它可以内联并消除调用开销。
我其实很喜欢侵入式模型。
multi_index_container 完成,但请注意 multi_index_container 具有某些缺点,这些缺点对于侵入式容器来说不是问题。侵入性很好
...您只需要知道自己在做什么(对于任何容器都是如此)。
【讨论】:
令人惊讶的是有多少人完全错误地理解了这一点(例如 Ziezi 的回答)。当事情真的很简单时,人们似乎把事情复杂化了。
在侵入式链表中,没有明确的“节点”结构/类。相反,“数据”结构/类本身包含对链表中其他数据的下一个和上一个指针/引用。
例如(侵入式链表节点):
struct Data {
Data *next;
Data *prev;
int fieldA;
char * fieldB;
float fieldC;
}
注意 next 和 prev 指针如何与实体的私有数据字段(例如 fieldA)并排并侵入。这“违反”了标准链表强制执行的关注点分离(见下文),但有利于大大减少查找特定节点的链表遍历量以及减少内存分配。
在侵入式链表中,“链表”本身通常是虚拟的,通常根本不需要创建链表结构/类。
相反,您可以简单地将头指针存储到某个所有者/管理器中的第一个数据项。 此管理器还包含添加/删除功能以根据需要更新指针。 欲了解更多信息,请参阅https://gameprogrammingpatterns.com/spatial-partition.html
拥有一对 next/prev 指针表示每个对象只能属于一个列表。但是,您当然可以根据需要添加多对 next/prev 指针(或定义一组 next/prev 指针)以支持多个列表中的对象。
在非侵入式(即标准)链表中,next/prev 指针是专用“节点”实体的一部分,而实际的 Data 实体只是该节点中的一个字段。
例如(非侵入式链表节点和数据):
struct Data {
int fieldA;
char * fieldB;
float fieldC;
}
struct Node {
Node *next;
Node *prev;
Data *data;
}
注意下一个/上一个指针如何不侵入实际的数据实体,并保持关注点的分离。
更新:
您可能会看到其他网站(例如 https://www.data-structures-in-practice.com/intrusive-linked-lists/)使用包含 next/prev 指针的“List”结构(实际上是一个节点),并且是“Data”结构/类中的单个侵入字段。
这确实从数据中隐藏了下一个/上一个指针,但是它需要执行指针运算来访问与列表(节点)关联的实际数据。
这种方法在我的选项中增加了不必要的复杂性(而不是直接嵌入下一个/上一个字段),只是为了隐藏下一个/上一个指针的可疑目标。如果您需要侵入性列表,请尽可能保持简单。 (此外,在托管内存语言中,无论如何都很难或不可能进行指针运算。)
【讨论】:
Data 类的内存分配数量与Ziezi 的相同。他的T data 成员不是std::unique_ptr<T> data;它与prev 和next 连续分配,就像您的示例一样。
以下是对列表有效的简短说明:
要存储的对象包含允许集成到容器中的附加信息。示例:
结构节点 { 下一个节点*; // 额外的 节点* 上一个; // 信息 T数据; }1。优点:
- 存储对象本身。
- 不涉及内存管理。
- 迭代速度更快。
- 更好的异常保证。
- 插入和删除对象的可预测性。 (不需要额外的(不可预测的)内存管理。)
- 更好的内存局部性。
2。缺点:
- 包含容器集成的附加数据。 (每种商店类型都必须适应(修改)容器要求。)
- 更改存储对象的内容时,请注意可能产生的副作用。(尤其是关联容器。)
- 插入对象的生命周期管理,独立于容器。
- 对象可能在从容器中擦除之前被处置,从而导致迭代器失效。
- 侵入式容器是不可复制和不可分配的。
对象不“知道”并包含有关要存储的容器的详细信息。示例:
结构节点 { T数据; }1。优点:
- 不包含有关容器集成的其他信息。
- 对象的生命周期由容器管理。 (不太复杂。)
2。缺点:
- 存储用户传递的值的副本。 (可就地施工。)
- 一个对象只能属于一个容器。 (或者容器应该存储指向对象的指针。)
- 存储副本的开销。 (每次分配的簿记。)
不可复制或不可移动的对象不能存储在非侵入式容器中。- 无法存储派生对象并仍保持其原始类型。 (切片 - 失去多态性。)
【讨论】:
Node 结构与典型的链表节点相同,模板化为T。您应该已经解释过,在侵入式列表中,对象类型继承了列表节点类型,有效地与侵入式容器类型耦合。
侵入式列表是对象本身就是列表的头部或单元格的列表。根据上下文,它们是好是坏。
在某些已定义的模块(不安全的类组一起工作)中,绑定类之间的关系可能是最好的方法。它们允许免费直接和全面管理诸如 unicity 之类的共同关系(例如:苹果不会在苹果树中出现两次,这不需要任何密钥,并且苹果不属于两个不同的树),它们是可导航的在两个方向上(直接访问给定苹果的苹果树和给定一些苹果树的苹果)。所有基本操作都是 O(1)(不在某些外部容器中搜索)。
两个模块之间的侵入性列表非常糟糕。因为它们将被捆绑在一起,而模块的正当性是对代码独立性的管理。
【讨论】: