C++第十讲:list

C++第十讲:list

list 是 STL 中第二个最常用的序列式容器 ,底层是带头结点的双向循环链表,完美解决了 vector 插入删除效率低的痛点。它和 vector 是互补关系:vector 擅长随机访问,list 擅长任意位置插入删除。


一、list 简介

1. 什么是 list

list 是 C++ 标准库提供的双向循环链表容器,每个元素存储在独立的节点中,节点通过指针连接。

  • 底层结构:带头结点的双向循环链表

  • 特点:

    • ✅ 任意位置插入删除效率 O (1)(不需要搬移元素)

    • ❌ 不支持随机访问(不能用[],访问元素需要遍历,O (N))

    • 没有容量概念,节点动态申请释放

2. 为什么需要 list

vector 的致命缺陷:任意位置插入删除效率极低(需要搬移后面所有元素,O (N)),且扩容会导致大量拷贝。

list 正好弥补了这个缺陷:插入删除只需要修改相邻节点的指针,效率 O (1)。


二、list 常用接口

使用 list 需要包含头文件:#include <list>,所有接口都在std命名空间中。

1. 构造函数

和 vector 完全一致:

构造函数 功能说明 示例
list<T>() 无参构造,空 list list<int> l1;
list<T>(size_t n, const T& val = T()) 构造 n 个 val list<int> l2(5, 10);
list<T>(const list<T>& l) 拷贝构造 list<int> l3(l2);
list<T>(InputIterator first, InputIterator last) 迭代器区间构造 list<int> l4(l2.begin(), l2.end());

2. 迭代器

list 的迭代器不是原生指针,而是对节点指针的封装。

迭代器 功能说明
begin()/end() 正向迭代器:begin 指向第一个数据节点,end 指向头节点
rbegin()/rend() 反向迭代器:rbegin 指向最后一个数据节点,rend 指向头节点

注意

list 的迭代器是双向迭代器 ,只支持++--操作,不支持+n-n(因为内存不连续):

cpp 复制代码
list<int> l = {1,2,3,4,5};
auto it = l.begin();
++it; // 正确,指向2
--it; // 正确,指向1
it += 2; // 错误!双向迭代器不支持+=

3. 容量操作

list 没有容量概念,不需要reservecapacity

函数 功能说明
size_t size() const 返回有效节点个数
bool empty() const 判断是否为空
void clear() 清空所有节点

4. 元素访问

list 不支持随机访问,只能访问头尾元素:

函数 功能说明
T& front() 返回第一个元素的引用
T& back() 返回最后一个元素的引用

5. 增删改查操作(list 的优势)

函数 功能说明 时间复杂度
void push_front(const T& val) 头插 O(1)
void pop_front() 头删 O(1)
void push_back(const T& val) 尾插 O(1)
void pop_back() 尾删 O(1)
iterator insert(iterator pos, const T& val) 在 pos 位置插入 val O(1)
iterator erase(iterator pos) 删除 pos 位置的元素 O(1)
void swap(list<T>& l) 交换两个 list O (1)(只交换头节点指针)

代码示例

cpp 复制代码
#include <iostream>
#include <list>
using namespace std;
​
int main() {
    list<int> l;
​
    // 尾插
    l.push_back(1);
    l.push_back(2);
    l.push_back(3);
​
    // 头插
    l.push_front(0);
​
    // 遍历
    for (auto e : l) {
        cout << e << " "; // 0 1 2 3
    }
    cout << endl;
​
    // 插入:在2前面插入10
    auto it = l.begin();
    while (it != l.end()) {
        if (*it == 2) {
            l.insert(it, 10);
            break;
        }
        ++it;
    }
​
    // 删除:删除1
    it = l.begin();
    while (it != l.end()) {
        if (*it == 1) {
            it = l.erase(it); // erase返回下一个节点的迭代器
        } else {
            ++it;
        }
    }
​
    for (auto e : l) {
        cout << e << " "; // 0 10 2 3
    }
    cout << endl;
​
    return 0;
}

三、核心难点:list 迭代器失效

1. 迭代器失效的原因

list 的迭代器指向某个节点,迭代器失效意味着该节点被删除,变成野指针。

2. 两种操作对迭代器的影响

✅ 插入操作:不会导致任何迭代器失效

插入节点只需要修改相邻节点的指针,原来的节点都还在,所以所有迭代器都有效。

❌ 删除操作:只会导致被删除节点的迭代器失效

删除某个节点后,只有指向该节点的迭代器失效,其他迭代器仍然有效。

3. 正确删除元素的写法

和 vector 类似,erase会返回被删除节点的下一个节点的迭代器

cpp 复制代码
// 正确:删除所有偶数
int main() {
    list<int> l = {1,2,3,4,5,6};
    auto it = l.begin();
    while (it != l.end()) {
        if (*it % 2 == 0) {
            it = l.erase(it); // 用返回值更新it
        } else {
            ++it;
        }
    }
    return 0;
}

