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

常见操作解析

  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。

相关推荐
Blockbuater_drug5 小时前
Dify 1.18 本地部署实战:Windows + WSL2 + Docker,从零跑到本地可用
windows·docker·容器·dify·大模型应用平台
cc_yy_zh6 小时前
笔记本关机自动重启解决办法
windows
AlanAshley9 小时前
Windows 错误代码 0x80284018:可能原因与解决办法
windows
2501_9159090610 小时前
Windows 打开 Xcode 工程,编译调试 Xcode 项目并和 Mac 同事协作
ide·windows·vscode·macos·ios·个人开发·xcode
vortex511 小时前
Windows Server IIS 运维配置指南:架构、身份、安全与排障
运维·windows·架构
如若1231 天前
Windows SSH + Headscale/Tailscale 组网及免密登录完整配置教程
运维·windows·ubuntu·ssh·内网穿透
ITHAOGE151 天前
下载 | Win11 25H2 官方正式版ISO映像!(10月更新、消费者版/专业版、商业版/企业版、26200.9711)
windows·科技·微软·电脑
John_Hui1 天前
Windows 10 安装版本:mysql-8.1.0-winx64.zip
数据库·windows·mysql
kingx31 天前
Windows Server安全配置深度复盘:关键 Payload 与解题思路
windows·靶场·web·加固
技术人生0011 天前
DirectX修复工具下载安装教程-Windows系统游戏dll修复工具下载安装修复教程
windows·游戏