文章目录
- 一、list的介绍
- 二、list的使用
-
- [1. 构造](#1. 构造)
- [2. 迭代器](#2. 迭代器)
- [3. 增 删 查 改](#3. 增 删 查 改)
- [4. list 迭代器失效问题](#4. list 迭代器失效问题)
- [5. list 排序问题](#5. list 排序问题)
- 三、list的模拟实现
-
- [0. 整体框架](#0. 整体框架)
- [1. 迭代器类](#1. 迭代器类)
-
- [1.1 operator->](#1.1 operator->)
- [1.2 临时对象](#1.2 临时对象)
- [1.3 const_iterator](#1.3 const_iterator)
- [2. list类](#2. list类)
-
- [2.1 begin / end](#2.1 begin / end)
- [2.2 构造 / 析构 / 拷贝构造 / 赋值重载](#2.2 构造 / 析构 / 拷贝构造 / 赋值重载)
- [2.3 增 删 查 改](#2.3 增 删 查 改)
- 四、list与vector的对比
一、list的介绍
- 底层是带头双向链表结构,需要额外空间保存节点信息(对于存储类型较小元素的大list来说这可能是一个重要的因素)
- 在常数范围内支持任意位置的插入和删除,效率通常优于array、vector和deque。
- 不支持随机访问,必须从头/尾开始找,访问特定元素需要线性时间。
二、list的使用
1. 构造
构造函数声明(constructor) | 功能说明 |
---|---|
list() | 【default】无参构造 |
list (size_type n, const value_type& val = value_type()) | 【fill】构造并初始化填充n个val |
list (const list& x) | 【copy】拷贝构造 |
list (InputIterator first, InputIterator last) | 【range】使用迭代器区间进行初始化构造 |
cpp
void TestList1()
{
list<int> l1; // 构造空的l1
list<int> l2(4, 100); // l2中放4个值为100的元素
list<int> l3(l2.begin(), l2.end()); // 用l2的[begin(), end())左闭右开的区间构造l3
list<int> l4(l3); // 用l3拷贝构造l4
// 以数组为迭代器区间构造l5 (数组名的
int array[] = { 16,2,77,29 };
list<int> l5(array, array + sizeof(array) / sizeof(int));
// 列表初始化(initializer list) --> C++11
// 以下三种写法都是一样的效果
list<int> l6({ 1,2,3,4,5 });
list<int> l66{ 1,2,3,4,5 };
list<int> l666 = { 1,2,3,4,5 };
list<int>::iterator it = l5.begin();
while (it != l5.end())
{
cout << *it << " ";
++it;
}
cout << endl;
for (auto& e : l6)
cout << e << " ";
cout << endl;
}
2. 迭代器
iterator的使用 | 功能说明 |
---|---|
begin + end (重点) | 获取第一个数据位置的iterator/const_iterator, 获取最后一个数据的下一个位置的iterator/const_iterator |
rbegin + rend | 获取最后一个数据位置的reverse_iterator,获取第一个数据前一个位置的reverse_iterator |
- begin与end为正向迭代器,对迭代器执行++操作,迭代器向后移动
- rbegin(end)与rend(begin)为反向迭代器,对迭代器执行++操作,迭代器向前移动
- list中的迭代器为双向迭代器,只支持++, -- 即只能前后移动一个节点
cpp
// 注意:遍历链表只能用迭代器和范围for
void PrintList(const list<int>& l)
{
// 注意这里l是const对象,调用返回的是const_iterator对象
for (list<int>::const_iterator it = l.begin(); it != l.end(); ++it)
{
cout << *it << " ";
// *it = 10; 编译不通过
}
cout << endl;
}
void TestList2()
{
int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
list<int> l(array, array + sizeof(array) / sizeof(array[0]));
// 使用正向迭代器正向list中的元素
// list<int>::iterator it = l.begin(); // C++98中语法
auto it = l.begin(); // C++11之后推荐写法
while (it != l.end())
{
cout << *it << " ";
++it;
}
cout << endl;
// 使用反向迭代器逆向打印list中的元素
// list<int>::reverse_iterator rit = l.rbegin();
auto rit = l.rbegin();
while (rit != l.rend())
{
cout << *rit << " ";
++rit;
}
cout << endl;
}
3. 增 删 查 改
接口名称 | 功能说明 |
---|---|
size | 获取有效节点个数 |
empty | 判断是否为空 |
front | 获取第一个节点的值 |
back | 获取最后一个节点的值 |
reserve | 改变vector的capacity |
push_front | 头插 |
push_back | 尾插 |
pop_front | 头删 |
pop_back | 尾删 |
insert | 指定位插入 |
erase | 指定位删除 |
swap | 交换两个list的成员 |
clear | 删除所有节点 |
cpp
// list插入和删除
// push_back/pop_back/push_front/pop_front
void TestList3()
{
int array[] = { 1, 2, 3 };
list<int> L(array, array + sizeof(array) / sizeof(array[0]));
// 在list的尾部插入4,头部插入0
L.push_back(4);
L.push_front(0);
PrintList(L);
// 删除list尾部节点和头部节点
L.pop_back();
L.pop_front();
PrintList(L);
}
// insert /erase
void TestList4()
{
int array1[] = { 1, 2, 3 };
list<int> L(array1, array1 + sizeof(array1) / sizeof(array1[0]));
// 获取链表中第二个节点
auto pos = ++L.begin();
cout << *pos << endl;
// 在pos前插入值为4的元素
L.insert(pos, 4);
PrintList(L);
// 在pos前插入5个值为5的元素
L.insert(pos, 5, 5);
PrintList(L);
// 在pos前插入[v.begin(), v.end)区间中的元素
vector<int> v{ 7, 8, 9 };
L.insert(pos, v.begin(), v.end());
PrintList(L);
// 删除pos位置上的元素
L.erase(pos);
PrintList(L);
// 删除list中[begin, end)区间中的元素,即删除list中的所有元素
L.erase(L.begin(), L.end());
PrintList(L);
}
// resize/swap/clear
void TestList5()
{
// 用数组来构造list
int array1[] = { 1, 2, 3 };
list<int> l1(array1, array1 + sizeof(array1) / sizeof(array1[0]));
PrintList(l1);
// 交换l1和l2中的元素
list<int> l2;
l1.swap(l2);
PrintList(l1);
PrintList(l2);
// 将l2中的元素清空
l2.clear();
cout << l2.size() << endl;
}
4. list 迭代器失效问题
- list 不是整块的连续物理空间,不存在扩容 / 缩容的概念,只有删除时会引发迭代器失效。(详细说明参考博主的vector解析中的 二、5.迭代器失效问题)
cpp
void TestListIterator()
{
int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
list<int> l(array, array + sizeof(array) / sizeof(array[0]));
auto it = l.begin();
while (it != l.end())
{
// erase()函数执行后,it所指向的节点已被删除,因此it无效,在下一次使用it时,必须先给其赋值
l.erase(it);
// it = l.erase(it); 正确写法
++it;
}
}
5. list 排序问题
结论:
- 一般不用:效率极其低下,接近vector的1/4,甚至比先拷贝给vector,排好序再拷贝回来的效率低一半(release版本)
cpp
// vector与list排序性能比较
void test_op1()
{
srand(time(0));
const int N = 10000000;
list<int> lt1;
list<int> lt2;
vector<int> v;
for (int i = 0; i < N; ++i)
{
auto e = rand() + i;
lt1.push_back(e);
v.push_back(e);
}
int begin1 = clock();
// 排序
sort(v.begin(), v.end());
int end1 = clock();
int begin2 = clock();
lt1.sort();
int end2 = clock();
printf("vector sort:%d\n", end1 - begin1);
printf("list sort:%d\n", end2 - begin2);
}
//先将list元素拷贝给vector,在vector中排好序再拷贝回list的性能
void test_op2()
{
srand(time(0));
const int N = 10000000;
list<int> lt1;
list<int> lt2;
for (int i = 0; i < N; ++i)
{
auto e = rand() + i;
lt1.push_back(e);
lt2.push_back(e);
}
int begin1 = clock();
// 拷贝vector
vector<int> v(lt2.begin(), lt2.end());
// 排序
sort(v.begin(), v.end());
// 拷贝回lt2
lt2.assign(v.begin(), v.end());
int end1 = clock();
int begin2 = clock();
lt1.sort();
int end2 = clock();
printf("list copy vector sort copy list sort:%d\n", end1 - begin1);
printf("list sort:%d\n", end2 - begin2);
}
三、list的模拟实现
0. 整体框架
cpp
template<class T>
struct ListNode
{
ListNode<T>* _next;
ListNode<T>* _prev;
T _data;
ListNode(const T& data = T())
:_next(nullptr)
,_prev(nullptr)
,_data(data)
{}
};
template<class T>
struct ListIterator
{
typedef ListNode<T> Node;
typedef ListIterator<T, Ref, Ptr> Self;
Node* _node; //迭代器本体,唯一成员变量 ---> 指针
ListIterator(Node* node)
:_node(node)
{}
// ... 各类迭代器行为
};
template<class T>
class list
{
typedef ListNode<T> Node;
public:
//typedef Node* iterator; 直接使用原生指针定义迭代器,不符合所需的迭代器行为,需要格外定义一个类进行封装
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;
// ...各类函数接口
private:
Node* _head;
};
list类封装 有关节点操作的相关接口
ListNode类 封装节点构造相关
ListIterator 封装list的迭代器相关
链表物理空间不连续,不能通过
++iterator
找下一个节点,必须用一个类封装原生指针然后手动控制其行为(重载运算符)使之符合(迭代器的最终效果要像原生指针对于数组一样使用,但是不是全部的功能都要实现,比如list的双向迭代器就不能
iterator + 4
,最多只能找前一个和后一个,因为效率太低,设计者没有必要支持)不需要显式写析构 / 拷贝构造 / 赋值:
- 节点的释放由list类控制,迭代器不涉及资源管理,默认生成的析构就够用
- ListIterator类唯一的成员变量(本体)就是指针,我们只是规范了它的行为,则拷贝 / 赋值时需要的就是浅拷贝,同一个指针指向同一块空间
1. 迭代器类
cpp
//V1 版本:写两个迭代器类,一个是普通迭代器,一个是const 迭代器
template<class T>
struct ListIterator
{
typedef ListNode<T> Node;
//typedef ListIterator<T, Ref, Ptr> Self;
typedef ListIterator<T> Self;
Node* _node; //迭代器本体,指针
ListIterator(Node* node)
:_node(node)
{}
//迭代器行为
Self& operator++()
{
_node = _node->_next;
return *this;
}
Self& operator--()
{
_node = _node->_prev;
return *this;
}
//后置++
Self operator++(int)
{
Self tmp(*this);
_node = _node->_next;
return tmp;
}
Self operator--(int)
{
Self tmp(*this);
_node = _node->_prev;
return tmp;
}
T& operator*()
{
return _node->_data;
}
T* operator->()
{
return &_node->_data;
}
bool operator!=(const Self& it)
{
return _node != it._node;
}
bool operator==(const Self& it)
{
return _node == it._node;
}
};
template<class T>
struct ListConstIterator
{
typedef ListNode<T> Node;
typedef ListConstIterator<T> Self;
Node* _node;
ListConstIterator(Node* node)
:_node(node)
{}
//...略,与普通迭代器一模一样
const T& operator*()
{
return _node->_data;
}
const T* operator->()
{
return &_node->_data;
}
// ...略,同上
};
//V2 版本:使用多参数模板,交给编译器实现const迭代器类
//const 和 非const 模板生成两个类
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)
{}
//迭代器行为
Self& operator++()
{
_node = _node->_next;
return *this;
}
Self& operator--()
{
_node = _node->_prev;
return *this;
}
//后置++
Self operator++(int)
{
Self tmp(*this);
_node = _node->_next;
return tmp;
}
Self operator--(int)
{
Self tmp(*this);
_node = _node->_prev;
return tmp;
}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
bool operator!=(const Self& it)
{
return _node != it._node;
}
bool operator==(const Self& it)
{
return _node == it._node;
}
};
1.1 operator->
专门给没有重载流插入<<运算符的类提供的(对于自定义类型,流插入可以直接使用,而自定义类型需要类设计者自己重载控制),下见示例类 Pos。该类没有重载流插入,我们只能使用最原始的方法,利用指针手动访问该类的成员变量_row, _col
cout << (*it)._row
该段代码很好理解,先解引用得Pos类对象,再用Pos类对象访问其成员cout << it->_row
功能同上,先获取Pos对象指针再使用Pos类对象指针访问其成员,但是仔细观察一下,是不是少了一个箭头啊?实际上这是编译器的省略行为,该段代码等同于cout << it.operator->()->_row
即先调用迭代器类中 重载的-> 获取Pos类对象的指针再用 原生-> 访问其成员
cpp
struct Pos
{
int _row;
int _col;
Pos(int row = 0, int col = 0)
:_row(row)
, _col(col)
{}
};
void test_list2()
{
list<Pos> lt1;
lt1.push_back(Pos(100, 100));
lt1.push_back(Pos(200, 200));
lt1.push_back(Pos(300, 300));
list<Pos>::iterator it = lt1.begin();
while (it != lt1.end())
{
//cout << (*it)._row << ":" << (*it)._col << endl;
// 为了可读性,省略了一个->
cout << it->_row << ":" << it->_col << endl;
//cout << it->->_row << ":" << it->->_col << endl;
cout << it.operator->()->_row << ":" << it.operator->()->_col << endl;
++it;
}
cout << endl;
}
1.2 临时对象
begin返回的是临时对象,临时对象具有常性,那为什么返回的迭代器可以++,---呢?
为了使用的便利,编译器经过了特殊处理,实际上临时对象的常性介于const和非const之间,可以调用非静态成员函数,也可以被修改,但是不能被非const对象引用
1.3 const_iterator
const迭代器 专给const类对象调用,类似于 const T* ,迭代器本身可以修改,指向的内容不可以通过迭代器修改(const是对迭代器本身的修饰,如果用其他方法修改其指向的内容,迭代器管不着)
故对迭代器行为中涉及解引用且修改的进行约束:
- 对迭代器解引用行为要返回const类型值(通过const迭代器获取的指向内容 不可以修改)
如何写?
方法1:再创建一个const版本的迭代器类
方法2:使用多参数模板,交给编译器干(实际上还是会创建两个类)
cpp
//方法1:
template<class T>
struct ListConstIterator
{
typedef ListNode<T> Node;
typedef ListConstIterator<T> Self;
//const Node* _node; 为了直接使用多模板参数省事,可以不加const,因为迭代器的行为完全由我们手动控制,
//只要我们不在其解引用时修改即可。但是如果有人手动调用迭代器内部的指针进行解引用修改是可以的(没大病不会这么干)
Node* _node;
ListConstIterator(Node* node)
:_node(node)
{}
//...略,与普通迭代器一模一样
const T& operator*()
{
return _node->_data;
}
const T* operator->()
{
return &_node->_data;
}
};
//方法2:
//const 和 非const 模板生成两个类
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)
{}
//迭代器行为
Self& operator++()
{
_node = _node->_next;
return *this;
}
Self& operator--()
{
_node = _node->_prev;
return *this;
}
//后置++
Self operator++(int)
{
Self tmp(*this);
_node = _node->_next;
return tmp;
}
Self operator--(int)
{
Self tmp(*this);
_node = _node->_prev;
return tmp;
}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
bool operator!=(const Self& it)
{
return _node != it._node;
}
bool operator==(const Self& it)
{
return _node == it._node;
}
};
2. list类
2.1 begin / end
cpp
iterator begin()
{
return iterator(_head->_next); //类的匿名对象,下同
}
const_iterator begin() const
{
return const_iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
const_iterator end() const
{
return const_iterator(_head);
}
2.2 构造 / 析构 / 拷贝构造 / 赋值重载
cpp
//头结点创建(链表为空),由于多个接口都要调用,故单独提出来封装成函数
void empty_init()
{
_head = new Node();
_head->_next = _head;
_head->_prev = _head;
}
list()
{
empty_init();
}
list(initializer_list<T> il)
{
empty_init();
for (const auto& e : il)
push_back(e);
}
//lt2(lt1)
list(const list<T>& lt)
{
empty_init();
for (const auto& e : lt)
push_back(e);
}
list<T>& operator=(list<T> lt)
{
swap(_head, lt._head);
return *this;
}
~list()
{
clear();
delete _head;
_head = nullptr;
}
2.3 增 删 查 改
cpp
void clear()
{
auto it = begin();
while (it != end())
{
it = erase(it);
}
}
iterator insert(iterator pos, const T& x)
{
Node* cur = pos._node;
Node* newnode = new Node(x);
Node* prev = cur->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
iterator erase(iterator pos)
{
assert(pos != end());
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* next = cur->_next;
prev->_next = cur->_next;
next->_prev = prev;
delete cur;
return iterator(next);
}
void push_back(const T& x)
{
insert(end(), x);
}
void pop_back()
{
erase(--end());
}
void push_front(const T& x)
{
insert(begin(), x);
}
void pop_front()
{
erase(begin());
}
四、list与vector的对比
vector | list | |
---|---|---|
底层结构 | 动态顺序表,一段连续空间 | 带头结点的双向循环链表 |
随机访问 | 支持随机访问,访问某个元素效率O(1) | 不支持随机访问,访问某个元素效率O(N) |
插入和删除 | 任意位置插入和删除效率低,需要搬移元素,时间复杂度为O(N),插入时有可能需要增容,增容:开辟新空间,拷贝元素,释放旧空间,导致效率更低 | 任意位置插入和删除效率高,不需要搬移元素,时间复杂度为O(1) |
空间利用率 | 底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高 | 底层节点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低 |
迭代器 | 原生态指针 | 对原生态指针(节点指针)进行封装 |
迭代器失效 | 插入,删除时 | 删除时 |
使用场景 | 需要高效存储,支持随机访问,不关心插入删除效率 | 大量插入和删除操作,不关心随机访问 |