C++入门自学Day11-- List类型(初识)

  往期内容回顾

          Vector类的自实现

         Vector类(注意事项)

          初识Vector

          String类的自实现

         String类的使用(续)  

         String类(续)

         String类(初识)


C++ std::list全面指南:从连续容器到节点容器

前言

在前面的学习中,我们已经熟悉了:

  • std::string:本质是“动态字符数组”,连续存储。

  • std::vector:通用的“连续动态数组”,随机访问 O(1)、扩容几何增长。 

但在需要频繁在中间插入/删除、或保持指针/引用稳定时,连续存储并不友好。

这就是 std::list(双向链表) 的用武之地:它用节点(node)存放元素,每个节点维护前驱/后继指针,通过指针重连完成 O(1) 的插入/删除,并且在大多数修改下迭代器、指针、引用保持有效

一句话对比:vector 追求 局部性与随机访问;list 追求 插删稳定性与指针/迭代器稳定。


主要内容预览

本文将分节介绍:

  1. 1、std::list 的数据结构与特性

  2. 2、与 vector/string 的核心对比

  3. 3、常用接口与示例(构造、遍历、插删、特殊算法)

  4. 4、迭代器与失效规则

  5. 5、复杂度与性能、何时使用/避免

  6. 6、进阶:splice/merge/sort/unique/remove 的“节点级”威力

  7. 7、常见坑与最佳实践

  8. 8、小结与选型建议


一、数据结构与核心特性

1.1 节点式存储

std::list<T> 是双向链表。每个节点包含:

  • 一个 T 对象(元素本体)

  • 两个指针:prev / next(前驱/后继)

后果:

  • 内存不连续,节点分散在堆上。

  • 没有随机访问(不支持 operator[]、at),只能顺着迭代器走。

  • 在任意位置 插入/删除 元素都是 O(1)(已知位置)。

1.2 迭代器类别

  • std::list 的迭代器是 双向迭代器(BidirectionalIterator)

  • 只能 ++it / --it,不能 it + n。

  • 与 vector 的 随机访问迭代器 不同,所以很多需要随机访问的算法(如 std::sort)不能直接用于 list。

1.3 size() 复杂度

  • 现代实现通常是 O(1)

  • 历史上(老旧实现)可能是 O(n)。

  • std::forward_list 的 size() 仍可能是 O(n)。


二、与 vector / string的核心对比

维度

string / vector(连续)

list(节点)

内存布局

连续

分散(每元素一个节点)

随机访问

O(1)

不支持

中间插入/删除

O(n)(需要挪动大段元素)

O(1)(指针重连)

push_back

均摊 O(1),扩容时重分配

O(1)

迭代器稳定性

扩容或中间插删会大量失效

除被删元素外通常都有效

缓存友好

非常好(局部性强)

较差(指针追踪)

内存开销

小(头部管理 + 连续块)

大(每节点两指针 + 分配器开销)

结论:

  • 大量随机访问遍历占主:优先 vector。

  • 需要频繁在中间插删、需要稳定迭代器/引用:考虑 list。

  • 如果只是首尾插删且仍希望较好的随机访问:考虑 deque。


三、快速上手:常用接口与示例

1、list的构造

int main() {
    list<int> L1;                // 空
    list<int> L2(5, 42);         // 5 个 42
    list<int> L3 = {1, 2, 3};    // 列表初始化
    list<int> L4(L3);            // 拷贝构造
    list<int> L5(::move(L3));      // 移动构造(L3 被掏空)

    cout << L2.size() << endl;   // 通常 O(1)
    cout << boolalpha << L1.empty() << endl;
    for(auto e:L3){
        cout << e <<" " ;
    }
    cout << endl;
    for(auto e:L4){
        cout << e <<" " ;
    }
    cout << endl;
    for(auto e:L5){
        cout << e <<" " ;
    }
    cout << endl;
    return 0;
}

输出描述:

5
true

1 2 3 
1 2 3 


2 遍历(不支持随机访问)

list不支持运算符[ ];

