C/C++编程:STL 序列式容器源码学习

1059 篇文章 289 订阅

容器分类

在STL编程中,容器时我们经常会用到的一种数据结构,容器分为序列式容器和关联式容器。

两者的本质区别在于:序列式容器时通过元素在容器中的位置顺序存储和访问元素,而关联容器则是通过key存储和读取元素

分类如下
在这里插入图片描述

vector

vector基本数据结构

基本上,STL里面所有的容器的源码都包含至少三个部分:

  • 迭代器,遍历容器的元素,控制容器空间的边界和元素的移动
  • 构造函数,满足容器的多种初始化
  • 属性的获取,比如begin()和end()

vector也是如此:

template <class T, class Alloc = alloc>
class vector {
public:
	 // 定义 vector ⾃身的嵌套型别
	 typedef T value_type;
	 typedef value_type* pointer;
	 typedef const value_type* const_pointer;
	 // 定义迭代器, 这⾥就只是⼀个普通的指针
	 typedef value_type* iterator;
	 typedef const value_type* const_iterator;
	 typedef value_type& reference;
	 typedef const value_type& const_reference;
	 typedef size_t size_type;
	 typedef ptrdiff_t difference_type;
	 ...
protected:
	 typedef simple_alloc<value_type, Alloc> data_allocator; // 设置其空间配置器
	 iterator start; // 当前使⽤空间的头
	 iterator finish; // 当前使⽤空间的尾
	 iterator end_of_storage; // 当前可⽤空间的尾
	 ...
}

构造函数

vector 有多个构造函数, 为了满⾜多种初始化

在这里插入图片描述
可以看到,这里面,要么都初始化成功,要么一个都不初始化并释放以及抛出异常。

因为vector式一种类模板,所以,我们并不需要手动释放内存,声明周期结束之后就自动调用析构从而释放调用空间。当然,我们也可以直接调用析构函数释放内存

void deallocate() {
 if (start)
 data_allocator::deallocate(start, end_of_storage - start);
}
// 调⽤析构函数并释放内存
~vector() {
 destroy(start, finish);
 deallocate();
}

属性获取

这里需要注意的式因为end()返回的是finish,而finish是指向最后一个元素的后一个位置的指针,所以使用end()的时候要注意

public:
 // 获取数据的开始以及结束位置的指针. 记住这⾥返回的是迭代器, 也就是 vector 迭代器就是该类型的指针.
 iterator begin() { return start; }
 iterator end() { return finish; }
 reference front() { return *begin(); } // 获取值
 reference back() { return *(end() - 1); }
 const_iterator begin() const { return start; }// 获取右值
 const_iterator end() const { return finish; }
 
 const_reference front() const { return *begin(); }
 const_reference back() const { return *(end() - 1); }
 
 size_type size() const { return size_type(end() - begin()); } // 数组元素的个数
 size_type max_size() const { return size_type(-1) / sizeof(T); } // 最⼤能存储的元素
个数
 size_type capacity() const { return size_type(end_of_storage - begin()); } // 数组的
实际⼤⼩
 bool empty() const { return begin() == end(); }
 //判断 vector 是否为空, 并不是⽐较元素为 0,是直接⽐较头尾指针。

push 和 pop 操作

vector的push和pop操作都只是针对尾进行操作,这里说的尾部是指数据的尾部。

当调用push_back()插入新元素的时候,首先会检查是否有备用空间,如果有就在备用空间上构造元素,并调整迭代器finish
在这里插入图片描述
当如果没有备⽤空间,就扩充空间(重新配置-移动数据-释放原空间),这⾥则是调⽤了 insert_aux 函数。

在这里插入图片描述

在上⾯这张图⾥,可以看到,push_back 这个函数⾥⾯⼜判断了⼀次 finish != end_of_storage 这是因为啥呢?原因是insert_aux函数可能还会被其他函数调用。

在下面的else分支里面,我们可以看到vector的动态扩容机制:如果原空间大小为0则分配1个元素,如果大于0则分配原空间两倍的新空间,然后把数据拷贝过去
在这里插入图片描述

pop 元素:

public:
	 //将尾端元素拿掉 并调整⼤⼩
	 void pop_back() {
	 --finish;//将尾端标记往前移动⼀个位置 放弃尾端元素
	 destroy(finish);
 }

erase元素

erase函数清除指定位置的元素,其重载函数用于清除一个范围内的所有元素。实际实现就是将删除元素后面所有元素往前移动,对于vector来说删除元素的操作开销还是很大的,所以vector不适合频繁的删除操作,毕竟它是一个数组

//清楚[first, last)中的所有元素
 iterator erase(iterator first, iterator last) {
	 iterator i = copy(last, finish, first);
	 destroy(i, finish);
	 finish = finish - (last - first);
	 return first;
 }
 //清除指定位置的元素
 iterator erase(iterator position) {
	 if (position + 1 != end())
		 copy(position + 1, finish, position);//copy 全局函数
	 } 
	 --finish;
	 destroy(finish);
	 return position;
 }
 
 void clear() {
 	erase(begin(), end());
 }

