深入理解 STL::list:链表的底层原理与使用场景

一. list是什么

继 string 与 vector 之后,我们终于来到了 STL 中另一位成员 list。它的底层实现基于双向链表这一经典的数据结构。

1. 什么是链表

链表是一种将数据通过指针连接在一起的数据结构。它不像数组那样将所有元素保存在连续的内存中,而是每一个元素也就是结构体(称为节点)都通过指针指向下一个节点

以双向链表为例,我们通常这样定义一个节点:

template<class T>
struct listNode
{
    ListNode(const T& data = T())
        : _data(data)
        , _prev(nullptr)
        , _next(nullptr)
    {}

    T _data;            // 存储实际内容
    listNode<T>* _prev; // 指向前一个节点的指针
    listNode<T>* _next; // 指向下一个节点的指针
}

list 的典型特性:

  • 插入 / 删除效率高:O(1) 时间复杂度,只需要删除节点并修改指针即可

  • 不支持随机访问:不能像数组一样使用下标,list[i]

  • 支持双向迭代器(可以从前往后,也可以从后往前遍历)

  • 每次访问某个位置都要从头到尾一个个跳过去

2. list的迭代器失效问题

实现 vector 时经常会遇到迭代器失效问题,这是因为 vector 本质上是一段连续的内存空间,当触发扩容或缩容时,指向原内存空间的迭代器都会失效。

但是 list 的实现原理则完全不同,由于其本质上是一个双向链表,每个节点都是一个独立的内存块通过指针连接在一起,由于内存空间不连续,插入或删除节点时只需调整相邻节点的指针,其他节点的地址保持不变,因此指向这些节点的迭代器依然有效。

删除 node2 节点时,只需调整 node1 和 node3 的指针即可。指向这两个节点的迭代器不会失效,因为它们的地址并没有变。

3. 什么是哨兵位

在链表中,哨兵位是一个特殊的“辅助节点”,它不存储用户的真实数据,仅作为边界标记存在。
通常的设计是:

  • 空链表:哨兵的前驱和后继都指向自己。

  • 非空链表:第一个元素的前驱、最后一个元素的后继都指向哨兵,从而把链表首尾“闭合”起来。

在 STL 的 list 实现中,哨兵就是 _head 节点,它始终存在,永远不会被删除。

二. list的模拟实现

模拟实现 list 的基本成员:

template<class T>
struct listNode
{
    // 构造函数:创建节点时可传入数据,默认为 T()(值初始化)
    listNode(const T& data = T())
        : _data(data), _prev(nullptr), _next(nullptr)
    {}

    T _data;             // 节点存储的数据
    listNode<T>* _prev;  // 指向前驱节点
    listNode<T>* _next;  // 指向后继节点
};

template<class T>
class list
{
    typedef listNode<T> Node; // 节点类型别名

public:
    // 初始化空链表:建立一个哨兵节点(环形结构)
    // 空链表时 _head 的前驱和后继都指向自己
    void empty_init()
    {
        _head = new Node;
        _head->_prev = _head->_next = _head;
        _size = 0;
    }
    
    // 尾插:在链表尾部插入一个新节点
    void push_back_node(const T& val = T())
    {
        // 如果链表还没有初始化,先构建一个空链表
        if(_head == nullptr)
        {
            empty_init();
        }
        
        // 创建新节点
        Node* newNode = new Node(val);

        // 获取尾节点:由于是环形,尾节点就是 _head->_prev
        Node* tail = _head->_prev;

        // 插入到尾部(tail 和 _head 之间)
        newNode->_prev = tail;
        newNode->_next = _head;

        tail->_next = newNode;
        _head->_prev = newNode;

        ++_size; // 更新链表大小
    }

private:
    Node* _head = nullptr; // 头结点(哨兵位,永远存在)
    size_t _size = 0;      // 链表中元素个数
};

