C++STL之list的使用和模拟实现
Posted 小赵小赵福星高照~
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++STL之list的使用和模拟实现相关的知识,希望对你有一定的参考价值。
list
文章目录
首先学习list,不妨我们先来看看文档中它是怎么说明的:
中文翻译:
list是序列式容器,允许在序列内的任何位置进行插入和删除操作,并且该容器可以前后双向迭代。
list容器在底层被实现为双向链表;双向链表中的每个元素存储在不同且不相关的存储位置。每个节点是通过与指向它前面元素的指针和指向它后面元素的指针进行关联的。
list与 forward_list 非常相似:主要区别在于 forward_list 对象是单链表,因此它们只能向前迭代,以让其更小更高效。
与其他序列式容器(array、vector和deque)相比,list通常在拥有迭代器的容器内任何位置插入、提取和移动元素方面表现更好
与这些其他序列容器相比,list 和 forward_lists 的主要缺点是它们无法直接访问元素的位置;例如,要访问列表中的第六个元素,必须从已知位置(如开头或结尾)迭代到该位置,这在这些位置之间的距离上需要线性时间。它们还消耗一些额外的内存来保存与每个元素相关联的信息
可以看到list是模板,它可以接收任何类型。
list成员函数的使用
list的构造函数
list的构造函数如上:默认构造函数、迭代器构造函数等等,list有多种初始化的方式:
#include<iostream>
#include<list>
using namespace std;
void test_list1()
list<int> lt1;
list<int> lt2(10,5);
list<int> lt3(lt2.begin(),lt2.end());
vector<int> v=1,2,3,4,5;
list<int> lt4(v.begin(),v.end());
list有默认构造函数,可以不传参数
list有这样的构造函数:
它用来构造n个值为val的节点
list<int> lt2(10,5);
list还有这样的构造函数:
它可以用一段迭代器区间来进行构造:
list<int> lt3(lt2.begin(),lt2.end());
可以用其他容器的迭代器区间进行构造:
vector<int> v = 1,2,3,4,5 ;
list<int> lt4(v.begin(), v.end());
list也支持赋值:
lt1 = lt4;
可以看到lt4已经赋值给了lt1
list的遍历方式
1、迭代器遍历
void test_list2()
vector<int> v = 1,2,3,4,5 ;
list<int> lt4(v.begin(), v.end());
list<int>::iterator it4 = lt4.begin();
while(it4!=lt4.end())
cout<<*it4<<" "<<endl;
it4++;
cout<<endl;
2、范围for遍历:
void test_list3()
vector<int> v = 1,2,3,4,5 ;
list<int> lt4(v.begin(), v.end());
for(auto e:lt4)
cout<<e<<" ";
cout << endl;
因为list不支持随机访问,所以不存在[]+下标进行遍历
assign
给list对象分配新的内容代替当前的内容,并且修改它的size
lt3.assign(5,3);//重新给值
for(auto e:lt3)
cout<<e<<" ";
cout<<endl;
push_back和push_front
尾插和头插
void test_list4()
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_front(10);
lt.push_front(20);
lt.push_front(30);
lt.push_front(40);
for(auto e:lt)
cout<<e<<" ";
cout<<endl;
可以看到成功的进行了尾插和头插
pop_back和pop_front
尾删和头删
void test_list5()
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_front(10);
lt.push_front(20);
lt.push_front(30);
lt.push_front(40);
for(auto e:lt)
cout<<e<<" ";
cout<<endl;
lt.pop_back();//删除需要保证有数据
lt.pop_back();
lt.pop_back();
lt.pop_back();
lt.pop_front();
for(auto e:lt)
cout<<e<<" ";
cout<<endl;
可以看到成功的进行了尾删和头删
insert
在任意位置进行插入
void test_list6()
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
list<int>::iterator pos = find(lt.begin(),lt.end(),2);
if(pos!=lt.end())
//找到
lt.insert(pos,20);
//这里pos是否会失效呢?不会失效
//原因是list和vector的结构不同
//因为底层空间是物理连续数组。可能扩容,导致野指针问题,不扩容,挪动数据,也导致pos意义变了
//list insert不会失效,因为list是一个个独立节点,2前面插入数据是新增节点,pos还是指向2这个节点的,所以不会失效
可以看到已经成功插入
**有一个问题:这里的pos会不会像vector一样会失效呢?**不知道迭代器失效问题可以看看这篇文章:
答案是不会,原因是list和vector的结构不同,vector因为底层是物理连续数组,插入时可能会扩容,导致野指针问题,不扩容,挪动数据也导致pos的意义遍历,而对于list,list的每一个节点不一定连续,插入时不需要挪动数据,insert后不会失效因为list是一个个独立的节点,这里在2的前面插入数据是插入新增的节点,pos还是指向2这个节点的,所以不会失效
erase
在任意位置进行删除
void test_list7()
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
list<int>::iterator pos = find(lt.begin(),lt.end(),2);
if(pos!=lt.end())
//找到
lt.erase(pos);
//erase这里会失效,因为pos指向的节点已经被释放了,出现野指针
cout<<*pos<<endl;
*pos = 100;
可以看到我们erase时不用一个迭代器来接收时报错了,原因是erase时这里pos会失效,因为pos指向的节点已经被释放了,出现野指针
当我们用pos来接收时:
这里说明了erase返回的迭代器是指向删除节点的下一个节点
clear
void test_list8()
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
for(auto e : lt)
cout<<e<<" ";
cout<<endl;
lt.clear();
可以看到已经清空
那么我们知道list是带头双向循环链表,那么头节点被删除了吗?
答案是没有,我们看下面的测试:
void test_list8()
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
for(auto e : lt)
cout<<e<<" ";
cout<<endl;
lt.clear();
//但是头节点没有清,还可以插入
lt.push_back(10);
lt.push_back(20);
lt.push_back(30);
lt.push_back(40);
for(auto e : lt)
cout<<e<<" ";
cout<<endl;
可以看到头节点没有清楚,这里我们还能正常插入
swap
进行list对象的交换
所有的容器进行交换,都尽量用库里面的swap函数,方便而且不容易出错
void test_list9()
list<int> lt;
list<int> lt1;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt1.push_back(10);
lt1.push_back(20);
lt1.push_back(30);
lt1.push_back(40);
lt.swap(lt1);
for (auto e : lt)
cout << e << " ";
cout << endl;
for (auto e : lt1)
cout << e << " ";
cout << endl;
可以看到已经交换成功了
还有一个算法中的swap:
lt.swap(lt1);
swap(lt,lt1);//尽量不要用这个
//对于C++98版本,交换效果一样,但是效率不一样
//vector/string容器也类似
我们尽量不要用这个算法中的swap函数,对于C++98版本,这两个交换函数交换效果一样,但是效率不一样,vector/string容器也类似,为什么呢?因为它是深拷贝式的交换:
而list类成员函数中的swap,只是交换指针(成员变量)就可以了,故两个容器要交换,尽量使用容器自己的swap,不要使用库函数的swap
sort
void test_list10()
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(30);
lt.push_back(4);
for(auto e : lt)
cout<<e<<" ";
cout<<endl;
lt.sort();
for(auto e : lt)
cout<<e<<" ";
//默认排升序
//排降序
greater<int> g;
lt.sort(g);
cout<<endl;
可以看到已经排好序,但是list里的sort效率低,一般很少用
效率测试对比:
void testOP()
srand(time(0));
const int N = 10000;
list<int> lt;
vector<int> v;
resize(N);
for (int i = 0; i < N; ++i)
v[i] = rand();
lt.push_back(v[i]);
int begin1 = clock();
sort(v.begin(), v.end());
int end1 = clock();
int begin2 = cLock();
lt.sort();
//sort(lt.begin(),lt.end())//error
int end2 = clock();
cout << "vector sort" << end1 - begin1 << endl;
cout << "list sort" << end2 - begin2 s<< endl;
unique
即**”删除”序列中所有相邻的重复元素(只保留一个)。此处的删除,并不是真的删除,而是指重复元素的位置被不重复的元素给占领了(详细情况,下面会讲)。由于它”删除”的是相邻的重复元素,所以在使用unique函数之前,一般都会将目标序列进行排序。**
void test_list11()
list<int> lt;
lt.push_back(1);
lt.push_back(1);
lt.push_back(4);
lt.push_back(30);
lt.push_back(4);
for(auto e : lt)
cout<<e<<" ";
cout<<endl;
lt.sort();
lt.unique();//去重
for(auto e : lt)
cout<<e<<" ";
cout<<endl;
可以看到已经成功去重
remove
传一个参数值,如果有的话删掉,没有的话不进行处理,从容器中移除所有等于 val 的元素。 这会调用这些对象的析构函数,并通过移除的元素数量来减小容器大小。
void test_list12()
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(<以上是关于C++STL之list的使用和模拟实现的主要内容,如果未能解决你的问题,请参考以下文章
[C/C++]详解STL容器3--list的功能和模拟实现(迭代器失效问题)