在这里插入图片描述
清除范围内的元素,第一步要将finish迭代器后面的元素拷贝回去,然后返回拷贝完成的尾部迭代器,最后在删除之前的。

删除指定位置的元素就是将指定位置后面的所有元素向前移动,最后析构掉最后一个一个

insert插入元素

vector 的插⼊元素可以分为三种情况:

  • 如果备⽤空间⾜够且插⼊点的现有元素多于新增元素
  • 如果备⽤空间⾜够且插⼊点的现有元素⼩于新增元素
  • 如果备⽤空间不够;

我们⼀个⼀个来分析。
(1)插⼊点之后的现有元素个数 > 新增元素个数
在这里插入图片描述
(2)插⼊点之后的现有元素个数 <= 新增元素个数
在这里插入图片描述
(3)如果备⽤空间不⾜

在这里插入图片描述

这里需要注意的是迭代器失效问题。稍微的迭代器失效问题就是由于元素空间重新配置导致之前访问的元素不在了,总的来说有两种:

  • 由于插入元素,使得容器元素整体迁移导致存放原容器元素的空间不再有效,从而使得指向原空间的迭代器失效
  • 由于删除元素,使得某些元素次序发生变化导致原本指向某元素的迭代器不再指向期望指向的元素

前⾯提到的⼀些全局函数,这⾥总结⼀下:

  • copy(a,b,c):将(a,b)之间的元素拷⻉到(c,c-(b-a))位置
  • uninitialized_copy(first, last, result): 具体作⽤是将 [first,last)内的元素拷⻉到 result 从前往后拷⻉
  • copy_backward(first, last, result): 将 [first,last)内的元素拷⻉到 result 从后往前拷⻉

小结

最后要注意的是vector的成员函数都不做边界检查(at方法会抛异常),使用者要自己确保迭代器和索引值的合法性

vector 的优缺点。

优点:

  • 在内存中分配一块连续的内存空间进行存取,可以像数组一样操作,动态扩容
  • 随机访问方便,支持下标访问和vector.at()操作
  • 节省空间

缺点:

  • 由于其顺序存储的特性,vector插入删除操作的时间复杂度是O(n)
  • 只能在末端进行pop和push
  • 当多态长度超过默认分配大小后,要整体重新分配、拷贝和释放空间

vector的缺点也很明显,在频率较高的插入和删除时效率就太低了

list

list时一种双向链表,它的设计更加复杂一点,好处时每次插入或者删除一个元素,就配置或者释放一个元素,list对于空间的运用有绝对的精准,一点也不浪费。而且对于任何位置的元素插入或者删除,list永远是常数空间。

注意:list源码里其实分了两个部分,一个部分是list结构,另一部分是list节点结构。

那为什么 list 节点分为了两个部分,⽽不是在⼀个结构体⾥⾯呢? 也就是说为什么指针变ᰁ和数据变ᰁ分开定义呢?

这是因为给迭代器做铺垫,因为迭代器遍历的时候是不需要数据成员的,只需要前后指针就可以遍历list

在这里插入图片描述

list 数据结构-节点

__list_node用来实现节点,数据结构中就存储前后指针和属性

template <class T> struct __list_node {
	 // 前后指针
	 typedef void* void_pointer;
	 void_pointer next;
	 void_pointer prev;
	 // 属性
	 T data;
};

在这里插入图片描述

基本类型

template<class T, class Ref, class Ptr> struct __list_iterator {
	 typedef __list_iterator<T, T&, T*> iterator; // 迭代器
	 typedef __list_iterator<T, const T&, const T*> const_iterator;
	 typedef __list_iterator<T, Ref, Ptr> self; 
	 
	 // 迭代器是bidirectional_iterator_tag类型
	 typedef bidirectional_iterator_tag iterator_category;
	 typedef T value_type;
	 typedef Ptr pointer;
	 typedef Ref reference;
	 typedef size_t size_type;
	 typedef ptrdiff_t difference_type;
	 ...
}

构造函数

template<class T, class Ref, class Ptr> struct __list_iterator {
	 ...
	 // 定义节点指针
	 typedef __list_node<T>* link_type;
	 link_type node;
	 // 构造函数
	 __list_iterator(link_type x) : node(x) {}
	 __list_iterator() {}
	 __list_iterator(const iterator& x) : node(x.node) {}
 ...
};

重载

template<class T, class Ref, class Ptr> struct __list_iterator {
	 ...
	 bool operator==(const self& x) const { return node == x.node; }
	 bool operator!=(const self& x) const { return node != x.node; }
	 ...
	 // ++和--是直接操作的指针指向next还是prev, 因为list是⼀个双向链表
	 self& operator++() {
		 node = (link_type)((*node).next);
		 return *this;
	 }
	 self operator++(int) {
		 self tmp = *this;
		 ++*this;
		 return tmp;
	 }
	 self& operator--() {
		 node = (link_type)((*node).prev);
		 return *this;
	 }
	 self operator--(int) {
		 self tmp = *this;
		 --*this;
		 return tmp;
	 }
};

list 结构

list自己定义了嵌套类型满足traits编程,list迭代器是bidirectional_iterator_tag类型,并不是一个普通指针

