[STL]序列式容器

2 篇文章 0 订阅

1 vector

定义概要


class vector
{
public:
	typedef value_type* iterator;	//迭代器就是指针
	typedef value_type& reference;
. . .
protected:
	iterator start;					//目前使用空间的头
	iterator finish;				//目前使用空间的尾
	iterator end_of_storage;		//目前可用空间的尾
public:

	//O(1)
	iterator begin() {return start;}					//第一个元素的迭代器
	iterator end()   {return finish;}					//最后一个元素的迭代器
	int size() 		 {return (end() - begin());} 		//当前元素个数
	int capacity() 	 {return end_of_storage()-begin();}	//容量
	bool empty()  	 {return begin()==end();}			//判断是否为空
	reference font() {return *begin();}					//第一个元素的引用
	reference end()  {return *(end()-1);}				//最后一个元素的引用
	void pop_back()  {--finish;destory(finish);}		//取出最尾端元素
	
	//O(n)
	iterator erase(iterator position){...}				//清除某位置上的元素
	iterator erase(iterator first,iterator last){...}	//清除[first, last)
	void clear(){erase(begin(),end());}					//清除所有元素
	
	void resize(int new_size, const T& x){				//调整size,不改变capacity
		if(new_size < size())				  //只需要将后面的截断
			erase(begin()+new_size,end());
		else								  //在后面补上x
			insert(end(),new_size - size(),x);//调用insert,可能会改变容量
	}
	
	void push_back(const T&x){...}						//在末尾插入
	void insert(iterator pos,int n,int x){..}			//在pos位置插入n个x,可能改变容量
}

空间管理


在这里插入图片描述

空间增长

一般情况下,vector的空间是只增不减的。vector的容量capacity是动态增长的,它并不是在原空间之后续接新空间(因为无法确保原空间之后有可供配置的空间),而是以原capacity的两倍另外配置一块新的空间,然后将原内容拷贝过来,并释放原空间。因此,对vector的任何操作,一旦引起空间重新配置,指向原vector放入所有迭代器都失效了。可能引起空间重新配置的操作有:push_back() insert() resize()

https://blog.csdn.net/yangshiziping/article/details/52550291
1. 为什么是空间成倍而不是以固定值大小增长?

  • 每次增长m倍,一直增长到n,需要复制的元素个数为:
    在这里插入图片描述
    因此,均摊复杂度为O(1);

  • 每次增长k个元素空间,一直增长到n,需要复制的元素个数为:
    在这里插入图片描述
    因此,均摊复杂度为O(n);

2.为什么倍数是1~2
参考:Milo Yip的回答 - 知乎
在这里插入图片描述
如果是以k = 2倍的方式增长,那么原先的分配空间将刚好不能被下一次所重复利用,例如4+8 < 16,4+8+16 < 32,因此最好就选k < 2;
比如取k = 1.5,可以看到在几次扩展之后,前面的空间可以再次被利用到,而不是每次都要去寻找一个更大的空间。当然,之前的空间能够重新被分配的前提是这些内存是连续的;

3. 一些疑问
疑问1:在上图中,既然在后面有连续的内存可以分配,为什么不在原来的基础上追加呢?

  • 一般分配器不支持 realloc(),即使支持也需要有 in-place 扩展功能。一般在应用层面只能重新分配一块新内存,然后把旧的数据复制过去,再释放之前的内存;
释放vector的内存空间

需要注意,clear() erase()等操作并不会释放内存空间,也就是不会改变capacity,那么何时会释放vector的内存空间呢?有2种情形:

  • vector离开作用域,自动调用析构函数;
  • 调用swap(),手动释放内存,swap交换技巧实现内存释放的思想是:假设需要释放my_v的内存空间,使用vector的默认构造函数建立临时vector对象tmp_v,再调用swap()函数,交换my_v和tmp_v的空间,这样my_v就具有临时对象的大小,而临时对象随即被析构,从而其占用的空间也被释放。
vector<int>().swap(my_v);
等价于:
{
	vector<int> tmp_v();	//一个空的容器
	tmp_v.swap(my_v);		//tmp_v拥有my_v的内存空间
}
//tmp_v离开作用域,其内存空间(也就是之前my_v的内存)被释放
vector<int> my_v;
cout << "新建一个空的vector my_v, 其capacity为" << my_v.capacity() << endl;
	
for(int i = 0; i < 10;i++)  my_v.push_back(i);
cout << "插入元素后,my_v的capacity为 " << my_v.capacity() << endl;
	
vector<int>().swap(my_v);
cout << "与临时vector进行swap()后,my_v的capacity为" << my_v.capacity() << endl;

在这里插入图片描述


迭代器


vector的迭代器就是指针:

vector<int>:: iterator iter1;		//类型是int*
vector<A>::   iterator iter2;		//类型是A*
迭代器何时失效

迭代器失效意味着它指向的位置使能无效的,迭代器失效的情形有:

  • 空间重新配置,前面已经提到,引起空间重新配置的操作有push_back() insert() resize()
  • insert() resize(),当insert()不引起空间重新配置时,也会使插入位置后面的迭代器失效,resize()可能会调用insert()
  • erase() clear()erase()会使被清除元素后面的迭代器失效,clear()相当于erase( begin(),end() )

实例



2 list

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

list的节点(node)


list的本身和list节点的是不同的结构,需要分开设计。下面先介绍list的节点(node):

template<class T>
struct __list_node{
	typedef void* void_pointer;
	void_pointer prev;
	void_pointer next;
	T data;
}

显然,这是一个双向链表:
在这里插入图片描述

list的迭代器


