一:list特性
list为带哨兵位双向循环链表,支持任意位置的插入和删除。
与(array,vector,deque)相比,list的移除元素效率更高。
最大缺陷是不支持[]重载,不支持随机访问,只能通过迭代器进行线性开销的迭代。
二:list的排序
list无法使用算法库中的sort排序,算法库的sort底层是快排,需要三数取中,需要传入随机访问迭代器,所以list不适用。但是list的类域中自身提供了一种sort排序,底层是归并排序。
通过图片分析,list的排序比vector调用算法库中的排序效率低,甚至比先将list数据拷贝给vector,再调用算法库的排序的效率还要低。
三:迭代器
1:迭代器失效
erase迭代器失效
insert迭代器不失效
2:迭代器种类
单向迭代器,只能++,如单链表和哈希表。
双向迭代器,支持++和--,如双向循环链表
随机访问迭代器,支持++--和+-,如vector和string。
4:迭代器的价值
使用封装,不能暴露底层的实现细节
提供统一的访问方式,降低使用成本,这也是STL设计巧妙之处。
5:模拟实现
5.1:原生指针迭代器和封装迭代器类
vector和string在物理空间上是连续的,我们之前模拟实现的时候,采用typedef将原生指针定义为迭代器,vector和string是双向迭代器,迭代器的++和--都是跨过一个对象的大小,也就是T的大小。因为在物理空间连续,所以可以直接用原生指针。
但是list在物理空间上不连续,如果用原生指针,因为不清楚前后节点在物理空间上谁是前谁是后,所以用原生的++--就无法访问到下一个节点,因此我们可以使用封装+运算符重载实现迭代器。
对于每一个节点,在C语言的时候是用struct,在这里因为不知道节点是该给公共权限还是私有,所以也直接设计成struct。
template<class T>
struct list_node
{
list_node<T>* prev;
list_node<T>* next;
T _data;
//构造函数
list_node(const T& x)
:prev(nullptr)
,next(nullptr)
,_data(x)
{}
};
迭代器代码
template<class T,class Ref, class Ptr>
struct __list_iterator//采用封装迭代器的方式
{
typedef list_node<T> node;
typedef __list_iterator<T, Ref, Ptr> Self;
node* pnode;//表示当前迭代器自己指向的节点
__list_iterator(node* p)
:pnode(p)
{}
Ptr operator->()
{
return &pnode->_data;
}
Self& operator++()
{
//隐含this
pnode = pnode->next;
return *this;
}
Ref operator*()
{
return pnode->_data;
}
bool operator!=(const Self& it)
{
return pnode != it.pnode;
}
};
迭代器模板中的Ref和Ptr后续讲解。迭代器类中设计好相关运算符的重载即可。
5.2:list类初步代码
template<class T>
class list
{
typedef list_node<T> node;
public:
typedef __list_iterator<T,T&,T*> iterator;
typedef __list_iterator<T, const T&,const T*> const_iterator;
5.3:const迭代器
const迭代器错误写法
typedef __list_iterator<T> iterator;
const list<T>::iterator it=lt.begin();
如果给it迭代器添加const权限,那么it就无法更改,也就是it无法++--,这样显然不行。
因此我们可以考虑给迭代器类模板添加第二个参数。在list类模板中将普通迭代器和const迭代器使用同一个类模板创建2种类型,也就是第二个参数分别传普通的T和const T。因为const迭代器无法修改迭代器所指向的内容,因此传引用没有拷贝开销,所以第二个参数分别传T&和const T&。
因为多了一个参数,比如设计*重载的时候,返回值需要设置为__list_iterator<T, Ref>&,这样名字过长,因此我们可以这样。(Ptr下面再讲)
typedef __list_iterator<T, Ref, Ptr> Self;
这里非const和const迭代器采用添加Ref参数的好处是,如果不这样写,就需要在迭代器类模板中分别实现每个函数的const和非const版本,这样一来代码重复量太大,不美观。
5.4:->重载
list类模板中,T是对象的类型,如果T是struct类型,想要通过迭代器解引用访问T对象的数据,就必须在迭代器类中提供->重载。
Ptr operator->()
{
return &pnode->_data;
}
这里使用Ptr,是考虑有const和非const的迭代器,在list中我们就可以这样传参。
typedef __list_iterator<T,T&,T*> iterator;
typedef __list_iterator<T, const T&,const T*> const_iterator;
因为如果访问struct的成员,需要使用->,而struct指针可以做到,因此第三个参数要设计成指针形式,对于非const传T*,对于const传const T*,因此在迭代器的类模板中就可以添加第三个参数Ptr,表示指针,那么->重载的返回值就可以为Ptr。
list<Pos>::const_iterator it = lt.begin();
while (it != lt.end())
{
//it->_row++;
cout << it->_row << ":" << it->_col << endl;
我们一般可能遇到这种情景,为什么->这样重载就可以访问到数据呢?
it->我们知道应该是it.operator->(),这样的返回值是Pos*,Pos*_row这如何访问数据?
实际上,这里it->_row的本来面貌是it->->_row,这样就容易懂了,Pos*->_row就是结构体指针访问到数据,而编译器为了可读性,在这里省略了一个->,因此这里比较难懂。
5.5:模拟总代码
template<class T>
class list
{
typedef list_node<T> node;
public:
typedef __list_iterator<T,T&,T*> iterator;
typedef __list_iterator<T, const T&,const T*> const_iterator;
list()
{
empty_initialize();
}
iterator begin()
{
//返回的是迭代器
//用匿名对象
return iterator(_head->next);
}
iterator end()
{
//返回的是迭代器
//用匿名对象
//end是最后一个节点的下一个位置
return iterator(_head);
}
void push_back(const T& x)
{
insert(end(), x);
}
void push_front(const T& x)
{
insert(begin(), x);
}
void pop_front()
{
erase(begin());
}
void pop_back()
{
erase(--end());
}
iterator insert(iterator pos, const T& x)
{
node* newnode = new node(x);
node* cur = pos.pnode;//记录当前位置
node* prev = pos.pnode->prev;
prev->next = newnode;
newnode->prev = prev;
newnode->next = cur;
cur->prev = newnode;
++size;
return iterator(newnode);
}
iterator erase(iterator pos)
{
node* prev = pos.pnode->prev;
node* next = pos.pnode->next;
prev->next = next;
next->prev = prev;
delete pos.pnode;
--size;
return iterator(next);
}
list(const list<T>& lt)
{
empty_initialize();
list<T> temp(lt.begin(), lt.end());
swap(temp);//临时对象出函数自动调用析构,注意先给原先链表初始化,否则随机值给了temp,出错
}
list<T>& operator=(list<T> lt)
{
//千万别传引用
swap(lt);
}
template<class InputIterator>
list(InputIterator first, InputIterator last)
{
empty_initialize();
while (first != last)
{
push_back(*first);
++first;
}
}
void empty_initialize()
{
_head = new node(T());
_head->next = _head;
_head->prev = _head;
size = 0;
}
size_t getsize()const
{
return size;
}
bool empty()const
{
return size == 0;
}
~list()
{
clear();
}
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);
}
}
void swap(list<T>& lt)
{
std::swap(_head, lt._head);
std::swap(size, lt.size);
}
private:
node* _head;//哨兵位的头结点
size_t size;
};
5.6:拷贝构造现代写法
和前面一样,直接 使用算法库的swap是一个深拷贝,代价太大,所以我们可以换个思路使用算法库的swap交换头节点即可,因为list只要头结点知道就可以访问下面的数据。
void swap(list<T>& lt)
{
std::swap(_head, lt._head);
std::swap(size, lt.size);
}
拷贝构造比如lt1(lt2),目的是将lt2的内容给lt1,不能改变lt2的内容,因此需要传const&
拷贝构造不能直接传值传参,必须用传引用传参,否则无限递归。
list(const list<T>& lt)
{
empty_initialize();
list<T> temp(lt.begin(), lt.end());
swap(temp);//临时对象出函数自动调用析构,注意先给原先链表初始化,否则随机值给了temp,出错
}
//构造函数
template<class InputIterator>
list(InputIterator first, InputIterator last)
{
empty_initialize();
while (first != last)
{
push_back(*first);
++first;
}
}
而如果通过临时对象构造一个lt2,可以再设计一个前后迭代器的构造函数,一开始设计的时候是没有empty这个函数的,因为如果将list初始化的代码直接放在构造函数中,这里我们设计拷贝构造的时候,lt1对象还没有实例化,仍然是一个类,所以不会去调用构造函数,所以头节点就是一个随机值。所以如果将随机值交换给temp,temp出了当前函数作用域后自动调用析构,析构的就是随机值,因此我们需要将初始化的代码单独使用一个empty函数。
5.7:=重载现代写法
和之前一样,用传值传参,不改变形参的特性。
list<T>& operator=(list<T> lt)
{
//千万别传引用
swap(lt);
}
四:list和vector的区别
底层结构:vector是动态顺序表,连续空间,list是带头双向循环链表,物理空间不连续。
随机访问:vector支持[]重载随机访问,效率O(1),list不支持随机访问,访问效率O(N)。
插入和删除:vector任意位置插入和删除的效率比较低(尤其是前中部分元素),需要挪动数据,时间复杂度为O(N),插入的时候需要增容,增容是异地扩容,拷贝元素,释放旧空间,效率更低。list任意位置插入和删除元素效率高,效率为O(1),不需要挪动数据。
空间利用率:vector底层为连续空间,不易造成内存碎片,空间利用率高,高速缓存命中率高,list底层节点动态开辟,小节点容易造成内存碎片,空间利用率低,高速缓存命中率低。
迭代器:vector是原生态指针,list采用封装+函数重载。
迭代器失效:vector的insert可能导致扩容,迭代器失效,erase也会导致当前pos位置的迭代器失效。list的insert迭代器不会失效,erase导致当前迭代器失效。
使用场景:vector适用于高效存储,随机访问,不关心插入删除效率。list适用于大量插入和删除操作,不关心随机访问。