在这里插入图片描述
list在定义 node 节点时, 定义的不是⼀个指针。这⾥要注意。

template <class T, class Alloc = alloc>
class list {
protected:
	 typedef void* void_pointer;
	 typedef __list_node<T> list_node; // 节点
	 typedef simple_alloc<list_node, Alloc> list_node_allocator; // 空间配置器
	public: 
	 // 定义嵌套类型
	 typedef T value_type;
	 typedef value_type* pointer;
	 typedef const value_type* const_pointer;
	 typedef value_type& reference;
	 typedef const value_type& const_reference;
	 typedef list_node* link_type;
	 typedef size_t size_type;
	 typedef ptrdiff_t difference_type;
	 
protected:
	 // 定义⼀个节点, 这⾥节点并不是⼀个指针.
	 link_type node;

public:
	 // 定义迭代器
	 typedef __list_iterator<T, T&, T*> iterator;
	 typedef __list_iterator<T, const T&, const T*> const_iterator;
	 ...
};

list 构造和析构函数实现

构造函数前期准备:

  • 每个构造函数都会场景一个空的node节点,为了保证我们在执行任何操作都不会修改迭代器
  • list 默认使⽤ alloc 作为空间配置器,并根据这个另外定义了⼀个 list_node_allocator,⽬的是更加⽅便以节点⼤⼩来配置单元。
template <class T, class Alloc = alloc>
class list {
protected:
	 typedef void* void_pointer;
	 typedef __list_node<T> list_node; // 节点
	 typedef simple_alloc<list_node, Alloc> list_node_allocator; // 空间配置器

其中,list_node_allocator(n)表示配置 n 个节点空间。以下四个函数,分别⽤来配置,释放,构造,销毁⼀个节点。

class list {
protected:
	 // 配置⼀个节点并返回
	 link_type get_node() { return list_node_allocator::allocate(); }
	 // 释放⼀个节点
	 void put_node(link_type p) { list_node_allocator::deallocate(p); }
    // 产⽣(配置并构造)⼀个节点带有元素初始值
	link_type create_node(const T& x) {
			 link_type p = get_node();
			 __STL_TRY {
			 construct(&p->data, x);
		 }
		 __STL_UNWIND(put_node(p));
		 return p;
	 }
	//销毁(析构并释放)⼀个节点
	 void destroy_node(link_type p) {
		 destroy(&p->data);
		 put_node(p);
	 }
	 // 对节点初始化
	 void empty_initialize() {
		 node = get_node();
		 node->next = node;
		 node->prev = node;
	 } 
};

基本属性获取

template <class T, class Alloc = alloc>
class list {
 ...
public:
 	iterator begin() { return (link_type)((*node).next); } // 返回指向头的指针
 	const_iterator begin() const { return (link_type)((*node).next); }
 	iterator end() { return node; } // 返回最后⼀个元素的后⼀个的地址
 	const_iterator end() const { return node; }
 
 	// 这⾥是为旋转做准备, rbegin返回最后⼀个地址, rend返回第⼀个地址. 我们放在配接器⾥⾯分析
	 reverse_iterator rbegin() { return reverse_iterator(end()); }
	 const_reverse_iterator rbegin() const {
		 return const_reverse_iterator(end());
	 }
	 reverse_iterator rend() { return reverse_iterator(begin()); }
	 const_reverse_iterator rend() const {
	 	return const_reverse_iterator(begin());
	 }
 
	 // 判断是否为空链表, 这是判断只有⼀个空node来表示链表为空.
	 bool empty() const { return node->next == node; }
	 // 因为这个链表, 地址并不连续, 所以要⾃⼰迭代计算链表的⻓度.
	 size_type size() const {
		 size_type result = 0;
		 distance(begin(), end(), result);
		 return result;
	 }
	 size_type max_size() const { return size_type(-1); }
	 // 返回第⼀个元素的值
	 reference front() { return *begin(); }
 	const_reference front() const { return *begin(); }
 	// 返回最后⼀个元素的值
	 reference back() { return *(--end()); }
 	const_reference back() const { return *(--end()); }
	 
	 // 交换
	 void swap(list<T, Alloc>& x) { __STD::swap(node, x.node); }
	 ...
};

template <class T, class Alloc>
inline void swap(list<T, Alloc>& x, list<T, Alloc>& y) {
	 x.swap(y);
 }

list 的头插和尾插

因为list是一个循环的双链表,所以pussh和pop就必须实现是在头插⼊, 删除还是在尾插⼊和删除。

在 list 中,push 操作都调⽤ insert 函数, pop 操作都调⽤ erase 函数。

template <class T, class Alloc = alloc>
class list {
	 ...
	 // 直接在头部或尾部插⼊
	 void push_front(const T& x) { insert(begin(), x); }
	 void push_back(const T& x) { insert(end(), x); }
	 // 直接在头部或尾部删除
	 void pop_front() { erase(begin()); }
	 void pop_back() {
		 iterator tmp = end();
		 erase(--tmp);
	 }
	 ...
}

上⾯的两个插⼊函数内部调⽤的 insert 函数。

class list {
 ...
public:
	 // 最基本的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;
	 }

这⾥需要注意的是