list 的基本成员相对简洁, 只需要一个头结点即可, 作为整个链表的访问入口

1. 构造函数

1.1 默认构造

初始化一个空环,只有哨兵位,将 prev 和 next 指针都指向自己

list()
{
    empty_init();
}

1.2 填充构造

指定 n 个空间并用 val 初始化

list(size_t n, const T& val = T())
{   
    empty_init();
    
    while(n--)
    {
        push_back_node(val); // 循环尾插 n 个节点
    }
}

1.3 区间构造

实现逻辑基本和填充构造相同

template<class InputIterator>
list(InputIterator first, InputIterator last)
{
    empty_init();

    while(first != last)
    {
        push_back_node(*first); // 尾插一个节点
        first++;                // 移动到下一个元素
    }
}

1.4 拷贝构造

list(const list<T>& lt)
{
    empty_init();
    
    for(auto& e : lt)
    {
        push_back_node(e);
    }
}

1.5 初始化列表构造(C++11 新增)

在 C++11 之前,如果我们想用一组数据初始化一个容器,只能先建空容器,再手动一次次插入,非常繁琐。

所以C++11 引入了 初始化列表构造,例如:

list<string> names{"Alice", "Jack", "Tom"};

这样我们使用花括号就可以初始化容器,避免一次次的插入,并且在我们阅读代码的时候可以立刻看到容器里面的值,大大提升可读性

list(initializer_list<T> lt)
{
    empty_init();
    
    for(auto& e : lt)
    {
        push_back_node(e);
    }
}

这个构造函数可以理解为 C++11 引入的语法糖,本质是用 initializer_list 来统一支持 大括号初始化。让容器初始化变得更优雅

1.6 析构函数

在带头双向链表中,析构函数的任务就是逐个释放所有节点的内存,最后释放哨兵节点,把整个链表彻底清空。

~list()
{
    if(_head == nullptr)
        return;

    Node* cur = _head->next;
    while(cur != _head)
    {
        Node* next = cur->_next; // 从 head->_next 开始遍历,直到回到哨兵节点
        delete cur;              // 删除所有有效节点
        cur = next;              // 更新cur
    }

    delete _head;                // 最后删除哨兵节点
    _head = nullptr;
    _size = 0;
}

2. 迭代器

首先需要了解一个前提,为什么list的迭代器需要用类来实现而不是像vector一样使用指针呢

1) 底层布局不同:连续 vs 非连续

vector底层是连续内存。元素在内存中紧挨着,指针相加减就能到相邻元素,所以随机访问也成立。而 list 底层是由双向链表实现的非连续内存。相邻元素靠指针链接而不是地址相邻,所以用 T* 根本无法找到下一个或上一个元素,只能通过节点指针跳转。

2) 迭代器语义不同:随机访问 vs 双向

vector 迭代器属于随机访问迭代器,天然等价于 T*(支持 it + n,it [n]),而list迭代器是双向迭代器,只要求 ++ 和 --,不支持加减偏移、下标访问。我们需要把向前,向后跳转实现为 node->prev / node->next,这就必须封装一个类来保存节点指针 node* 并定义 operator++/--。

2.1 模拟实现

template<class T>
class Iterator
{
    typedef ListNode<T> Node;
    typedef Iterator<T> Self; 

    Node* _node;

    Iterator(Node* node)
        :_node(node)
    {}
    
    T& operator*()
    {
        return _node->_data;
    }

    T* operator->()
    {
        return &(_node->_data);
    }

    Self& operator++()
    {
        _node = _node->_next;
        return *this;
    }

    Self operator++(int)
    {
        Self tmp(_node); // 后置++,先把自增前的内容存起来
        _node = _node->_next;
        return tmp;    
    }
    
    Self& operator--()
    {
        _node = _node->_prev;    
        return *this;
    }

    Self operator--(int)
    {
        Self tmp(_node); // 后置--,先存储
        _node = _node->_prev;
        return tmp;
    }
    
