本篇将介绍 STL 中 list 的使用及实现原理
list是一种序列式容器,它采用链式结构,其中的元素采用节点方式储存,每个节点独占“一块”内存,它对于空间的运用有绝对的精准,一点也不会浪费。为什么呢?如下:
- 完整实现代码
list概述
我们知道,vector中的数据是连续的储存在一块静态内存上,而此处的list与之不同,它使用“节点”的方式储存数据,节点之间采用指针链接。每一个数据独占一个节点,每当插入数据时,它申请一个节点的内存,在删除数据时,它释放一个节点内存。
基于上述原因,它不会浪费任何空间,对空间的运用有着绝对的精准;这样做带来的另一个好处是在任何位置插入或者删除数据时,它的时间复杂度永远是常数;但也因为这样,list不能像vector那样通过下标随机访问元素,它只能使用迭代器依序巡防其中元素。
内部结构
节点结构:
节点代码
template <class T>
struct __list_node {//list节点
typedef void* void_pointer;
void_pointer next;//指向下一个节点的指针
void_pointer prev;//指向前一个节点的指针
T data;//数据元素
};
- list结构代码
template <class T, class Alloc = alloc>
class list {//list结构,省略的内部很多代码
//...
protected:
typedef __list_node<T> list_node;
//...
public:
typedef list_node* link_type;//link_type就是节点的指针类型
//...
protected:
link_type node;//重点是这个,它是链表的头结点
//...
};
我们可以看到,list不仅是一个双向链表,而且是一个循环链表。在它的内部只是简单的维护了一个指向list_node的指针,该指针表示链表的头结点。
当链表为空时,它的next
和prev
都指向自己,下面代码说明了这一点:
//...
protected:
void empty_initialize() {
node = get_node();//获取一个节点
node->next = node;//指向自己
node->prev = node;//指向自己
}
//...
public:
list() { empty_initialize(); }//无参数的构造函数
基本操作
对于list,在接口上它与vector相似,但在内部实现上千差万别。
我们先看它包括(但不限于)哪些接口:
push_back();//尾插
pop_back();//尾删
push_front();//头插
pop_front();//头删
insert();//任意位置插入
erase();//删除某个节点
remove();//移除某个值
unique();//去重复
与vector相比,list多了头插、头删、去重等几个接口,这些接口在特定的场合下有很大的作用。
下面通过源码窥探它的内部奥秘。
- insert()
如上图所示,我们在链表的尾部插入了几个数据。
那么它的内部是怎样做的呢?
iterator insert(iterator position, const T& x) {
link_type tmp = create_node(x);//创建一个节点
tmp->next = position.node;//改变指针
tmp->prev = position.node->prev;
(link_type(position.node->prev))->next = tmp;
position.node->prev = tmp;
return tmp;
}
结合代码和图片看,它的插入操作:
- 用要插入的x创建一个新节点;
- 让新的节点的next指向position,prev指向position的前一个节点
- 让posotion的前一个节点的next指向新节点,position的prev指向新节点
通过上面三步就在list中插入了一个新节点。也可以看出,在某个位置插入一个节点,实际上是把新节点插到该位置的前面。
上面是STL最底层的插入算法,对于insert(),它有多种插入接口,这些插入接口在底层都调用了上面的。其它接口如下:
iterator insert (iterator position, const value_type& val);
//将value插入到position位置处;void insert (iterator position, size_type n, const value_type& val);
//在position的前面插入n个value,下图证明之:
- template < class InputIterator>
void insert (iterator position, InputIterator first, InputIterator last);
//将first至last之间(左闭右开)的数据插入到position之前。
下图证之:
- erase()
代码:
iterator erase(iterator position) {
link_type next_node = link_type(position.node->next);
link_type prev_node = link_type(position.node->prev);
prev_node->next = next_node;
next_node->prev = prev_node;
destroy_node(position.node);
return iterator(next_node);
}
我们还是通过画个图来解释:
相比于插入来说,删除更简单一点,它是这样做的:
- 让前一个节点的next指向下一个节点;
- 让下一个节点的prev指向前一个节点;
- 释放被删除的节点。
当然删除也提供了多重接口:
- iterator erase (iterator position);
//删除position处的节点;- iterator erase (iterator first, iterator last);
//删除fisrt至last间的节点
- push_back()
下面我们看看push_back()
内部做了什么。
void push_back(const T& x) { insert(end(), x); }
是不是很吃惊,没错! push_back()
就是这么简单,它只是简单调用了insert()
接口
- pop_back()
void pop_back() {
iterator tmp = end();
erase(--tmp);
}
pop_back()
稍有不同,它先取得尾节点,在减减之,之后调用erase()
,为什么要这样呢?原因在于,STL中,几乎所有的区间表示,都是左闭右开,所以end()
标记的是最后一个节点的下一个节点,要删除最后一个节点,当然得如上面那样做了。
- remove()
template <class T, class Alloc>
void list<T, Alloc>::remove(const T& value) {
iterator first = begin();
iterator last = end();
while (first != last) {
iterator next = first;
++next;
if (*first == value) erase(first);
first = next;
}
}
关于remove()
,它用来删除list中所有值为value的节点,注意是“所有”。
它的逻辑再简单不过了,将list遍历一遍,并调用erase()
擦掉值域与value相等的节点。不过,注意到在删除的时候,需要保存被删节点的下一个节点,因为这会牵扯到迭代器失效的问题。
迭代器失效简单来说就是:当对(某些)容器进行增(删、改)操作之后,(可能会)导致其内存重新分配,而原来的迭代器将(可能)指向非法的位置。
如此一来,如不对原迭代器处理而直接使用之,将发生不可预料的错误。
所以对上述问题的处理方法之一是:在删之前保存该迭代器之前的位置,在删除之后,再将之前的位置节点赋给该迭代器。
- uniqie()
如上图所示,unique()
将链表重连续的重复元素删掉,
template <class T, class Alloc>
void list<T, Alloc>::unique() {
iterator first = begin();
iterator last = end();
if (first == last) return;
iterator next = first;
while (++next != last) {
if (*first == *next)
erase(next);
else
first = next;
next = first;
}
}
上面是它的代码。先用两个迭代器分别指向第一个和第二个元素,然后通过循环比较两个迭代器的值,若相等则删掉第二个值,否则两个迭代器都后移,这样将链表遍历一遍就可以去掉链表中所有“连续的”重复元素了。
总结
在上面我们分析了list的结构以及部分的操作,其实关于list常用的也就是上面那几个了。
下面来对比总结一下vector和list的异同。
vector和list都是序列式容器,这里的序列式指的是元素可序的,说通俗一点就是,它们内部元素之间的结构就像用绳子串起来一样,都支持在尾部插入、删除数据,都不用手动管理空间,它们内部算法都会帮它们动态的增长空间,内部都储存同类型的数据元素。但是不同的是:
vector:
- 内部管理了一块静态的空间,在需要增大时要通过一些繁琐的操作(开辟新空间、搬移元素、释放旧空间)完成;
- 在非尾端插入删除数据的效率极低(需要大量的搬移元素);
- 迭代器是普通类型的指针;
- 支持通过下标随机访问元素。
list:
- 内部管理了一个双向循环链表,每个数据独自占有“一块空间”,在插入、删除时只需处理单个节点即可,不过所整体也占用了更大的空间(除数据之外还要储存两个指针);
- 在任何位置插入数据的时间复杂度都是常数级的,相对于vector,在非尾位置增删数据效率更高;
- 迭代器需要单独维护一个list的节点;
- 不支持随机访问元素。比如,欲访问第6个元素,vector只需
v[5]
即可访问到,而list必须从头节点开始遍历至第六个节点。
——谢谢!
参考资料:
- 模拟实现的list