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

相关推荐
专注VB编程开发20年1 小时前
vb.net datatable新增数据时改用数组缓存
java·linux·windows
仙剑魔尊重楼1 小时前
专业音乐制作软件fl Studio 2025.2.4.5242中文版新功能
windows·音乐·fl studio
rjc_lihui2 小时前
Windows 运程共享linux系统的方法
windows
失忆爆表症3 小时前
01_项目搭建指南:从零开始的 Windows 开发环境配置
windows·postgresql·fastapi·milvus
阿昭L3 小时前
C++异常处理机制反汇编(三):32位下的异常结构分析
c++·windows·逆向工程
梦帮科技17 小时前
Node.js配置生成器CLI工具开发实战
前端·人工智能·windows·前端框架·node.js·json
Lois_Luo17 小时前
关闭Win10强制所有应用以管理员身份运行
windows
luoyayun36118 小时前
实现Windows系统标题栏颜色跟随主题动态切换
windows·系统标题栏颜色·标题栏颜色
猫头虎19 小时前
如何解决 OpenClaw “Pairing required” 报错:两种官方解决方案详解
网络·windows·网络协议·macos·智能路由器·pip·scipy
呉師傅1 天前
【使用技巧】Adobe Photoshop 2024调整缩放与布局125%后出现点菜单项漂移问题的简单处理
运维·服务器·windows·adobe·电脑·photoshop