    // 比较底层节点的地址
    bool operator==(const Self& it) { return _node == it._node; }

    bool operator!=(const Self& it) { return _node != it._node; }
}

2.2 const迭代器

在 STL 容器中,几乎所有容器都需要同时提供 iterator 和 const_iterator (只读元素),如果每个都手写上面一套 operator*,operator->,++/--,代码会有大量重复和冗余,并且可读性也很低。

我们可以通过添加模板参数的方式来复用上面所写的代码:

template<class T, class Ref, class Ptr>
class Iterator
{
    typedef ListNode<T> Node;
    typedef Iterator<T, Ref, Ptr> Self; 

    Node* _node;
    Ref operator*()
    {
        return _node->_data;
    }

    Ptr operator->()
    {
        return &(_node->_data);
    }

    Self& operator++()
    {
        _node = _node->_next;
        return *this;
    }

    //...
}

在 List 类中,我们可以这么定义迭代器类型:

template<class T>
class List
{
    typedef Iterator<T, T&, T*> iterator;
    typedef Iterator<T, const T&, const T*> const_iterator;
}

这样,我们就完美的完成了代码的复用,一份代码同时实现了 iterator 和 const_iterator

2.3 反向迭代器

list 和其他 STL 容器一样,也提供反向迭代器用于从尾到头遍历。反向迭代器本质上是对正向迭代器的一个包装器,反向迭代器的前进操作对应底层正向迭代器的后退操作,反之亦然。

反向迭代器内部保存的是基迭代器,指向正向序列中当前元素的下一个位置(例如 rbegin()  的基迭代器就是正向迭代器的 end() )。因此,反向迭代器在实现解引用时,必须先向前移动一个位置再返回该位置的元素引用

template <class Iterator, class Ref, class Ptr>
class reverseIterator
{
    typedef reverseIterator<Iterator, Ref, Ptr> Self;

    Iterator _it;
    
    reverseIterator(Iterator it)
        : _it(it)
    {}

    Ref operator*()
    {    
        Iterator tmp = _it;
        return *(--tmp);
    }

    Ptr operator->()
    {
        // 使用addressof无条件取地址,避免因为&运算符重载而取到其他值
        return std::addressof(operator*());
    }

    Self& operator++()
    {
        --_it; // 反向迭代器自增等于正向迭代器自减
        return *this;
    }

    Self operator++(int)
    {
        Iterator tmp = _it;
        ++_it;
        return tmp;
    }

    Self& operator--() { ++_it; return *this; }
    Self operator--(int) { Iterator tmp = _it; ++_it; return tmp; }

    // 按值比较调用正向迭代器的 operator== operator!=
    bool operator==(const Self& rit) const { return _it == rit._it; }
    bool operator!=(const Self& rit) const { return _it != rit._it; }
}

同样,在 List 中可以这么定义:

template<class T>
class List
{
    typedef Iterator<T, T&, T*> iterator;
    typedef Iterator<T, const T&, const T*> const_iterator;\

    typedef reverseIterator<iterator, T&, T*> reverse_iterator;
    typedef reverseIterator<const_iterator, const T&, const T*>
        const_reverse_iterator;
}

2.4 迭代器相关接口

iterator begin() { return iterator(_head->_next); }
iterator end() { return iterator(_head); }

const_iterator begin() const { return const_iterator(_head->_next); }
const_iterator end() const { return const_iterator(_head); }

因为哨兵位是无效节点,所以 begin 返回的应是下一个节点

reverse_iterator rbegin() { return reverse_iterator(end()); }
reverse_iterator rend() { return reverse_iterator(begin()); }

const_reverse_iterator crbegin() const { return const_reverse_iterator(end()); }
const_reverse_iterator crend() const { return const_reverse_iterator(begin()); }

3. 增删查改

3.1 insert / erase

在指定位置插入数据以及删除数据