  • 节点实际是以node空节点开始的
  • 插入操作时将元素插入到指定位置的前一个地址进行插入的

删除操作

删除元素的操作大多数由erase函数来实现的,其他的所有函数都是直接或者间接调用erase。

list是链表,所以链表怎么实现删除,list就怎么操作:先保留前驱和后继节点,再调整指针位置即可。

由于它是双向环状链表,只要把边界操作处理好,那么再头部或者尾部插入元素操作几乎是一模一样的,同样,在头部和尾部删除元素也是一样的。

template <class T, class Alloc = alloc>
class list {
	 ...
	 iterator erase(iterator first, iterator last);
	 void clear(); 
	 // 参数是⼀个迭代器 修改该元素的前后指针指向再单独释放节点就⾏了
	 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);
	 }
	 ...
 };
...
}

list内部提供一种所谓的迁移操作(transfer):将某连续范围的元素迁移到某个特定位置之前,技术上实现并不难,就是节点之间的指针移动,只要明⽩了这个函数的原理,后⾯的 splice,sort,merge 函数也就⼀⼀知晓了,我们来看⼀下 transfer 的源码:

template <class T, class Alloc = alloc>
class list {
 ...
protected:
	 void transfer(iterator position, iterator first, iterator last) {
		 if (position != last) {
			 (*(link_type((*last.node).prev))).next = position.node;
			 (*(link_type((*first.node).prev))).next = last.node;
			 (*(link_type((*position.node).prev))).next = first.node; 
			 link_type tmp = link_type((*position.node).prev);
			 (*position.node).prev = (*last.node).prev;
			 (*last.node).prev = (*first.node).prev;
			 (*first.node).prev = tmp;
		 }
	 }
 ...
};

在这里插入图片描述

  • splice函数: 将两个链表进⾏合并:内部就是调⽤的 transfer 函数。
  • merge 函数: 将传⼊的 list 链表 x 与原链表按从⼩到⼤合并到原链表中(前提是两个链表都是已经从⼩到⼤排序了). 这⾥ merge 的核⼼就是 transfer 函数。
  • reverse 函数: 实现将链表翻转的功能:主要是 list 的迭代器基本不会改变的特点, 将每⼀个元素⼀个个插⼊到begin 之前。
  • sort 函数: list 这个容器居然还⾃⼰实现⼀个排序,看⼀眼源码就发现其实内部调⽤的 merge 函数,⽤了⼀个数组链表⽤来存储 2^i 个元素, 当上⼀个元素存储满了之后继续往下⼀个链表存储, 最后将所有的链表进⾏ merge归并(合并), 从⽽实现了链表的排序。
  • 赋值操作: 需要考虑两个链表的实际⼤⼩不⼀样时的操作
    • 原链表⼤ : 复制完后要删除掉原链表多余的元素
    • 原链表⼩ : 复制完后要还要将x链表的剩余元素以插⼊的⽅式插⼊到原链表中
  • resize 操作:重新修改 list 的⼤⼩。
    • 传⼊⼀个 new_size,如果链表旧⻓度⼤于 new_size 的⼤⼩, 那就删除后⾯多余的节点
  • clear 操作: 清除所有节点。
    • 遍历每⼀个节点,销毁(析构并释放)⼀个节点
  • remove 操作: 清除指定值的元素。
    • 遍历每⼀个节点,找到就移除
  • unique 操作: 清除数值相同的连续元素,注意只有“连续⽽相同的元素”,才会被移除剩⼀个。
    • 遍历每⼀个节点,如果在此区间段有相同的元素就移除之

小结

list是一种双向链表。每个节点都包含一个数据域、一个前驱指针prev和后继指针next。

由于其链表特性,实现同样的操作,相对于STL中的通用算法,list的成员函数通常有更高的效率,内部仅需做一些指针的操作,因此尽可能选择list成员函数。

优点:

  • 不适用连续内存完成动态操作
  • 在内部方便插入和删除
  • 可以在两端进行push和pop操作

缺点:

  • 不支持随机访问,即下标擦澳洲和at
  • 相对于vector占用内存较多

deque

在这里插入图片描述
deque和vector的差异:

  • deque允许常数时间内对头端或者尾端进行元素的插入或者移除操作
  • deque没有所谓的容器概念,因为它是动态的以分段连续空间组合而成随时可以增加一块新的空间并拼接起来

虽然deque也提供随机访问的迭代器,但是它的迭代器和前面两种容器的都不一样,其设计相当复杂和精妙,因此,会对各种运算产生一定影响,除非必要,尽可能的选择vector而非deque

deque的中控器

deque在逻辑上看起来是连续空间,内部是由一段一段的定量连续空间构成。

一旦有必要在deque的前端或者尾端增加新空间,就配置一段定量的连续空间,串接在整个deque的头部或者尾部。

设计 deque 的⼤师们,想必是让deque的最大挑战就是在这些分段的定量连续空间上,维护其整体连续的假象,并提供其随机存取的接口,从而避开了像vector那样的“重新配置-复制-释放”开销三部曲。这样一来,虽然开销降低了,但是迭代器架构就很复杂了。

