C++STL---list知识汇总

前言

学习完list,我们会对STL中的迭代器有进一步的认识。list底层有很多经典的东西,尤其是他的迭代器。而list的结构是一个带头双向循环链表。

list没有reserve和resize,因为它底层不是连续的空间,它是用时随时申请,不用时随时释放。他也不支持随机访问,所以没有operator[ ]。而他有头插,尾插,头删,尾删,以及任意位置的插入删除。

严格来说,C++中list实际有两个:

第一个forward_list是单链表,它是C++11新增加的,它的使用场景很少。它不支持尾插,尾删,因为单链表尾插尾删的效率很低。并且它对任意位置做插入删除操作是在当前位置之后,因为当前位置之前得找前一个,也是一个O(n)的实现。唯一的优势也就是每个结点少一个指针。

第二个list是我们要学习的带头双向循环链表。

list的介绍

1.列表是序列容器,允许在序列中的任何位置进行恒定时间O(1)的插入和擦除操作,以及双向迭代。

2.列表容器底层是双链表;双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向前一个元素和后一个元素。

3.list与forward_list非常相似:主要区别在于forward_listobject是单个链表,因此它们只能向前迭代,以换取更小、更高效。

4.与其他基本标准序列容器(array,vector和deque)相比,list在任意位置插入,删除结点的效率更高。

5.list和forward_list最大的缺陷是不支持在任意位置的随机访问,其次,list还需要一些额外的空间,以保存每个结点之间的关联信息(对于存储的类型较小元素来说这可能是一个重要的因素)。

list的使用

1.list的构造
constructor 接口说明
list() 构造空的list对象
list(size_t n,const value_type& val =value_type()) 构造list对象中包含n个值为val的元素
list(const list& x) 拷贝构造
lsit(Inputiterator first,Inputiterator last) 用迭代区间构造list对象

这里我们只要记住用迭代区间构造list对象时,也可以用其他容器的迭代器就行,其他没什么好说的。

cpp 复制代码
#include<iostream>
#include<vector>
#include<list>
using namespace std;
int main()
{
    vector<int> v = { 1, 2, 3, 4, 5 };
	list<int> lt1(v.begin(), v.end());//用vector的迭代器构造list对象	
    return 0;
}
2.list迭代器的使用
iterator 接口说明
begin && end 返回第一个元素的迭代器 && 返回最后一个元素下一个位置的迭代器
rbegin + rend 返回第一个元素的 reserve_iterator,即 end 位置 && 返回最后一个元素下一个位置的 reverse_iterator,即 begin 位置

我们需要记住两点:

1.在遍历的时候vector和string我们可以使用小于或不等于做判断条件,但是list只能使用不等于。因为list内不是连续的空间。

2.因为list不支持operator[ ],所以我们遍历list的时候不能使用[ ]的方式遍历了

cpp 复制代码
		vector<int> v = { 1, 2, 3, 4};
		list<int> lt(v.begin(), v.end());
        list<int>::iterator it1 = lt.begin();
		while(it1 != lt.end())//这里只能使用!=,不能使用<
		{
			cout << *it1 << " ";
			++it1;	
		}
		cout << endl
3.list容量大小的函数(empty && size)

|-------|----------------|
| empty | 判断list对象是否为空 |
| size | 返回list中有效结点的个数 |

4.list结点接收的函数(front && back)

|-------|--------------------|
| front | 返回list第一个结点的数据的引用 |
| back | 返回list最后一个结点的数据的引用 |

5.list修改函数

|------------|-------------------|
| push_back | 尾插一个值为val的结点 |
| push_front | 头插一个值为val的结点 |
| pop_back | 尾删一个结点 |
| pop_front | 头删一个结点 |
| insert | 在pos位置中插入值为val的结点 |
| erase | 删除pos位置的结点 |
| swap | 交换两个list对象中的元素 |
| clear | 清空list对象中的有效元素 |

下面我们在一段代码中用例子来解释我们需要注意的地方:

cpp 复制代码
#include<iostream>
#include<algorithm>
#include<vector>
#include<list>
#include<functional>
using namespace std;

