
◆博主名称:少司府
欢迎来到少司府的博客☆*: .。. o(≧▽≦)o .。.:*☆
⭐数据结构系列个人专栏:
⭐C++基础个人专栏:
⭐琢玉成器终有时,笔底生花夺锦归
目录
[1.1 标准库里的list](#1.1 标准库里的list)
[1.2 迭代器访问与push_back、push_front](#1.2 迭代器访问与push_back、push_front)
[1.3 emplace_back()](#1.3 emplace_back())
[1.4 insert()与find()](#1.4 insert()与find())
[1.5 reverse()与sort()](#1.5 reverse()与sort())
[1.6 unique()去重、splice()转移](#1.6 unique()去重、splice()转移)
[2.1 单节点的模拟实现](#2.1 单节点的模拟实现)
[2.2 iterator](#2.2 iterator)
[2.3 list](#2.3 list)
[2.3.1 框架](#2.3.1 框架)
[2.3.2 begin()和end()](#2.3.2 begin()和end())
[2.3.3 默认构造和拷贝构造](#2.3.3 默认构造和拷贝构造)
[2.3.4 =重载和析构](#2.3.4 =重载和析构)
[2.3.5 插入删除](#2.3.5 插入删除)
一、list基础相关
1.1 标准库里的list

如图,我们点击链接就可以跳转到标准文档中查看list的相关介绍。
list是一个序列容器,它允许恒定时间内的插入删除 ,可以在任意的序列位置,并且支持迭代器。
1.2 迭代器访问与push_back、push_front

如图,list的迭代器的用法和vector、string没什么区别。
但是,需要注意的是:

list的迭代器是双向迭代器,前面我们学的vector和string的迭代器是随机迭代器,forward_list(单链表)的迭代器是单向迭代器,而后面要学的stack和queue没有迭代器,他们本身也是属于空间配置器的范畴。
cpp
sort(lt.begin(), lt.end()); // error,不支持,要求随机迭代器,使用不匹配的迭代器会报错
如图,这行代码会报错,原因就是sort(排序算法,底层是自省排序)要求的是随机迭代器。传入双向迭代器属于"缩小了范围"。
cpp
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_front(3);
lt.push_front(4);
list<int>::iterator it = lt.begin();
while (it != lt.end())
{
cout << *it << ' ';
it++;
}
cout << endl;
for (auto e : lt) cout << e << ' ';
cout << endl;
我们来看这段代码,push_back、push_front分别是尾插和头插,且,list支持迭代器也支持范围for。

结果如图,我们可以看到,sort排序默认按照从小到大排。
要用到sort,我们需要algorithm的算法头文件。
1.3 emplace_back()
emplace_back的具体用法和push_back类似,都是尾插,都支持传类型。
cpp
list<int> lt;
lt.emplace_back(1);
lt.emplace_back(2);
lt.emplace_back(3);
lt.emplace_back(4);
for (auto e : lt) cout << e << ' '; // 与push_back类似
cout << endl;
list<A> lt1;
A aa1(1, 1);
lt1.push_back(aa1);
lt1.push_back(A(2, 2));
lt1.emplace_back(aa1);
lt1.emplace_back(A(2, 2));
cout << endl;
// 支持直接传构造A对象的参数emplace_back
lt1.emplace_back(3, 3); // 前面是构造+拷贝构造,这里相当于直接构造
有所区别的是:empalce_back支持直接传对象的参数。

结果如图,在push_back的时候,一直是构造+拷贝构造,但是emplace_back直接传对象参数的话相当于直接构造。
1.4 insert()与find()

如图,insert支持在迭代器位置插入删除 ,find查找函数属于std算法库的,如果没找到目标值x就返回第二个迭代器参数。
1.5 reverse()与sort()

如图,list中有自己的reverse逆置,也可以调用算法库里面的逆置,算法库里面的reverse需要传入迭代器区间参数。
cpp
lt.sort(greater<int>());
sort默认是升序排序的,如果想要降序,需要传入一个仿函数。
1.6 unique()去重、splice()转移
cpp
list<int> lt;
lt.push_back(5);
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(4);
lt.sort();
lt.unique(); // 去重,要求链表是有序的
for (auto e : lt) cout << e << ' ';
cout << endl;
如图,list中的unique函数去重要求链表是有序的。

结果如图,去重删去了多余的4。
splice使用的两种场景:
1)、把一个链表的节点转移到另一个链表。
cpp
// 把一个链表的节点转移到另一个链表
std::list<int> mylist1, mylist2;
std::list<int>::iterator it;
// set some initial values:
for (int i = 1; i <= 4; ++i)
mylist1.push_back(i); // mylist1: 1 2 3 4
for (int i = 1; i <= 3; ++i)
mylist2.push_back(i * 10); // mylist2: 10 20 30
it = mylist1.begin();
++it; // points to 2
mylist1.splice(it, mylist2);
如图,mylist1: 1 10 20 30 2 3 4,剪切转移,mylist2的值转移到mylist1中2的前面,之后mylist2就空了。
2)、移动当前链表本身的节点
cpp
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
lt.push_back(6);
for (auto e : lt) cout << e << ' ';
cout << endl;
int x;
cin >> x;
auto i = find(lt.begin(), lt.end(), x);
lt.splice(lt.begin(), lt, i, lt.end());
for (auto e : lt) cout << e << ' ';
cout << endl;
如图,find找到x返回相应位置的迭代器,splice截取x及其之后的链表数据并将其头插到链表lt中。

二、list模拟实现
2.1 单节点的模拟实现
cpp
template<class T>
struct list_node
{
T _data;
list_node<T>* _next;
list_node<T>* _prev;
list_node(const T& data = T())
:_data(data)
,_next(nullptr)
,_prev(nullptr)
{ }
};
如图,我们用一个类模板来模拟实现单节点list_node,双向链表list的node节点需要前驱指针prev和指向下一个节点的指针next,自己实现默认构造。

如图,库里面的node节点的细节封装在_List_node_base类中。
2.2 iterator
在实现迭代器 的时候,我们会发现普通迭代器和const迭代器有很多功能是相同的,那么我们是否可以只实现一个,另一个交给编译器复用代码呢?答案是肯定的。
我们可以通过模板来实现这样一个功能:
cpp
template<class T,class Ref,class Ptr>
struct list_iterator
{
typedef list_node<T> Node;
typedef list_iterator<T,Ref,Ptr> Self;
Node* _node;
list_iterator(Node* node)
:_node(node)
{ }
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
Self& operator++()
{
_node = _node->_next;
return *this;
}
Self& operator--()
{
_node = _node->_prev;
return *this;
}
Self operator++(int)
{
Self tmp(*this);
_node = _node->_next;
return tmp;
}
Self operator--(int)
{
Self tmp(*this);
_node = _node->_prev;
return tmp;
}
bool operator!=(const Self& s)
{
return _node != s._node;
}
bool operator==(const Self& s)
{
return !(*this != s);
}
};
如图,模板参数中T是类型;Ref是T类型的引用,在实例化时可以传普通T引用,也可以是const T引用;
Ptr是T的指针,传参和引用一样。
例如:
cpp
typedef list_iterator<T,T&,T*> iterator; // 用模板来实现普通iterator和const iterator两种类
typedef list_iterator<T,const T&,const T*> const_iterator;
这样,我们就实现了iterator和const_iterator两个迭代器。
2.3 list
2.3.1 框架
cpp
template<class T>
class list
{
typedef list_node<T> Node;
public:
//typedef list_iterator<T> iterator;
//typedef const_list_iterator<T> const_iterator;
typedef list_iterator<T,T&,T*> iterator; // 用模板来实现普通iterator和const iterator两种类
typedef list_iterator<T,const T&,const T*> const_iterator;
private:
Node* _head;
size_t _size;
};
如图,成员变量就只有哨兵位头节点的指针和数据个数_size。
我们将迭代器typedef一下方便书写。
2.3.2 begin()和end()
cpp
iterator begin()
{
// return iterator(_head->_next);
return _head->_next; // 隐式类型转换
}
iterator end()
{
// return iterator(_head);
return _head;
}
const_iterator begin() const
{
return _head->_next;
}
const_iterator end() const
{
return _head;
}
如图,我们实现普通版本和const版本的begin和end。
可以直接返回_head->_next,发生隐式类型转换返回iterator类型。
2.3.3 默认构造和拷贝构造
cpp
void empty_init()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
_size = 0;
}
list()
{
empty_init();
}
如图,默认构造和拷贝构造都需要初始化链表,因此我们先封装一个接口empty_init。
申请一个哨兵位的头节点,并且自己指向自己。
cpp
list(initializer_list<int> il)
{
empty_init();
for (auto& e : il)
{
push_back(e);
}
}
list(const list<T>& lt)
{
empty_init();
for (auto& e : lt)
{
push_back(e);
}
}
如图,先初始化链表,再利用范围for遍历尾插到链表中。
其中,initializer_list 是C++11引入的初始化列表代理类 ,内部保存首元素指针+元素长度,核心作用是让容器、自定义类可以直接使用 {} 批量初始化。
2.3.4 =重载和析构
cpp
list<T>& operator=(list<T> lt)
{
swap(lt);
return *this;
}
~list()
{
clear();
delete _head;
_head = nullptr;
}
void clear()
{
auto it = begin();
while (it != end())
{
it = erase(it);
}
}
void swap(list<T>& lt)
{
std::swap(_head, lt._head);
std::swap(_size, lt._size);
}
= 的重载方式和之前一样,调用swap完成,= 的参数 list<T> lt 在传入是编译器会生成临时对象,实际上传入的是实参的拷贝,swap交换之后析构的是临时对象的资源,不需要自己额外写tmp。
2.3.5 插入删除
cpp
void push_back(const T& x)
{
//Node* newnode = new Node(x);
//Node* tail = _head->_prev;
//tail->_next = newnode;
//newnode->_prev = tail;
//newnode->_next = _head;
//_head->_prev = newnode;
//_size++;
insert(end(), x);
}
iterator insert(iterator pos, const T& x)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(x);
newnode->_next = cur;
cur->_prev = newnode;
newnode->_prev = prev;
prev->_next = newnode;
_size++;
return newnode;
}
iterator erase(iterator pos)
{
assert(pos != end());
Node* prev = pos._node->_prev;
Node* next = pos._node->_next;
prev->_next = next;
next->_prev = prev;
delete pos._node;
_size--;
return next;
}
void pop_back()
{
erase(--end());
}
void pop_front()
{
erase(begin());
}
void push_front(const T& x)
{
insert(begin(), x);
}
size_t size() const
{
return _size;
}
bool empty() const
{
return _size == 0;
}
如图,push_back可以直接复用insert,插入删除的逻辑在数据结构部分就讲了,这里不再过多赘述。具体看这里:点击查看双向链表章节
三、list与vector的对比
vector与list都是STL中非常重要的序列式容器,由于两个容器的底层结构不同,导致其特性以及应用场景不同。
|------------|----------------------------------------------------------|--------------------------------|
| | vector | list |
| 底层存储结构 | vector底层是一块连续的空间,缓存命中率高,支持下标的随机访问 | list底层空间不连续,缓存命中率低,不支持下标的随机访问。 |
| 插入删除 | 插入删除效率低,需要搬移元素,时间复杂度为O(N),插入时有可能需要增容,拷贝元素释放旧空间开辟新空间效率更低。 | 效率较高,只需要申请新节点或销毁该节点,再改变指针指向。 |
| 迭代器 | 调用原生迭代器指针 | 自己封装实现迭代器 |
| 使用场景 | 需要高效存储,支持随机访问,不关心插入删除效率 | 大量插入删除,不支持下标随机访问 |
本期的分享就到这里,如果觉得博主的文章比较对胃口的话,可以点一个小小的关注~
您的三连是我持续更新的动力~