因此数据结构的设计和迭代器前进或后退等操作都⾮常复杂

deque采用一块所谓的map注意不是STL里面的map容器作为中控器,其实就是一小块连续空间,其中的每个元素都是指针,指向另外一段较大的连续线性空间,叫做缓冲区。缓冲区才是deque的存储空间主体

#ifndef __STL_NON_TYPE_TMPL_PARAM_BUG
template <class T, class Ref, class Ptr, size_t BufSiz>
class deque {
public:
	 typedef T value_type;
	 typedef value_type* pointer;
	 ...
protected:
	 typedef pointer** map_pointer;
	 map_pointer map;//指向 map,map 是连续空间,其内的每个元素都是⼀个指针。
	 size_type map_size;
	 ...
}

在这里插入图片描述

deque 的迭代器

deque都是分段连续空间,维持其“整体连续”假象的任务,就靠它的迭代器实现,也就是operator++和operator–两个运算子上面。

在看源码之前,我们可以思考⼀下,如果让你来设计,你觉得 deque 的迭代器应该具备什么样的结构和功能呢?

  • 首先,既然是连续分段,迭代器应该能指出当前的连续空间在哪里
  • 其次,因为缓冲区有边界,迭代器应该还要能够判断,当前是否处于缓冲区的边缘,如果是,一旦前进或者后退,就必须跳转到下一个或者上一个缓冲区
  • 然后,也是上面两点的前提,迭代器必须能够随时控制中控器。

有了这样的思想准备之后,我们再来看源码,就显得容易理解⼀些了。

template <class T, class Ref, class Ptr, size_t BufSiz>
struct __deque_iterator {
	 // 迭代器定义
	 typedef __deque_iterator<T, T&, T*, BufSiz> iterator;
	 typedef __deque_iterator<T, const T&, const T*, BufSiz> const_iterator;
	 static size_t buffer_size() {return __deque_buf_size(BufSiz, sizeof(T)); }
	 // deque是random_access_iterator_tag类型
	 typedef random_access_iterator_tag iterator_category;
	 // 基本类型的定义, 满⾜traits编程
	 typedef T value_type;
	 typedef Ptr pointer;
	 typedef Ref reference;
	 typedef size_t size_type;
	 typedef ptrdiff_t difference_type;
	 // node
	 typedef T** map_pointer;
	 map_pointer node;
	 typedef __deque_iterator self;
	 ...
}

deque 的每⼀个缓冲区由设计了三个迭代器(为什么这样设计?)

struct __deque_iterator {
	 ...
	 typedef T value_type;
	 T* cur;
	 T* first;
	 T* last;
	 typedef T** map_pointer;
	 map_pointer node;
 ...
};

那,为什么要这样设计呢?回到前⾯我们刚才说的,因为它是分段连续的空间,下图描绘了deque 的中控器、缓冲区、迭代器之间的相互关系 :
在这里插入图片描述
每⼀段都指向⼀个缓冲区 buffer,⽽缓冲区是需要知道每个元素的位置的,所以需要这些迭代器去访问。

  • 其中 cur 表示当前所指的位置;
  • first 表示当前数组中头的位置;
  • last 表示当前数组中尾的位置。

这样就⽅便管理,需要注意的是 deque 的空间是由 map 管理的, 它是⼀个指向指针的指针, 所以三个参数都是指向当前的数组,但这样的数组可能有多个,只是每个数组都管理这3个变量。

那么,缓冲区⼤⼩是谁来决定的呢?这⾥呢,⽤来决定缓冲区⼤⼩的是⼀个全局函数:

inline size_t __deque_buf_size(size_t n, size_t sz) {
 return n != 0 ? n : (sz < 512 ? size_t(512 / sz): size_t(1));
}
//如果 n 不为0,则返回 n,表示缓冲区⼤⼩由⽤户⾃定义
//如果 n == 0,表示 缓冲区⼤⼩默认值
//如果 sz = (元素⼤⼩ sizeof(value_type)) ⼩于 512 则返回 521/sz
//如果 sz 不⼩于 512 则返回 1

假设我们现在构造了⼀个 int 类型的 deque,设置缓冲区⼤⼩等于 32,这样⼀来,每个缓冲区可以容纳32/sizeof(int) = 8(64位系统) 个元素。经过⼀番操作之后,deque 现在有 20 个元素了,那么成员函数 begin()和 end() 返回的两个迭代器应该是怎样的呢?如下图所示:

在这里插入图片描述
20 个元素需要 20/(sizeof(int)) = 5(图中只展示3个) 个缓冲区。所以 map 运⽤了三个节点。迭代器 start 内的cur 指针指向缓冲区的第⼀个元素,迭代器 finish 内的 cur 指针指向缓冲区的最后⼀个元素(的下⼀个位置)。

注意,最后⼀个缓冲区尚有备⽤空间,如果之后还有新元素插⼊,则直接插⼊到备⽤空间。

deque 迭代器的前进和后退操作

operator++ 操作代表是需要切换到下⼀个元素,这⾥需要先切换再判断是否已经到达缓冲区的末尾。

