目录
一、list类简介
(一)概念
(二)list与string和vector的区别
二、list类使用
(一)构造函数
(二)迭代器
(三)list capacity
(四)list element access
(五)list modifiers
1、push_back与emplace_back的区别
2、注意
(六)list特有的接口
1、splice
2、sort
三、list类模拟实现
一、list类简介
(一)概念
在 C++ 中,list
是标准模板库(STL)中的一个容器类。它属于序列容器,用于存储一系列的元素。list
容器是一个带头双向循环链表,这意味着每个节点包含数据以及指向前一个节点和后一个节点的指针。具体声明如下:
list类模板的第一个参数是类型,第二个参数是内存池(提高内存申请效率),在学习过程中关注第一个参数即可。
注意:该类的头文件为<
list>
,在使用list类模板之前,需要包含这个头文件,例如:
#include <list>
(二)list与string和vector的区别
list类中访问、遍历与修改的核心方式是使用迭代器,并没有【 下标+ [ ] 】的概念(虽然可以实现,但效率极低),因为元素之间的物理空间并不连续,只是逻辑上连续。而string和vector既可以使用迭代器,也可以使用【 下标+ [ ] 】进行访问、遍历与修改。
相对与string和vector接口,list类模板接口没有扩容的概念了,访问数据也只有访问头节点与尾节点的概念,修改部分接口最主要的就是头插与尾插,头删与尾删;swap操作也是直接交换地址,若直接使用算法库中的swap函数的话会进行深拷贝,效率会低。同时也有list类自身专用的接口,在本篇博客中作介绍。
二、list类使用
(一)构造函数
构造函数( constructor ) | 接口说明 |
---|---|
list (size_type n, const value_type& val = value_type()) | 构造的 list 中包含 n 个值为 val 的 元素 |
list () | 构造空的 list |
拷贝构造函数 | |
list (InputIterator first, InputIterator last) | 用 [first, last) 区间中的元素构造 list |
(二)迭代器
可暂时将迭代器理解成一个指针,该指针指向list中的某个节点。
函数声 明 | 接口说明 |
---|---|
begin() + end() | 返回第一个元素的迭代器+ 返回最后一个元素下一个位置的迭代器 |
rbegin() + rend() | 返回第一个元素的 reverse_iterator, 即r end 位置; 返回最后一个元素下一个位 置的 reverse_iterator, 即r begin 位置 |
迭代器图解如下:
注意:
① begin与end为正向迭代器,对迭代器执行++操作,迭代器向后移动;
② rbegin(end)与rend(begin)为反向迭代器,对迭代器执行++操作,迭代器向前移动。
(三)list capacity
函数声明 | 接口说明 |
---|---|
empty | 检测 list 是否为空,是返回 true ,否则返回 false |
size | 返回 list 中有效节点的个数 |
(四)list element access
函数声明 | 接口说明 |
---|---|
front | 返回 list 的第一个节点中值的引用 |
back | 返回 list 的最后一个节点中值的引用 |
(五)list modifiers
函数声明 | 接口说明 |
---|---|
push_front | 在 list 首节点前插入值为 val 的节点 |
pop_front | 删除 list 中第一个节点 |
push_back | 在 list 尾部插入值为 val 的结点 |
pop_back | 删除 list 中最后一个节点 |
insert | 在 pos 位置中插入值为 val 的节点 |
erase | 删除 pos 位置的节点 |
swap | 交换两个 list 中的节点 |
clear | 清空 list 中的有效节点 |
emplace_front | 类似于头插 |
emplace_back | 类似于尾插 |
1、push_back与emplace_back的区别
① push_back的参数是模板类型的引用值,什么类型的list就要传什么类型的值进去,是固定的。传递参数过程如下代码所示:
class Pos
{
public:Pos(int row = 0, int col = 0):_row(row), _col(col){}private:int _row;int _col;
};void test02()
{Pos p1(1, 2);list<Pos> lt1;lt1.push_back(p1);lt1.push_back(Pos(24,36));lt1.push_back({ 24,36 });
}
先构造出一个Pos对象,然后使用push_back进行尾插,第二、三个push_back函数的参数中构造的是Pos类的匿名对象和隐式转化的临时对象,临时对象有常性,所以push_back的参数使用const value_type& val接收。
② emplace_back是一个函数模板,它的可变模板参数可以传多个参数,能够自动识别实参并推导类型;但不能走隐式类型转化,因为传 {3,3} 的话会被自动推导为【用{ }括起来的列表】,因此形参并不知道是什么类型,就会报错,原因是类型不明确。而使用push_back的list已经实例化为Pos类型的对象,所以push_back的形参已经默认为是Pos类型了,可以进行隐式类型转化。
lt1.emplace_back({ 24,36 });//报错
但可以使用下面这种方法:
lt1.emplace_back( 24,36 );
可以直接传初始化Pos对象的值,它的可变模板参数会把这两个数识别为 int 整形,会把这两个整形变成参数包往后传,直接初始化节点对象(直接构造),会更加高效。支持传递构造对象的参数。现阶段知道有这样的用法就行。
也只有emplace_back直接传递构造对象的参数的效率会高,传递对象或匿名对象的时候,效率与push_back没有区别。代码如下所示:
void test03()
{Pos p2(1, 2);list<Pos> lt2;lt2.emplace_back(p2);//构造 + 拷贝构造lt2.emplace_back(Pos(24, 36));//构造 + 拷贝构造//lt2.emplace_back({ 24,36 });这样写会因为类型不明确而报错lt2.emplace_back(24,36);//直接构造,比前面的效率都高
}
编译器也可能把【匿名对象Pos(24,36)】的参数直接传给节点进行构造,提高效率。
2、注意
list里面没有find,但可以使用算法库中的find函数,参数为一段迭代区间,没找到就返回迭代空间的最后一个迭代器,如it.end(),找到之后就返回目标节点的位置,可以进行插入或删除操作。
(六)list特有的接口
函数声明 | 接口说明 |
---|---|
reverse | 逆置 |
merge | 两个链表归并,有序的列表归并后依旧有序,会把参数中的列表拿走进行归并, 归并后内容为空 |
unique | 去重复值,只保留一个,前提为有序,需要提前使用sort进行排序 |
remove | 去除,可以理解为是 find 和 erase 的结合,删除的是迭代器指向位置的数据 |
remove_if | 后面凡是带_if的都是满足了条件的才会进行,参数是仿函数 |
splice | 转移,把一个链表的值转移给另一个链表,参数中被转移的链表完成转移操作后为空。 |
sort | 排序(默认升序) |
1、splice
splice可以用来调整链表中节点的顺序,最近最少用缓存算法LRU会使用到:
void test04()
{list<int> lt1 = { 1,2,3,4,5 };//LRUint x;while (cin >> x){auto pos = find(lt1.begin(), lt1.end(), x);if (pos != lt1.end())lt1.splice(lt1.begin(), lt1, pos);//在lt1链表中把pos指向的数据转移到lt1.begin()前面}
}
中断输入:输入【ctrl+z加换行】或【ctrl+c】结束流,正常结束,后面的程序继续执行。
注意:linux中【ctrl+c】是杀死程序。
2、sort
① 使用list中的sort,排的是升序,可以理解为底层传的是【小于 < 】的概念(仿函数less)。若想要升序们可以sort后使用reverse进行逆置,也可以在底层传【大于 > 】的概念(仿函数greater),这些仿函数都是类模板,支持任意类型的比较。如下代码所示:
void test05()
{list<int> lt1 = { 1,4,5,3,2 };list<int> lt2 = { 1,4,5,3,2 };//排升序lt1.sort();for (const auto& i : lt1){cout << i << " ";}cout << endl;//排降序//greater<int> gt;有名对象少用,匿名对象用得更多一点lt2.sort(greater<int>());for (const auto& i : lt2){cout << i << " ";}cout << endl;
}
② 使用vector进行排序,但vector并没有sort这个接口,需要调用算法库中的sort:
void test06()
{vector<int> v1 = { 1,4,5,3,2 };vector<int> v2 = { 1,4,5,3,2 };sort(v1.begin(), v1.end());//升序sort(v2.begin(), v2.end(), greater<int>());//降序}
那为什么list不用算法库中的sort而要自己实现sort接口?因为list的迭代器不匹配算法库中sort函数参数中迭代器的类型。迭代器在功能角度有以下分类:
- 单向迭代器:只支持++;例如:forward_list(forword是向前的意思) / unoredered_xxx(哈希表系列);
- 双向迭代器:支持++ / --;例如:list / map / set(链表和树);
- 随机迭代器:支持++ / -- / + / - ;例如:string/vector。
三种迭代器在某种程度上是继承关系:随机迭代器是父类(范围最大),双向迭代器是随机迭代器的子类(父类的特殊类,范围略小),单向迭代器是双向迭代器的子类(子类的特殊类,范围最小);那么函数参数支持双向迭代器的,为随机迭代器的类也可以使用,但单向迭代器不可用。
算法库中的sort函数参数接收的迭代器类型为随机迭代器(底层为快排,可以在c++的参考手册中查询得到函数参数接收的迭代器类型,或者在c++的参考手册中函数参数的标准命名就有暗示),而list迭代器的类型为双向迭代器,所以无法使用算法库中的sort函数,需要自己实现一个sort排序的接口。
③ 两种排序的效率对比:
代码一:直接在vector和list进行sort对比
void test07()
{srand(time(0));//使随机值的种子每次运行都不同const int N = 1000000;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();cout << "vector 排序时间:" << end1 - begin1 << endl;cout << "list 排序时间:" << end2 - begin2 << endl;}
在release版本运行的结果:
代码二:把 lt2 的代码传给vector进行排序再把数据传递回来 && 直接在sort进行排序进行对比
void test08()
{srand(time(0));//使随机值的种子每次运行都不同const int N = 1000000;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<int> v1(lt1.begin(), lt1.end());//拷贝vectorsort(v1.begin(), v1.end());//vector排序lt1.assign(v1.begin(), v1.end());//拷贝回lt2int end1 = clock();int begin2 = clock();lt2.sort();//list直接排int end2 = clock();cout << "list copy vector sort copy list sort:" << end1 - begin1 << endl;cout << "list sort:" << end2 - begin2 << endl;
}
在release版本运行的结果:
总结:若是少量数据的话使用list的sort接口进行排序的效率与vector调用算法库的sort接口的效率差不多,但是有大量数据的话vector调用算法库的sort排序效率会快很多。
三、list类模拟实现
成员全部公有的类可以用关键字struct进行声明;成员公私有混合的类用关键字class进行声明。
#pragma once
#include<assert.h>
#include<iostream>
using namespace std;namespace zyb
{//成员全部公有的类可以用关键字struct进行声明//成员公私有混合的类用关键字class进行声明//【节点类模板】//调用该类模板的作用:创建一个有数据但无指向的结点template<class T>struct list_Node{T _date;//存放数据list_Node<T>* _next;//指向前一个结点的指针list_Node<T>* _prev;//指向后一个结点的指针//全缺省做默认构造,new Node()时会自动调用,实现节点的数据初始化//缺省值给T的匿名对象来接收多种类型;//T在list创建时就已经定下list_Node(const T& x = T()):_date(x), _next(nullptr), _prev(nullptr){}};//【节点指针为核心的迭代器类模板】//以【节点模板类的指针】为核心,进行一系列操作符重载,使其符合迭代器的行为template<class T>struct list_Iterator//不进行限定,因为list会不断用到该结构体的成员,也会像节点一样作为隐形封装{//给【节点模板类】另起一个名,意为节点类型using Node = list_Node<T>;//给【迭代器模板类】另起一个名,意为迭代器类型using Self = list_Iterator<T>;Node* _node;//成员变量,是节点类型的指针list_Iterator(Node* node)//使用【节点模板类指针】构造迭代器:_node(node){}T& operator*()//实现迭代器的正确解引用{retrun _node->_date;}//出了该函数_data不会消失,因为节点还存在;//使用引用可以用别名修改_data,达到和指针一样解引用了能够修改指向元素的数据//返回数据本身T* operator->()//实现迭代器的对复合数据类型的正确引用{retrun &_node->_date;//先与成员访问运算符结合,再与取地址符结合;返回的是数据的地址}//返回类型为T的指针类型Self& operator++()//前置++{_node = _node->_next;return *this;//节点还在,可以使用引用返回}Self operator++(int)//后置++{Self tmp(*this);//先备份一个原来位置的指向就行_node = _node->_next;return tmp;}Self& operator--()//前置--{_node = _node->_prev;return *this;//节点还在,可以使用引用返回}Self operator--(int)//后置--{Self tmp(*this);_node = _node->_prev;return tmp;}bool operator!=(const Self& s)//判断迭代器是否相等{return _node != s._node;}};//创建list类模板//链表,元素为节点,访问节点中的内容需要正确行为的迭代器template<class T>class list{//默认私有,节点模板类,意为节点类型using Node = list_Node<T>;public://迭代器模板类,意为迭代器类型using iterator = list_Iterator<T>;iterator begin()//定义迭代器的指向{return iterator(_head->_next);}iterator end()//定义迭代器的指向{return iterator(_head);}void empty_init()//对节点进行空初始化{_head = new Node();//new了一个结点的空间,并且调用了结点类模板的默认构造(有数据,无指向)_head->_next = _head;//修改指向_head->_prev = _head;//修改指向}list()//无参的默认构造{//调用节点的空初始化函数创造出一个指向自己的头结点,//数据为T类型的默认构造出来的数据empty_init();}void push_back(const T& x)//插入数据{Node* new_node = new Node(x);Node* tail = _head->_prev;new_node->_prev = tail;tail->_next = new_node;new_node->_next = _head;_head->_prev = new_node;}private:Node* _head;//指向头结点的指针(哨兵位指针)};
}
以上内容仅供分享,若有错误,请多指正