void Print_list(const list<int>& l1){
    list<int>::const_iterator it = l1.begin();
    while(it != l1.end()){
        cout<< *it <<" ";
        ++it;
    }
    cout<<endl;
}
int main(){
    list<int> l1;
    l1.push_back(1);
    l1.push_back(2);
    l1.push_back(3);
    l1.push_back(4);

    for(auto e: l1){
         cout << e <<" " ;
    }
    for (auto rit = L5.rbegin(); rit != L5.rend(); ++rit) cout << *rit << ' ';
    cout<<endl;
    Print_list(l1);

输出描述:

1 2 3 4

4 3 2 1

1 2 3 4


3、 首尾与任意位置插删

void test2(){
    list<int> L = {1, 3, 5};
    L.push_front(0);   // 0 1 3 5
    L.push_back(7);    // 0 1 3 5 7
    L.pop_front();     // 1 3 5 7
    L.pop_back();      // 1 3 5
    Print_list(L);
}

int main(){
    test2();
}
与 vector 的重要差异: 这些插删操作不会移动其他元素对象本体,因此 指向其他元素的指针/引用/迭代器仍然有效(被删元素除外)。
输出描述:1 3 5 

insert() —— 在任意位置插入

        作用:在迭代器 pos 指向的位置前面插入元素。

        时间复杂度:O(1)(已知迭代器位置的情况下);注意链表的空间不连续,迭代器不支持加法。

常用重载:

iterator insert(iterator pos, const T& value);               // 插入一个值
iterator insert(iterator pos, size_type count, const T& val);// 插入 count 个相同值
template<class InputIt>
iterator insert(iterator pos, InputIt first, InputIt last);  // 插入范围 [first, last)
iterator insert(iterator pos, T&& value);                    // 插入右值

 代码实现:

void test3(){
    list<int> L = {1, 3, 5};
    list<int>::iterator it = L.begin();
    it++;
    L.insert(it,2);   // 0 1 3 5
    Print_list(L);
    L.insert(L.end(),3,6);
    Print_list(L);
}


int main(){
    test3();
}

输出描述:

1 2 3 5 
1 2 3 5 6 6 6 

template<class Inputiterator, class T>
Inputiterator find(Inputiterator first, Inputiterator last,const T& val){
    while(first != last){
        if(*first == val){
            return first;
        }
        ++first;
    }
    return last;
}


void test4(){
    list<int> L = {1,2,3,4,5,6};
    list<int>::iterator it = find(L.begin(),L.end(),3);
    L.insert(it,8);   // 0 1 3 5
    if(it!=L.end()){
        Print_list(L);
    }
    // L.insert(L.end(),3,6);
}

输出描述:

1 2 8 3 4 5 6 


注意:迭代器与失效规则(与 vector 强烈对比)

  • 插入/删除:

    • 删除:仅被删元素的迭代器/引用失效;其他全部保持有效。

    • 插入:不使现有元素的迭代器/引用失效

  • 对比 vector: 扩容或中间插删会导致大批迭代器失效,指针/引用也可能失效。

测试:

void test6(){
    list<int> L = {1,2,3,4,5,6};
    list<int>::iterator pos = find(L.begin(),L.end(),3);
    pos = L.erase(pos);
    L.insert(pos,3);
    Print_list(L);
}

输出描述:

zsh: segmentation fault  "/Users/junye/Desktop/cplusplus/"List

正确做法:用 erase 返回值

void test6(){
    list<int> L = {1,2,3,4,5,6};
    list<int>::iterator pos = find(L.begin(),L.end(),3);
    pos = L.erase(pos);
    L.insert(pos,3);
    Print_list(L);
}

输出结果:

1 2 3 4 5 6 


4. emplace() / emplace_front() / emplace_back() —— 原地构造

作用:在指定位置直接构造对象,避免不必要的复制或移动。

时间复杂度:O(1)

示例

struct Person {
    string name;
    int age;
    Person(string n, int a) : name(n), age(a) {}
};

list<Person> people;
people.emplace_back("Alice", 25); // 尾部直接构造
people.emplace_front("Bob", 30);  // 头部直接构造
auto itp = people.begin();
++itp;
people.emplace(itp, "Charlie", 28); // 任意位置构造

特点

  • 和 insert() 相比,emplace() 省略了临时对象的构造+拷贝/移动过程。

  • 推荐在构造复杂对象时使用。


5. splice() —— 从另一个 list移动节点

作用:将另一个 list 的节点直接接入当前 list,不复制数据,而是直接修改指针。

时间复杂度:O(1)(节点链接修改)

示例

list<int> l1 = {1, 2, 3};
list<int> l2 = {10, 20, 30};

auto it = l1.begin();
++it; // 指向 2
l1.splice(it, l2); // 将 l2 所有元素插入到 l1 的 2 前面

特点

  • 节省内存拷贝,适合大数据结构的快速转移。

  • 目标和源必须是同类型 list。

举个例子:

void test7(){
    list<int> L1 = {1,2,3,4,5,6};
    list<int> L2 = {1,2,3,4,5,6};
    L1.splice(L1.end(),L2);
    Print_list(L1);
    Print_list(L2);
}
void test8(){
    list<int> L1 = {1,2,3,4,5,6};
    list<int> L2 = {1,2,3,4,5,6};
    L1.splice(L1.end(),L2,++L2.begin(),--L2.end());
    Print_list(L1);
    Print_list(L2);
}


int main(){
    test7();
    test8();
}

输出描述:

1 2 3 4 5 6 1 2 3 4 5 6 

1 2 3 4 5 6 2 3 4 5 
1 6
 

6、其他常用的一些操作

merge:线性合并两个有序 list(稳定)

list<int> A = {1,3,5};
list<int> B = {2,4,6};
A.merge(B);                      // A: 1 2 3 4 5 6, B 空

sort:链表专用排序(稳定排序)

list<string> L = {"bbb","a","cc"};
L.sort();                        // a, bbb, cc
L.sort([](auto& x, auto& y){ return x.size() < y.size(); });

unique:移除相邻重复元素

list<int> L = {1,1,2,3,3,3,4};
L.unique();                      // 1 2 3 4(只去相邻重复)
L.unique([](int a, int b){ return abs(a-b) <= 1; });

remove / remove_if:按值(或谓词)删除元素

list<int> L = {1,2,3,2,4};
L.remove(2);                     // 删除所有等于 2 的元素
L.remove_if([](int x){ return x%2==0; });  // 删除所有偶数

splice/merge/sort/unique:

  • 节点只是“换链接”或“重排顺序”,元素本体地址不变,因此迭代器、引用仍有效(但可能属于另一个容器,或顺序发生变化)


总结

  • std::list 是典型的节点式容器

    • 优势:已知位置插删 O(1)、迭代器/引用稳定、强大的节点级算法(splice/merge/sort/unique/remove)。

    • 劣势不支持随机访问缓存局部性差内存开销大,纯遍历吞吐通常不及 vector。

  • 选型心法

    • 读多写少、随机访问 → vector;

    • 频繁在中间插删 & 需要稳定迭代器 → list;

    • 双端频繁操作且需随机访问 → deque。

  • 真正发挥 list 价值的,是节点级别的 O(1) 操作(特别是 splice/merge),这在 vector/string 的连续模型里是做不到的。

学会在“连续容器思维”(vector/string)与“节点容器思维”(list)之间切换,
你就能根据需求做出更贴近底层的、性能与可维护性兼顾的容器选型

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值