self& operator++() {
	 ++cur; //切换⾄下⼀个元素
	 if (cur == last) { //如果已经到达所在缓冲区的末尾
		 set_node(node+1); //切换下⼀个节点
		 cur = first; 
	 }
	 return *this; 
}

operator-- 操作代表切换到上⼀个元素所在的位置,需要先判断是否到达缓冲区的头部,再后退

self& operator--() { 
	 if (cur == first) { //如果已经到达所在缓冲区的头部
		 set_node(node - 1); //切换前⼀个节点的最后⼀个元素
		 cur = last; 
	 }
	 --cur; //切换前⼀个元素
	 return *this; 
}

deque 的构造和析构函数

构造函数. 有多个᯿载函数, 接受⼤部分不同的参数类型. 基本上每⼀个构造函数都会调⽤create_map_and_nodes,这就是构造函数的核⼼, 待会就来分析这个函数实现

template <class T, class Alloc = alloc, size_t BufSiz = 0>
class deque {
 ...
public: // Basic types
	 deque() : start(), finish(), map(0), map_size(0){
		 create_map_and_nodes(0);
	 } // 默认构造函数
	 deque(const deque& x) : start(), finish(), map(0), map_size(0) {
		 create_map_and_nodes(x.size());
		 __STL_TRY {
			 uninitialized_copy(x.begin(), x.end(), start);
		 }
		 __STL_UNWIND(destroy_map_and_nodes());
	 }
	 // 接受 n:初始化⼤⼩, value:初始化的值
	 deque(size_type n, const value_type& value) : start(), finish(), map(0), map_size(0) {
	 	fill_initialize(n, value);
	  }
	 deque(int n, const value_type& value) : start(), finish(), map(0), map_size(0) {
		 fill_initialize(n, value);
	 }
	 deque(long n, const value_type& value) : start(), finish(), map(0), map_size(0){
		 fill_initialize(n, value);
	 }
 ...

下⾯我们来学习⼀下 deque 的中控器是如何配置的

void deque<T,Alloc,BufSize>::create_map_and_nodes(size_type_num_elements) {
	 //需要节点数= (每个元素/每个缓冲区可容纳的元素个数+1)
	 //如果刚好整除,多配⼀个节点
	 size_type num_nodes = num_elements / buffer_size() + 1;
	 //⼀个 map 要管理⼏个节点,最少 8 个,最多是需要节点数+2
	 map_size = max(initial_map_size(), num_nodes + 2);
	 map = map_allocator::allocate(map_size);
	// 计算出数组的头前⾯留出来的位置保存并在nstart.
	 map_pointer nstart = map + (map_size - num_nodes) / 2;
	 map_pointer nfinish = nstart + num_nodes - 1;
	 map_pointer cur;//指向所拥有的节点的最中央位置
	 ...
}

注意:deque 的 begin 和 end 不是⼀开始就是指向 map 中控器⾥开头和结尾的,⽽是指向所拥有的节点的最中央位置

这样带来的好处是可以使得头尾两边扩充的可能性一样大,换句话说,因为deque是头尾插入都是O(1),所以deque在头和尾都留有空间方便头尾插入。

那么,什么时候 map 中控器 本身需要调整⼤⼩呢?触发条件在于 reserve_map_at_back 和
reserve_map_at_front 这两个函数来判断,实际操作由 reallocate_map 来执⾏。

// 如果 map 尾端的节点备⽤空间不⾜,符合条件就配置⼀个新的map(配置更⼤的,拷⻉原来的,释放原来的)
void reserve_map_at_back (size_type nodes_to_add = 1) {
	 if (nodes_to_add + 1 > map_size - (finish.node - map))
		 reallocate_map(nodes_to_add, false);
}
// 如果 map 前端的节点备⽤空间不⾜,符合条件就配置⼀个新的map(配置更⼤的,拷⻉原来的,释放原来的)
void reserve_map_at_front (size_type nodes_to_add = 1) {
	 if (nodes_to_add > start.node - map)
		 reallocate_map(nodes_to_add, true);
}

deque 的插⼊元素和删除元素

最开始构造函数调用 create_map_and_nodes 函数,考虑到deque实现前后插入时间复杂度为O(1),保证了在前后流出空间,所以push和pop都可以在前面的数组进行操作。

因为deque能够双向操作,所以其push和pop操作都类似list可以直接有对应的操作,需要注意的是list是链表,并不会涉及到界线的判断,而deque是由数组来存储的,就需要随时对界线进行判断。

push实现:

template <class T, class Alloc = alloc, size_t BufSiz = 0>
class deque {
 ...
public: // push_* and pop_*
	 // 对尾进⾏插⼊
	 // 判断函数是否达到了数组尾部. 没有达到就直接进⾏插⼊
	 void push_back(const value_type& t) {
	 if (finish.cur != finish.last - 1) {
		 construct(finish.cur, t);
		 ++finish.cur;
	 }
	 else
		 push_back_aux(t);
	 }
	 // 对头进⾏插⼊
	 // 判断函数是否达到了数组头部. 没有达到就直接进⾏插⼊
	 void push_front(const value_type& t) {
		 if (start.cur != start.first) {
			 construct(start.cur - 1, t);
			 --start.cur;
		 }
		 else
			 push_front_aux(t);
	 }
	 ...
};

pop 实现

template <class T, class Alloc = alloc, size_t BufSiz = 0>
class deque {
 ...
public:
	 // 对尾部进⾏操作
	 // 判断是否达到数组的头部. 没有到达就直接释放
	 void pop_back() {
		 if (finish.cur != finish.first) {
		 	--finish.cur;
		 	destroy(finish.cur);
		 }
		 else
			 pop_back_aux();
	  }
	 // 对头部进⾏操作
	 // 判断是否达到数组的尾部. 没有到达就直接释放
	 void pop_front() {
		 if (start.cur != start.last - 1) {
		 	destroy(start.cur);
		 	++start.cur;
		 }
		 else
			 pop_front_aux();
	 }
	 ...
};

reserve_map_at⼀类函数. pop和push都先调⽤了reserve_map_at_XX函数, 这些函数主要是为了判断前后空间是否⾜够.

删除操作

因为deque是由数组构成的,所以地址空间是连续的,删除也像vector一样,要移动所有的元素。

deque为了保证效率尽可能高,就判断删除的位置是中间偏后还是中间偏前来进行移动。

template <class T, class Alloc = alloc, size_t BufSiz = 0>
class deque {
 ...
public: // Erase
 iterator erase(iterator pos)
 {
 iterator next = pos;
 ++next;
 difference_type index = pos - start;
 // 删除的地⽅是中间偏前, 移动前⾯的元素
 if (index < (size() >> 1))
 {
 copy_backward(start, pos, next);
 pop_front();
 }
 // 删除的地⽅是中间偏后, 移动后⾯的元素
 else {
 copy(next, finish, pos);
 pop_back();
 }
 return start + index;
 }
 // 范围删除, 实际也是调⽤上⾯的erase函数.
  iterator erase(iterator first, iterator last);
 void clear();
 ...
};

最后讲⼀下 insert 函数
deque 源码的基本每⼀个insert ᯿载函数都会调⽤了 insert_auto 判断插⼊的位置离头还是尾⽐较近。