namespace std
{
	void test1()
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);

		lt.pop_front();
		lt.pop_front();
		lt.pop_front();
		lt.pop_front();
		//lt.pop_front();  //注意在头删、尾删时要保证list里还有数据,否则这里会报断言错误

		for (const auto& e : lt)
		{
			cout << e << " ";
		}
		cout << endl;
	}
	void test2()
	{
		list<int> lt;
		lt.push_back(10);
		lt.push_back(20);
		lt.push_back(30);
		lt.push_back(40);
		
		list<int>::iterator pos = find(lt.begin(), lt.end(), 30);//list里也没有提供find函数,所以这里使用的是algorithm里的
		if (pos != lt.end())
		{
			lt.insert(pos, 3);
		}
		for (const auto& e : lt)
		{
			cout << e << " ";
		}
		cout << endl;

		lt.clear();//clear不会把头节点清除,这里还可以继续插入数据
		lt.push_back(1);
		lt.push_back(2);
		for (const auto& e : lt)
		{
			cout << e << " ";
		}
		cout << endl;
	}
	void test3()
	{
		list<int> lt1;
		lt1.push_back(1);
		lt1.push_back(2);

		list<int> lt2;
		lt2.push_back(2);
		lt2.push_back(1);

		list<int> lt3;
		lt3.push_back(1);
		lt3.push_back(2);
		lt3.push_back(3);
		lt3.push_back(4);

		//对于swap,建议使用容器里的,而不建议使用算法里的。它们效果一样,但是效率不一样
		lt1.swap(lt2);        //这是std::list::swap,专门为了list设计的,效率会更高
		//swap(lt1, lt2);     //这是std::swap,这是算法中的,通用的,底层发生的是深拷贝,效率低
		for (const auto& e : lt1)
		{
			cout << e << " ";
		}
		cout << endl;
		for (const auto& e : lt2)
		{
			cout << e << " ";
		}
		cout << endl;

		//所有的排序都满足,>是降序,<是升序,这里默认是升序
		//这个也是一个类模板,它是一个仿函数,所在头<functional>,后面我们会实现,sort所在头<algorithm>
		greater<int> g;
		lt3.sort(g);
		lt3.sort(greater<int>());//同上,可以直接写成匿名对象
		for (const auto& e : lt3)
		{
			cout << e << " ";
		}
		cout << endl;
		//sort(lt3.begin(), it3.end());     //error
		//vector可以使用算法提供的sort()函数,但是list不行,因为本质sort()会用两个迭代器相减,而list的迭代器不支持减!!

		//unique的功能是去重,是algorithm提供的,去重的前提是排序,升序降序都行,如果不排序它只能去重相邻的数据
		lt3.unique();
		for (const auto& e : lt3)
		{
			cout << e << " ";
		}
		cout << endl;

		//erase需要先find,而remove可以直接删除,有就全删,没有就不删,由algorithm提供
		lt3.remove(2);
		for (const auto& e : lt3)
		{
			cout << e << " ";
		}
		cout << endl;

		//reverse的功能是逆置,对于带头双向循环链表的逆置比单链表简单,由algorithm提供
		lt3.reverse();
		for (const auto& e : lt3)
		{
			cout << e << " ";
		}
		cout << endl;

		//merge的功能是合并
		//splice的功能是转移,它转移的是节点不是数据,很特殊的场景下才会使用到,我们以后在了解LRU可能还会再接触到
	}
}
int main()
{
	//std::test1();
	//std::test2();
	std::test3();
	return 0;
}

注意:

1.C++98不论什么容器都建议使用各自容器里的 swap,而不建议使用算法里的 swap。

因为无论什么容器使用算法中的swap()时都会涉及到深拷贝问题,并且需要深拷贝三次,代价极大。

而容器内的swap会根据各个容器的特性,进行交换。例如,两个list对象交换的时候,只需要交换两个对象的头指针指向就行;两个vector对象交换的时候,只需要交换两个vector对象中_start、_finish、_endofstorage三个指针的指向即可,不用做深拷贝,也就提高了效率。

