C++双向循环链表实现详解

好的,我们来详细解析一下如何使用C++实现一个基于双向循环链表list。这种结构是C++标准库 std::list 的常见实现方式之一。

核心思想:双向循环链表

双向循环链表的核心在于每个节点(Node)都包含三个部分:

  1. 数据:存储该节点的实际值。
  2. 前驱指针:指向链表中的上一个节点。
  3. 后继指针:指向链表中的下一个节点。

与普通双向链表不同,循环链表的特殊之处在于:

  • 链表的头节点前驱指针 指向链表的尾节点
  • 链表的尾节点后继指针 指向链表的头节点
  • 整个链表形成了一个环状结构。

实现关键组件

  1. 节点类 Node 这是链表的基本构成单元。

    cpp 复制代码
    template <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) {}
    };
  2. 链表类 List 封装链表操作,包含一个特殊的哨兵节点

    cpp 复制代码
    template <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 本身。
  • 避免空指针 :所有节点(包括首尾)都有有效的 prevnext 指针,指向哨兵或其他有效节点。
  • 统一操作:在链表头部插入/删除和在链表尾部插入/删除的逻辑变得对称和一致。

常见操作解析

  1. push_front(const T& value) - 在头部插入

    cpp 复制代码
    void 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指向新节点(新节点成为第一个)
    }
  2. push_back(const T& value) - 在尾部插入

    cpp 复制代码
    void 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指向新节点(新节点成为最后一个)
    }
  3. pop_front() - 删除头部元素

    cpp 复制代码
    void 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;
    }
  4. pop_back() - 删除尾部元素

    cpp 复制代码
    void 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;
    }
  5. insert(iterator position, const T& value) - 在指定位置前插入 (需要先实现迭代器)

    cpp 复制代码
    iterator 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); // 返回指向新节点的迭代器
    }
  6. erase(iterator position) - 删除指定位置的元素

    cpp 复制代码
    iterator 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

相关推荐
长行2 小时前
Python|Windows 安装 DeepSpeed 安装方法及报错 Unable to pre-compile async_io 处理
windows·python·deepspeed
玖釉-3 小时前
[Vulkan 学习之路] 11 - 组装流水线:固定功能阶段 (Fixed Functions)
c++·windows·图形渲染
几道之旅4 小时前
我错了,mklink /D _isaac_sim C:\isaacsim和直接新建快捷方式原来不一样
windows
鹿角片ljp5 小时前
Java IO流案例:使用缓冲流恢复《出师表》文章顺序
java·开发语言·windows
FL16238631296 小时前
[C++][cmake]基于C++在windows上onnxruntime+opencv部署yolo26-seg的实例分割onnx模型
c++·windows·opencv
非凡ghost6 小时前
SoftPerfect Network Scanner(网络扫描管理工具)
网络·windows·学习·软件需求
H Corey7 小时前
Java--面向对象之继承与多态
java·开发语言·windows·学习·算法·intellij-idea
獨枭7 小时前
拉票中......
windows
玖釉-8 小时前
Windows 下 VS2022 编译运行 Khronos Vulkan Samples 全避坑指南
c++·windows·图形渲染