侵入式容器
STL中的std::list是常用的链表,但是该种链表是非侵入式的容器,常见的侵入式容器有boost::intrusive::list; 所谓的侵入式和非侵入式,其实指的是容器是否入侵了对容器所容纳的元素类型。
对于 std::list,其节点和原始对象的关系如下:
|--------------|
| next |
| prev |
| |--------| |
| | OBJECT | |
| |--------| |
|--------------|
可以看到,链表节点其实是对原始对象的一层包裹,并加上了前后指针;每次插入一个对象分配节点时时,都需要将原始对象拷贝进去。
如果原始对象很大,或者它不支持拷贝,那么怎么办呢?此时可以考虑让链表存储对象的指针(比如 std::list<T*>),但是这仍免不了额外的内存分配以及数据拷贝(此时有一个指针的拷贝,以及三个指针大小的内存分配,使用智能指针的话则更多,并且还会影响引用计数)。
而对于 boost::intrusive::list,其节点和原始对象的关系如下:
|--------------|
| OBJECT |
|--------------|
对象即节点,节点即对象;也就是说侵入式容器直接将对象本身作为链表节点,所以并不会产生额外的内存分配以及数据拷贝;而链表之所以是链表,是因为元素存在着指向其他元素的链接,所以直接使用原始对象作为链表节点的话,就需要对象中本身就包含指向其他元素的指针,这是使用侵入式容器的前提(precondition),所以对象在设计的时候就需要将其考虑进去,所以说它是侵入式的。
*倘若元素只存储在一个容器中,那么二者的效率并没有区别:都需要为对象和前后指针分配内存,无非是原始对象由容器还是对象本身来分配的问题,而且 std::list 会替用户管理对象的生命周期,使用起来还会更加方便。*但是在 LRU 的情况下,对象需要出现在两个容器中,这个时候侵入式容器的价值就体现出来了。
总结:
- 非侵入式链表(如std::list):由链表来管理对象的生命周期,且不需要用户维护前后指针,但是缺点在于它需要进行额外的内存分配以及数据拷贝。
- 侵入式链表(如boost::intrusive::list):需要用户自己在对象中加上前后指针,对象的生命周期由用户自己管理,但是不需要额外的内存分配以及数据拷贝。
侵入式容器在平常的业务开发中使用的比较少,但是在许多框架中则使用得非常广泛,比如 Linux 内核中的 struct list_head,Nginx 中的 ngx_queue_t 和 ngx_rbtree_t;
以上内容取自:https://www.chenjianyong.com/blog/2022/03/boost_intrusive_container.html
**侵入式链表(Intrusive List)**是一种数据结构的设计模式,在这种模式中,链表节点自身负责维护其在链表中的链接信息,而不是由外部的数据结构或者容器来管理这些信息。这里的“侵入式”主要指的是链表的操作直接作用于链表中的元素本身,即元素节点内部包含了用于链接到其他节点的指针或其他相关字段。这种方式使得链表操作更加高效,因为不需要额外的数据结构来存储链接信息,但同时也意味着链表节点的设计需要考虑这些额外的链接逻辑。
下面是一些关键点来帮助理解侵入式链表的概念:
节点设计:
每个链表节点都必须包含额外的成员变量来支持链表操作,如next和prev指针,或者更复杂的双向链表所需的额外指针。
这些成员变量是节点类的一部分,而非链表类的一部分。
链表操作:
插入、删除等链表操作直接修改这些节点内的指针,而不需要外部的链表管理器。
链表的头节点通常也包含一个指向链表中的第一个节点的指针,以及可能指向最后一个节点的指针(对于双向链表)。
优势:
减少了额外的数据结构需求,使得内存使用更为紧凑。
提高了效率,因为不需要额外的查找或间接访问操作。
可以方便地实现多链表结构,即一个节点可以同时属于多个链表。
劣势:
节点类的设计变得更加复杂,因为需要包含链表操作相关的成员变量和方法。
如果不正确地管理链表节点,可能会导致内存泄漏或其他问题。
应用场景:
当需要频繁地进行链表操作时,侵入式链表可以提供更好的性能。
在需要节点能够同时存在于多个链表中的情况下,侵入式链表非常有用。
举一个简单的例子,假设有一个Node类,它包含了一些数据成员和用于链表操作的指针成员:
class Node {
public:
int data;
Node *next;
Node(int data) : data(data), next(nullptr) {}
};
然后你可以使用这个Node类来创建一个简单的单向链表,并且链表的所有操作都会直接作用于这些Node对象上。
侵入式链表的概念在C++中尤其常见,特别是在那些对性能要求较高的系统编程场景中。许多C++标准库容器如std::list实际上是采用了一种非侵入式的实现方式,其中链表容器负责管理所有节点的链接信息。而在一些特定的应用场景下,开发者会选择使用侵入式链表以获得更高的性能或灵活性。
可以方便地实现多链表结构,即一个节点可以同时属于多个链表。 这个怎么理解?
在侵入式链表中,一个节点可以方便地属于多个链表,这是通过在节点内部维护多个指向其他节点的指针来实现的。这种设计允许节点同时参与多个链表,而不需要额外的数据结构来管理这种关系。下面是一个更详细的解释:
侵入式链表的基本概念
在侵入式链表中,每个节点都包含额外的成员变量来支持它在链表中的链接信息。这些成员变量通常包括指向下一个节点的指针(next)和指向前一个节点的指针(prev),如果是双向链表的话。
多链表结构
当一个节点可以属于多个链表时,它需要维护多个链接信息。这意味着节点需要包含多个指针来支持这种结构。下面是一个示例,说明一个节点如何同时属于两个链表:
class MultiListElement {
public:
int data;
MultiListElement *next1, *prev1; // 用于链表1
MultiListElement *next2, *prev2; // 用于链表2
MultiListElement(int data) : data(data), next1(nullptr), prev1(nullptr), next2(nullptr), prev2(nullptr) {}
};
在这个例子中,MultiListElement类包含四个额外的指针:next1和prev1用于链表1,而next2和prev2用于链表2。这使得一个节点可以同时存在于两个链表中。
如何实现多链表结构
节点类的设计:
每个节点类需要包含足够的指针来支持它所属的每个链表。例如,如果有两个链表,就需要包含两组指针。
链表的插入和删除操作:
当向链表中插入节点时,需要更新节点内部的指针,使其能够正确地链接到链表中的其他节点。
同样地,当从链表中删除节点时,也需要更新这些指针以断开链接。
管理多个链表:
通常,每个链表都会有一个头节点,该头节点包含指向链表中第一个节点的指针。
当需要在多个链表中移动节点时,可以通过更新节点内的指针来轻松实现这一点。
示例代码
下面是一个简单的示例,展示了如何创建两个链表,并使一个节点同时属于这两个链表:
#include <iostream>
class MultiListElement {
public:
int data;
MultiListElement *next1, *prev1; // 用于链表1
MultiListElement *next2, *prev2; // 用于链表2
MultiListElement(int data) : data(data), next1(nullptr), prev1(nullptr), next2(nullptr), prev2(nullptr) {}
// 添加到链表1
void addToList1(MultiListElement *&head1) {
this->next1 = head1;
if (head1 != nullptr) {
head1->prev1 = this;
}
head1 = this;
}
// 添加到链表2
void addToList2(MultiListElement *&head2) {
this->next2 = head2;
if (head2 != nullptr) {
head2->prev2 = this;
}
head2 = this;
}
};
int main() {
MultiListElement *head1 = nullptr;
MultiListElement *head2 = nullptr;
MultiListElement *node1 = new MultiListElement(1);
MultiListElement *node2 = new MultiListElement(2);
// 将node1添加到链表1
node1->addToList1(head1);
// 将node2添加到链表1
node2->addToList1(head1);
// 将node1添加到链表2
node1->addToList2(head2);
// 打印链表1
for (MultiListElement *cur = head1; cur != nullptr; cur = cur->next1) {
std::cout << cur->data << " ";
}
std::cout << std::endl;
// 打印链表2
for (MultiListElement *cur = head2; cur != nullptr; cur = cur->next2) {
std::cout << cur->data << " ";
}
std::cout << std::endl;
delete node1;
delete node2;
return 0;
}
在这个例子中,node1同时属于head1和head2两个链表。node2只属于head1链表。通过在MultiListElement类中定义两个独立的指针组,我们可以轻松地管理这种多链表结构。
总结来说,侵入式链表的设计使得一个节点可以方便地属于多个链表,只需要在节点内部维护相应的链接信息即可。这种设计非常适合那些需要在多个列表中高效移动节点的应用场景。
非侵入式链表(Non-intrusive List)与侵入式链表的主要区别在于节点是如何管理其在链表中的链接信息的。在非侵入式链表中,链表节点本身并不包含用于链接到其他节点的额外信息。相反,这些信息是由外部的数据结构或容器来维护的。下面是两种链表类型的对比:
侵入式链表的特点
节点设计:每个链表节点都包含额外的成员变量来支持链表操作,如next和prev指针。
链表操作:插入、删除等操作直接修改这些节点内的指针,而不需要外部的链表管理器。
优势:减少了额外的数据结构需求,使得内存使用更为紧凑;提高了效率,因为不需要额外的查找或间接访问操作。
劣势:节点类的设计变得更加复杂,因为需要包含链表操作相关的成员变量和方法。
非侵入式链表的特点
节点设计:节点仅包含数据成员,而不包含用于链接到其他节点的额外成员变量。
链表操作:链表的操作由外部的数据结构或容器来管理,通常会有一个额外的结构来存储节点间的链接信息。
优势:节点类的设计更简单,不需要额外的链表操作相关的成员变量和方法。
劣势:需要额外的数据结构来存储链接信息,这可能会增加内存使用量和降低效率。
示例
侵入式链表节点
class IntrusiveListNode {
public:
int data;
IntrusiveListNode *next;
IntrusiveListNode *prev;
IntrusiveListNode(int data) : data(data), next(nullptr), prev(nullptr) {}
};
非侵入式链表节点
class NonIntrusiveListNode {
public:
int data;
NonIntrusiveListNode(int data) : data(data) {}
};
非侵入式链表容器
template<typename T>
class NonIntrusiveList {
public:
struct Node {
T *item;
Node *next;
Node *prev;
};
Node *head;
Node *tail;
NonIntrusiveList() : head(nullptr), tail(nullptr) {}
~NonIntrusiveList() {
while (head) {
Node *tmp = head;
head = head->next;
delete tmp;
}
}
void push_back(T *item) {
Node *newNode = new Node{item, nullptr, tail};
if (tail) {
tail->next = newNode;
} else {
head = newNode;
}
tail = newNode;
}
void pop_front() {
if (!head) return;
Node *tmp = head;
head = head->next;
if (head) {
head->prev = nullptr;
} else {
tail = nullptr;
}
delete tmp;
}
};
总结
**侵入式链表:**节点自身包含链接信息,适用于需要高效操作和紧凑内存使用的情况。
**非侵入式链表:**节点不包含链接信息,由外部容器维护链接信息,适用于节点类设计需要简单且不需要考虑链表操作的情况。
在选择使用哪种链表时,需要根据具体的应用场景和需求来决定。侵入式链表通常在需要高性能和低内存开销的应用中更为适用,而非侵入式链表则在节点设计需要简单且不需要特别关注链表操作的情况下更为合适。
可以直接使用侵入式链表
提供一个来自开源代码brpc里面的侵入式链表butil::LinkedList(butil/containers/linked_list.h);
// Copyright (c) 2009 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef BUTIL_CONTAINERS_LINKED_LIST_H_
#define BUTIL_CONTAINERS_LINKED_LIST_H_
#include "butil/macros.h"
// Simple LinkedList type. (See the Q&A section to understand how this
// differs from std::list).
//
// To use, start by declaring the class which will be contained in the linked
// list, as extending LinkNode (this gives it next/previous pointers).
//
// class MyNodeType : public LinkNode<MyNodeType> {
// ...
// };
//
// Next, to keep track of the list's head/tail, use a LinkedList instance:
//
// LinkedList<MyNodeType> list;
//
// To add elements to the list, use any of LinkedList::Append,
// LinkNode::InsertBefore, or LinkNode::InsertAfter:
//
// LinkNode<MyNodeType>* n1 = ...;
// LinkNode<MyNodeType>* n2 = ...;
// LinkNode<MyNodeType>* n3 = ...;
//
// list.Append(n1);
// list.Append(n3);
// n2->InsertBefore(n3);
//
// Lastly, to iterate through the linked list forwards:
//
// for (LinkNode<MyNodeType>* node = list.head();
// node != list.end();
// node = node->next()) {
// MyNodeType* value = node->value();
// ...
// }
//
// Or to iterate the linked list backwards:
//
// for (LinkNode<MyNodeType>* node = list.tail();
// node != list.end();
// node = node->previous()) {
// MyNodeType* value = node->value();
// ...
// }
//
// Questions and Answers:
//
// Q. Should I use std::list or butil::LinkedList?
//
// A. The main reason to use butil::LinkedList over std::list is
// performance. If you don't care about the performance differences
// then use an STL container, as it makes for better code readability.
//
// Comparing the performance of butil::LinkedList<T> to std::list<T*>:
//
// * Erasing an element of type T* from butil::LinkedList<T> is
// an O(1) operation. Whereas for std::list<T*> it is O(n).
// That is because with std::list<T*> you must obtain an
// iterator to the T* element before you can call erase(iterator).
//
// * Insertion operations with butil::LinkedList<T> never require
// heap allocations.
//
// Q. How does butil::LinkedList implementation differ from std::list?
//
// A. Doubly-linked lists are made up of nodes that contain "next" and
// "previous" pointers that reference other nodes in the list.
//
// With butil::LinkedList<T>, the type being inserted already reserves
// space for the "next" and "previous" pointers (butil::LinkNode<T>*).
// Whereas with std::list<T> the type can be anything, so the implementation
// needs to glue on the "next" and "previous" pointers using
// some internal node type.
namespace butil {
template <typename T>
class LinkNode {
public:
// LinkNode are self-referential as default.
LinkNode() : previous_(this), next_(this) {}
LinkNode(LinkNode<T>* previous, LinkNode<T>* next)
: previous_(previous), next_(next) {}
// Insert |this| into the linked list, before |e|.
void InsertBefore(LinkNode<T>* e) {
this->next_ = e;
this->previous_ = e->previous_;
e->previous_->next_ = this;
e->previous_ = this;
}
// Insert |this| as a circular linked list into the linked list, before |e|.
void InsertBeforeAsList(LinkNode<T>* e) {
LinkNode<T>* prev = this->previous_;
prev->next_ = e;
this->previous_ = e->previous_;
e->previous_->next_ = this;
e->previous_ = prev;
}
// Insert |this| into the linked list, after |e|.
void InsertAfter(LinkNode<T>* e) {
this->next_ = e->next_;
this->previous_ = e;
e->next_->previous_ = this;
e->next_ = this;
}
// Insert |this| as a circular list into the linked list, after |e|.
void InsertAfterAsList(LinkNode<T>* e) {
LinkNode<T>* prev = this->previous_;
prev->next_ = e->next_;
this->previous_ = e;
e->next_->previous_ = prev;
e->next_ = this;
}
// Remove |this| from the linked list.
void RemoveFromList() {
this->previous_->next_ = this->next_;
this->next_->previous_ = this->previous_;
// next() and previous() return non-NULL if and only this node is not in any
// list.
this->next_ = this;
this->previous_ = this;
}
LinkNode<T>* previous() const {
return previous_;
}
LinkNode<T>* next() const {
return next_;
}
// Cast from the node-type to the value type.
const T* value() const {
return static_cast<const T*>(this);
}
T* value() {
return static_cast<T*>(this);
}
private:
LinkNode<T>* previous_;
LinkNode<T>* next_;
DISALLOW_COPY_AND_ASSIGN(LinkNode);
};
template <typename T>
class LinkedList {
public:
// The "root" node is self-referential, and forms the basis of a circular
// list (root_.next() will point back to the start of the list,
// and root_->previous() wraps around to the end of the list).
LinkedList() {}
// Appends |e| to the end of the linked list.
void Append(LinkNode<T>* e) {
e->InsertBefore(&root_);
}
LinkNode<T>* head() const {
return root_.next();
}
LinkNode<T>* tail() const {
return root_.previous();
}
const LinkNode<T>* end() const {
return &root_;
}
bool empty() const { return head() == end(); }
private:
LinkNode<T> root_;
DISALLOW_COPY_AND_ASSIGN(LinkedList);
};
} // namespace butil
#endif // BUTIL_CONTAINERS_LINKED_LIST_H_