STL源码学习之序列式容器(sequence containers)


总所周知,常用的数据结构不外乎数组、链表、树、堆栈、队列、散列表、集合、映射表等等。根据 “数据在容器中的排列” 特性,这些数据结构分为序列式和关联式两种。
请添加图片描述

序列式容器

所谓序列式容器,其中所有元素都可序,但未必有序。C++语言本身提供了一个序列式容器array,STL 另外再提供 vectorlistdequestackqueuepriority_queue 等等序列式容器。其中 stackqueue 由于只是将 deque 改头换面而已,技术上被归类为一种适配器。

vector

概述

vector是动态空间,随着元素的加入,他的内部机制会自行扩充元素以容纳新元素。因此,vector的运用对于内存的合理利用和灵活性有很大的帮助,我们再也不必因为害怕空间不足而一开始就要求一个大块头array了,我们可以安心使用vector,吃多少用多少。

vector的数据结构

运用start, finish, end_of_storage 三个迭代器,便可轻易地提供首尾标示、大小、容量等等机能。使用的迭代器为 Random Access
请添加图片描述

vector的内存管理:constructor、push_back

所谓动态增加大小,并不是在原有空间之后续接新空间,而是以原大小的两倍另外配置一块比较大空间,然后将原内容拷贝过来,才开始在原内容之后构造新元素,并释放原空间。因此,对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了。请务必小心。
请添加图片描述
请添加图片描述
请添加图片描述

list

list的节点

template < class T>
struct __list_node {
	typedef void* void_pointer;
	void_pointer prev; 			// 型别为 void*。其实可设为 __list_node<T>*
	void_pointer next;
	T data;
};

list的迭代器

list不能向vector那样使用普通指针作为迭代器,因为list节点不保证在空间中连续存在。list迭代器必须有能力指向list节点,并完成正确的递增,递减,取值,成员存取等操作。

STL list是双向链表,提供bidirectioinal iterators。list的插入和结合都不会引起迭代器失效,删除操作也只是引起指向删除元素的迭代器失效。

list数据结构

STL list不仅是一个双向链表,而且是一个环状双向链表。所以它只需要一个指针,就可以完整表示整个list。

template <class T, class Alloc = alloc>
class list {
protected:
	typedef __list_node<T> list_node;
	typedef list_node* link_type;
protected:
	link_type node;	//只需要一个指针,就可以表示整个环状双向链表
}

deque

vector是单向开口的连续空间,deque是双向开口的连续空间。所谓双向,是可以在头尾两端分别做插入和删除操作。从技术观点,vector也可以在头尾两端操作,但效率奇差。

deque和vector的最大差异,一个是deque允许常数时间在头部进行插入删除,一个是在于deque没有容量观念,因为它是以动态的分段连续组合而成。换句话说,向vector那种空间不足而重新分配一个大的空间,再移动元素释放原空间的操作在deque是不会发生的。也因此,deque没有空间保留功能。

虽然deque也提供random access iterator,但它的迭代器不是普通指针,其复杂度不可与vector不可以道里计,这当然影响了各个层面。因此,除非必要,我们尽量选择vector而不是deque。对deque的排序,为了高效率,可以将deque先完整复制到一个vector,将vector排序后,再复制回deque。

deque中控器

deque是由一段一段定量连续空间构成,一旦有必要在deque前端或尾端增加空间,便配置一段定量连续空间,串接在deque前端或尾端。deque的最大任务,是维持其整体连续的假象,并提供随机存取的接口。避开了“重新配置,移动,释放”的轮回,deque的代价就是复杂的迭代器。

deque即为分段连续,就必须有中央控制,而为了维持整体连续假象,数据结构的设计和迭代器的前进后退等操作都颇为繁琐,deque的代码实现分量比vector和list都多的多。

请添加图片描述

deque采用一块map作为主控,这里的map是一小块连续空间,其中每个元素(也就是每块node)都是指针,指向另一块较大的连续空间,称为缓冲区。缓冲区才是deque的存储空间主体,SGI STL允许我们指定缓冲区大小,默认值0表示使用512bytes缓冲区。

deque迭代器

deque是分段连续空间,维持其整体连续假象的任务落在了operator++operator–两个运算子身上。

首先我们思考一下,deque迭代器应该具备什么结构。首先它必须能够指出缓冲区在哪儿,其次必须能够判断自己是否已经处于其所在缓冲区边缘,如果是,一旦前进或后退就必须跳至前一个或下一个缓冲区。为了能够正确跳跃,deque必须随时掌握管控中心map。

中控器,缓冲器,迭代器的相互关系:

假设我们现在产生一个deque,并令其缓冲区大小为32,那么每个缓冲区可容纳32/sizeof(int)=8个元素。经过某些操作之后,deque拥有20个元素,那么其begin()end()传回来的迭代器如下图所示。这两个迭代器事实上一直保持在deque中,名为start()finish()

20个元素需要20/8=3个缓冲区,所以map运用了三个节点。迭代器start内的cur当然指向缓冲区中的第一个元素,迭代器finish内的cur当然指向缓冲中的最后元素(下一个位置)。

deque迭代器的加、减、前进和后退操作,在行进过程中可能会需要调用set_node跳一个缓冲区。

请添加图片描述

deque的数据结构

template<class T, class Alloc=alloc, size_t BufSiz = 0>
class deque{
public:
	typedef T value_type;
	typedef value_type* pointer;
	typedef size_t size_type;
public:
	typedef __deque_iterator<T, T&, T*, BufSiz> iterator;
protected:
	typedef pointer* map_pointer;
protected:
	iterator start;
	iterator finish;
	map_pointer map;
	size_type map_size;
...
}

请添加图片描述

stack

stack是一种先进后出的数据结构,只有一个出口,允许在最顶端进行新增,移除和获取值,对于其他元素没有任何方法进行存取,换句话说,stack不允许遍历。

以某种既有容器作为底部结构,改变其接口,使其符合“前进后出”的特性,形成stack是很容器的。deque是一个双向开口的结构,若以deque为底部容器封闭其头端接口,就可以形成一个stack。因此,SGI STL便以deque作为缺省情况的stack底部结构,stack的实现因而非常简单。

因为stack是以底部容器完成其所有工作,而具有这种“修改某物接口,形成另一种风貌“的性质,所以也称为配接器。因此STL stack往往不称为容器,而称为配接器。

stack定义完整列表

template <class T, class Sequence = deque<T> >
class stack {
	friend bool operator== __STL_NULL_TMPL_ARGS(const stack&, const stack&);
	friend bool operator< __STL_NULL_TMPL_ARGS(const stack&, const stack&);
public:
	typedef  typename Sequence::value_type value_type;
	typedef typename Sequence::size_type size_type;
	typedef typename Sequence::reference reference;
	typedef typename Sequencd::const_reference const_reference;
protected:
	Sequence c;	//底层容器
public:
	//以下完全利用Sequence c的操作,完成stack的操作
	boll empty() const { return c.empty(); }
	size_type size() const { return c.size(); }
	reference top() { return c.back(); }
	const_reference top() const { return c.back(); }
	void push(const value_type& x) { c.push_back(x); }
	void pop() { c.pop_back(); }
};
template < class T, class Sequence>
bool operator==(const stack<T, Sequence>& x, const stack<T, Sequence>& y) {
	return x.c == y.c;
}

template <class T, class Sequence>
bool operator<(const stack<T, Sequence>& x, const stack<T, Sequence>& y) {
	return x.c < y.c;
}

stack没有迭代器

stack所有元素的进出都必须符合“先进后出”的原则,只有stack顶端的元素才能够被外界存取,stack不提供走访功能,也不提供迭代器。

以list作为stack的底层容器

除了deque以外,list也是双向开口的数据结构。上述源码中的stack的各种函数list也都具备。若以list为底部容器并封装头部接口,一样可以得到stack。

queue

queue是一种先进先出的数据结构,有两个出口。queue允许新增移除,从低端加入,从顶端取元素。但除了低端加入,顶端取出外,没有其他任何方法可以存取queue其他元素。换言之,queue不接受遍历。

以某种既有容器作为底部结构,将其接口改变,使之符合先进先出的特性,形成一个queue,很容易做到。deque是双向开口的数据结构,若以deque为底部结构,封闭其低端出口和前端入口,就可以得到一个queue。因此,SGI STL便以deque作为其缺省底部结构。

由于queue是以底部容器完成其所有工作,而具有“修改某物接口,使之符合另一种风貌”的特性者,称为配接器。因此STL queue往往不被称为容器,而被称为容器配接器。

queue没有迭代器

queue所有元素都必须符合先进先出条件。只有queue顶端元素才能被外界获取。queue不提供遍历功能,也不提供迭代器。

以list作为queue的底层容器

除deque外,list也是双向开口容器。queue使用的底层容器的函数list也都具备。因此,以list为底部结构封装其头端开口,一样可以得到queue。

heap(隐式表述)

heap并不属于STL容器,它是个幕后英雄,扮演priority queue的角色。priority queue允许用户以任何次序推入元素,但取出时必须从优先级最高也就是数值最高的数取出。binary max heap正具有这样的特性,适合作为priority queue的底部机制。

