前言
C++引入了面向对象的思想,相比于C语言,一个类能更好地对一些数据结构进行管理和操作。
在C语言中,我们动态开辟一个个的节点,并且用指针将他们连接起来,形成链式结构,链式结构在物理上不连续,在逻辑上连续
在C++中,基于面向对象的思想,用来管理这链式结构的类便应运而生,从本质上讲,list是带头双向循环链表
目录
1.list的简介
我们学习STL时,文档是我们的利器,学会查文档会让学习事半功倍,以下是两个C++文档网站:- 官网:www.cppreference.com
- 常用网站(更新至C++11):www.cplusplus.com
list的文档介绍:
- list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
- list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向
其前一个元素和后一个元素。 - list与forward_list非常相似:最主要的不同在于forward_list是单链表,只能朝前迭代,已让其更简单高效。
- 与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率更好。
(优点)
- 与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list还需要一些额外的空间,以保存每个节点的相关联信息
(缺点)
STL作为泛型编程的典范,我们的list类自然就是一个类模板
template <class T>
class list {
//...
}
模板参数T很显然,就是我们想要在节点里插入的元素的类型,可以是int, char等内置类型数据,也可以是string, vector等自定义类型数据
list <int> lt1;
list <char> lt2;
list <vector<int>> lt3;
list <string> lt4;
再来看一下list类的成员变量
Node* _head;------>节点指针
节点是一个结构体,list用一个结构体指针来维护整张链表,相信大家对于节点并不陌生
template <class T>
struct __list_node {
__list_node(const T& x = T())
: _next(nullptr)
, _prev(nullptr)
, _data(x) {
}
__list_node<T>* _next;
__list_node<T>* _prev;
T _data;
};
在这里,节点也是用类模板定义出来的,因为要和list的模板参数T
我们typedef了一下
typedef __list_node<T> Node;
2.vector的常见接口及模拟实现
2.1 list类对象获取元素和迭代器的接口
和string、vector的迭代器是原生指针不同,由于list的每个节点的物理地址不连续,所以list的迭代器不能用原生指针来替代
于是我们创建了一个类iterator,将解引用、++等操作进行了类的运算符重载,从表观的调用上达到了和原生指针相同的效果
由于迭代器分为普通迭代器和const迭代器,参照STL源码,我们将迭代器设计成带有3个模板参数的一个类
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<T, Ref, Ptr>(Node* node)
: _node(node) {
}
Ref operator*() {
return _node->_data;
}
self operator++() {
_node = _node->_next;
return __list_iterator(_node);
}
self operator++(int) {
__list_iterator<T, Ref, Ptr> tmp(_node);
_node = _node->_next;
return tmp;
}
self operator--() {
_node = _node->_prev;
return __list_iterator(_node);
}
self operator--(int) {
__list_iterator<T, Ref, Ptr> tmp(_node);
_node = _node->_prev;
return tmp;
}
bool operator!=(const self& it2) {
return (_node != it2._node);
}
bool operator==(const self& it2) {
return (_node == it2._node);
}
Ptr operator->() {
//In order to get structures' member if T is struct type
return &_node->_data;
}
};
这样一来,解引用一个迭代器,虽然表面上是解引用了一个类,但是运算符重载告诉我们实际上解引用拿到了iterator类的成员_node的_data
其他操作同样道理,注意的是,list带头结点,也就是哨兵位
接口名称 | 接口作用 |
---|---|
begin() | 返回头节点的下一个节点的迭代器 |
end() | 返回头节点的迭代器 |
接口的模拟实现:
//Iterator/
iterator begin() {
return iterator(_head->_next);
}
iterator end() {
return iterator(_head);
}
const_iterator begin() const{
return const_iterator(_head->_next);
}
const_iterator end() const{
return const_iterator(_head);
}
2.2 list类对象的常见构造
函数名 | 功能 |
---|---|
list | 无参构造 |
list(int n, const T& x = T()) | 构造并初始化n个x(x的缺省值为0) |
template <class InputInerator> list(InputInerator first, InputInerator last) | 使用迭代器进行初始化构造 |
list(const vector&v) | 拷贝构造函数 |
~list() | 析构函数 |
operator= | 赋值重载,将一个list对象赋值给另一个list对象 |
多种构造函数的使用:
void TestList()
{
std::list<int> first; // 无参构造
std::list<int> second (4,100); //构造并初始化4个100
std::list<int> third (second.begin(),second.end()); // 用second的迭代器区间构造
std::list<int> fourth (third); // 拷贝构造
}
接口的模拟实现:
- 构造函数
//无参默认构造函数
list()
:_head(new Node)
{
_head->_next = _head;
_head->_prev = _head;
_head->_data = T();
}
//构造并初始化n个x
list(int n, const T& x = T())
:_head(new Node) {
_head->_next = _head;
_head->_prev = _head;
_head->_data = T();
Node* cur = _head;
for(size_t i = 0; i < (size_t)n; i++) {
Node* prev = cur;
Node* next = cur->_next;
Node* newnode = new Node(x);
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = next;
next->_prev = newnode;
cur = cur->_next;
}
}
//用迭代器区间构造
template <class InputIterator>
list(InputIterator first, InputIterator last)
:_head(new Node)
{
_head->_next = _head;
_head->_prev = _head;
_head->_data = T();
while(first != last) {
push_back(*first);
first++;
}
}
- 拷贝构造
list(const list<T>& lt)
:_head(new Node)
{
_head->_next = _head;
_head->_prev = _head;
_head->_data = T();
for(auto e: lt) {
push_back(e);
}
}
- 析构函数
//析构函数
~list() {
clear(); //复用的clear函数在下面modify里面讲
delete _head;
_head = nullptr;
}
- 赋值重载
//赋值重载
vector& operator=(vector v) {
swap(v);
return *this;
}
2.3 list类对象的容量操作
接口名称 | 接口作用 |
---|---|
size() | 返回数组中元素个数 |
empty() | 判断数组是否为空数组 |
接口的模拟实现:
- size()
size_t size() {
size_t ret = 0;
Node* cur = _head;
while (cur->_next != _head) {
cur = cur->_next;
ret++;
}
return ret;
}
- empty()
// 判空
bool empty() {
return _head->_next == _head;
}
2.4 list类对象获取/修改元素接口
接口名称 | 接口作用 |
---|---|
front() | 获取第一个有效节点元素的值 |
back() | 获取最后一个有效节点元素的值 |
iterator insert(iterator pos, const T& x) | 在pos迭代器之前插入1个元素为x的节点 |
push_front(const T& val) | 头插1个元素为val的节点 |
push_back(const T& val) | 尾插1个元素为val的节点 |
erase(iterator pos) | 删除pos迭代器对应的节点 |
erase(iterator first, iterator last) | 删除迭代器左闭右开区间内的节点 |
pop_front() | 头删一个节点 |
pop_back() | 尾删一个节点 |
clear() | 清除所有元素并将size置为0 |
swap() | 交换两个类对象 |
注意下insert和erase的返回值
- insert的插入是在pos前面插,返回的是被插的节点的迭代器
- erase删完之后返回被删的最后一个节点的下一个位置的迭代器
- 还有用erase删完有效节点后如果再去删,STL的list并没给断言错误,但是也运行崩溃;我这边直接用assert拿捏
接口的模拟实现:
front()和back()纠结了一下如果只剩一个头节点会怎样,用STL的试了一下会直接运行崩掉,所以我模拟时候加了assert看看是不是只剩头节点
- front()
const T& front() {
assert(!empty());
return *begin();
}
- back()
const T& back() {
assert(!empty());
return _head->_prev->_data;
}
- insert()
//插入元素,在迭代器之前插
//inserting new elements before the element at the specified position
//return An iterator that points to the first of the newly inserted elements.
iterator insert(iterator pos, const T& val) {
Node* cur = _head;
while(cur->_next != pos._node) {
cur = cur->_next;
}
Node* prev = cur;
Node* next = cur->_next;
Node* newnode = new Node(val);
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = next;
next->_prev = newnode;
return iterator(newnode);
}
- push_front()
void push_fornt(const T& val) {
Node* head = _head->_next;
Node* newnode = new Node(val);
_head->_next = newnode;
newnode->_prev = _head;
newnode->_next = head;
head->_prev = newnode;
}
- push_back()
void push_back(const T& val) {
Node* tail = _head->_prev;
Node* newnode = new Node(val);
tail->_next = newnode;
newnode->_prev = tail;
newnode->_next = _head;
_head->_prev = newnode;
}
- erase()返回被删节点的下一个节点的迭代器
//return An iterator pointing to the element that followed the last element erased by the function call
iterator erase(iterator pos) {
assert(!empty());
Node* cur = _head;
while(cur != pos._node) {
cur = cur->_next;
}
Node* prev = cur->_prev;
Node* next = cur->_next;
prev->_next = next;
next->_prev = prev;
delete cur;
cur = nullptr;
return iterator(next);
}
iterator erase(iterator first, iterator last) {
iterator it = first;
while(it != last) {
it = erase(it);
}
return it;
}
- pop_front()
void pop_front() {
assert(!empty());
Node* head = _head->_next;
_head->_next = head->_next;
head->_next->_prev = _head;
delete head;
head = nullptr;
}
- pop_back()
void pop_back() {
assert(!empty());
Node* tail = _head->_prev;
tail->_prev->_next = _head;
_head->_prev = tail->_prev;
delete tail;
tail = nullptr;
}
- clear()
void clear() {
erase(begin(), end());
}
- swap()
void swap(list<T>& lt) {
if(_head != lt._head) {
std::swap(_head, lt._head);
}
}
3.刷题
老规矩奉上若干链表的习题leetcode:
4.vector和list的对比
vector是一个顺序表,物理上连续,逻辑上也连
- 支持下标随机访问,O(1)
- 尾插尾删比较快,O(1)
- 头插头删、中间插入删除时间复杂度华为O(N)
list是一个带头双向循环链表,物理上不连续,逻辑上连续
- 不支持下标随机访问,必须O(N)遍历
- 任意位置的插入/删除比较快,O(1)
vector | list | |
---|---|---|
底层结构 | 动态顺序表,一段连续空间 | 带头结点的双向循环链表 |
随机访问 | 支持随机访问,访问某个元素效率O(1) | 不支持随机访问,访问某个元素效率O(N) |
插入和删除 | 任意位置插入和删除效率低,需要搬移元素,时间复杂度为O(N),插入时有可能需要增容,增容:开辟新空间,拷贝元素,释放旧空间,导致效率更低 | 任意位置插入和删除效率高,不需要搬移元素,时间复杂度为O(1) |
空间利用率 | 底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高 | 底层节点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低 |
迭代器 | 原生指针 | 把节点指针封装为结构体 |
迭代器失效 | 在插入元素时,要给所有的迭代器重新赋值,因为插入元素有可能会导致重新扩容,致使原来迭代器失效,删除时,当前迭代器需要重新赋值否则会失效 | 插入元素不会导致迭代器失效,删除元素时,只会导致当前迭代器失效,其他迭代器不受影响 |
使用场景 | 需要高效存储,支持随机访问,不关心插入删除效率 | 大量插入和删除操作,不关心随机访问 |
- 使用场景举个例子
vector:学生管理系统,因为我们需要大量的随机访问,去不停地看某某个学生的信息;另一方面,一个学校的学生不会频繁增加减少,一般一年的9月份新生入学才会增加,6月毕业生毕业才会减少
list:超市货物管理:每天都得增加新货物,减少卖掉的货物,有大量的插入删除;另一方面,超市管理者不会去经常随机访问某一件货物怎么样,只要另外维护一个某一类货物的整体情况的vector就行