(超详细)C++ STL list容器详解:从使用方法到双向链表底层原理全面解析

一、什么是list容器?

1.1 list是什么?

在C++ STL(Standard Template Library,标准模板库)中,list 是一种非常重要的序列式容器(Sequence Container)

它表示的是一个双向链表(Doubly Linked List)

简单来说:

list就是由多个节点连接起来的数据结构,每个节点保存数据,同时保存指向前一个节点和后一个节点的指针。

例如:

复制代码
#include <iostream>
#include <list>

using namespace std;

int main()
{
    list<int> mylist;

    mylist.push_back(10);
    mylist.push_back(20);
    mylist.push_back(30);

    for(auto e : mylist)
    {
        cout << e << " ";
    }

    return 0;
}

运行结果:

复制代码
10 20 30

表面上看:

复制代码
list<int>

和:

复制代码
vector<int>

使用起来非常类似。

但是它们内部的数据结构完全不同。


1.2 list在STL中的位置

STL中的容器大致可以分为三类:

① 序列式容器

特点:

数据按照插入顺序排列。

例如:

容器 底层结构
vector 动态数组
list 双向链表
deque 双端数组
forward_list 单链表

② 关联式容器

特点:

根据key自动组织数据。

例如:

容器 底层结构
set 红黑树
map 红黑树
unordered_map 哈希表

③ 容器适配器

例如:

容器 底层
stack deque
queue deque
priority_queue vector

所以:

复制代码
STL
 |
 +---序列式容器
       |
       +---vector
       |
       +---list
       |
       +---deque

list属于STL最基础、最经典的容器之一。


1.3 为什么需要list?

很多初学者会产生一个疑问:

已经有vector了,为什么还需要list?

因为vector虽然强大,但是存在一个明显的问题:

vector插入删除效率低

例如:

复制代码
vector<int> v;

v.push_back(1);
v.push_back(2);
v.push_back(3);

内存:

复制代码
[1][2][3]

如果现在我要:

复制代码
v.insert(v.begin(),0);

需要:

复制代码
原来:

[1][2][3]


插入0:

[0][1][2][3]

那么后面的元素必须整体移动:

复制代码
1移动
2移动
3移动

时间复杂度:

复制代码
O(N)

如果数据量巨大:

例如:

复制代码
100万个元素

移动成本非常高。


1.4 list解决vector什么问题?

list的设计目标:

解决频繁插入和删除的问题。

例如:

list:

复制代码
10 20 30

内部:

复制代码
      prev       prev
       ↓          ↓

NULL ←10 ⇄ 20 ⇄ 30→ NULL

       ↑          ↑
      next       next

如果在20前面插入15:

只需要修改几个指针:

原来:

复制代码
10 ⇄ 20

变成:

复制代码
10 ⇄ 15 ⇄ 20

不需要移动数据。

因此:

插入删除:

复制代码
O(1)

二、list底层原理

理解list,最重要的是理解:

list不是数组,而是一条链。


2.1 双向链表结构

list底层采用:

双向循环链表

(不同实现可能略有区别)

一个节点大概如下:

复制代码
template<class T>
struct Node
{
    Node<T>* prev; // 指向前一个节点

    T data;       // 保存数据

    Node<T>* next; // 指向后一个节点
};

例如:

保存:

复制代码
10 20 30

实际结构:

复制代码
        next             next

+------+-----+     +------+-----+     +------+-----+
|prev  | 10  | --> |prev  | 20  | --> |prev  | 30  |
|next  |     | <-- |next  |     | <-- |next  |     |
+------+-----+     +------+-----+     +------+-----+

每个节点包含:

① prev指针

保存:

前一个节点地址

例如:

复制代码
20节点.prev

指向

10节点

② data数据

保存:

用户存储的数据。

例如:

复制代码
list<int>

节点:

复制代码
data=int

如果:

复制代码
list<string>

节点:

复制代码
data=string

③ next指针

保存:

下一个节点地址

例如:

复制代码
20节点.next

指向

30节点

2.2 list内存布局

vector:

连续空间:

复制代码
地址:

1000 1004 1008

[10][20][30]

list:

不连续:

复制代码
地址:

2000          8000          5000

[10]  -----> [20] -----> [30]

节点在哪里并不重要。

只需要:

复制代码
next
prev

找到它。


2.3 为什么list插入效率高?

假设:

现在有:

复制代码
A ⇄ B

想插入:

X

变成:

复制代码
A ⇄ X ⇄ B

只需要:

修改四个指针:

复制代码
A.next=X;

X.prev=A;

X.next=B;

B.prev=X;

没有:

  • 元素移动

  • 内存搬迁

  • 大量复制

所以:

时间复杂度:

复制代码
O(1)

2.4 为什么list删除效率高?

删除B:

原来:

复制代码
A ⇄ B ⇄ C

删除后:

复制代码
A ⇄ C

只需要:

复制代码
A.next=C;

C.prev=A;

B节点释放:

复制代码
delete B;

所以:

删除:

复制代码
O(1)

三、list的特点


3.1 双向链表

这是list最核心特点。

每个节点:

保存两个方向:

复制代码
前驱

↓

节点

↓

后继

因此:

可以:

向前:

复制代码
--it

也可以:

向后:

复制代码
++it

3.2 非连续内存空间

vector:

复制代码
连续:

1000
1004
1008
1012

list:

复制代码
1000

↓

5000

↓

8000

每个节点独立申请空间。

优点:

插入删除方便。

缺点:

空间利用率低。

因为:

每个节点需要额外保存:

复制代码
prev指针
next指针

3.3 支持任意位置插入删除

例如:

复制代码
list<int> l={1,2,3,4};


auto it=l.begin();

++it;

l.insert(it,100);

结果:

复制代码
1 100 2 3 4

插入位置:

第二个元素前。


3.4 不支持随机访问

vector:

复制代码
v[5];

直接:

复制代码
O(1)

因为:

地址计算:

复制代码
首地址+5*sizeof(int)

但是list:

复制代码
节点1

↓

节点2

↓

节点3

↓

节点4

↓

节点5

必须一个一个找。

所以:

不存在:

复制代码
list[5]

也不存在:

复制代码
list.at(5)

访问:

只能:

复制代码
iterator

3.5 list迭代器特点

vector:

迭代器:

复制代码
随机迭代器

支持:

复制代码
it+5

例如:

复制代码
v.begin()+5

但是list:

只能:

复制代码
双向迭代器

支持:

复制代码
++

--

不支持:

复制代码
it+5

原因:

list节点不是连续内存。

四、list的基本使用

前面我们学习了list的底层原理。

但是作为C++程序员,我们不仅需要知道它是什么,还需要知道:

如何正确使用list完成实际开发任务。

list的头文件:

复制代码
#include <list>

命名空间:

复制代码
using namespace std;

4.1 创建list

4.1.1 默认构造

最简单的方式:

复制代码
list<int> l;

创建一个空list。

完整代码:

复制代码
#include <iostream>
#include <list>

using namespace std;

int main()
{
    // 创建一个空的list
    list<int> l;

    cout << "list大小:" << l.size() << endl;

    return 0;
}

运行结果:

复制代码
list大小:0

说明:

此时list中没有任何元素。


4.1.2 初始化列表构造

C++11之后支持:

复制代码
list<int> l={1,2,3,4,5};

完整代码:

复制代码
#include <iostream>
#include <list>

using namespace std;

int main()
{
    // 使用初始化列表创建list
    list<int> l={10,20,30,40};


    for(auto e:l)
    {
        cout<<e<<" ";
    }

    return 0;
}

运行结果:

复制代码
10 20 30 40

内部:

复制代码
head
 |
 v

10 <=> 20 <=> 30 <=> 40

4.1.3 拷贝构造

一个list初始化另一个list:

复制代码
list<int> l2(l1);

例如:

复制代码
#include <iostream>
#include <list>

using namespace std;


int main()
{
    list<int> l1={1,2,3};


    // 拷贝构造
    list<int> l2(l1);


    for(auto e:l2)
    {
        cout<<e<<" ";
    }


    return 0;
}

输出:

复制代码
1 2 3

注意:

这里是深拷贝

两个list:

复制代码
l1:

1 <=> 2 <=> 3


l2:

1 <=> 2 <=> 3

它们的数据节点完全独立。

修改:

复制代码
l2.push_back(4);

不会影响:

复制代码
l1

4.2 list容量相关接口


4.2.1 size()

作用:

