C++ STL list完全指南:实战派的入门到进阶技巧

一、先想清楚:你真的需要list吗?

在写代码前,先问自己一个问题:我需要的是“频繁插入删除”还是“随机访问”?
list和vector是STL中最常用的两个序列容器,但底层实现天差地别——我们用一张表把核心区别讲清楚:

C++ STL list完全指南:实战派的入门到进阶技巧

特性 list(双向链表) vector(动态数组)
存储方式 非连续内存(节点+指针) 连续内存(数组扩容)
插入/删除效率 任意位置O(1)(只需调整指针) 中间位置O(n)(需移动元素)
随机访问 不支持(不能用[]at() 支持(O(1)时间访问任意位置)
内存开销 额外存储指针(每个节点占更多内存) 内存连续,开销小
迭代器类型 双向迭代器(只能++/–) 随机访问迭代器(支持+=n)

举个例子:如果你要写一个“聊天记录”功能,需要频繁在消息列表头部插入新消息(比如置顶),或者删除中间某条消息——list就是最优解,因为插入删除不会移动其他元素;但如果是“成绩排行榜”,需要频繁按索引取第n名成绩,那vector更合适。

二、list的基础操作:从构造到遍历

1. 怎么构造一个list?

list的构造函数很灵活,直接看常用场景和代码:

构造方式 代码示例 说明
默认构造 list<int> l; 创建空list
n个相同元素 list<int> l(5, 10); 创建5个元素,每个都是10
范围构造(用其他容器) vector<int> v{1,2,3}; list<int> l(v.begin(), v.end()); 用vector的范围构造list
拷贝构造 list<int> l2(l1); 复制l1的所有元素到l2
移动构造(C++11+) list<int> l3(std::move(l2)); 转移l2的资源到l3(l2变为空)

2. 插入与删除:最常用的5个操作

list的插入删除是“拿手好戏”,直接上代码:

#include <list>
#include <iostream>
using namespace std;

int main() {
    list<int> l;
    // 1. 尾部插入
    l.push_back(1);  // l: [1]
    // 2. 头部插入
    l.push_front(0); // l: [0,1]
    // 3. 中间插入(用迭代器定位)
    auto it = l.begin(); ++it; // 指向1的位置
    l.insert(it, 2);           // l: [0,2,1](在it前插入2)
    // 4. 删除尾部
    l.pop_back();  // l: [0,2]
    // 5. 删除指定位置
    it = l.begin();
    l.erase(it);   // l: [2](删除头部元素)
    return 0;
}

注意insert的参数是“插入位置的迭代器”,插入的元素会放在该迭代器前面erase会返回下一个有效的迭代器(这点很重要,后面讲迭代器失效时会用到)。

3. 怎么遍历list?

list不支持[]访问,所以遍历只能用迭代器范围for循环(C++11+)

list<int> l{1,2,3,4};
// 方式1:迭代器遍历(最通用)
for (list<int>::iterator it = l.begin(); it != l.end(); ++it) {
    cout << *it << " "; // 输出:1 2 3 4
}
// 方式2:范围for(更简洁)
for (auto num : l) {
    cout << num << " ";
}
// 方式3:反向迭代器(从后往前)
for (auto rit = l.rbegin(); rit != l.rend(); ++rit) {
    cout << *rit << " "; // 输出:4 3 2 1
}

三、list的高级玩法:迭代器、排序与合并

1. 迭代器的“坑”:失效与正确用法

list的迭代器是双向迭代器,只能向前或向后移动,但不会因为插入操作失效(除非你删除了迭代器指向的节点)。比如:

list<int> l{1,2,3};
auto it = l.begin(); // 指向1
l.insert(it, 0);     // 插入0,it仍然指向1(有效!)
cout << *it << endl; // 输出1(正确)

删除操作会让迭代器失效,所以删除元素时要用erase的返回值更新迭代器

// 错误示例:删除所有偶数(会崩溃!)
for (auto it = l.begin(); it != l.end(); ++it) {
    if (*it % 2 == 0) {
        l.erase(it); // erase后it失效,++it会出错
    }
}
// 正确示例:用erase的返回值
for (auto it = l.begin(); it != l.end(); ) { // 注意:这里没有++it
    if (*it % 2 == 0) {
        it = l.erase(it); // erase返回下一个有效迭代器
    } else {
        ++it; // 只有不删除时才移动迭代器
    }
}

2. 排序:list自带的sort()

list不能用std::sort(因为std::sort需要随机访问迭代器),但list自己有成员函数sort(),用法很简单:

list<int> l{3,1,4,1,5};
// 默认升序排序
l.sort(); // l: [1,1,3,4,5]
// 自定义降序排序(用lambda)
l.sort([](int a, int b) { return a > b; }); // l: [5,4,3,1,1]

小技巧:list的sort是稳定排序(相同元素的相对位置不变),而std::sort是不稳定的——如果你的场景需要保持相同元素的顺序(比如“按时间排序的消息列表,相同时间的消息保持原顺序”),list的sort更合适。

3. 合并:把两个有序list合并成一个

list的merge()函数可以合并两个已排序的list,合并后仍然有序:

list<int> l1{1,3,5};
list<int> l2{2,4,6};
l1.merge(l2); // l1: [1,2,3,4,5,6],l2变为空

注意:merge的前提是两个list都已经按相同规则排序(比如都是升序),否则结果会错乱。

四、list的实战:用list实现LRU缓存

LRU(最近最少使用)缓存是面试高频题,而list的splice()函数(转移元素位置)是实现LRU的关键!

1. LRU的需求

  • 缓存有固定容量,满了就删除“最久未使用”的元素;
  • 访问(get)一个元素时,把它移到“最近使用”的位置(比如头部);
  • 插入(put)一个元素时,如果存在就更新值并移到头部,如果不存在就插入头部(容量满则删尾部)。

2. 用list+哈希表实现

#include <list>
#include <unordered_map>
#include <iostream>
using namespace std;

class LRUCache {
private:
    int capacity; // 缓存容量
    // list存储键值对:front是最近使用,back是最久未使用
    list<pair<int, int>> cache;
    // 哈希表:键 -> list中的迭代器(快速找到元素位置)
    unordered_map<int, list<pair<int, int>>::iterator> map;

public:
    LRUCache(int cap) : capacity(cap) {}

    // 获取缓存值
    int get(int key) {
        auto it = map.find(key);
        if (it == map.end()) {
            return -1; // 不存在该键
        }
        // 把访问的元素移到list头部(splice是list的成员函数,O(1)时间)
        cache.splice(cache.begin(), cache, it->second);
        return it->second->second; // 返回值
    }

    // 插入/更新缓存
    void put(int key, int value) {
        auto it = map.find(key);
        if (it != map.end()) {
            // 存在,更新值并移到头部
            it->second->second = value;
            cache.splice(cache.begin(), cache, it->second);
            return;
        }
        // 不存在,检查容量
        if (cache.size() == capacity) {
            // 删除最久未使用的元素(list尾部)
            auto last = cache.back();
            map.erase(last.first); // 从哈希表删除键
            cache.pop_back();      // 从list删除尾部
        }
        // 插入新元素到头部
        cache.push_front({key, value});
        map[key] = cache.begin(); // 哈希表记录迭代器
    }
};

// 测试代码
int main() {
    LRUCache cache(2);
    cache.put(1, 1); // 缓存:[(1,1)]
    cache.put(2, 2); // 缓存:[(2,2), (1,1)]
    cout << cache.get(1) << endl; // 1,缓存变为[(1,1), (2,2)]
    cache.put(3, 3); // 容量满,删除(2,2),缓存变为[(3,3), (1,1)]
    cout << cache.get(2) << endl; // -1(已删除)
    return 0;
}

关键亮点cache.splice(cache.begin(), cache, it->second)——这个操作把it->second指向的元素移动到list的头部,没有拷贝或移动元素本身,只是调整了指针,所以是O(1)时间复杂度,完美符合LRU的性能要求!

五、常见陷阱:避开这些坑

  1. 不要用[]访问list:list没有重载operator[],如果你写l[0]会直接编译错误,要用l.front()(头部)或l.back()(尾部)。
  2. 迭代器失效要处理:删除元素时一定要用it = l.erase(it)更新迭代器,否则会访问失效内存。
  3. 不要用std::sort排序list:会编译错误!必须用list自带的l.sort()
  4. splice的参数顺序splice(目标位置, 源list, 源迭代器),不要搞反顺序。
  5. list的size()是O(n)吗?:C++11之前,list的size()是O(n)(需要遍历计数),C++11之后是O(1)(存储了size变量)——如果你的编译器支持C++11+,可以放心用size()

最后:总结一下list的核心场景

  • 需要频繁插入删除中间或头部元素时;
  • 不需要随机访问元素时;
  • 需要稳定排序时;
  • 实现LRU、FIFO队列等数据结构时。

如果你能记住这些场景,下次写代码时就能快速判断:“哦,这个需求该用list!”

原创文章,作者:,如若转载,请注明出处:https://zube.cn/archives/164

(0)