若果使用list作为priority queue的底层机制,元素插入可享常数时间,取极值需要进行线性扫描。我们也可以在插入前先进行排序,使元素值总是有序的,但这样一来,取极值和删除操作达到最高效率,元素的插入只有线性。

我们也可以以二叉搜索树作为底部实现,这么一来,插入极值都是O(logn),但是一来二叉搜索树需要有足够的随机性,而来二叉搜索树并不容易实现。priority queue的效率最好在二叉搜索树和queue之间,binary heap便是合适的选择。

binary heap就是一种完全二叉树,除底层叶子节点外,都是满的,而最底层的叶子节点从左至右又不能有空隙。完全二叉树没有节点漏洞
的一个极大的好处在于,我们可以用array来存储节点。假设动用一个小技巧,将array的0元素设置为极大或极小值,那么当完全二叉树某个节点位于array的i处,其左子节点必然在2i处,右子节点必然在2i+1处,父节点必位于i/2处。通过这个简单的位置规则,我们可以用array轻易实现完全二叉树,这种以array表示tree的方法,我们称之为隐式表述法。

这样我们用一个array和一组heap算法就可以实现priority queue,array的缺点是无法动态改变大小,heap需要这个功能,所以我们用vector来替代。

heap分为max-heapmin-heap,前者的每个节点的键值大于等于其子节点的键值,后者每个节点的键值小于等于子节点的键值。max-heap最大值在根节点,min-heap最小值在根节点,都位于array或者vector的起头处。STL提供max-heap,今天讨论的也都是max-heap。

heap算法

push_heap算法

为了满足完全二叉树特性,新加入的元素一定放在最底层做叶子节点,并填补从左至右第一个空格,也就是把新元素插入底层vector的end()处。

新元素是否适合当前位置呢?我们执行一个上溯程序,将新节点与父节点比较,比父节点大,就交换,一直上溯,直到不需要交换或者到根节点为止。

以下为push_heap的实现细节。函数接受两个迭代器作为vector的头尾,并且新元素已经插入到容器的最尾端,如果不符合这两个条件,执行结果未可知。

pop_heap算法

pop操作取走max-heap的根节点(其实是放在vector的最后一个元素),为了满足完全二叉树的特性,必须将最下一层最右边的叶子节点拿掉,我们的任务是为这个叶子节点找一个合适的位子。

为了满足max-heap特性,我们进行下溯,将根节点拿掉(形成一个洞)填入上述失去生存空间的叶子节点值,在将它拿来和其两个子节点比较键值,并与较大的那个对调位置,如此一直下放,直到洞的值大于左右子节点或者到叶子节点为止。

pop_heap具体实现。该函数接受两个迭代器作为底层容器的头尾,如果不满足这个条件,结果不可预期。

pop_heap之后,最大值元素只是被放在了容器的最尾端,并未被移走。如果要取其值,可用back(),如果要移走,可用pop_back()。

sort_heap算法
既然每次pop_heap可获得一个键值最大的值,那么持续pop_heap,每次向前缩减一个元素,程序执行完毕,我们就有一个递增的序列。

下面是sort_heap的实现细节,函数接受两个迭代器作为底层容器的头尾两端,如果不满足这个条件,结果未可知。sort_heap之后的heap不再是一个合法的heap了。

template <class RandomAccessIterator>
void sort_heap(RandomAccessIteraor first, RandomAccessIterator last) {
	while (last - first > 1)
		pop_heap(first, last--);
}

make_heap算法

这个算法就是将一段数据转化为一个heap。

heap没有迭代器

heap所有元素都必须遵循完全二叉树排列规则,所以heap不支持遍历,也不提供迭代器。

priority queue

priority queue是一个拥有权值观念的queue,它允许加入新元素,移除旧元素,审视元素值等操作。由于这是一个queue,只允许低端插入,顶端获取,除此之外无其他办法存取元素。priority queue并非按照元素推入次序排列,而是按照权值排列,权值最高排在最前面。缺省情况下priority queue利用一个max-heap完成,max-heap可以满足priority queue的依权值高低自动递增排序的特性。

priority queue定义完整列表
priority queue缺省以vector为底部容器,再加上heap处理规则。queue为配接器,priority queue也称为配接器。

template <class T, class Sequence = vector<T>, class Compare = less<typename Sequence::value_type> >
class priority_queue {
public:
	typedef  typename Sequence::value_type value_type;
	typedef typename Sequence::size_type size_type;
	typedef typename Sequence::reference reference;
	typedef typename Sequencd::const_reference const_reference;
protected:
	Sequence c;	//底层容器
	Compare comp;	//元素大小比较标准
public:
	priority_queue() :c() {}
	explicit priority_queue(const Compare& x) :c(), comp(x) {}

