一、什么是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;