获取list中元素数量。

示例:

复制代码
#include <iostream>
#include <list>

using namespace std;


int main()
{
    list<int> l={10,20,30};


    cout<<l.size()<<endl;


    return 0;
}

结果:

复制代码
3

4.2.2 empty()

作用:

判断list是否为空。

返回:

  • true:为空

  • false:非空

代码:

复制代码
#include<iostream>
#include<list>

using namespace std;


int main()
{

    list<int> l;


    if(l.empty())
    {
        cout<<"list为空"<<endl;
    }
    else
    {
        cout<<"list不为空"<<endl;
    }


    return 0;
}

输出:

复制代码
list为空

实际开发中:

经常这样写:

复制代码
while(!l.empty())
{
    l.pop_front();
}

4.3 元素访问

list不像vector:

没有:

复制代码
operator[]

例如:

错误:

复制代码
l[3];

因为链表无法直接定位。

但是list提供:

  • front()

  • back()


4.3.1 front()

作用:

获取第一个元素。

代码:

复制代码
#include<iostream>
#include<list>

using namespace std;


int main()
{

    list<int> l={10,20,30};


    cout<<l.front()<<endl;


    return 0;
}

输出:

复制代码
10

4.3.2 back()

作用:

获取最后一个元素。

代码:

复制代码
#include<iostream>
#include<list>


using namespace std;


int main()
{

    list<int> l={10,20,30};


    cout<<l.back()<<endl;


    return 0;
}

输出:

复制代码
30

4.4 list修改操作


4.4.1 push_back()

作用:

尾插。

代码:

复制代码
#include<iostream>
#include<list>

using namespace std;


int main()
{

    list<int> l;


    l.push_back(10);
    l.push_back(20);
    l.push_back(30);


    for(auto e:l)
    {
        cout<<e<<" ";
    }


    return 0;
}

输出:

复制代码
10 20 30

底层:

第一次:

复制代码
NULL <-10-> NULL

第二次:

复制代码
NULL <-10<=>20-> NULL

第三次:

复制代码
NULL <-10<=>20<=>30-> NULL

复杂度:

复制代码
O(1)

4.4.2 push_front()

作用:

头插。

代码:

复制代码
#include<iostream>
#include<list>

using namespace std;


int main()
{

    list<int> l;


    l.push_front(10);
    l.push_front(20);
    l.push_front(30);


    for(auto e:l)
    {
        cout<<e<<" ";
    }


    return 0;
}

运行:

复制代码
30 20 10

因为每次插入:

都放到头部。


4.4.3 pop_back()

删除尾部元素。

代码:

复制代码
list<int> l={1,2,3};


l.pop_back();

结果:

复制代码
1 2

4.4.4 pop_front()

删除头部。

代码:

复制代码
list<int> l={1,2,3};


l.pop_front();

结果:

复制代码
2 3

4.4.5 insert()

作用:

在指定位置插入元素。

格式:

复制代码
iterator insert(iterator pos,const T& val);

示例:

复制代码
#include<iostream>
#include<list>

using namespace std;


int main()
{

    list<int> l={10,20,30};


    // 找到第二个元素位置
    auto it=l.begin();

    ++it;


    // 在20前插入100
    l.insert(it,100);



    for(auto e:l)
    {
        cout<<e<<" ";
    }


    return 0;
}

运行:

复制代码
10 100 20 30

过程:

插入前:

复制代码
10 <=> 20 <=>30

插入后:

复制代码
10 <=>100<=>20<=>30

4.4.6 erase()

作用:

删除指定位置元素。

代码:

复制代码
#include<iostream>
#include<list>


using namespace std;


int main()
{

    list<int> l={10,20,30};


    auto it=l.begin();


    ++it;


    // 删除20
    l.erase(it);



    for(auto e:l)
    {
        cout<<e<<" ";
    }


    return 0;
}

结果:

复制代码
10 30

erase返回值

这是非常重要的知识点。

erase之后:

原来的迭代器失效。

所以:

错误:

复制代码
l.erase(it);

++it;

正确:

复制代码
it=l.erase(it);

因为:

erase返回删除位置之后的迭代器。

例如:

复制代码
list<int> l={1,2,3,4};


auto it=l.begin();