list不能像vector那样用普通指针作为迭代器,因为它的节点不保证在连续的存储空间存在。list的迭代器必须有能力指向list的节点,并且能够正确地递增、递减、取值等操作。

list迭代器有一些重要的性质:

  • 插入操作和接合操作(splice)都不会造成原有list迭代器失效;
  • list的元素删除操作(erase)也只有“指向被删除元素”的那个迭代器失效,其他迭代器不受影响。

下面是list迭代器的设计:


在这里插入图片描述

template<class T, class Ref, class Ptr>
struct __list_iterator
{
	typedef __list_iterator<T, T&, T*>  iterator;
	typedef __list_iterator<T, Ref, Ptr> self;
	
	typedef T value_type;
	typedef Ptr pointer;
	typedef Ref reference;
	
	typedef __list_node<T>* link_type;
	link_type node;			//迭代器内部有一个普通指针,用于指向list节点

	//构造函数
	__link_iterator(link_type x):node(x) { }
	...
	
	bool operator==(const self& x)const
	{
		return node == x.node;
	}
	
	reference operator*()const 		{  return (*node).date; }
	pointer operator->()const		{  return &(operator*());}

	//前缀++
	self& operator++()
	{
		node = (link_type)((*node).next);
		return *this;
	}
	
	//后缀++
	self operator++(int)
	{
		self tmp = *this;
		++*this;		//调用前缀++
		return tmp;
	}

	//前缀--与后缀--同理
	...
}

list的数据结构


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

template<class T, class Alloc = alloc>
class list
{
protected:
	typedef __list_node<T>  list_node;
public:
	typedef list_node*  link_type;

protected:
	link_type node;		//只需要一个指针,就可以表示整个环状双向链表

public:
	iterator begin() { return (link_type)((*node).next); }
	iterator end()   { return node; }
	
	bool empty() const
	{
		return node->next == node;
	}

	//取头结点的内容(元素值)
	reference front()   { return *begin(); }
	//取尾结点的内容(元素值)
	reference back()	{ return *end(); }
}

在这里插入图片描述

list的元素操作

insert()
//在迭代器position所指位置插入一个节点,内容为x
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;
}

在这里插入图片描述
在这里插入图片描述

size()

复杂度为O(N),调用distance():

size_t size() const
{
	size_t res = 0;
	distance(begin(),end(),res);	//全局函数P98,并非list特有
	return res;
}
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;
	destory_node(position.node);
	return iterator(next_node);
}
clear()
//遍历链表,并逐个将节点销毁
void clear()
{
	link_type cur = (link_type) node->next;		//begin()
	while(cur != node)							//遍历每一个节点
	{
		link_type tmp = cur;
		cur = (link_type) cur->next;
		destory_node(tmp);
	}
	//恢复node原始状态,即只有一个空白节点的状态
	node->next = node;
	node->prev = node;
}
remove()和unique()
//将数值为value的所有元素移除
void remove(const T& value)
{
	iterator first = begin();
	iterator last = end();
	while(first != last)		//遍历每个节点
	{
		iterator next = first;
		++next;
		if(*first == value)  earse(first);
		first = next;
	}
}

//移除数值相同的连续元素,注意,只有“连续而相同的元素”才会被移除剩一个
void 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;
	}
}
reverse()
transfer()

transfer()是splice(),merge()和sort()的基础。

splice()
merge()

将两个有序的list合并成一个有序的list。

sort()

由于STL算法的sort只接受RandomAccessIterator,因此list需要自己实现自己的排序算法,采用的是归并排序:

template <class T>
void list<T>::sort()
{
	//下面判断空链表,或仅有一个元素
	//使用size()<=1来判断复杂度是O(N),因此直接判断节点更快一些:
	if(node->next == node || link_type(node->next)->next == node)
		return;
	
	//一些新的lists,作为中介数据存放区
	list<T> carry;
	list<T> counter[64];
	int fill = 0;
	while(!empty())
	{
		carry.splice(carry.begin(), *this, begin());
		int i = 0;
		while(i < fill && !counter[i].empty())
		{
			counter[i].merge(carry);
			carry.swap(counter[i++]);
		}
		carry.swap(counter[i]);
		if(i == fill)  ++fill;
	}
	
	//最后将counter数组merge起来,再放回this中
	for(int i = 1;i < fill; i++)
		counter[i].merge(counter[i-1]);
	swap(counter[fill-1]);
}

假设初始链表为:
在这里插入图片描述
每次循环的过程如下:
在这里插入图片描述
counter数组在每次循环的变化如下:
在这里插入图片描述
list.sort不同于传统的归并排序,传统的归并排序需要在O(1)时间内找到数组的中点,而list无法办到,因此采用上面的办法,并且还避免了递归,十分巧妙。

3 deque

vector是单向开口的连续空间,deque则属一种双向开口的连续性空间。所谓双向开口,是指可以在头尾两端分别做元素的插入和删除操作,当然vector也可以在头尾两端进行操作(从技术观点),但是其头部操作效率极差。

deque相较于vector有以下特点:

  • deque允许在常数时间内对头端进行元素的插入或删除操作;
  • deque没有容量(capacity)观念,因为他是动态地以分段连续空间组合而成,随时可以增加一块新的空间链接起来;换句话说,deque不会像vector那样有“因旧空间不足而重新配置一块更大的空间,并将元素复制过去,再释放旧空间”的情况;
  • deque的迭代器不再是普通指针,它的迭代器比较复杂,导致其多种操作的效率不如vector;例如要对deque进行排序操作,为了提高效率,可将deque完整复制到一个vector,将vector排序后,再复制回deque;
  • 除非必要,我们应尽量使用vector而非deque。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值