一.list的介绍:
1.list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
2.list的底层是双向链表结构,带有哨兵位的头结点 。
4.由于底层是链表结构,list通常在任意位置进行插入,删除元素的执行效率更好。
6.在list这个容器里面,是不支持[]等操作的。
list的底层:(双向带头循环链表)
二.list的使用:
1.list的构造:
(1)empty container constructor (default constructor):构造空的list。
list<int> mylist;
(2) fill constructor:构造一个有n个元素的链表,每个元素的值都是val。
list<int> list4(10, 1);
for (auto e : list4) {
cout << e << " ";
}
//如上代码就成功构造了一个有10个1的链表
(3) range constructor:使其元素数量与区间 [first,last) 内的元素数量相同,并且每个元素都是根据该区间内对应位置的元素原位构造(emplace-constructed)而成,保持相同的顺序。
vector<int> t1 = { 1,2,3,4,5,6,7,8,9 };
int a[] = { 1,2,3,4 };
list<int> list2(t1.begin(), t1.end());
list<int> list3(a, a + 4);
//值得关注的是,要求传递的是迭代器,但是也可以传数组的指针
(4)copy constructor (and copying with allocator):拷贝构造。(同其他的容器一模一样,不多介绍如何使用)
(5)暂时不介绍,后面再介绍。
(6)initializer list constructor:简单点来说就是把想存进list的值全部用一个花括号括起来,每个值用逗号分隔开,然后用这些值构造一个list。
list<int> list1 = { 1,2,3,4,5,6,7,8,9 };
2.capacity:
其中max_size其实用的不多,这点在vector里面也一样,上面两个和其他容器的用法一模一样。
3.element access:
这两个函数返回的分别是链表的第一个元素和最后一个元素。
4.Modifiers:
(挑一些特殊和重要的讲不讲的和其他容器使用差不多)
(1)assign:在 C++ 标准库中,std::list
的 assign()
方法用于替换链表中的所有元素,支持多种赋值方式。它会清空原链表,再将新元素赋值给链表。
list<int> numbers;
numbers.assign(3, 100);
std::vector<int> src = {1, 2, 3, 4};
std::list<int> numbers;
numbers.assign(src.begin(), src.end()); // 链表变为 {1, 2, 3, 4}
std::list<char> chars;
chars.assign({'a', 'b', 'c'}); // 链表变为 {'a', 'b', 'c'}
(2)emplace_front:在链表的首部直接构造一个新元素,跟push_front的功能差不多,但是二者的效率可能会有一些不同。
(3)insert:提供了很多不同的版本:
分别举例:
int main ()
{
std::list<int> mylist;
std::list<int>::iterator it;
// set some initial values:
for (int i=1; i<=5; ++i) mylist.push_back(i); // 1 2 3 4 5
it = mylist.begin();
++it; // it points now to number 2 ^
mylist.insert (it,10); // 1 10 2 3 4 5
//在pos位置insert一个val
// "it" still points to number 2 ^
mylist.insert (it,2,20); // 1 10 20 20 2 3 4 5
//在pos位置insert指定数目的val
--it; // it points now to the second 20 ^
std::vector<int> myvector (2,30);
mylist.insert (it,myvector.begin(),myvector.end());
// 1 10 20 30 30 20 2 3 4 5
//在pos位置inset一段迭代器区间所对应的值
std::cout << "mylist contains:";
for (it=mylist.begin(); it!=mylist.end(); ++it)
std::cout << ' ' << *it;
std::cout << '\n';
return 0;
}
list里面的list由于不会扩容,所以没有迭代器失效的问题,但是返回值也是迭代器。
(4)erase:
erase提供了两种方式,在pos位置删除和删除一段迭代器区间。如同vector一样,erase有迭代器失效的效果,所以返回的是pos位置的下一个迭代器位置。
(5)swap:交换两个链表的内容:
int main() {
std::list<int> a = {1, 2, 3};
std::list<int> b = {4, 5};
a.swap(b); // 交换 a 和 b 的内容
std::cout << "a: ";
for (int x : a) std::cout << x << " "; // 输出: 4 5
std::cout << "\nb: ";
for (int x : b) std::cout << x << " "; // 输出: 1 2 3
}
5.operations:
(1)splice:
std::list
的 splice()
方法用于将一个链表的元素转移到另一个链表中,且不会导致元素的复制或移动,而是直接修改链表的内部指针,因此效率极高(时间复杂度为 O (1))。这个函数用到的地方不是很多,但是后面会有大用处。将一个链表的元素转移到另一个链表中,另一个链表也可以是自己本身。
(2)remove:
std::list
的 remove()
方法用于删除链表中所有等于特定值的元素。它会遍历整个链表,移除所有匹配的元素,并自动调整链表结构,保持元素间的连续性。需要传递的参数就是要删除的值val
remove_if就是和上面的作用差不多,但是需要满足一定的条件。
(3)unique:
std::list
的 unique()
方法用于移除链表中相邻的重复元素,只保留其中一个。它会遍历链表,将相邻且值相等的元素压缩为单个元素,从而使链表中的元素唯一(仅针对相邻元素)。
(4)merge:
std::list
的 merge()
方法用于将另一个有序链表合并到当前链表中,合并后当前链表仍保持有序。使用这个函数的前提是,两个链表必须已经按相同顺序排序。
int main() {
std::list<int> a = {1, 3, 5};
std::list<int> b = {2, 4, 6};
// 将 b 合并到 a 中,a 仍保持有序
a.merge(b);
// 输出: 1 2 3 4 5 6
for (int num : a) {
std::cout << num << " ";
}
// b 变为空链表
std::cout << "\nb size: " << b.size(); // 输出: 0
}
合并之后被合并的那个链表会变成空的链表。
(5)sort:
由于迭代器类型的原因,sort不能使用<algorithm>头文件里面的sort,所以单独写了一个sort,但是在list里面的这个sort是归并排序,在进行大量数据排序的时候效率并没有算法库里面的sort效率高,因此使用的时候经常将list里面的元素放到vector里面,如何排好序后再assign回list里面。
原因在下面解释。
(6)reverse:
不能使用算法库里面的reverse的原因和sort一样。
三.迭代器类型:
迭代器有很多种,是根据容器的底层来决定的。inputiterator(输入迭代器),forwarditerator(前向迭代器),bidirectioniterator(双向迭代器),randmaccessiterator(随机访问迭代器)。
inputiterator迭代器和forwarditerator都是只支持++,*的操作,双向迭代器支持++,--,*的操作,随机迭代器支持不仅支持前面所有的,还支持额外的随机访问,比如+n,> , <, 迭代器之间的减法等操作。
一个容器是否能使用算法库里面的函数,需要看迭代器类型,在前面列举的类型里面,按顺序是包含关系。这是按照迭代器的功能来划分的。
那么为什么list容器不能调用sort函数呢,list容器是双向迭代器,而sort要求的是随机迭代器,所以不能调用,反之,如果有一个随机迭代器的容器,那么它是可以取使用要求是双向迭代器的函数的。
四.补充一些知识:
----> 计算机的存储体系如图所示:
->平时最关注的就是主存以及本地磁盘。
->其中主存是带电的,存储速度很快,没有电就不能存储数据,一旦断电,RAM (主存一般指ram,然后ram又分为dram和sram)中存储的数据就会立即丢失,这就是为什么主存是易失性存储器。而本地磁盘是不带电的,没有电它也可以存储数据,在断电后仍能保存数据,属于非易失性存储器。
->我们书写一个顺序表或者链表,它都是存放在主存(也就是通常说的内存)里面的,数据结构就是在内存中对数据进行管理,所以当然是在主存上的。我们对顺序表,链表的数据进行遍历和修改的时候,是cpu来完成的,但是cpu是不能直接访问内存的,形象一点说,这是因为它太“快”了,而它觉得内存太慢了。这时候就有一个cpu高速缓存的概念了:
cpu的机制是这样的,比如说我要访问一个指针,cpu先进缓存,如果这个指针在缓存里面,那么就叫做“命中”,这样cpu就可以直接访问了。如果“不命中”的情况下,内存的数据(当然不可能是内存的全部数据)会先进入缓存里面,然后再让cpu去“命中”。在这样的条件下就有一个缓存命中率的概念产生,对于顺序表和链表先说结论就是顺序表的缓存命中率高,而链表的缓存命中率低。内存在把数据给缓存的时候,假设要用的数据是4个字节,它就会从内存里面多拿连续的一部分给缓存(这是局部性原理预取相邻的数据的效果,但是具体多取多少,这是又是硬件等多方面因素决定的)。稍微应用一下豆包的话来类比一下这个局部性原理:
由于顺序表的结构优势,使得在上述原理下“命中率”变高,虽然在链表的一个节点的后面会多拿一些数据,但是包含下一个节点的概率不大,所以链表的“命中率低”。
--------取自《深度理解计算机系统》--------。