while(it!=l.end())
{

    if(*it==3)
    {
        it=l.erase(it);
    }

    else
    {
        ++it;
    }

}

删除3:

结果:

复制代码
1 2 4

4.4.7 clear()

作用:

清空list。

代码:

复制代码
list<int> l={1,2,3};


l.clear();


cout<<l.size();

输出:

复制代码
0

注意:

clear会释放所有节点。


4.4.8 resize()

作用:

改变list大小。

例如:

复制代码
list<int> l={1,2,3};


l.resize(5);

结果:

复制代码
1 2 3 0 0

为什么?

因为新增元素默认初始化。


指定值:

复制代码
l.resize(6,100);

结果:

复制代码
1 2 3 100 100 100

缩小:

复制代码
list<int> l={1,2,3,4,5};


l.resize(3);

结果:

复制代码
1 2 3

后面的元素被删除。


4.5 list遍历方式


4.5.1 普通迭代器遍历

代码:

复制代码
#include<iostream>
#include<list>


using namespace std;


int main()
{

    list<int> l={10,20,30};


    list<int>::iterator it=l.begin();


    while(it!=l.end())
    {
        cout<<*it<<" ";

        ++it;
    }


    return 0;
}

输出:

复制代码
10 20 30

4.5.2 const_iterator遍历

作用:

只能读取,不能修改。

代码:

复制代码
#include<iostream>
#include<list>


using namespace std;


int main()
{

    list<int> l={1,2,3};


    list<int>::const_iterator it=l.begin();


    while(it!=l.end())
    {

        cout<<*it<<" ";

        ++it;
    }


    return 0;
}

如果:

复制代码
*it=100;

错误。

因为:

const_iterator保护数据。


4.5.3 范围for遍历

C++11推荐方式。

代码:

复制代码
#include<iostream>
#include<list>


using namespace std;


int main()
{

    list<int> l={10,20,30};


    for(auto e:l)
    {
        cout<<e<<" ";
    }


    return 0;
}

输出:

复制代码
10 20 30

底层:

实际上编译器帮你转换:

复制代码
for(auto it=l.begin();it!=l.end();++it)
{
    auto e=*it;
}

五、list迭代器详细讲解

list的核心难点:

为什么list没有下标访问,却可以使用迭代器?


5.1 什么是list迭代器?

迭代器本质:

对指针的一种封装。

普通数组:

复制代码
int* p;

可以:

复制代码
p++;

但是链表:

节点:

复制代码
1000地址

5000地址

8000地址

不能:

复制代码
1000+1

所以list设计了自己的迭代器。


5.2 list为什么不能随机访问?

因为:

list:

复制代码
head

 |
10

 |
20

 |
30

 |
40

访问第四个:

必须:

复制代码
10
 ↓
20
 ↓
30
 ↓
40

时间:

复制代码
O(N)

所以:

没有:

复制代码
operator[]

5.3 list迭代器如何移动?

支持:

复制代码
++
--

例如:

复制代码
list<int> l={1,2,3};


auto it=l.begin();


++it;

变化:

复制代码
1

↓

2

++it和it++区别

++it

前置++:

先移动:

后使用。

例如:

复制代码
++it;
cout<<*it;

it++

后置++:

先使用:

再移动。

例如:

复制代码
cout<<*it;

it++;

性能区别:

对于list:

二者区别很小。

但是:

推荐:

复制代码
++it;

因为:

后置++需要生成临时对象。

六、list中的排序和操作函数

前面我们学习了list的基本增删改查。

但是STL中的list并不仅仅是一个简单的链表。

它还提供了一些非常强大的成员函数:

  • sort()

  • reverse()

  • unique()

  • merge()

  • remove()

  • remove_if()

这些函数充分体现了:

STL不仅提供数据结构,还提供针对数据结构优化的算法。


6.1 sort():排序

6.1.1 sort作用

sort()用于对list中的元素进行排序。

例如:

复制代码
list<int> l={5,2,8,1,3};

调用:

复制代码
l.sort();

结果:

复制代码
1 2 3 5 8

6.1.2 基本使用

代码:

复制代码
#include<iostream>
#include<list>

using namespace std;


int main()
{

    list<int> l={5,3,8,1,2};


    // 对list排序
    l.sort();


    for(auto e:l)
    {
        cout<<e<<" ";
    }


    return 0;
}