错误写法

cpp 复制代码
// 错误:erase后it失效,++it会崩溃
int main() {
    list<int> l = {1,2,3,4};
    auto it = l.begin();
    while (it != l.end()) {
        if (*it % 2 == 0) {
            l.erase(it); // it失效
        }
        ++it; // 访问失效的迭代器
    }
    return 0;
}

四、list 底层原理与模拟实现

1. 底层结构

list 的底层是带头结点的双向循环链表,包含两部分:

  1. 节点结构:每个节点包含数据、前驱指针、后继指针

  2. list 类:只包含一个指向头节点的指针

节点结构

cpp 复制代码
template<class T>
struct ListNode {
    T _data;       // 数据
    ListNode* _prev; // 前驱指针
    ListNode* _next; // 后继指针
​
    ListNode(const T& data = T())
        : _data(data)
        , _prev(nullptr)
        , _next(nullptr)
    {}
};

list 类结构

cpp 复制代码
template<class T>
class list {
public:
    // 迭代器类型(后面实现)
    typedef ListIterator<T> iterator;
    typedef ListIterator<const T> const_iterator;
​
private:
    ListNode<T>* _head; // 头节点指针
};

2. 核心函数模拟实现

2.1 构造与析构

cpp 复制代码
// 创建头节点
void CreateHead() {
    _head = new ListNode<T>();
    _head->_prev = _head;
    _head->_next = _head;
}
​
// 无参构造
list() {
    CreateHead();
}
​
// 构造n个val
list(size_t n, const T& val = T()) {
    CreateHead();
    for (size_t i=0; i<n; ++i) {
        push_back(val);
    }
}
​
// 拷贝构造(深拷贝)
list(const list<T>& l) {
    CreateHead();
    for (const auto& e : l) {
        push_back(e);
    }
}
​
// 赋值运算符重载(现代版)
list<T>& operator=(list<T> l) {
    swap(l);
    return *this;
}
​
// 析构函数
~list() {
    clear();
    delete _head;
    _head = nullptr;
}
​
// 清空所有节点
void clear() {
    iterator it = begin();
    while (it != end()) {
        it = erase(it);
    }
}
​
// 交换两个list
void swap(list<T>& l) {
    std::swap(_head, l._head);
}

2.2 尾插与尾删

cpp 复制代码
// 尾插
void push_back(const T& val) {
    ListNode<T>* newNode = new ListNode<T>(val);
    ListNode<T>* tail = _head->_prev;
​
    // 链接节点
    tail->_next = newNode;
    newNode->_prev = tail;
    newNode->_next = _head;
    _head->_prev = newNode;
}
​
// 尾删
void pop_back() {
    ListNode<T>* tail = _head->_prev;
    ListNode<T>* prevTail = tail->_prev;
​
    // 链接节点
    prevTail->_next = _head;
    _head->_prev = prevTail;
​
    delete tail;
}

2.3 插入与删除

cpp 复制代码
// 在pos位置插入val
iterator insert(iterator pos, const T& val) {
    ListNode<T>* cur = pos._node;
    ListNode<T>* prev = cur->_prev;
    ListNode<T>* newNode = new ListNode<T>(val);
​
    // 链接节点
    prev->_next = newNode;
    newNode->_prev = prev;
    newNode->_next = cur;
    cur->_prev = newNode;
​
    return iterator(newNode); // 返回新节点的迭代器
}
​
// 删除pos位置的节点
iterator erase(iterator pos) {
    ListNode<T>* cur = pos._node;
    ListNode<T>* prev = cur->_prev;
    ListNode<T>* next = cur->_next;
​
    // 链接节点
    prev->_next = next;
    next->_prev = prev;
​
    delete cur;
    return iterator(next); // 返回下一个节点的迭代器
}

3. list 迭代器的实现(面试重点)

list 的迭代器不是原生指针 ,而是一个封装了节点指针的类,需要重载*->++--等运算符。

迭代器类定义

cpp 复制代码
template<class T, class Ref, class Ptr>
struct ListIterator {
    typedef ListNode<T> Node;
    typedef ListIterator<T, Ref, Ptr> Self;
​
    Node* _node; // 指向节点的指针
​
    ListIterator(Node* node)
        : _node(node)
    {}
​
    // 解引用:返回节点数据的引用
    Ref operator*() {
        return _node->_data;
    }
​
    // 箭头:返回节点数据的指针
    Ptr operator->() {
        return &_node->_data;
    }
​
    // 前置++
    Self& operator++() {
        _node = _node->_next;
        return *this;
    }
​
    // 后置++
    Self operator++(int) {
        Self temp(*this);
        _node = _node->_next;
        return temp;
    }
​
    // 前置--
    Self& operator--() {
        _node = _node->_prev;
        return *this;
    }
​
    // 后置--
    Self operator--(int) {
        Self temp(*this);
        _node = _node->_prev;
        return temp;
    }
​
    // 比较
    bool operator!=(const Self& s) const {
        return _node != s._node;
    }
​
    bool operator==(const Self& s) const {
        return _node == s._node;
    }
};