insert:在指定迭代器的位置之前插入数据

iterator insert(iterator pos, const T& val = T())
{
    Node* newNode = new Node(val);
    Node* cur = pos._node; // 先记录下来迭代器的指向
    Node* prevNode = cur->_prev;

    // 插入的逻辑实在迭代器指向的节点前插入
    newNode->_prev = prevNode;
    newNode->_next = cur;

    prevNode->_next = newNode;
    cur->_prev = newNode;

    ++_size;
    
    return iterator(newNode); // 返回新节点的位置
}

erase:删除指定迭代器指向的数据

iterator erase(iterator pos)
{
    assert(pos != end()); // 先确保删除的节点不是end()无效节点

    Node* cur = pos._node;
    Node* prevNode = cur->_prev;
    Node* nextNode = cur->_next;

    prevNode->_next = nextNode;
    nextNode->_prev = prevNode;

    delete cur;

    --_size;    

    return iterator(nextNode);
}

insert / erase 的返回值

在容器操作里插入或删除元素后,原迭代器可能失效。即便是 list 能保证大多数迭代器有效,但指向被删元素的迭代器一定失效。所以标准库约定 insert 返回指向新插入元素的迭代器,erase 返回指向删除位置后一个元素的迭代器。这样用户就能顺着返回值继续走操作,而不必重新定位。

有了返回值我们就可以这样写

// 连续插入,insert的返回值充当下一个位置
auto it = list.insert(pos, 10);
it = list.insert(it, 44); // 可以在接下来的位置继续插入

// 连续删除,每一次删除后使用erase的返回值继续前进
for(auto it = list.begin(); it != end(); )
{
    if(*it % 2 == 0)
        it = list.erase(it); // 删除并获得下一个元素的位置
    else
        ++it;
}

如果没有返回值,我们就必须自己手动 find 新位置,既麻烦又容易用到了已失效的迭代器而导致崩溃。

3.2 头插/删,尾插/删

分别在头部插入/删除,尾部插入/删除,直接复用insert/erase即可

// 调用 insert 在 end() 位置插入,相当于尾插
void push_back(const T& val = T()) { insert(end(), val); }
void pop_back() { erase(--end()); }

// 调用 insert 在 begin() 位置插入,相当于头插
void push_front(const T& val = T()) { insert(begin(), val); }
void pop_front() { erase(begin()); }

3.3 容量操作

模拟实现也很简单:

bool empty() { return _size == 0; }
size_t size() { return _size; }

// 系统允许的最大size_t值除以单个节点大小
size_t max_size() { return SIZE_MAX / sizeof(Node); }

3.4 其他接口

splice:常数时间拼接

整段节点从一个 list 搬到另一个(或同一个)里,不拷贝元素和分配内存,只是改指针

std::list<int> a{1,2,3}, b{7,8,9};
auto pos = std::next(a.begin());     // 让迭代器后移一步指向 2
a.splice(pos, b);                    // 把 b 全部接到 2 前:a= {1,7,8,9,2,3}

remove / remove_if: 按值/条件删除

list 自带按值/按条件删除。

bool single_digit (const int& value) { return (value < 10); }

lst.remove(0);                       // 删除所有等于 0 的元素
lst.remove_if(single_digit);         // 删除所有10以下的元素
a.sort(); b.sort();
a.merge(b); // a 变成合并后的有序序列,b 清空

unique:去重相邻元素

仅移除相邻的重复元素

std::list<int> lst{1,1,2,2,2,3,1};
lst.unique();                 // -> {1,2,3,1}(只删除相邻的)

sort / merge / reverse:链表特有整形操作

sort :对 list 原地排序,不需要额外内存

List<int> list = {1, 3, 5, 2, 4};
lst.sort(); // list = {1, 2, 3, 4, 5}

merge:把已排序的链表合并到 *this 并保持有序,节点级拼接。

