list

本篇将介绍 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的指针,该指针表示链表的头结点。

当链表为空时,它的nextprev都指向自己,下面代码说明了这一点:

//...
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;
  }

这里写图片描述

结合代码和图片看,它的插入操作:

  1. 用要插入的x创建一个新节点;
  2. 让新的节点的next指向position,prev指向position的前一个节点
  3. 让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);
  }

我们还是通过画个图来解释:
这里写图片描述
相比于插入来说,删除更简单一点,它是这样做的:

  1. 让前一个节点的next指向下一个节点;
  2. 让下一个节点的prev指向前一个节点;
  3. 释放被删除的节点。

当然删除也提供了多重接口:

  • 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必须从头节点开始遍历至第六个节点。

——谢谢!


参考资料

【作者:果冻 http://blog.youkuaiyun.com/jelly_9

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值