STL中list的底层实现机制及常用操作

一、list概述

list是双向链表,相较于vector的连续性空间,list就显得复杂许多,它的好处是每次插入或删除一个元素,就配置或释放一个元素空间。因此,list对于空间的运用有绝对的精准,一点也不浪费。而且,对于任何位置的元素插入或元素移除,list永远是常数时间。

二、list的迭代器

list不能再像vector一样以普通指针作为迭代器,因为其节点不保证在存储空间中连续存在。list迭代器必须有能力指向list的节点,并且有能力进行正确的递增、递减、取值、成员存取等操作。

由于SGI list是一个双向链表,迭代器必须具备前移、后移的能力,所以list提供的是Bidirectional Iterator。如下图所示:
在这里插入图片描述
以下是list迭代器的设计:

template<class T, class Ref, class Ptr>
struct __list_iterator {
	/* 遵守STL迭代器规范,需要定义这5个类型 */
	typedef bidirectional_iterator_tag iterator_category; //双向迭代器
	typedef T value_type;
	typedef Ptr pointer;
	typedef Ref reference;
	typedef ptrdiff_t difference_type;
	
	typedef __list_node<T>* link_type; //链表的节点	

	link_type node; //节点指针
};

list的迭代器含有一个节点指针,指向list维护的链表中的某个节点。
list迭代器的前置++操作,就是前进一个节点:

self& operator++() { 
    node = (link_type)((*node).next); //迭代器的node指针指向链表下一个元素
    return *this;
}

list迭代器的后置++操作:

self operator++(int){
	self tmp = *this;
	++*this;
	return tmp;

对于list迭代器的前置–和后置–,也是同样的原理。
再看一看取值操作:

reference operator*() const { return (*node).data; } //返回所指节点中的数据值

迭代器的成员存取操作:

pointer operator-> const { return &(operator*()); }  //迭代器的成员存取运算子的标准做法

list有一个重要性质:插入操作(insert)和接合操作(splice)都不会造成原有的list迭代器失效。这在vector是不成立的。因为vector的插入操作可能造成记忆体重新配置,导致原有的迭代器全部失效。甚至list的元素删除操作(erase)也只有“指向被删除元素”的那个迭代器失效,其他迭代器不受影响。

三、list的数据结构

SGI list不仅是一个双向链表,而且还是一个环状双向链表。所以它只需要一个指针,便可以完整表现整个链表:

template <class T, class Alloc = alloc>
class list {
	typedef __list_node<T> list_node; //节点
	typedef list_node* link_type;     //链接节点
	
	typedef simple_alloc<list_node, Alloc> list_node_allocator; //空间配置器
	
	typedef __list_iterator<T, T&, T*>    iterator;            //迭代器
	
protected:
	link_type node; //成员
	
};

list内部的成员只有link_type node,其类型为list_node*,表示链表中的一个节点。下面来看一看list_node的定义:

template <class T>
struct __list_node {
  typedef void* void_pointer;
  void_pointer next; //指向下一个元素的指针
  void_pointer prev; //指向上一个元素的指针
  T data; //数据
};//如下图所示:

在这里插入图片描述
如果让指针node指向可以置于尾端的一个空白节点,node便符合STL对于“前闭后开”区间的要求,成为last迭代器。如下图所示:
在这里插入图片描述
虚线表示指向上一个元素,实线表示指向下一个元素。

四、list的构造与析构

默认构造函数

void empty_initialize() { 
    node = get_node();                             //分配内存
    node->next = node;
    node->prev = node;
}

list() { empty_initialize(); }

node是list中的链表头节点,当默认构造的时候,list里面没有元素,所以初始化一个链表头,首先分配内存,然后将首尾指针指向自己。

拷贝构造函数

template <class InputIterator>
void range_initialize(InputIterator first, InputIterator last) {
	empty_initialize();                            //初始化链表头
	insert(begin(), first, last);                  //插入所有元素
}

list(const list<T, Alloc>& x) {
    range_initialize(x.begin(), x.end());
}

list的拷贝构造会调用range_initialize,range_initialize函数会先初始化list的链表头,然后再通过insert来复制所有的节点,下面来看一看insert:

template <class T, class Alloc>
void list<T, Alloc>::insert(iterator position, const T* first, const T* last) {
  for ( ; first != last; ++first)
    insert(position, *first);
}

list的insert函数会从头到尾,调用全局函数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;
}

insert首先会分配一个节点,然后插入双向链表中。如下图所示:
在这里插入图片描述
析构函数

~list() {
    clear();
    put_node(node);
}

clear()的作用是清除头结点外的所有结点,其定义如下:

template <class T, class Alloc> 
void list<T, Alloc>::clear()
{
  link_type cur = (link_type) node->next;           //得到第一个元素
  while (cur != node) {                             //遍历链表
    link_type tmp = cur;
    cur = (link_type) cur->next;                    //从链表删除节点
    destroy_node(tmp);                              //释放节点
  }
  node->next = node;
  node->prev = node;
}

clear函数会遍历整个链表,将每个元素从链表中删除,然后释放它,destroy_node的定义如下:

void destroy_node(link_type p) {
    destroy(&p->data);                               //析构对象
    put_node(p);                                     //释放节点
}

destroy_node首先通过destroy析构对象,然后通过put_node释放内存,put_node定义如下:

/* 通过空间配置器释放内存 */
void put_node(link_type p) { list_node_allocator::deallocate(p); }

五、插入和删除元素

插入元素
可以通过insert和push_front还有push_back插入元素,insert表示在指定位置插入节点,上面已经讨论过了,这里不讨论了,下面来看一看push_front和push_back。

push_front()表示在链表头插入节点,其定义如下:

void push_front(const T& x) { insert(begin(), x); }

push_back()表示在链表尾插入节点,其定义如下:

void push_back(const T& x) { insert(end(), x); }

注:所谓“插入”,是指“插入在…之前”。

删除元素
可以通过erase、pop_front、pop_back删除节点。erase删除指定节点,pop_front删除链表第一个节点,pop_back删除链表最后一个节点。

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);
  }

首先将指定节点从双向链表中删除,然后调用destroy_node析构还有释放节点。

pop_front()定义如下:

void pop_front() { erase(begin()); }

pop_back()定义如下:

void pop_back() { 
    iterator tmp = end();
    erase(--tmp);
}

end()指向的是最后一个节点的下一个位置,所以需要将迭代器减一(即往后移动一个位置)才能得到最后一个节点。

六、其他操作

transfer()
将某连续范围的元素迁移到某个特定位置之前。技术上讲很简单,节点直接的指针移动而已。这个操作为其他复杂操作如splice,sort,merge等奠定了良好的基础。

void transfer(iterator position, iterator first, iterator last) {  
    if (position != last) {  
      (*(link_type((*last.node).prev))).next = position.node; //(1)  
      (*(link_type((*first.node).prev))).next = last.node;    //(2)  
      (*(link_type((*position.node).prev))).next = first.node;//(3)  
      link_type tmp = link_type((*position.node).prev);       //(4)  
      (*position.node).prev = (*last.node).prev;              //(5)  
      (*last.node).prev = (*first.node).prev;                 //(6)  
      (*first.node).prev = tmp;                               //(7)  
    }  
 }  //如下图所示

在这里插入图片描述

七、总结

初始化操作

list<Type> a;                                        //定义一个Type类型的链表a
list<Type> a(10);                                    //定义一个Type类型的链表a,并设置初始大小为10
list<Type> a(10, 1);                                 //定义一个Type类型的链表a,并设置初始大小为10,且初始值都为1
list<Type> a(b);                                     //定义并用链表b初始化链表a_

除此之外,还可以直接使用数组来初始化向量:

int array[] = {1, 2, 3, 4, 5};
list<int> a(array, array+5);                         //将数组n的前5个元素作为链表a的初值

与迭代器相关

begin();                                             //链表的第一个元素
end();                                               //指向最后一个元素的下一个位置
rbegin();                                            //反向迭代器指针,指向最后一个元素
rend();                                              //反向迭代器指针,指向第一个元素的前一个元素

与容量相关

empty();                                             //判断链表为空
size();                                              //容器大小
resize();                                            //重置有效元素个数

与元素访问相关

front();                                             //访问链表中第一个元素
back();                                              //访问链表中最后一个元素

添加元素

push_front(const T& x);                              //头部添加元素
push_back(const T& x);                               //尾部添加元素
insert(iteratpr it, const T& x);                     //任意位置插入一个元素
insert(iterator it, int n, const T& x);              //任意位置插入n个相同元素
insert(iterator it, iterator first, iterator last);  //插入另一个向量的[first,last]间的数据

删除元素

pop_front();                                         //头部删除元素
pop_back();                                          //尾部删除元素
erase(iterator it);                                  //任意位置删除一个元素
erase(iterator first, iterator last);                //删除[first, last]之间的元素
clear();                                             //清空所有元素

其他操作

assign(int nSize, const T& x);                       //类似于初始化时用数组进行赋值
swap(list&);                                         //交换两个同类型容器的元素
merge();                                             //合并两个列表的元素(默认升序排列)
splice(iterator it, list&);                          //在任意位置拼接入另一个list
unique();                                            //删除容器中相邻的重复元素

可以看到,list 与 vector、deque 的用法基本一致,除了以下几处不同:

● list 为双向迭代器,故不支持it+=i;

● list 不支持下标访问和at方法访问。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值