a.sort(); b.sort();
a.merge(b); // a 变成合并后的有序序列,b 清空

reverse:就地反转链表,改指针即可。

List<int> list = {1, 2, 3, 4, 5};
mylist.reverse(); // list = 5 4 3 2 1

这些操作都不整体失效已有迭代器;被搬迁/被删除节点的迭代器按各自语义处理。

emplace / emplace_back / emplace_front:原地构造

省一次临时对象的拷贝/移动,构造开销大的类型更有价值

struct Person { std::string name; int age; };
std::list<Person> ps;
ps.emplace_back("Alice", 20);        // 直接在尾部构造
ps.emplace(ps.begin(), "Bob", 30);   // 在迭代器前原地构造

assign / swap / clear / resize

  • assign(n, val) / assign(first, last):重置内容。

  • swap:常数时间交换两个 list 的内部指针。

  • clear:删除所有元素(但保留内部结构/哨兵)。

  • resize(n, val)扩张补元素 / 收缩删尾部。

3.5 查找元素:find

list 没有像 vector 那样的 find 成员函数,是因为链表不能随机访问,而且 STL 的设计理念是容器只提供最基本的管理接口,把算法独立出来。查找元素时使用 <algorithm> 里的 std;;find 即可。

标准库里的 find 采用了线性查找:

template<class InputIterator, class T>
find(Iterator first, Iterator last, const T& val)
{
    while(first != last)
    {
        if(*first == val)
            return first; // 找到了直接返回
        ++first;
    }
    return last;          // 找不到返回last
}

从 first 走到 last,遇到匹配值就返回迭代器,否则返回 last。

三. 完整接口一览表

类别函数名 / 操作符说明
构造函数list(), list(size_t n, const T& val), list(It first, It last), list(std::initializer_list<T>)默认、填充、迭代器区间、初始化列表构造
拷贝      移动构造list(const list&), list(list&&)深拷贝构造 / 移动构造(转移节点指针)
赋值操作operator=, assign(n, val), assign(first, last), assign(init_list)重置内容:按个数 / 区间 / 初始化列表
容量管理size(), empty(), max_size()查询当前元素个数、是否为空、理论最大量(与分配器/节点大小相关)
元素访问front(), back()(及 const 版本)访问首/尾元素
迭代器begin(), end(), cbegin(), cend(), rbegin(), rend(), crbegin(), crend()正向/反向迭代器(list 为双向迭代器)
增加元素push_front(const T&), push_back(const T&), emplace_front(args...), emplace_back(args...), insert(pos, const T&), emplace(pos, args...)头/尾插与原地构造;在 pos 前插入/原地构造
删除元素pop_front(), pop_back(), erase(pos), erase(first, last), clear(), resize(n, val=T())头/尾删;删单个或区间;清空;调整大小
列表特有操作splice(pos, list& other), splice(pos, list& other, it), splice(pos, list& other, first, last)常数时间拼接/搬移节点(不拷贝元素)
内容清理remove(const T&), remove_if(UnaryPred), unique(), unique(BinaryPred)按值/按谓词删除;相邻去重(常与 sort 配合)
序列整形sort(), sort(Compare), merge(list& other), merge(list& other, Compare), reverse()原地稳定排序;合并有序链表;反转
比较操作==, !=, <, <=, >, >=词典序比较
交换操作swap(list& other)交换两个链表(指针层面,常数时间)
查找(算法)std::find(first, last, val)(非成员)list 不提供成员 find;使用 <algorithm> 通用算法

四. 总结

本文从“list 是什么”讲起,展示了如何用代码模拟实现 list 的构造、容量管理、增删查改和迭代器封装。相比 vector,list 基于双向链表,插入、删除在常数时间完成,不需要移动元素;但不支持随机访问,迭代器只支持双向遍历。通过这些实现,我们可以理解 std;;list 的常用接口和特点,知道何时选择链表这种容器。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值