运行:

复制代码
1 2 3 5 8

6.1.3 为什么list不能使用algorithm中的sort?

很多初学者会这样写:

复制代码
sort(l.begin(),l.end());

结果:

编译失败。

为什么?

因为:

STL中的:

复制代码
std::sort

要求:

必须支持随机访问迭代器。

例如:

vector:

复制代码
v.begin()+5

可以。

但是list:

复制代码
it+5

不存在。


所以:

list提供自己的:

复制代码
list::sort()

因为它知道自己的底层结构。


6.1.4 降序排序

默认:

升序。

如果想:

降序:

需要传入比较函数。

代码:

复制代码
#include<iostream>
#include<list>


using namespace std;


bool cmp(int a,int b)
{
    return a>b;
}


int main()
{

    list<int> l={5,2,9,1};


    l.sort(cmp);


    for(auto e:l)
    {
        cout<<e<<" ";
    }


    return 0;
}

结果:

复制代码
9 5 2 1

6.2 reverse():反转链表

作用

将list中的元素顺序反转。

例如:

原:

复制代码
1 2 3 4

调用:

复制代码
l.reverse();

结果:

复制代码
4 3 2 1

代码:

复制代码
#include<iostream>
#include<list>

using namespace std;


int main()
{

    list<int> l={1,2,3,4};


    l.reverse();


    for(auto e:l)
    {
        cout<<e<<" ";
    }


    return 0;
}

输出:

复制代码
4 3 2 1

底层原理:

链表:

复制代码
NULL
 |
1 <=> 2 <=> 3 <=> 4
 |
NULL

reverse实际上:

交换每个节点:

复制代码
next
prev

方向。


6.3 unique():删除连续重复元素

作用

删除连续重复的数据。

注意:

它只能删除:

连续重复

不是所有重复。


例如:

复制代码
list<int> l={1,1,2,3,3,3,4};

调用:

复制代码
l.unique();

结果:

复制代码
1 2 3 4

代码:

复制代码
#include<iostream>
#include<list>


using namespace std;


int main()
{

    list<int> l={1,1,2,3,3,3,4};


    l.unique();


    for(auto e:l)
    {
        cout<<e<<" ";
    }


    return 0;
}

结果:

复制代码
1 2 3 4

但是:

如果:

复制代码
list<int> l={1,2,1,3};

调用:

复制代码
unique();

结果:

复制代码
1 2 1 3

因为:

没有连续重复。


实际使用:

通常:

先排序:

复制代码
l.sort();

l.unique();

例如:

原:

复制代码
3 1 2 3 1

排序:

复制代码
1 1 2 3 3

去重:

复制代码
1 2 3

6.4 merge():合并两个有序list

作用

将两个:

已经排序的list

合并。

例如:

list1:

复制代码
1 3 5

list2:

复制代码
2 4 6

merge之后:

复制代码
1 2 3 4 5 6

代码:

复制代码
#include<iostream>
#include<list>


using namespace std;


int main()
{

    list<int> l1={1,3,5};

    list<int> l2={2,4,6};



    l1.merge(l2);



    for(auto e:l1)
    {
        cout<<e<<" ";
    }



    return 0;
}

结果:

复制代码
1 2 3 4 5 6

注意:

merge之后:

复制代码
l2

为空。

因为:

节点直接转移。


6.5 remove():删除指定元素

作用:

删除所有等于某个值的元素。

例如:

复制代码
list<int> l={1,2,3,2,4};

执行:

复制代码
l.remove(2);

结果:

复制代码
1 3 4

代码:

复制代码
#include<iostream>
#include<list>


using namespace std;


int main()
{

    list<int> l={1,2,3,2,4};


    l.remove(2);


    for(auto e:l)
    {
        cout<<e<<" ";
    }


    return 0;
}

输出:

复制代码
1 3 4

6.6 remove_if():按照条件删除

作用:

删除满足条件的数据。

例如:

删除所有偶数。

代码:

复制代码
#include<iostream>
#include<list>


using namespace std;


bool isEven(int x)
{
    return x%2==0;
}


int main()
{

    list<int> l={1,2,3,4,5,6};


    l.remove_if(isEven);



    for(auto e:l)
    {
        cout<<e<<" ";
    }


    return 0;
}

输出:

复制代码
1 3 5

