我们在上一节中了解了vector这个容器,但是它有一些缺点
vector缺点:
1.头部和中部插入删除效率低,O(N)因为需要挪动数据
2.插入数据空间不够需要增容,增容需要开辟空间,拷贝数据,释放旧空间,代价很大
优点:支持下标的随机访问,间接就很好地支持排序二分堆算法等
所以我们出现了list容器,可以弥补vector的一些缺陷
list优点:
1.list头部,中间插入不再需要挪动数据,效率高。O(1)
2.list插入数据是新增节点,不需要增容
缺点:
不支持随机访问
因为他们两个形成了很好的互补,所以在使用的时候是相辅相成的,其实也就是我们之间学习过的顺序表与链表
所以我们这一章来介绍一下list
list的介绍及使用
list 的文档介绍1. list 是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。2. list 的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。3. list 与 forward_list 非常相似:最主要的不同在于 forward_list 是单链表,只能朝前迭代,已让其更简单高效。4. 与其他的序列式容器相比 (array , vector , deque) , list 通常在任意位置进行插入、移除元素的执行效率更好。5. 与其他序列式容器相比, list 和 forward_list 最大的缺陷是不支持任意位置的随机访问,比如:要访问 list的第6 个元素,必须从已知的位置 ( 比如头部或者尾部 ) 迭代到该位置,在这段位置上迭代需要线性的时间开销;list 还需要一些额外的空间,以保存每个节点的相关联信息 ( 对于存储类型较小元素的大 list 来说这可能是一个重要的因素)
其实list对于我们而言就是之前学习过的带头双向循环链表
list的构造
我们同vector与string一样,对ist用我们两种方式进行基础的遍历(list底层为链表,所以不支持[]遍历)
#include<iostream>
#include<list>
#include<Windows.h.>
using namespace std;
void print_list(const list<int>& lt)//只读迭代器
{
list<int>::const_iterator it1 = lt.begin();
while (it1 != lt.end())
{
cout << *it1 << " ";
++it1;
}
cout << endl;
}
void test_list1()
{
list<int> lt1;
lt1.push_back(1);
lt1.push_back(2);
lt1.push_back(3);
lt1.push_back(4);
list<int>::iterator it1 = lt1.begin();//正常迭代器
while (it1 != lt1.end())
{
cout << *it1 << " ";
++it1;
}
cout << endl;
list<int>lt2(lt1);//拷贝构造
print_list(lt2);
list<int>lt3;
lt3.push_back(10);
lt3.push_back(20);
lt3.push_back(30);
lt3.push_back(40);
lt1 = lt3;//赋值
//只要一个容器支持迭代器,就可以支持范围for
for (auto e : lt1)//范围for遍历
{
cout << e << " ";
}
cout << endl;
list<int>::reverse_iterator rit = lt1.rbegin();//反向迭代器
while (rit != lt1.rend())
{
cout << *rit << " ";
++rit;
}
cout << endl;
}
//迭代器方向划分:单项(forword_list),双向(list),随机(vector)
//使用属性:正向,反向+const
int main()
{
test_list1();
system("pause");
return 0;
}
接下来我们对list的常用接口进行测试
正常尾插,头插,尾删头删
插入:无法直接随机插入,因为底层是链表,所以需要借助迭代器找到位置随后插入,并且完成插入之后pos指针还会留在插入位置
顺逆序打印
注意:这里有与vector一样的迭代器失效的问题,只是list中的是当删除后it就失去了指向,会变为随机值,erase会返回删除后的下一个节点解决了这个问题
迭代器失效即迭代器所指向的节点的无效,即该节 点被删除了 。因为 list 的底层结构为带头结点的双向循环链表 ,因此 在 list 中进行插入时是不会导致 list 的迭代 器失效的,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响 。
list的深度剖析及模拟实现
#pragma once
namespace wxy
{
template<class T>//创建模板
struct __list_node//定义节点结构体
{
__list_node<T>*_next;
__list_node<T>*_prev;
T _data;
__list_node(const T& x=T())//结点初始化列表
: _data(x)
, _next(nullptr)
, _prev(nullptr)
{}
};
// __list_iterator<T, T&, T*> -> iterator
// __list_iterator<T, const T&, const T*> -> const_iterator
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)
{}
//*it
Ref operator*()//重载*it,返回引用(节点中存的值)
{
return _node->_data;//节点数据
}
Ptr operator->()//重载->,对指针的解引用,返回节点
{
return &_node->_data;
}
//++it,前++
Self& operator++()//迭代器中++重载,完成指向下一节点的操作
{
_node = _node->_next;
return *this;//返回指向后的节点
}
//it++后++
Self operator++(int)
{
Self tmp(*this);//将+之前的结点存在tmp中
//_node = _node->_next;
++(*this);//将指针指向下一节点
return tmp;//返回+之前的结点
}
//it--
Self& operator--()
{
_node = _node->_prev;
return *this;
}
Self operator--(int)
{
__list_iterator<T> tmp(*this);
//_node = _node->_prev;
--(*this);
return tmp;
}
//it!=end()
bool operator!=(const Self& it)//重载!=
{
return _node != it._node;
}
bool operator==(const Self& it)
{
return _node == it._node;
}
};
//以上都为迭代器与节点的定义,以及其相关运算符的重载
template<class T>
class list//list类
{
typedef __list_node<T> Node;//说明节点
public:
typedef __list_iterator<T, T&, T*> iterator;//说明普通迭代器
typedef __list_iterator<T, const T&, const T*> const_iterator;//说明只读迭代器
iterator begin()//对begin的调用函数
{
return iterator(_head->_next);//返回的是头结点的下一节点,因为在双向循环链表当中第一个节点为头节点下一个
}
const_iterator begin()const//对只读迭代器中的begin调用
{
return const_iterator(_head->_next);
}
iterator end()//对end进行调用
{
return iterator(_head);//返回的是头节点,因为在双向循环链表中,头节点就是遍历的最后一个节点
}
const_iterator end()const//只读迭代器中end调用
{
return const_iterator(_head);
}
//带头双向循环链表
list()//list构造函数
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
list(const list<T>& lt)//list拷贝构造
{
_head = new Node;//指针赋初值
_head->_next = _head;
_head->_prev = _head;
/*const_iterator it = lt.begin();//迭代器方式依次遍历赋值
while (it != lt.end())
{
push_back(*it);
++it;
}*/
for (auto e : lt)//范围for赋值,本质还是迭代器
{
push_back(e);
}
}
//lt1=lt3
/*list<T> operator=(const list<T>& lt)//重载=(拷贝构造并赋值)
{
if (this != <)//lt1=lt1
{
clear();//清理lt1
for (auto e : lt)//范围for将lt3中的值依次赋给lt1
{
push_back(e);
}
}
return *this;//返回lt1
}*/
//lt1=lt3
list<T>& operator=(list<T> lt)//拷贝构造现代写法,传入lt3,会引起拷贝构造出一份lt3
{
swap(_head, lt._head);//直接交换本来的lt1(this)与拷贝构造出来的lt3,相当于将头指针指向交换
return *this;//返回lt1(也就是拷贝出的lt3)
}//原先的lt1(this),指向了拷贝出的lt3,而存有lt1内容的lt3则在出作用域时完成析构
~list()//析构函数
{
clear();
delete _head;
_head = nullptr;
}
void clear()//清理函数
{
iterator it = begin();
while (it != end())
{
erase(it++);//依次迭代销毁
}
}
void push_back(const T& x)//尾插,同我们的双向带头链表
{
/*Node* tail = _head->_prev;
Node* newnode = new Node(x);
tail->_next = newnode;
newnode->_prev = tail;
newnode->_next = _head;
_head->_prev = newnode;*/
insert(end(), x);//在end(头结点前)插入
}
void pop_back()
{
//erase(iterator(_head->prev));//释放迭代器头结点的前一个
erase(--end());
}
void push_front(const T& x)
{
insert(begin(), x);
}
void pop_front()
{
erase(begin());
}
void insert(iterator pos, const T& x)//插入,同双向带头链表
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(x);
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
}
void erase(iterator pos)//删除
{
assert(pos != end());//头节点所在的迭代器是end
Node* cur = pos._node;
Node*prev = cur->_prev;
Node* next = cur->_next;
delete cur;
prev->_next = next;
next->_prev = prev;
}
private:
Node* _head;
};
上面便是我们的list的关键接口的实现,构造了一个简易的架构,剖析了在stl中list是如何实现的,下面我们对上面的情况进行一些测试
经典的插入输出
插入删除并且调用只读迭代器进行输出
拷贝构造以及=赋值
list与vector的对比
事实上,这两个容器的区别就是链表与顺序表的区别