好的,我们来详细解析一下如何使用C++实现一个基于双向循环链表 的 list。这种结构是C++标准库 std::list 的常见实现方式之一。
核心思想:双向循环链表
双向循环链表的核心在于每个节点(Node)都包含三个部分:
- 数据:存储该节点的实际值。
- 前驱指针:指向链表中的上一个节点。
- 后继指针:指向链表中的下一个节点。
与普通双向链表不同,循环链表的特殊之处在于:
- 链表的头节点 的前驱指针 指向链表的尾节点。
- 链表的尾节点 的后继指针 指向链表的头节点。
- 整个链表形成了一个环状结构。
实现关键组件
-
节点类
Node这是链表的基本构成单元。cpptemplate <typename T> struct Node { T data; // 节点存储的数据 Node* prev; // 指向前一个节点的指针 Node* next; // 指向后一个节点的指针 // 构造函数,方便创建节点 Node(const T& d = T(), Node* p = nullptr, Node* n = nullptr) : data(d), prev(p), next(n) {} }; -
链表类
List封装链表操作,包含一个特殊的哨兵节点。cpptemplate <typename T> class List { private: // 哨兵节点 (sentinel node) Node<T>* sentinel; // ... 其他成员函数 (size, iterator等) 的实现 public: // 构造函数:初始化哨兵节点并使其形成循环 List() { sentinel = new Node<T>(); // 创建一个不存储数据的节点 sentinel->prev = sentinel; // 前驱指向自己 sentinel->next = sentinel; // 后继指向自己 } // 析构函数:释放所有节点内存 ~List() { while (!empty()) { pop_front(); // 或者实现一个clear函数 } delete sentinel; // 最后删除哨兵节点 } // 判断链表是否为空 (只有哨兵节点) bool empty() const { return sentinel->next == sentinel; } // ... 实现 push_front, push_back, pop_front, pop_back, insert, erase 等操作 };
哨兵节点的重要性
- 简化边界条件处理 :哨兵节点永远存在。链表中的第一个实际节点是
sentinel->next,最后一个实际节点是sentinel->prev。空链表时,这两个指针都指向sentinel本身。 - 避免空指针 :所有节点(包括首尾)都有有效的
prev和next指针,指向哨兵或其他有效节点。 - 统一操作:在链表头部插入/删除和在链表尾部插入/删除的逻辑变得对称和一致。
常见操作解析
-
push_front(const T& value)- 在头部插入cppvoid push_front(const T& value) { Node<T>* newNode = new Node<T>(value, sentinel, sentinel->next); // 新节点的prev指向哨兵,next指向原第一个节点 sentinel->next->prev = newNode; // 原第一个节点的prev指向新节点 sentinel->next = newNode; // 哨兵的next指向新节点(新节点成为第一个) } -
push_back(const T& value)- 在尾部插入cppvoid push_back(const T& value) { Node<T>* newNode = new Node<T>(value, sentinel->prev, sentinel); // 新节点的prev指向原最后一个节点,next指向哨兵 sentinel->prev->next = newNode; // 原最后一个节点的next指向新节点 sentinel->prev = newNode; // 哨兵的prev指向新节点(新节点成为最后一个) } -
pop_front()- 删除头部元素cppvoid pop_front() { if (empty()) throw std::out_of_range("List is empty"); Node<T>* toDelete = sentinel->next; // 要删除的是第一个节点 sentinel->next = toDelete->next; // 哨兵的next指向第二个节点 toDelete->next->prev = sentinel; // 第二个节点的prev指向哨兵 delete toDelete; } -
pop_back()- 删除尾部元素cppvoid pop_back() { if (empty()) throw std::out_of_range("List is empty"); Node<T>* toDelete = sentinel->prev; // 要删除的是最后一个节点 sentinel->prev = toDelete->prev; // 哨兵的prev指向倒数第二个节点 toDelete->prev->next = sentinel; // 倒数第二个节点的next指向哨兵 delete toDelete; } -
insert(iterator position, const T& value)- 在指定位置前插入 (需要先实现迭代器)cppiterator insert(iterator position, const T& value) { Node<T>* posNode = position.node; // 假设迭代器内部持有Node指针 Node<T>* newNode = new Node<T>(value, posNode->prev, posNode); // 新节点插入在posNode之前 posNode->prev->next = newNode; posNode->prev = newNode; return iterator(newNode); // 返回指向新节点的迭代器 } -
erase(iterator position)- 删除指定位置的元素cppiterator erase(iterator position) { if (position == end()) throw std::out_of_range("Cannot erase end iterator"); Node<T>* toDelete = position.node; Node<T>* nextNode = toDelete->next; toDelete->prev->next = toDelete->next; toDelete->next->prev = toDelete->prev; delete toDelete; return iterator(nextNode); }
迭代器的实现
为了使链表支持像 std::list 一样的遍历(如 for (auto it = list.begin(); it != list.end(); ++it)),需要实现迭代器类。
cpp
template <typename T>
class List {
// ... 之前的代码
public:
class iterator {
private:
Node<T>* node; // 当前指向的节点
public:
iterator(Node<T>* n = nullptr) : node(n) {}
// 解引用运算符,获取当前节点的数据
T& operator*() {
return node->data;
}
// 前置++运算符,移动到下一个节点
iterator& operator++() {
node = node->next;
return *this;
}
// 前置--运算符,移动到上一个节点
iterator& operator--() {
node = node->prev;
return *this;
}
// 后置++运算符
iterator operator++(int) {
iterator old = *this;
++(*this);
return old;
}
// 后置--运算符
iterator operator--(int) {
iterator old = *this;
--(*this);
return old;
}
// 比较运算符
bool operator==(const iterator& rhs) const {
return node == rhs.node;
}
bool operator!=(const iterator& rhs) const {
return node != rhs.node;
}
};
// 返回指向第一个实际元素的迭代器
iterator begin() {
return iterator(sentinel->next);
}
// 返回指向哨兵节点的迭代器 (作为结束标志)
iterator end() {
return iterator(sentinel);
}
// const版本的begin/end
// ... (类似实现,返回const_iterator)
};
复杂度分析
得益于双向循环链表的结构:
- 插入/删除操作 :在已知位置(通过迭代器指定)进行插入或删除操作的时间复杂度是 O(1)。这包括在头部 (
push_front,pop_front)、尾部 (push_back,pop_back) 和中间 (insert,erase) 的操作。 - 随机访问 :通过索引访问元素(如
list[5])的时间复杂度是 O(n),因为需要从头或尾遍历链表。这是链表与数组 (std::vector) 的主要区别之一。 - 查找:查找特定元素的时间复杂度也是 O(n)。
总结
使用双向循环链表 配合哨兵节点 是实现C++ list 的一种高效且优雅的方式。它确保了:
- 插入和删除操作的高效性(O(1))。
- 首尾操作的对称性和高效性。
- 边界条件处理的简化(通过哨兵节点)。
- 支持双向遍历(通过迭代器)。
理解这种底层实现有助于更深入地使用和理解C++标准库中的 std::list。