  • 如果离头进:则先将头往前移动,调整将要移动的距离,⽤ copy 进⾏调整。
  • 如果离尾近:则将尾往前移动,调整将要移动的距离,⽤ copy 进⾏调整。

注意 : push_back是先执⾏构造在移动 node, ⽽ push_front 是先移动 node 在进⾏构造. 实现的差异主要是finish是指向最后⼀个元素的后⼀个地址⽽first指向的就只第⼀个元素的地址. 下⾯ pop 也是⼀样的。

其他:

  • reallocate_map:判断中控器的容ᰁ是否够⽤,如果不够⽤,申请更⼤的空间,拷⻉元素过去,修改 map 和start,finish 的指向。
  • fill_initialize 函数::申请空间,对每个空间进⾏初始化,最后⼀个数组单独处理. 毕竟最后⼀个数组⼀般不是会全部填充满。
  • clear函数. 删除所有元素. 分两步执⾏:⾸先从第⼆个数组开始到倒数第⼆个数组⼀次性全部删除,这样做是考虑到中间的数组肯定都是满的,前后两个数组就不⼀定是填充满的,最后删除前后两个数组的元素。
  • deque的swap操作也只是交换了start, finish, map, 并没有交换所有的元素.
  • resize函数. ᯿新将deque进⾏调整, 实现与list⼀样的.
  • 析构函数: 分步释放内存.

小结

deque其实是在功能上合并了vector和list。

优点:

  • 随机访问方便,即⽀持 [ ] 操作符和 vector.at();
  • 在内部⽅便的进⾏插⼊和删除操作;
  • 可在两端进⾏ push、pop

缺点:设计比较复杂,采用分段连续空间,所以占用内存比较多

使用区别:

  • 如果需要高效的随机存取,而不关心插入和删除的效率,用vector
  • 如果需要大量的删除和删除,而不关心随机存取,用list
  • 如果你需要随机存取,而且关心两端数据的插入和删除,则用deque

以deque为底层容器的适配器

最后介绍三种常用的数据结构,准确来说是一种适配器,底层都是以其他容器为基准

  • 栈-stack:先⼊后出,只允许在栈顶添加和删除元素,称为出栈和⼊栈。
  • 队列-queue:先⼊先出,在队⾸取元素,在队尾添加元素,称为出队和⼊队。
  • 优先队列-priority_queue:带权值的队列。

stack 和 queue 的底层其实就是使⽤ deque,⽤ deque 为底层容器封装

