目录
前言
经过前几章节的学习,我们对STL的vector和string容器接口的使用已经非常深刻了,这里我们继续学习STL中的list------也就是链表;
一、链表的分类
实际上,前面的文章中,我们用C语言实现了一个单链表,在STL中,实际上也分单链表和双向循环带头链表,而我们大部分使用的都是双向带头循环链表;单链表的讲解点击这里(C语言版)
本文主要讲解双向带头循环链表的使用,也就是list;
二、构造与析构
想学会使用一种容器,我们首先得知道其构造如何调用;(析构出作用域自动调用)
构造如下:
赋值重载:
void test_list1()
{
// 默认构造
list<int> lt1;
for (auto& e : lt1)
{
cout << e << " ";
}
cout << endl;
// n个data构造
list<int> lt2(8, 6);
for (auto& e : lt2)
{
cout << e << " ";
}
cout << endl;
// 区间构造
int array1[] = { 0, 1, 2, 3, 4, 5, 6, 7 };
list<int> lt3(array1, array1 + 5);
for (auto& e : lt3)
{
cout << e << " ";
}
cout << endl;
// 拷贝构造
list<int> lt4(lt3);
for (auto& e : lt4)
{
cout << e << " ";
}
cout << endl;
// 赋值重载
list<int> lt5;
lt5 = lt4;
for (auto& e : lt5)
{
cout << e << " ";
}
cout << endl;
}
实际上,以上构造接口的调用与前面的string与vector几乎差不多;因此,熟练使用其中一个,其他上手也不难;
三、迭代器
在list中,想要遍历一个链表中所有数据,迭代器就是最好的选择,迭代器也是所有STL容器中最通用的遍历方法,因为链表的数据不是储存在一段连续的空间中,因此,我们想通过方括号的方式访问成员就不太现实了,以及后面我们要学的树形结构的容器,都是使用迭代器的方式访问数据,因此方括号的遍历方式并不是常态,而恰恰迭代器的访问遍历才是我们常态;
1、迭代器的种类
在前面的学习中,我们并没有提出迭代器种类这一概念,但实际上,迭代器分为三类;分别是;
- 随机迭代器
- 双向迭代器
- 单项迭代器
随机迭代器就像我们vector的迭代器,可以指定访问某个数据,可通过加减O(1)的时间内访问某个数据;
双向迭代器主要就是我们今天学的 list 了,其虽然不是一段连续的内存空间,但我们可以对其进行++或--来依次遍历容器中的每一个数据;
单向迭代器主要是我们的容器 forward_list ,也就是我们的单链表,由于其迭代器只能往一个方向遍历,不能双向访问;
2、list迭代器的使用
list的迭代器也是有如下几种类型的接口,分别为普通,常量,反向三种;
void PrintList(const list<int>& lt)
{
// 常量迭代器
list<int>::const_iterator cit = lt.cbegin();
while (cit != lt.cend())
{
cout << *cit << " ";
cit++;
}
cout << endl;
}
void test_list2()
{
list<int> lt1;
lt1.push_back(1);
lt1.push_back(2);
lt1.push_back(3);
lt1.push_back(4);
// 普通迭代器遍历
list<int>::iterator it = lt1.begin();
// 注意此处不能写成 <= 了,因为储存的空间不连续
while (it != lt1.end())
{
cout << *it << " ";
it++;
}
cout << endl;
// 范围for
for (auto& e : lt1)
{
cout << e << " ";
}
cout << endl;
// 反向迭代器
list<int>::reverse_iterator rit = lt1.rbegin();
while (rit != lt1.rend())
{
cout << *rit << " ";
rit++;
}
cout << endl;
PrintList(lt1);
}
四、容量相关接口
与前面几个容器相比,list的容量接口就少了很多;
void test_list3()
{
list<int> lt1;
lt1.push_back(1);
lt1.push_back(2);
lt1.push_back(3);
lt1.push_back(4);
lt1.push_back(5);
// 当前元素个数
cout << lt1.size() << endl;
// 是否为空
cout << lt1.empty() << endl;
// 最大可容纳元素个数
cout << lt1.max_size() << endl;
}
五、修改相关接口
这部分接口看着挺多,但实际上,我们大部分都认识,比如assign是给list新的内容,代替原来的老内容,push_back、push_front、pop_back、pop_front分别是头插、尾插、尾删、头删;insert是指定位置插入数据,erase是指定位置删除数据,swap是交换两个链表的头指针,resize是设置某个链表的值,规则与vector的resize相同,clear则是清空链表;其余emplace系列可暂时不管;下面将演示其中几种;
void test_list4()
{
list<int> lt1;
lt1.push_back(1);
lt1.push_back(2);
lt1.push_back(3);
lt1.push_back(4);
lt1.push_back(5);
// 某个位置插入一个值
lt1.insert(lt1.begin(), 100); // 100 1 2 3 4 5
// 某个位置插入n个data
list<int>::iterator pos = find(lt1.begin(), lt1.end(), 3);
lt1.insert(pos, 3, 30); // 100 1 2 30 30 30 3 4 5
// 某个位置插入某段区间的值
vector<int> v1(lt1.begin(), lt1.end());
pos = find(lt1.begin(), lt1.end(), 5);
lt1.insert(pos, v1.begin() + 2, v1.begin() + 4); // 100 1 2 30 30 30 3 4 2 30 5
for (auto& e : lt1)
{
cout << e << " ";
}
cout << endl;
// 删除某个位置的值
lt1.erase(lt1.begin()); // 1 2 30 30 30 3 4 2 30 5
// 删除某个区间的值
pos = find(lt1.begin(), lt1.end(), 3);
lt1.erase(lt1.begin(), pos); //3 4 2 30 5
for (auto& e : lt1)
{
cout << e << " ";
}
cout << endl;
}
六、其他成员接口
在list中还有一些功能性的接口,但是用的并不是很多,也比较特殊,这里可以了解了解;(无需完全记住,用的时候查看文档即可) 文档链接
void test_list5()
{
list<int> lt1;
lt1.push_back(1);
lt1.push_back(2);
lt1.push_back(3);
lt1.push_back(4);
lt1.push_back(5);
list<int> lt2;
lt1.push_back(10);
lt1.push_back(20);
lt1.push_back(30);
// splice(把某个链表的数据转移到另一个链表的某个位置中)
lt1.splice(lt1.begin(), lt2);
// 此时 lt1: 10 20 30 1 2 3 4 5
// lt2: empty
// remove 移除某个链表中所有的val
lt1.remove(2); // 10 20 30 1 3 4 5
}
unique则负责去重,merge负责排序;因为算法库中的sort只能用随机迭代器,故list也实现了一个排序(merge),reverse则是反转链表;如果这些用法如果用的时候再去文档中查询即可;