七、list与vector区别

list和vector都是STL中最常用的序列容器。

但是二者设计思想完全不同。

比较 vector list
底层结构 动态数组 双向链表
内存布局 连续 不连续
随机访问 支持 不支持
\[\]访问 支持 不支持
头部插入 效率低 O(1)
尾部插入 O(1) O(1)
中间插入 O(N) O(1)
删除效率 O(N) O(1)
空间利用率
缓存友好性

7.1 底层结构区别

vector:

复制代码
连续空间


1000
1004
1008


[1][2][3]

list:

复制代码
1000       8000      5000


[1] ---> [2] ---> [3]

7.2 随机访问区别

vector:

复制代码
v[100];

直接定位。

原因:

地址:

复制代码
首地址+100*sizeof(T)

list:

访问第100个:

需要:

复制代码
第1个
 ↓
第2个
 ↓
...
 ↓
第100个

所以:

效率:

复制代码
O(N)

7.3 插入删除效率区别

假设:

在中间插入。

vector:

复制代码
1 2 3 4


插入100


1 2 100 3 4

需要移动:

3、4。


list:

复制代码
1 <=>2<=>3


插入100


1<=>2<=>100<=>3

只修改指针。


7.4 空间利用率区别

vector:

只保存:

复制代码
数据

list:

保存:

复制代码
数据

prev指针

next指针

例如:

int:

4字节。

list节点:

可能:

复制代码
4 + 8 +8
=
20字节以上

空间浪费明显。


7.5 使用场景区别

使用vector:

适合:

  • 查询多

  • 遍历多

  • 插入删除少

例如:

学生成绩:

复制代码
10000个成绩

经常查询

使用list:

适合:

  • 插入删除频繁

  • 不需要随机访问

例如:

聊天消息列表:

不断删除旧消息。


八、list模拟实现思想

学习STL源码之前:

建议自己模拟实现一个简易list。

目的:

理解:

  • 节点设计

  • 迭代器

  • 指针操作


8.1 节点设计

首先:

定义节点。

复制代码
template<class T>
struct ListNode
{

    ListNode<T>* prev;

    ListNode<T>* next;

    T data;


};

一个节点:

包含:

复制代码
前指针

数据

后指针

8.2 list结构

list内部:

保存:

一个头节点。

类似:

复制代码
class list
{

private:

    Node* head;

};

8.3 push_back实现思想

尾插:

假设:

复制代码
A <=> B

插入:

C

需要:

复制代码
B.next=C

C.prev=B

C.next=head

本质:

修改几个指针。


8.4 insert实现思想

插入X:

原:

复制代码
A <=> B

插入:

复制代码
A <=> X <=> B

代码思想:

复制代码
pos前一个节点.next=X;

X.prev=前一个节点;

X.next=pos;

pos.prev=X;

8.5 erase实现思想

删除B:

原:

复制代码
A <=> B <=> C

变:

复制代码
A <=> C

操作:

复制代码
A.next=C;

C.prev=A;

delete B;
相关推荐
烟花落o3 小时前
C++ 从 C 到进阶 ②:引用、inline 、nullptr
c++·引用·inline·nullptr·c++入门
liulilittle3 小时前
MOE路由:路由(logits: top-k/8)
c++·人工智能·算法·机器学习·llm
旖旎夜光3 小时前
LeetCode 11:盛最多水的容器(双指针问题) —— 题解
数据结构·c++·算法·leetcode·双指针
凤山老林3 小时前
在 Apple 芯片的 Mac 上通过 VMware Fusion 使用 Windows 11
windows·macos·vmware·虚拟机
不弃君3 小时前
在 Windows 上用 MinGW 搭建 lwIP 调试环境:方法与踩坑实录
windows·网络协议·tcp/ip·嵌入式·mingw·lwip
ITHAOGE154 小时前
下载 | Win11 24H2 正式版更新!(系统ISO映像、多合一版本、26100.8973、Windows 11)
windows·科技·微软·电脑
我不会插花弄玉5 小时前
15.map,set下:AVL树,红黑树和map,set的封装【由浅入深-C++】
c++·stl
hold?fish:palm5 小时前
20 旋转图像
c++·算法·leetcode
此生决int6 小时前
深入理解C++系列(10)——lsit
开发语言·c++