在 list 类中定义迭代器

cpp 复制代码
template<class T>
class list {
public:
    // 普通迭代器和const迭代器
    typedef ListIterator<T, T&, T*> iterator;
    typedef ListIterator<T, const T&, const T*> const_iterator;
​
    // begin和end
    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);
    }
​
    // ... 其他成员函数
};

4. 反向迭代器的实现

反向迭代器的本质是对正向迭代器的封装

  • 反向迭代器的++ = 正向迭代器的--

  • 反向迭代器的-- = 正向迭代器的++

cpp 复制代码
template<class Iterator, class Ref, class Ptr>
struct ReverseIterator {
    Iterator _it; // 正向迭代器
​
    ReverseIterator(Iterator it)
        : _it(it)
    {}
​
    Ref operator*() {
        Iterator temp = _it;
        --temp;
        return *temp;
    }
​
    Ptr operator->() {
        return &(operator*());
    }
​
    // 反向++ = 正向--
    ReverseIterator& operator++() {
        --_it;
        return *this;
    }
​
    // 反向-- = 正向++
    ReverseIterator& operator--() {
        ++_it;
        return *this;
    }
​
    bool operator!=(const ReverseIterator& s) const {
        return _it != s._it;
    }
};

五、list vs vector(面试必背)

对比项 vector list
底层结构 动态顺序表,连续内存 带头结点的双向循环链表,非连续
随机访问 ✅ 支持,O (1) ❌ 不支持,O (N)
插入删除效率 尾插 O (1),其他位置 O (N)(需要搬移元素) 任意位置 O (1)(只修改指针)
空间利用率 高,连续内存,缓存利用率高 低,每个节点需要额外存储两个指针,容易产生内存碎片
迭代器类型 随机访问迭代器(原生指针) 双向迭代器(封装节点指针)
迭代器失效 扩容和 erase 会导致所有迭代器失效 只有 erase 会导致被删除节点的迭代器失效
适用场景 频繁随机访问,少量插入删除 频繁任意位置插入删除,不需要随机访问

关键结论

  • 优先使用 vector,除非有大量任意位置的插入删除操作

  • vector 和 list 是互补关系,不是替代关系


六、经典 OJ 实战

1. 反转链表

cpp 复制代码
// 迭代法
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* prev = nullptr;
        ListNode* cur = head;
        while (cur) {
            ListNode* next = cur->next;
            cur->next = prev;
            prev = cur;
            cur = next;
        }
        return prev;
    }
};

2. 合并两个有序链表

cpp 复制代码
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        ListNode* dummy = new ListNode(0);
        ListNode* cur = dummy;
        while (list1 && list2) {
            if (list1->val < list2->val) {
                cur->next = list1;
                list1 = list1->next;
            } else {
                cur->next = list2;
                list2 = list2->next;
            }
            cur = cur->next;
        }
        cur->next = list1 ? list1 : list2;
        return dummy->next;
    }
};

七、本章核心总结

  1. list 底层是带头结点的双向循环链表,任意位置插入删除 O (1),不支持随机访问

  2. 迭代器是封装节点指针的类,重载了 *、->、++、-- 等运算符

  3. 迭代器失效:只有删除时会导致被删除节点的迭代器失效,插入不会失效

  4. 反向迭代器是对正向迭代器的封装,++ 对应正向 --

  5. 和 vector 对比:vector 擅长随机访问,list 擅长插入删除,根据场景选择

相关推荐
贾伟康1 天前
【天体运行模拟|03】HarmonyOS ArkTS 场景选择实战:组织行星、卫星与天文现象入口
list·harmonyos·arkts·arkui·页面路由
charlie1145141913 天前
deque、list 与 forward_list:vector 之外的三个选择
开发语言·数据结构·c++·list·开源项目
lzx_0023 天前
list(全)
数据结构·list
小手cool5 天前
JSON.parseObject 返回 List 时不能强制转换的解决方案
java·json·list
豆沙沙包?5 天前
C++~~~stack容器、queue容器、list容器(p45-P56)
c++·windows·list
淡海水6 天前
03-05-线性-Array-List-LinkedList与Span-所有权与成本模型选型
c#·list·编译·array·clr·机器码
牛油果子哥q6 天前
C++序列式容器深度精讲:vector/list/deque底层实现、扩容原理、迭代器失效、性能对比、工程选型避坑
开发语言·c++·list
余额瞒着我当琳7 天前
C++ list第二讲数据结构修炼:迭代器源码 + 栈队列适配器 + LeetCode 三道高频题
数据结构·c++·list
淡海水8 天前
03-02-线性-List-T-动态数组布局-扩容与操作成本
数据结构·windows·c#·list·编译·clr·机器码