2.关于迭代器的补充

从使用功能的角度分类:正向,反向,const,非const

从容器底层结构分类:单向,双向,随机

如单链表,哈希表迭代器就是单向,特点是能++,不能--;双向循环链表,map迭代器就是双向,特点是能++,也能--;string,vector,deque迭代器就是随机迭代器,特征是不仅能++,--,还能+,-,一般随机迭代器底层都是一个连续的空间。

这里我们可以通过算法里函数参数的命名来推断出他的含义:

我们看,例如sort函数内参数命名为RandomAccessIterator,也就是随机迭代器;而reverse函数内参数命名为BidirectionalIterator,也就是双向迭代器。

而我们使用的时候,要注意,比如:reserve函数的参数是双向迭代器,而string能不能使用呢?答案是可以的,因为string是随机迭代器,它满足双向迭代器的所有功能。但是例如:list能使用sort函数吗?答案是不能的,因为list迭代器是双向的,不满足随即迭代器的功能。也就是说功能多的是可以执行参数迭代器功能少的函数的,这实际上就是一种继承关系。

而我们这里就要明白一个道理:容器是用来存储数据的,而根据封装的要求,他的成员变量一般是私有的,而封装的本质就是通过合法的渠道去进行操作,那我可以提供成员函数来供使用者合法的操作,但是这样的话也不太好,因为底层结构差异大了以后,每种容器的操作起来的差别也会很大。例如,string,vector底层是数组,我们通过数组的方式进行操作,list底层是链表我们通过链表的方式进行操作,而其他复杂的数据结构,操作的方式会更不一样。所以**迭代器的本质就是不破坏容器的封装性,不暴露容器底层实现细节的情况下,提供统一的方式去操作容器中存储的数据。**只要我们会其中一种容器的迭代器,我们用其他容器的迭代器也没有问题。所以迭代器被称为"容器和算法之间的胶合剂"。

6.list迭代器失效问题

在具体介绍list迭代器之前,我们可以先将迭代器暂时理解为指针,迭代器失效就是迭代器所指向的结点无效,即该结点被删除了。list底层结构为带头双向循环链表,所以对list进行insert操作时不会导致list迭代器失效,只有在erase的时候才会失效,并且失效的只有被删除的结点,其他迭代器不会收到影响。

也就是说:

  • vector insert,pos会失效,因为它的物理空间是一个连续的数组,首先它可能会扩容,就会导致野指针问题;就算不扩容,挪动了数据,pos的意义也改变了,所以pos也失效了
  • vector erase,pos会失效,因为此时pos的意义已经改变了,为了解决这个问题,我们可以使用pos重新接收erase函数的返回值,其指向删除元素的下一个元素
  • list insert,pos不会失效,因为list底层是一个链表,每个结点都是独立的,insert后的数据属于新增的结点,而pos还是指向原来的位置
  • erase pos,pos会失效,因为pos指向的结点已经被释放了,为了解决这个问题,和vector erase的解决方法一样,我们也可以使用pos重新接收erase函数的返回值

以上就是本章的所有内容,谢谢大家!!!

相关推荐
SRY122404192 小时前
javaSE面试题
java·开发语言·面试
李元豪3 小时前
【智鹿空间】c++实现了一个简单的链表数据结构 MyList,其中包含基本的 Get 和 Modify 操作,
数据结构·c++·链表
无尽的大道3 小时前
Java 泛型详解:参数化类型的强大之处
java·开发语言
ZIM学编程3 小时前
Java基础Day-Sixteen
java·开发语言·windows
我不是星海3 小时前
1.集合体系补充(1)
java·数据结构
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
UestcXiye3 小时前
《TCP/IP网络编程》学习笔记 | Chapter 9:套接字的多种可选项
c++·计算机网络·ip·tcp
一丝晨光4 小时前
编译器、IDE对C/C++新标准的支持
c语言·开发语言·c++·ide·msvc·visual studio·gcc
阮少年、4 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
代码小鑫4 小时前
A027-基于Spring Boot的农事管理系统
java·开发语言·数据库·spring boot·后端·毕业设计