STL源码剖析(四)序列式容器--list

1. 关于list

1.1 首先在介绍list之前先来观察一下list的节点结构:

template <class T>
struct __list_node {
	typedef void* void_pointer;
	void_pointer prev;
	void_pointer next;
	T data;
};
  • 从上述节点结构很清楚list是一个双向链表

1.2 与vector的区别

  • 相比于vector的连续线性空间,list显得更为复杂;但list每次插入或删除一个元素时,就将对应的空间释放掉,因此,对于任何位置的插入或元素删除,list永远是常数时间

2. list的迭代器

  • list中的元素由于都是节点,因此其迭代器递增时取用的是下一个节点,递减时取用上一个节点,取值时取的是节点的数据值,成员取用时取用的是节点的成员
  • 由于list是双向链表,迭代器必须具备前移、后移的能力,因此,list提供的是Bidirectional Iterators
  • 关于list的迭代器的设计,大抵就是递增、递减运算符、取值以及比较运算符的重载,在这就不细说

3. list的数据结构

  • list是一个双向链表,而且是一个环状双向链表:
//取首元素,node是尾端的一个空节点
iterator begin()  {  return  (link_type) ((*node).next); }
//取尾元素的下一个,即node
iterator end()     {  return node;  }
//为空,说明只有node
bool empty() const  {  return node->next == node; }
size_type size()  const {
	size_type result = 0;
	distance(begin(), end(), result);
	return result;
}
reference front()    { return *begin(); }
reference back()    {  return *(--end());  }
  • 可见,STL将node作为一个空节点放在尾端,与首元素、尾元素相连,形成一个环

4. list内存构造

  • 还是老样子,以实例说明:
#include <iostream>
#include <list>
#include <algorithm>
using namespace std; 

int main(int argc, char** argv) {
	list<int> ilist;
	cout << "size = " << ilist.size() << endl; //size = 0
	
	ilist.push_back(0);
	ilist.push_back(1);
	ilist.push_back(2);
	ilist.push_back(3);
	ilist.push_back(4);
	cout << "size = " << ilist.size() << endl;  //size = 5
	
	list<int>::iterator ite;
	for (ite = ilist.begin(); ite != ilist.end(); ++ite)
		cout << *ite << ' ';        //0 1 2 3 4 
	cout << endl;
	return 0;
}
  • 当创建一个list时,调用list的默认构造函数构造一个空list:
public:
	list()   {  empty_initialize();  }   //默认构造函数
protected:
	void empty_initialize() {
		node = get_node();       //配置一个节点空间
		node->next = node;
		node->prev = node;
	}
//既然说到配置空间,就将配置、释放、构造、销毁一并提了吧
//配置一个节点
link_type get_node() 	{ return list_node_allocator::allocate(); }  
//释放一个节点
void put_node(link_type p)	{ list_node_deallocator::deallocate(p); }
//产生一个节点,带有元素值
link_type create_node(const T& x)  {
	link_type p = get_node();
	construct(&p->data, x);
	return p;
}
//销毁一个节点
void destroy_node(link_type p) {
	destroy(&p->data);
	put_node(p);
}
  • 其中push_back一个元素,实则调用insert(与指针插入节点一样)

5. list的元素操作:

操作 功能
push_front 插入一个节点作为头节点
push_back 插入一个节点作为尾节点
erase 删除迭代器所指的节点
pop_front 移除头节点
pop_back 移除尾节点
clear 清除整个链表
remove 移除某个元素
unique 删除连续相同的元素的重复项
splice 将迭代器所指元素或链表接合于目的迭代器之前
merge 将两个链表合并
reverse 将链表逆置
sort 排序
  • 以下重点理解下transfer以及sort
  1. transfer:
// 将 [first,last) 内的所有元素搬移到 position 之前
void transfer(iterator position, iterator first, iterator last)  
{  
    if (position != last)   // 如果last == position, 则相当于链表不变化, 不进行操作  
    {  
        (*(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)
    }  
}

在这里插入图片描述

  • 理解了上述transfer的操作,splice、merge、reverse也就迎刃而解
  1. sort:这个sort源码虽然不长,但理解起来却是最为费劲的
    废话不多说,让我们来一起解析它:
//侯捷老师在本书中讲解本函数使用quick  sort
//但看过该段源码的都知道不是使用quick sort,而是merge sort,这应该是一处错误吧
template <class T, class Alloc>
void list<T, Alloc>::sort() {
	//如果是空链表或仅有一个元素,不进行任何操作
	if (node->next == node  || link_type(node_next)->next == node)
		return;
	list<T, Alloc> carry;
	list<T, Alloc> counter[64];   //维护数组,最大一层可存储2 ^64 - 1个元素
	int fill = 0;
	while (!empty())
	{
		//每次取出list中的一个元素
		carry.splice(carry.begin(), *this, begin());  
		int i = 0;
		while (i < fill && !counter[i].empty())  {
				counter[i].merge(carry);  //将当前carry与count[i]中的数据归并
				carry.swap(counter[i++]);  //交换carry与count[i]中的数据
		}
		carry.swap(counter[i]);       //将count[i]中的数据存入count[i+1]中
		if (i == fill)
			++fill;
	}
	for (int i = 1; i < fill; ++i)
		counter[i].merge(counter[i - 1]);
	swap(counter[fill - 1]);
}

具体分析:假定list中元素为45,21,1,35,28,3,6,75
则此时调用sort,逐步进行分析

  • fill = 0,carry = 45,此时i<fill,执行swap,则此时counter[0]中存有元素45 i = fill,fill = 1
  • fill = 1,carry = 21,此时i被重置为0,进入while循环,执行merge语句得到counter[0]{21,45},进行swap,carry{21,45},出循环,再swap得counter[1]{21,45},i= fill,fill=2
  • fill = 2,carry = 1,此时i被重置为0,但此时counter[0]为空,则跳过while执行外层swap得counter[0]{1}
  • fill = 2,i = 0,carry = 35,i < fill并且counter[0]不为空,进入while,执行merge得counter[0]{1,35},swap得到carry{1,35},此时i<fill继续,i= 2,merge得counter[1]{1,21,35,45},swap得到carry{1,21,35,45},i= fill,出while得counter[2]{1,21,35,45},i = fill, fill = 3
  • ....经过上面的分析,我们发现每次counter[i]达到2^(i+1)个元素时,会转给counter[i+1],因此可以得出当counter[2]达到8个元素时,会转给counter[3]得到counter[3]{1,3,6,21,28,35,45,75}
    1.其实我们观察,不难发现counter[64]数组每层最多维护2^(n) 个元素(n从0开始),则不难得出count[64]最多一次能处理2^64 -1个元素
    2.看完上述大概就能理解为何是一个归并排序,即每次两个元素merge完放入下一层中进行merge,然后4个再进入下一层merge,如此得到merge好的8个、16个…元素,实现一个归并排序

猜你喜欢

转载自blog.csdn.net/qq_38790716/article/details/84306863