	template <class InputIterator>
	priority_queue(InputIterator first, InputIterator last, const Compare& x) : c(first, last), comp(x) { make_heap(c.begin(), c.end(), comp); }
	template <class InputIterator>
	priority_queue(InputIterator first, InputIterator last) : c(first, last) { make_heap(c.begin(), c.end(), comp); }

	//以下完全利用Sequence c的操作,完成stack的操作
	boll empty() const { return c.empty(); }
	size_type size() const { return c.size(); }
	const_reference top() const { return c.front(); }
	void push(const value_type& x) {
		__STL_TRY{
			c.push_back(x);
			push_heap(c.begin(), c.end(), comp);
		}
		__STL_UNWIND(c.clear());
	}
	void pop() {
		__STL_TRY{
			pop_heap(c.begin(), c.end(), comp);
			c.pop_back();
		}
		__STL_UNWIND(c.clear()); 
	}
};

priority queue没有迭代器

priority queue进出都有一定的规则,只有顶端元素才有机会被外界取用,priority queue不提供遍历功能,也不支持迭代器。

slist

STL list是一个双向链表,slist是一个单向链表。slist不在标准规格之内。slist是forward iterator,list是bidirectional iterator,因此slist会受到很多限制,但是单向链表耗用空间更小,操作更快,不失为另一种选择。

slist和list一样,插入、移除,接合操作都不会使原有迭代器失效。根据STL的习惯,插入操作都是将元素插入在指定点之前。作为单向链表,slist没有任何办法可以回头定出前一个位置,因此它必须从头找起。换句话说,slist除了在开头附近,其他位置上采用insert和erase操作都不明智。这就是slist相较list的最大缺点,为此,slist提供insert_after()和erase_after()供选择。

基于同样的效率考虑,slist不提供push_back,只提供push_front

slist节点

slist节点和迭代器设计,在结构上比list复杂的多,运用继承关系,因此在型别转换上有复杂的表现。

//单向链表的节点基本结构
struct __slist_node_base {
	__slist_node_base* next;
};
//单向链表的节点结构
template <class T>
struct slist_node:public __slist_node_base {
	T data;
};
//全局函数:已经某一节点,插入节点于其后
inline __slist_node_base* __slist_make_link(__slist_node_base* prev_node, __slist_node_base* new_node) {
	//令new_node的下一个节点为prev的下一个节点
	new_node->next = prev_node->next;
	prev_node->next = new_node;	//令prev的下一个节点为new_node
	return new_node;
}
//全局函数:单向链表的大小
inline size_t __slist_size(__slist_node_base* node) {
	size_t result = 0;
	for (;node!=0;node=node->next)
		++result;
	return result;
} 
slist迭代器
//单向链表的迭代器基本结构
struct __slist_iterator_base {
	typedef size_t size_type;
	typedef ptrdiff_t difference_type;
	typedef forward_iterator_tag iterator_category; //注意,单向
	__slist_node_base* node;	//指向节点基本结构
	__slist_iterator_base(__slist_node_base* x):node(x) {}
	void incr() {node=node->next;}	//前进一个节点
	bool operator==(const __slist_iterator_base& x) const {
		return node==x.node;
	}
	bool operator!=(const __slist_iterator_base& x) const {
		return node!=x.node;
	}
};
//单向链表的迭代器结构
template <class T, class Ref, class Ptr>
struct __slist_iterator : public __slist_iterator_base {
	typedef __slist_iterator<T, T&, T*>	iterator;
	typedef __slist_iterator<T, const T&, const T*>	const_iterator;
	typedef __slist_iterator<T, Ref, Ptr> self;
	typedef T value_type;
	typedef Ptr pointer;
	typedef Ref reference;
	typedef __slist_node<T> list_node;
	__slist_iterator(list_node* x) : __slist_iterator_base(x) {}
	//调用slist<T>::end()时会造成__slist_iterator(0),于是调用上述函数
	__slist_iterator():__slist_iterator_base(0) {}
	__slist_iterator(const iterator& x):__slist_iterator_base(x.node) {}
	reference operator*() const {return ((list_node*)node)->data;}
	pointer operator->() const {return &(operator*());}
	self operator++() {
		incr();
		return *this;
	}
	self operator++(int) {
		self tmp = *this;
		incr();
		return tmp;
	}
}

注意,比较两个slist迭代器是否相等,由于我们没有重载operator==,所以会调用__slist_iterator_base::operator==,根据其定义,我们知道是由__slist_node_base* node是否相等而定的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值