 #ifndef __STL_LIMITED_DEFAULT_TEMPLATES
template <class T, class Sequence = deque<T> >
#else
template <class T, class Sequence>
#endif
class stack {
public:
 typedef typename Sequence::value_type value_type;
 typedef typename Sequence::size_type size_type;
 typedef typename Sequence::reference reference;
 typedef typename Sequence::const_reference const_reference;
protected:
 Sequence c;
#ifndef __STL_LIMITED_DEFAULT_TEMPLATES
template <class T, class Sequence = deque<T> >
#else
template <class T, class Sequence>
#endif
class queue {
public:
 typedef typename Sequence::value_type value_type;
 typedef typename Sequence::size_type size_type;
 typedef typename Sequence::reference reference;
 typedef typename Sequence::const_reference const_reference;
protected:
 Sequence c;

heap

heap并不是一个容器,所以它没有实现自己的迭代器,也就没有遍历操作,它只是一种算法

push_heap 插⼊元素

插⼊函数是push_heap. heap只接受RandomAccessIterator类型的迭代器

template <class RandomAccessIterator>
inline void push_heap(RandomAccessIterator first, RandomAccessIterator last) {
	 __push_heap_aux(first, last, distance_type(first), value_type(first));
}
template <class RandomAccessIterator, class Distance, class T>
inline void __push_heap_aux(RandomAccessIterator first, RandomAccessIterator last,
Distance*, T*) {
	 // 这⾥传⼊的是两个迭代器的⻓度, 0, 还有最后⼀个数据
	 __push_heap(first, Distance((last - first) - 1), Distance(0), T(*(last - 1)));
}

pop_heap 删除元素

pop操作其实并没有真正的删除数据,只是将数据放到最后,并且没有指向最后的元素⽽已, 这⾥arrary也可以使⽤, 毕竟没有对数组的⼤⼩进⾏调整. pop的实现有两种, 这⾥都罗列了出来, 另⼀个传⼊的是 cmp 伪函数.

template <class RandomAccessIterator, class Compare>
inline void pop_heap(RandomAccessIterator first, RandomAccessIterator last,  Compare comp) {
	 __pop_heap_aux(first, last, value_type(first), comp);
}
template <class RandomAccessIterator, class T, class Compare>
inline void __pop_heap_aux(RandomAccessIterator first,  RandomAccessIterator last, T*, Compare comp) {
	 __pop_heap(first, last - 1, last - 1, T(*(last - 1)), comp distance_type(first));
}
template <class RandomAccessIterator, class T, class Compare, class Distance>
inline void __pop_heap(RandomAccessIterator first, RandomAccessIterator last, RandomAccessIterator result, T value, Compare comp, Distance*) {
	 *result = *first;
	 __adjust_heap(first, Distance(0), Distance(last - first), value, comp);
}
template <class RandomAccessIterator, class T, class Distance>
inline void __pop_heap(RandomAccessIterator first, RandomAccessIterator last, RandomAccessIterator result, T value, Distance*) {
	 *result = *first; // 因为这⾥是⼤根堆, 所以first的值就是最⼤值, 先将最⼤值保存.
	 __adjust_heap(first, Distance(0), Distance(last - first), value);
}

make_heap 将数组变成堆存放

template <class RandomAccessIterator>
inline void make_heap(RandomAccessIterator first, RandomAccessIterator last) {
 __make_heap(first, last, value_type(first), distance_type(first));
}
template <class RandomAccessIterator, class T, class Distance>
void __make_heap(RandomAccessIterator first, RandomAccessIterator last, T*,
 Distance*) {
	 if (last - first < 2) return;
	 // 计算⻓度, 并找出中间的根值
	 Distance len = last - first;
	 Distance parent = (len - 2)/2;
	 
	 while (true) {
		 // ⼀个个进⾏调整, 放到后⾯
		 __adjust_heap(first, parent, len, T(*(first + parent)));
		 if (parent == 0) return;
		 parent--;
	 }
}

sort_heap 实现堆排序

其实就是每次将第⼀位数据弹出从⽽实现排序功能.

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

priority_queue

上⼀节分析 heap 其实就是为 priority_queue 做准备. priority_queue 是⼀个优先级队列, 是带权值的. ⽀持插⼊和删除操作, 其只能从尾部插⼊,头部删除, 并且其顺序也并⾮是根据加⼊的顺序排列的。

priority_queue 因为也是队列的⼀种体现, 所以也就跟队列⼀样不能直接的遍历数组, 也就没有迭代器.priority_queue 本身也不算是⼀个容器, 它是以 vector 为容器以 heap为数据操作的配置器。

类型定义

#ifndef __STL_LIMITED_DEFAULT_TEMPLATES
template <class T, class Sequence = vector<T>,
 class Compare = less<typename Sequence::value_type> >
#else
template <class T, class Sequence, class Compare>
#endif
class priority_queue {
public:
	 // 符合traits编程规范
	 typedef typename Sequence::value_type value_type;
	 typedef typename Sequence::size_type size_type;
	 typedef typename Sequence::reference reference;
	 typedef typename Sequence::const_reference const_reference;
protected:
	 Sequence c; // 定义vector容器的对象
	 Compare comp; // 定义⽐较函数(伪函数)
	 ...
};

属性获取

priority_queue 只有简单的 3 个属性获取的函数, 其本身的操作也很简单, 只是实现依赖了 vector 和 heap 就变得⽐较复杂。

class priority_queue {
 ...
public:
 bool empty() const { return c.empty(); }
 size_type size() const { return c.size(); }
 const_reference top() const { return c.front(); }
 ...
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值