STL原理与构建——阅读笔记
前情提要:
本文不对任何算法做过多深入的解释,算法都很基础,建议以下所有内容等基础算法都会了之后再食用更佳哦~
文章目录
- 一、介绍STL
- 1.1 概述
- 1.2 六大组件
- 二、迭代器
- 2.1 迭代器
- 2.2 特性萃取机
- 三、容器
- 3.1 概述
- 3.2 向量容器(vector)
- 特点
- 实现
- 成员函数
- 3.3 堆容器(heap)
- 特点
- 实现
- 3.4 优先队列(priority_queue)
- 特点
- 实现
- 成员函数
- 3.5 双端队列(deque)
- 特点
- 实现
- 成员函数
- 3.6 链表容器(list)
- 特点
- 实现
- 成员函数
- 3.7 栈容器(stack)
- 特点
- 实现
- 成员函数
- 3.8 队列容器(queue)
- 特点
- 实现
- 成员函数
- 3.9 红黑树容器(RBTree)
- 特点
- 实现
- 3.10 集合容器(set/multiset)
- 特点
- 实现
- 成员函数
- 3.11 字典容器(map/multimap)
- 特点
- 实现
- 成员函数
- 3.12 哈希表容器(hash_table)
- 特点
- 实现
- 3.13 哈希集合(hash_set)
- 特点
- 实现
- 成员函数
- 3.14 哈希字典(hash_map)
- 特点
- 实现
- 成员函数
- 四、算法
- 4.1 概述
- 4.2 copy
- 介绍
- 实现
- 4.3 partial_sort
- 介绍
- 实现
- 4.4 sort
- 介绍
- 实现
一、介绍STL
1.1 概述
为了提高复用性,STL(Standard Template Library,标准模版库)是C++提出的一套数据结构(data strutrues)与算法(algorithm)的标准与模版。
STL广义上分为:容器(container)、算法(algorithm)、迭代器(iterator)。
1.2 六大组件
六大组件:容器、算法、迭代器、仿函数、适配器(配接器)、空间适配器。可以彼此之间组合套用。
- 容器:各种数据结构,如vector、string、map;
- 算法:各种常用算法,如sort、find、copy;
- 迭代器:实现了容器与算法之间的衔接,算法中通过迭代器实现对容器的操作,每个容器都具有专属的迭代器。原生指针(native pointer)本质上也是一种迭代器;
- 仿函数:类似于函数,通过类(class)重载opertor()实现的类似函数的表达方式;
- 适配器:用于修饰容器、仿函数获迭代器接口的东西;
- 空间配置器:负责空间的配置与管理。一个实现了动态空间配置、空间管理、空间释放的类。
组件之间的交互关系:容器通过空间配置起获取数据存储空间、算法通过迭代器存储访问容器的内容、仿函数协助算法实现更多策略、适配器可以修饰仿函数。
二、迭代器
2.1 迭代器
迭代器是一种行为类似指针的对象,而指针的各种行为中最常见的便是解引用(*)和成员访问(->),此外还有自增(++)。因此,迭代器最重要的编程工作是对operator*和operator->进行重载工作。
迭代器一般会和某种容器相关联,指向某种类型的元素,这种元素的类型(value_type)叫做相应类型。其中常用的相应类型有五种:
- value_type:迭代器所指对象的类型;
- difference_type:两个迭代器之间的距离;
- reference:容器内元素的引用;
- pointer:容器内元素的指针;
- iterator_category:表示迭代器类型(多数情况会根据迭代器类型激活重载函数)。
2.2 特性萃取机
既然有相应类型的存在,我们就会将其变成参数或返回值,此时当前已有的判断方法就无能为力了。我们需要一种东西来帮我们获取我们需要的相应类型,那就是“特性萃取”技术。
为了获得迭代器的相应类型,STL采用了一种称为特性萃取的技术,能够获得迭代器(包括原生指针)的相应类型:
template <class Iterator>
struct iterator_traits {typedef typename Iterator::iterator_category iterator_category;typedef typename Iterator::value_type value_type;typedef typename Iterator::difference_type difference_type;typedef typename Iterator::pointer pointer;typedef typename Iterator::reference reference;
};
还有针对原生指针的特化版本:
template <class T>
struct iterator_traits<T*> { // 特化版本,接受一个T类型指针typedef random_access_iterator_tag iterator_category;typedef T value_type;typedef ptrdiff_t difference_type;typedef T* pointer;typedef T& reference;
};template <class T>
struct iterator_traits<const T*> { // 特化版本,接受一个T类型const指针typedef random_access_iterator_tag iterator_category;typedef T value_type;typedef ptrdiff_t difference_type;typedef const T* pointer;typedef const T& reference;
};
为了符合STL规范,用户自定义的迭代器必须添加这些相应类型,特性萃取机才能有效运作(所以我们会看见定义的迭代器中均会在前面写上定义特性萃取机的代码)。
三、容器
3.1 概述
STL容器是讲运用最广泛的一些数据结构实现了出来。常用的包含:数组(array)、链表(list)、树(tree)、栈(stack)、队列(queue)、映射表(map)。
根据容器的排列特性,这些容器分为序列式容器与关联式容器:
- 序列式容器:强调值的顺序,每个元素位置固定,除非用插入删除,否则位置不变。如vector、stack、queue;
- 关联式容器:非线性的树形结构,元素之间没有固定的顺序。关联式容器也会为值选择一个key,起到索引作用,便于查找。如set/multiset、map/multimap。
3.2 向量容器(vector)
特点
- 查询、尾插、尾删时间复杂度为O(1);
- 头部插入删除的代价很大(需要大量移动);
- 内存两倍增长;
- 线性连续空间;
- 维护三个迭代器:start(起点)、finish(数据终点)、end_of_storage(空间终点)。
实现
本质上是一段连续的数组空间,vector通过对数据的观测,动态改变数组的空间大小。具体步骤为:
- 如果新增的元素导致当前分配的空间不足,则创建一个全新的、空间大小为原来两倍的数组;
- 将原来数组中的数据迁移到新的数组中;
- 迁移完成后释放原来的数组空间。
ps:其中判定数组空间大小是否不足凭借特点5中所述的三个迭代器,三个迭代器的具体作用如下:
- start确定起点位置,以此获得数组头部;
- finish确定数据终点位置,确定最后一个数据的位置;
- end_of_storage确定数组空间尾部,判断数据量是否超过数组大小。
(ps:代码实现后期有空再补~)
成员函数
函数 | 作用 |
---|---|
assign(first,last) | 用迭代器first,last所指定的元素取代容器中的元素 |
assign(num,val) | 用val的num份副本取代2元素 |
at(n) | 等价于[]运算符,返回容器中位置n的元素 |
front() | 返回容器中第一个元素的引用 |
back() | 返回容器中最后一个元素的引用 |
begin() | 返回容器中第一个元素的迭代器 |
end() | 返回容器中最后一个元素的下一个迭代器(不可解引用) |
max_size() | 返回容器类型的最大容量(2^30-1=0x3FFFFFFF) |
capacity() | 返回容器当前开辟的空间大小 |
size() | 返回容器中现有元素的个数(<=capacity) |
clear() | 清空容器中所有元素 |
empty() | 如果容器为空,返回真 |
erase(start,end) | 删除迭代器[start ,end)所指定范围内的元素 |
erase(i) | 删除迭代器i所指向的元素,返回指向删除元素下一位置的迭代器 |
insert(i,x) | 把x插入到迭代器i所指定的位置之前 |
insert(i,n,x) | 把x的n份副本插入到迭代器i所指定的位置之前 |
insert(i,start,end) | 在i位置插入在[start,end)区间的数据,无返回值。 |
push_back(x) | 尾插 |
op_back() | 删除容器最后一个元素 |
rbegin() | 返回一个反向迭代器,该迭代器指向的元素越过了容器中的最后一个元素 |
rend() | 返回一个反向迭代器,该迭代器指向容器中第一个元素 |
reverse() | 反转元素顺序 |
resize(n,x) | 把容器的大小改为n,新元素的初值赋为x |
swap(vector1) | 交换2个容器的内容 |
3.3 堆容器(heap)
特点
- heap是一个顺序容器;
- 我们平常使用heap,其实用的都是priority_queue(优先队列),所以heap本身并不算是STL中的容器,他作为pq的帮手而存在;
- heap是一棵完全二叉树,可以用数组实现,由于其需要动态改变大小,heap的底层由vector实现(这也是我把这节放在vector后的原因);
- STL默认为大根堆,可以通过修改比较方法变为小根堆;
- 新插入的节点为最后一个叶子节点,通过上滤移动到堆中的正确位置。
实现
以下从堆的插入维护上来讲解heap源码。
堆的插入(push_heap)性质正如我们所学过的堆,插入到完全二叉树的最后一个叶子节点上(由于vector是按层次遍历模拟树形结构,即在vector容器中存储到尾端),然后通过存储值的比较进行上滤。到此算法上便不再多讲,下面主要看源码的构建。
template <class RandomAccessIterator> // 新元素已插入容器尾部,此函数用于上滤维护heap
inline void push_heap(RandomAccessIterator first, RandomAccessIterator last) {// __push_heap_aux为内部调用函数,见下文。其中first、last表示vector首尾的迭代器,distance_type和value_type都是通过特性萃取机(iterator_traits)提取迭代器的相应类型:difference_type和value_type,详解也在下文。__push_heap_aux(first, last, distance_type(first), value_type(first));
}
上面的代码就是push_heap的最外层设计,其中distance_type和value_type是自定义函数提取器,提取过程如下:
template <class Iterator>
inline typename iterator_traits<Iterator>::difference_type*
distance_type(const Iterator&) { // 决定迭代器difference_typereturn static_cast<typename iterator_traits<Iterator>::difference_type*>(0);
}template <class Iterator>
inline typename iterator_traits<Iterator>::value_type*
value_type(const Iterator&) { // 决定迭代器value_typereturn static_cast<typename iterator_traits<Iterator>::value_type*>(0);
}
ps:解释一下上面的两个问题:
inline typename是什么意思(因为我一开始不懂所以备注):inline是内联函数都清楚,typename也是一个关键字,详细的作用为:
在类外部访问类中的名称时,可以使用类作用域操作符,调用通常存在三种:静态数据成员、静态成员函数和嵌套类型:Mydata::value,Mydata::function,Mydata::str。
由于多种情况导致代码含义可能不明确,如:
T::iterator *iter
可以理解为左右两边的乘法(看作类成员),也可以理解为实现一个指针(理解为一种类型)。在以上这种歧义中,就可以使用typename这个关键字表明是将后面的访问定义为类型。上文代码同理。
这里使用static_cast把0进行转换,是因为我们需要迭代器内嵌的类型,不关心具体值,所以可以直接用0代替。
接下来看一下__push_heap_aux的源码:
template <class RandomAccessIterator, class Distance, class T>
inline void __push_heap_aux(RandomAccessIterator first,RandomAccessIterator last, Distance*, T*) {// __push_heap代码见下文,此处解释四个参数的含义:// first:容器的开头位置;// Distance((last - first) - 1):新插入元素的下标位置;// Distance(0):根元素(树根)的下标值;// T(*(last - 1)):新增元素的值,用于上滤比较的基础。__push_heap(first, Distance((last - first) - 1), Distance(0), T(*(last - 1)));
}
以下为__push_heap核心代码:
template <class RandomAccessIterator, class Distance, class T>
void __push_heap(RandomAccessIterator first, Distance holeIndex,Distance topIndex, T value) {Distance parent = (holeIndex - 1) / 2; // 计算父节点下标(根据完全二叉树的性质)while (holeIndex > topIndex && *(first + parent) < value) { // 判断是否到达根节点&&上滤条件是否满足*(first + holeIndex) = *(first + parent); // 上滤成功,节点下移holeIndex = parent; // 插入元素位置上移parent = (holeIndex - 1) / 2; // 插入元素的父节点位置上移} *(first + holeIndex) = value; // 在最后的落点赋值
}
以上是heap的源码构建,算法原理略过后,其余的函数构建方式基本相同。
3.4 优先队列(priority_queue)
特点
- 拥有权值的queue,权值最高者永远排在最前面;
- 以vector为底层容器,配合heap的一套泛型算法,容器本身内部很简单,都是调用其他容器的接口;
- 无遍历行为,所以无迭代器。
实现
通过堆简单实现,代码如下:
template <class T, class Sequence = vector<T>, class Compare = less<typename Sequence::value_type> >
class priority_queue {....
protected:Sequence c; // 底层容器Compare comp; // 比较标准// 构造函数,将vector变为heap....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); }// pq相关操作bool 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); // 先压入vectorpush_heap(c.begin(), c.end(), comp); // 再调整位置}__STL_UNWIND(c.clear());}void pop() {__STL_TRY {pop_heap(c.begin(), c.end(), comp); // 最大元素放vector尾部c.pop_back(); // 弹出}__STL_UNWIND(c.clear());}
};
成员函数
函数 | 用途 |
---|---|
top() | 返回队列首部第一个元素。 |
empty() | 如果队列空则返回真。 |
emplace(elem) | 原位构造元素并排序底层容器。 |
pop() | 删除队列首部第一个元素。 |
push(elem) | 在队列末尾加入一个元素。 |
size() | 返回队列中元素的个数。 |
c1.swap(c2) | 交换两个优先队列的内容。 |
3.5 双端队列(deque)
特点
- O(1)时间对两端进行插入删除;
- 分段连续的数组空间,随时增加空间进行拼接;
- 通过中控器(一段连续的数组)保存指向每条数组的多个指针,所指向的连续空间被称为缓冲区;
- 迭代器共有四个属性:cur(缓冲区中指向的位置)、first(当前缓冲区的头部)、last(当前缓冲区的尾部)、node(中控器中的位置);
- 维护两个迭代器:start(中控器中第一个有效的节点,对应缓冲区中的第一个有效数据)、finish(中控器中最后一个有效的节点,对应的最后一个有效数据)。
实现
本质上是通过增加与释放一段固定长度的连续空间,实现数据空间的修改。并通过中控器将不连续的多段缓冲区确定顺序。
数据增删时通过start、finish迭代器对首尾数据进行控制,如果在增加数据时空间不足,可以分为以下情况:
- 尾插空间不足:中控器在最后一个有效节点后的一个位置,新分配一个确定长度的连续空间,并从首部开始添加数据;
- 头插空间不足:中控器在第一个有效节点前一个位置,新分配一个确定长度的连续空间,并从尾部开始添加数据;
- 中控器空间不足:创建一个新的、更大的连续空间,拷贝原来的指针进来,然后释放原空间。
(ps:以上每个步骤要对start、finish迭代器进行维护)
(ps:代码实现后期有空再补~)
成员函数
函数 | 作用 |
---|---|
c.assign(beg,end) | 将[beg; end)区间中的数据赋值给c。 |
c.assign(n,elem) | 将n个elem的拷贝赋值给c。 |
c.at(idx) | 传回索引idx所指的数据,如果idx越界,抛出out_of_range。 |
c.back() | 返回最后一个数据,不检查这个数据是否存在。 |
c.begin() | 返回迭代器的第一个数据。 |
c.clear() | 移除容器中所有数据。 |
deque | 创建一个空的deque。 |
deque c1(c2) | c复制一个deque。 |
deque c(n) | 创建一个deque,含有n个数据,数据均已缺省构造产生。 |
deque c(n, elem) | 创建一个含有n个elem拷贝的deque。 |
deque c(beg,end) | 创建一个以[beg;end)区间的deque。 |
c.~deque() | 销毁所有数据,释放内存。 |
c.empty() | 判断容器是否为空。 |
c.end() | 指向迭代器中的最后一个数据地址。 |
c.erase(pos) | 删除pos位置的数据,返回下一个数据的位置。 |
c.erase(beg,end) | 删除[beg,end)区间的数据,返回下一个数据的位置。 |
c.front() | 返回容器的第一个元素。 |
get_allocator | 使用构造函数返回一个拷贝。 |
c.insert(pos,elem) | 在pos位置插入一个elem拷贝,传回新数据位置。 |
c.insert(pos,n,elem) | 在pos位置插入>n个elem数据。无返回值。 |
c.insert(pos,beg,end) | 在pos位置插入在[beg,end)区间的数据。无返回值。 |
c.max_size() | 返回容器中最大数据的数量。 |
c.pop_back() | 删除最后一个数据。 |
c.pop_front() | 删除头部数据。 |
c.push_back(elem) | 在尾部加入一个数据。 |
c.push_front(elem) | 在头部插入一个数据。 |
c.rbegin() | 传回一个反向队列的第一个数据。 |
c.rend() | 传回一个反向队列的最后一个数据的下一个位置。 |
c.resize(num) | 重新指定队列的长度。 |
c.size() | 返回容器中实际数据的个数。 |
c1.swap(c2) | 将c1和c2元素互换 |
3.6 链表容器(list)
特点
- 环状双向链表结构,有效利用空间;
- 插入、接合、删除操作,均不会对操作对象以外的迭代器有影响;
- 访问查询速度较慢;
- 同样是维护start、finish两个指针
实现
普通的环状双向链表结构,没啥特别的。
(ps:代码实现后期有空再补~)
成员函数
函数 | 作用 |
---|---|
assign() | 给list赋值 |
back() | 返回最后一个元素 |
begin() | 返回指向第一个元素的迭代器 |
clear() | 删除所有元素 |
empty() | 如果list是空的则返回true |
end() | 返回末尾的迭代器 |
erase() | 删除一个元素 |
front() | 返回第一个元素 |
get_allocator() | 返回list的配置器 |
insert() | 插入一个元素到list中 |
max_size() | 返回list能容纳的最大元素数量 |
merge() | 合并两个list |
pop_back() | 删除最后一个元素 |
pop_front() | 删除第一个元素 |
push_back() | 在list的末尾添加一个元素 |
push_front() | 在list的头部添加一个元素 |
rbegin() | 返回指向第一个元素的逆向迭代器 |
remove() | 从list删除元素 |
remove_if() | 按指定条件删除元素 |
rend() | 指向list末尾的逆向迭代器 |
resize() | 改变list的大小 |
reverse() | 把list的元素逆序 |
size() | 返回list中的元素个数 |
sort() | 给list排序 |
splice() | 合并两个list |
swap() | 交换两个list |
unique() | 删除list中重复的元素 |
3.7 栈容器(stack)
特点
- 默认情况完全由deque(底层容器)实现;
- 没有遍历行为,所以无迭代器;
- 可以自定义底层容器。
实现
template <class T, class Sequence = deque<T> >
class stack {....
protected:Sequence c; // 底层容器
public:bool 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); } // deque末端进void pop() { c.pop_back(); } // deque末端出
};
stack默认使用deque作为底层容器,但定义stack时可以自己定义底层容器,如:
stack<int, list> lstack;
由于list也具有empty、size、back、push_back、pop_back等,所以用list作为底层容器合法。
成员函数
函数 | 用途 |
---|---|
size() | 返回栈中的数据个数。 |
top() | 返回栈顶部第一个值。 |
push(elem) | 插入值到栈的顶部。 |
pop() | 删除栈顶部第一个值。 |
empty() | 判断栈是否为空。 |
emplace(elem) | 在栈顶原位置构造元素。 |
c1.swap(c2) | 交换两个栈的内容。 |
3.8 队列容器(queue)
特点
和栈一样。
实现
template <class T, class Sequence = deque<T> >
class queue {....
protected:Sequence c; // 底层容器
public:bool empty() const { return c.empty(); }size_type size() const { return c.size(); }reference front() { return c.front(); }const_reference front() const { return c.front(); }reference back() { return c.back(); }const_reference back() const { return c.back(); }void push(const value_type& x) { c.push_back(x); } // 尾端进void pop() { c.pop_front(); } // 前端出
};
和栈一样可以使用list等容器做底层容器。
成员函数
函数 | 用途 |
---|---|
back() | 返回最后一个元素。 |
empty() | 如果队列空则返回真。 |
front() | 返回队列首部第一个元素。 |
pop() | 删除队列首部第一个元素。 |
push() | 在队列末尾加入一个元素。 |
size() | 返回队列中元素的个数。 |
3.9 红黑树容器(RBTree)
特点
- STL中红黑树是不给外部使用的独立容器,是set/multiset与map/multimap的构建基础。
- 红黑树的节点和迭代器均采用双层结构(一层基础特性,一层范型数据):
- 节点:__rb_tree_node继承自__rb_tree_node_base;
- 迭代器:__rb_tree_iterator继承自__rb_tree_base_iterator。
实现
红黑树的性质详细可看红黑树容器(RBTree)——内部容器,本章只做实现讲解。
首先定义颜色标识:
typedef bool __rb_tree_color_type; // bool类型标识红黑树节点颜色的类型
const __rb_tree_color_type __rb_tree_red = false; // 红色为false
const __rb_tree_color_type __rb_tree_black = true; // 黑色为true
红黑树的基础节点(定义了一些基本特性):
struct __rb_tree_node_base {typedef __rb_tree_color_type color_type; // 颜色类型typedef __rb_tree_node_base* base_ptr; // 指向基础节点的指针color_type color; // 节点颜色base_ptr parent; // 父节点指针base_ptr left; // 左儿子指针base_ptr right; // 右儿子指针static base_ptr minimum(base_ptr x) { // 二叉搜索树求最小值(往左找到底)while (x->left != 0) x = x->left;return x;}static base_ptr maximum(base_ptr x) { // 二叉搜索树求最大值(往右找到底)while (x->right != 0) x = x->right;return x;}
};
上层节点(范型数据):
template <class Value>
struct __rb_tree_node : public __rb_tree_node_base {typedef __rb_tree_node<Value>* link_type; // 用于链接到各种亲戚到指针,同时能指定类型Value value_field; // 节点值
};
基础迭代器(红黑树的基础特性与操作):
struct __rb_tree_base_iterator {typedef __rb_tree_node_base::base_ptr base_ptr; // 指向基础节点的指针typedef bidirectional_iterator_tag iterator_category; // 内部的双向迭代器(不太懂待补)typedef ptrdiff_t difference_type;base_ptr node; // 迭代器和节点之间的纽带void increment() { // 迭代器++时使用....}void decrement() { // 迭代器--时使用....}
};
上文采用双向迭代器是因为红黑树也是一颗二叉搜索树,有规律可循,可以定义++与–,是双向的。
上层迭代器(范型数据):
template <class Value, class Ref, class Ptr>
struct __rb_tree_iterator : public __rb_tree_base_iterator {typedef Value value_type;typedef Ref reference;typedef Ptr pointer;// 多种类迭代器typedef __rb_tree_iterator<Value, Value&, Value*> iterator;typedef __rb_tree_iterator<Value, const Value&, const Value*> const_iterator;typedef __rb_tree_iterator<Value, Ref, Ptr> self;typedef __rb_tree_node<Value>* link_type; // 指向上层节点的指针__rb_tree_iterator() {} // 无参构造__rb_tree_iterator(link_type x) { node = x; } // 初始化node__rb_tree_iterator(const iterator& it) { node = it.node; } // 赋值构造// 解引用,注意link_type类型转换reference operator*() const { return link_type(node)->value_field; } pointer operator->() const { return &(operator*()); } // 箭头操作符// ++p与p++self& operator++() { increment(); return *this; }self operator++(int) {....}// --p与p--self& operator--() { decrement(); return *this; }self operator--(int) {....}
};
以下为红黑树的数据结构框架:
template <class Key, class Value, class KeyOfValue, class Compare, class Alloc = alloc>
class rb_tree {....typedef __rb_tree_node<Value> rb_tree_node;typedef simple_alloc<rb_tree_node, Alloc> rb_tree_node_allocator; // 空间配置器,一次分配一个节点typedef rb_tree_node* link_type; // 指向上层节点link_type get_node() { return rb_tree_node_allocator::allocate(); } // 获得一个节点空间void put_node(link_type p) { rb_tree_node_allocator::deallocate(p); } // 释放一个节点空间link_type create_node(const value_type& x) { // 分配并构造一个节点link_type tmp = get_node();construct(&tmp->value_field, x); // 省略构造return tmp;}....void destroy_node(link_type p) { // 析构并释放一个节点destroy(&p->value_field); // 省略析构put_node(p);}....protected:size_type node_count; // 记录节点数量link_type header; // 边界技巧(下文介绍)Compare key_compare; // 键值大小比较准则....static link_type& left(link_type x) { return (link_type&)(x->left); } // 左儿子static link_type& right(link_type x) { return (link_type&)(x->right); } // 右儿子// 通过调用底层获取极值....static link_type minimum(link_type x) { return (link_type) __rb_tree_node_base::minimum(x);}static link_type maximum(link_type x) {return (link_type) __rb_tree_node_base::maximum(x);}....
public:pair<iterator,bool> insert_unique(const value_type& x); // 节点键值唯一插入iterator insert_equal(const value_type& x); // 节点键值可重复插入void erase(iterator position); // 删除节点....public:// 以下函数在multimap和multiset中使用iterator find(const key_type& x);size_type count(const key_type& x) const;iterator lower_bound(const key_type& x);iterator upper_bound(const key_type& x);pair<iterator,iterator> equal_range(const key_type& x);
}
以上基本完成了所有模板介绍,以下介绍一下上面使用的一个技巧。在处理root边界情况的时候使用了一个header指针,看header初始化函数:
link_type& root() const { return (link_type&) header->parent; }
link_type& leftmost() const { return (link_type&) header->left; }
link_type& rightmost() const { return (link_type&) header->right; }void init() {header = get_node(); // 分配空间color(header) = __rb_tree_red; // 由于根节点一定为黑色,哨兵使用红色便于区分root() = 0; // header->parent = null,父节点制空leftmost() = minimum(header); // 左儿子指最小rightmost() = maximum(header); // 右儿子指最大
}
这里让哨兵header的左右儿子指向最小与最大也是个节省时间空间的操作,这样begin()与end()就可以在O(1)中求出来了:
iterator begin() { return leftmost(); }
iterator end() { return rightmost(); }
3.10 集合容器(set/multiset)
特点
- 通过红黑树实现(平衡的搜索树);
- 插入删除只需操作节点,所以不涉及内存移动与拷贝;
- 支持集合操作,如:交(set_intersection)、差(set_difference)、并(set_union)、对称差 (set_symmetric_difference) 等;
- 储存方式也是键值对,不过key与value相同;
- 迭代器定义为const iterator,所以set的键值都不允许修改;
- 插入删除的时间复杂度为O(log2n);
- set不允许存放两个实值相同的元素;multiset使用insert_equal机制允许插入重复键值
实现
红黑树详看
- 这一章节:红黑树容器(RBTree)——内部容器
- 这篇博客:红黑树(RBTree)原理
set/multiset大部分是调用红黑树实现的,差别也就是在插入与返回值,以下用set举例:
template <class Key, class Compare = less<Key>, class Alloc = alloc> // 默认采用递增排序
class set {public:// set的key和value相同,所以Key与Compare相同typedef Key key_type;typedef Key value_type;typedef Compare key_compare;typedef Compare value_compare;
private:// 这里使用仿函数identity作为rb_tree的KeyOfValue类型实参// identity直接将传入的参数返回(因为key/value相同)typedef rb_tree<key_type, value_type, identity<value_type>, key_compare, Alloc> rep_type;rep_type t; // 底层容器——红黑树
public:// const_iterator,迭代器无法写入(set不能修改实值)....typedef typename rep_type::const_iterator iterator;typedef typename rep_type::const_iterator const_iterator;....// 构造函数set() : t(Compare()) {}explicit set(const Compare& comp) : t(comp) {}....// 转调用rb_tree的接口key_compare key_comp() const { return t.key_comp(); }value_compare value_comp() const { return t.key_comp(); }iterator begin() const { return t.begin(); }iterator end() const { return t.end(); }....// 插入/删除typedef pair<iterator, bool> pair_iterator_bool; // second返回值表明是否插入成功pair<iterator,bool> insert(const value_type& x) { pair<typename rep_type::iterator, bool> p = t.insert_unique(x); return pair<iterator, bool>(p.first, p.second);}iterator insert(iterator position, const value_type& x) {typedef typename rep_type::iterator rep_iterator;return t.insert_unique((rep_iterator&)position, x);}....// set相关操作iterator find(const key_type& x) const { return t.find(x); }size_type count(const key_type& x) const { return t.count(x); }iterator lower_bound(const key_type& x) const { return t.lower_bound(x); }iterator upper_bound(const key_type& x) const { return t.upper_bound(x); }pair<iterator,iterator> equal_range(const key_type& x) const { return t.equal_range(x); }// set之间的比较方法friend bool operator== __STL_NULL_TMPL_ARGS (const set&, const set&);friend bool operator< __STL_NULL_TMPL_ARGS (const set&, const set&);
};
这里附注,可以不用内部find查找元素,用STL中的find去查找,但是这两个之间是有差别的。因为set是由红黑树构建的,使用STL的find反而回降低set查找的时间复杂度为O(n)(因为舍去了二叉树的查找性质),采用内部find的话时间复杂度为O(logn)。
成员函数
begin() | 返回容器的第一个迭代器 |
---|---|
end() | 返回容器的最后元素的下一个迭代器 |
clear() | 删除容器中的所有元素 |
empty() | 判断容器是否为空 |
insert() | 插入一个元素 |
erase() | 删除一个元素 |
size() | 返回当前容器的元素个数 |
rbegin() | 返回尾元素的逆向迭代器指针 |
reverse_iterator rend() | 返回首元素前一个位置的迭代器指针 |
find() | 查找一个元素,不存在返回s.end() |
lower_bound() | 返回第一个大于或等于给定关键值的元素 |
upper_bound() | 返回第一个大于给定关键值的元素 |
swap() | 交换两个集合元素 |
3.11 字典容器(map/multimap)
特点
- key值唯一;
- 通过红黑树(RBTree)变体的平衡二叉树实现;
- 用户基于key快速检索的能力;
- 元素按规则插入,不能指定位置;
- 迭代器可以修改value不能修改key;
- 基于key的快速查找复杂度为O(log2n);
- map插入时不允许拥有重复键,插入则无效;multimap插入时可以拥有重复键,所以函数返回值均为迭代器
实现
红黑树详看
- 这一章节:红黑树容器(RBTree)——内部容器
- 这篇博客:红黑树(RBTree)原理
map/multimap也是大部分基于红黑树实现,以map为例看一下框架结构:
template <class Key, class T, class Compare = less<Key>, class Alloc = alloc> // 默认采用递增顺序
class map {public:typedef Key key_type; // 键值类型typedef T data_type; // 实值类型typedef T mapped_type;typedef pair<const Key, T> value_type; // 注意const Key,表示无法修改键值typedef Compare key_compare;....private:// rb_tree的一个节点存储一个pair,select1st抽取出value_type(pair)内的first元素typedef rb_tree<key_type, value_type, select1st<value_type>, key_compare, Alloc> rep_type;rep_type t; // 底层容器——红黑树
public:....typedef typename rep_type::iterator iterator; // 实值可以修改....// 构造函数map() : t(Compare()) {}explicit map(const Compare& comp) : t(comp) {}....// 迭代器操作key_compare key_comp() const { return t.key_comp(); }value_compare value_comp() const { return value_compare(t.key_comp()); }iterator begin() { return t.begin(); }const_iterator begin() const { return t.begin(); }iterator end() { return t.end(); }....// 下标操作(键值索引实值),存在相同键值的节点则返回,否则插入再返回// insert返回pair<iterator, bool>,iterator指向红黑树的某个节点,bool为插入成功标识// 下标操作返回迭代器的第二个元素,即实值T& operator[](const key_type& k) {return (*((insert(value_type(k, T()))).first)).second;}// 插入/删除pair<iterator,bool> insert(const value_type& x) { return t.insert_unique(x); }iterator insert(iterator position, const value_type& x) {return t.insert_unique(position, x);}....// map相关操作iterator find(const key_type& x) { return t.find(x); }size_type count(const key_type& x) const { return t.count(x); }iterator upper_bound(const key_type& x) {return t.upper_bound(x); }pair<iterator,iterator> equal_range(const key_type& x) {return t.equal_range(x);}// map的比较操作friend bool operator== __STL_NULL_TMPL_ARGS (const map&, const map&);friend bool operator< __STL_NULL_TMPL_ARGS (const map&, const map&);
};
成员函数
map的
函数 | 作用 |
---|---|
begin() | 返回指向map头部的迭代器 |
clear() | 删除所有的元素 |
count() | 返回指定元素出现的次数 |
empty() | 判断容器是否为空 |
end() | 返回指向map末尾的迭代器 |
get_allocator() | 返回map的配置器 |
insert() | 添加元素 |
lower_bound() | 返回键值>=给定元素的第一个位置 |
upper_bound() | 返回>给定元素的第一个元素 |
max_size() | 返回可以容纳的最大元素个数 |
rbegin() | 返回一个指向map尾部的逆向迭代器 |
rend() | 返回一个指向map头部的逆向迭代器 |
find(k) | 返回指向第一个与键 k 匹配的 pair 的迭代指针,没找到返回指向map尾部的迭代器 |
erase() | 删除迭代器所指向的元素 |
swap() | 两个容器交换 |
size() | 返回map中元素的个数 |
multimap的
函数 | 作用 |
---|---|
begin() | 返回指向第一个元素的迭代器 |
clear() | 删除所有元素 |
count() | 返回一个元素出现的次数 |
empty() | 如果multimap为空则返回真 |
end() | 返回一个指向multimap末尾的迭代器 |
equal_range(k) | 该函数查找所有与 k 关联的值。返回迭代指针的 pair,它标记开始和结束范围 |
erase() | 删除元素 |
find() | 查找元素 |
get_allocator() | 返回multimap的配置器 |
insert() | 插入元素 |
key_comp() | 返回比较key的函数 |
lower_bound() | 返回键值>=给定元素的第一个位置 |
max_size() | 返回可以容纳的最大元素个数 |
rbegin() | 返回一个指向mulitmap尾部的逆向迭代器 |
rend() | 返回一个指向multimap头部的逆向迭代器 |
size() | 返回multimap中元素的个数 |
swap() | 交换两个multimaps |
upper_bound() | 返回键值>给定元素的第一个位置 |
value_comp() | 返回比较元素value的函数 |
3.12 哈希表容器(hash_table)
特点
SGI STL中的hashtable采用的开链方法,使用vector+list的方式解决碰撞问题(解决碰撞问题的办法有许多,线性探测、二次探测、开链等等)。
实现
首先列表上的节点结构如下:
template <class Value>
struct __hashtable_node {__hashtable_node* next; // 指向下一节点指针Value val; // 存储实际值
};
然后是hash_table的迭代器结构:
template <class Value, class Key, class HashFcn,class ExtractKey, class EqualKey, class Alloc>
struct __hashtable_iterator {typedef hashtable<Value, Key, HashFcn, ExtractKey, EqualKey, Alloc> hashtable;....typedef __hashtable_node<Value> node;// 定义迭代器相应类型typedef forward_iterator_tag iterator_category; // 前向迭代器typedef Value value_type;typedef ptrdiff_t difference_type;typedef size_t size_type;typedef Value& reference;typedef Value* pointer;node* cur; // 迭代器目前所指节点hashtable* ht; // 和hashtable之间的纽带// 构造函数__hashtable_iterator(node* n, hashtable* tab) : cur(n), ht(tab) {}__hashtable_iterator() {}// 运算符操作reference operator*() const { return cur->val; }pointer operator->() const { return &(operator*()); }iterator& operator++();iterator operator++(int);bool operator==(const iterator& it) const { return cur == it.cur; }bool operator!=(const iterator& it) const { return cur != it.cur; }
};
从上述的前向迭代器与运算符操作中不难知道,hash_table的迭代器不能后退,只有opertor的自增操作,代码如下:
template <class V, class K, class HF, class ExK, class EqK, class A>
__hashtable_iterator<V, K, HF, ExK, EqK, A>&
__hashtable_iterator<V, K, HF, ExK, EqK, A>::operator++() { // 注意类模板成员函数的定义const node* old = cur;cur = cur->next; // 移动到下一个nodeif (!cur) { // 到了list结尾size_type bucket = ht->bkt_num(old->val); // 根据节点值定位旧节点所在桶号while (!cur && ++bucket < ht->buckets.size()) // 计算下一个可用桶号cur = ht->buckets[bucket]; // 找到,另cur指向新桶的第一个node}return *this;
}
下面hashtable的结构,由于内容很多这里只列出主要代码:
template <class Value, class Key, class HashFcn, class ExtractKey, class EqualKey,class Alloc>
class hashtable { // hash_table数据结构
public:typedef Key key_type;typedef Value value_type;typedef HashFcn hasher; // 散列函数类型typedef EqualKey key_equal;typedef size_t size_type;typedef ptrdiff_t difference_type;....private:hasher hash; // 散列函数key_equal equals; // 判断键值是否相等ExtractKey get_key; // 从节点取出键值typedef __hashtable_node<Value> node;typedef simple_alloc<node, Alloc> node_allocator; // 空间配置器vector<node*,Alloc> buckets; // 桶的集合,可以看出一个桶实值上是一个node*size_type num_elements; // node个数....
}
SGI STL将hash_table的大小,也就是vector的大小设计为28个质数,并存放在一个数组中,如果不够则按数组中的数值向后增加:
static const int __stl_num_primes = 28; // 28个质数
static const unsigned long __stl_prime_list[__stl_num_primes] = {53, 97, 193, 389, 769,1543, 3079, 6151, 12289, 24593,49157, 98317, 196613, 393241, 786433,1572869, 3145739, 6291469, 12582917, 25165843,50331653, 100663319, 201326611, 402653189, 805306457, 1610612741, 3221225473, 4294967291
};
下面介绍插入操作,以insert_unique为例:
// 插入新元素,键值不能重复
pair<iterator, bool> insert_unique(const value_type& obj) {resize(num_elements + 1); // 判断vector是否需要扩充的函数(下午补充)return insert_unique_noresize(obj); // 直接插入obj(下文补充)
}
以上可以看出insert主要操作分两步:第一步是扩充(如果需要的话),第二步是插入。
首先解释resize代码:
template <class V, class K, class HF, class Ex, class Eq, class A>
void hashtable<V, K, HF, Ex, Eq, A>::resize(size_type num_elements_hint) { // 判断是否需要扩充vectorconst size_type old_n = buckets.size();if (num_elements_hint > old_n) { // 元素个数大于vector当前容量,则需要扩充vector// next_size为从前文预备的vector大小的数组中取下一个const size_type n = next_size(num_elements_hint); if (n > old_n) {vector<node*, A> tmp(n, (node*) 0); // 建立一个临时的vector作为转移目的地(为了重新映射)for (size_type bucket = 0; bucket < old_n; ++bucket) { // 一个桶一个桶进行转移node* first = buckets[bucket];while (first) { // 一个节点一个节点进行转移// 散列过程,对n取模size_type new_bucket = bkt_num(first->val, n); buckets[bucket] = first->next;// 从链表前端插入first->next = tmp[new_bucket]; tmp[new_bucket] = first;// first指向旧vector的下一个nodefirst = buckets[bucket];}buckets.swap(tmp); // 两个vector的内容互换,使buckets彻底转移}}}
}
代码基本思路就是:先扩充,再移动,最后交换。
然后是insert_unique_noresize的代码:
template <class V, class K, class HF, class Ex, class Eq, class A>
pair<typename hashtable<V, K, HF, Ex, Eq, A>::iterator, bool> // 返回值为pair
hashtable<V, K, HF, Ex, Eq, A>::insert_unique_noresize(const value_type& obj) { // 直接插入节点,无需扩充(因为分了步骤)const size_type n = bkt_num(obj); // 对obj进行散列,然后对vector大小取模,从而确定桶号node* first = buckets[n]; // first指向对应桶的第一个node// 查找是否有重复node,有则直接返回这个nodefor (node* cur = first; cur; cur = cur->next) if (equals(get_key(cur->val), get_key(obj))) return pair<iterator, bool>(iterator(cur, this), false);// 没有遇到相同node,则在list开头插入node* tmp = new_node(obj);tmp->next = first;buckets[n] = tmp;++num_elements;return pair<iterator, bool>(iterator(tmp, this), true);
}
3.13 哈希集合(hash_set)
特点
- 以hash_table为底层编写;
- 搜索和红黑树一样都很高效;
- hash_set没有排序。
实现
其内部结构形似普通的set容器,此处介绍几个不相同的函数。
插入操作全部都使用hash_table的insert_unique接口,代码如下:
pair<iterator, bool> insert(const value_type& obj) {pair<typename ht::iterator, bool> p = rep.insert_unique(obj);return pair<iterator, bool>(p.first, p.second);
}
然后是构造函数,代码如下:
private:typedef hashtable<Value, Value, HashFcn, identity<Value>, EqualKey, Alloc> ht;ht rep; // 底层机制——hash_tablepublic:hash_set() : rep(100, hasher(), key_equal()) {} // 设定默认大小为100
这里想要将hash_table中的vector的大小设定为100,并非真的设定为了100,而是与其最接近的内置质数197,所以hash_table桶的数量为197。
成员函数
函数 | 说明 |
---|---|
begin |
返回一个迭代器,此迭代器用于发现 hash_set 中的第一个元素。
|
cbegin |
返回一个常量迭代器,此迭代器用于发现 hash_set 中的第一个元素。
|
cend |
返回一个常量迭代器,此迭代器用于发现 hash_set 中最后一个元素之后的位置。
|
clear |
清除 hash_set 的所有元素。
|
count |
返回 hash_set 中其键与指定为参数的键匹配的元素数量。
|
crbegin |
返回一个常量迭代器,此迭代器用于发现反向 hash_set 中的第一个元素。
|
crend |
返回一个常量迭代器,此迭代器用于发现反向 hash_set 中最后一个元素之后的位置。
|
emplace |
将就地构造的元素插入到 hash_set 。
|
emplace_hint |
将就地构造的元素插入到 hash_set ,附带位置提示。
|
empty |
测试 hash_set 是否为空。
|
end |
返回一个迭代器,此迭代器用于发现 hash_set 中最后一个元素之后的位置。
|
equal_range |
返回一对迭代器,这两个迭代器分别用于发现 hash_set 中其键大于指定键的第一个元素,以及 hash_set 中其键等于或大于指定键的第一个元素。
|
erase |
从 hash_set 中的指定位置移除一个元素或元素范围,或者移除与指定键匹配的元素。
|
find |
返回一个迭代器,此迭代器用于发现 hash_set 中其键与指定键等效的元素的位置。
|
get_allocator |
返回用于构造 allocator 的 hash_set 对象的副本。
|
insert |
将一个元素或元素范围插入到 hash_set 。
|
key_comp |
检索用于对 hash_set 中的键进行排序的比较对象副本。
|
lower_bound |
返回一个迭代器,此迭代器指向 hash_set 中其键等于或大于指定键的第一个元素。
|
max_size |
返回 hash_set 的最大长度。
|
rbegin |
返回一个迭代器,此迭代器用于发现反向 hash_set 中的第一个元素。
|
rend |
返回一个迭代器,此迭代器用于发现反向 hash_set 中最后一个元素之后的位置。
|
大小 |
返回 hash_set 中的元素数量。
|
swap |
交换两个 hash_set 的元素。
|
upper_bound |
返回一个迭代器,此迭代器指向 hash_set 中其键等于或大于指定键的第一个元素。
|
value_comp |
检索哈希特征对象的副本,该哈希特征对象用于对 hash_set 中的元素键值进行哈希处理和排序。
|
3.14 哈希字典(hash_map)
特点
- 底层由hash_table实现,其余与map十分相同;
- 无特定排序。
实现
浅看一下hash_map的部分代码:
template <class Key, class T, class HashFcn = hash<Key>,class EqualKey = equal_to<Key>,class Alloc = alloc>
class hash_map {private:// 这里hash_table类型的第一个类型参数提供了pairtypedef hashtable<pair<const Key, T>, Key, HashFcn,select1st<pair<const Key, T> >, EqualKey, Alloc> ht;ht rep; // 底层机制——hash table....
}
所以这里是通过hash_table的每个node存储一个pair实现的。
成员函数
函数 | 说明 |
---|---|
at |
在 hash_map 中查找具有指定关键字值的元素。
|
begin |
返回一个迭代器,此迭代器用于发现 hash_map 中的第一个元素。
|
cbegin |
返回一个常量迭代器,此迭代器用于发现 hash_map 中的第一个元素。
|
cend |
返回一个常量迭代器,此迭代器用于发现 hash_map 中最后一个元素之后的位置。
|
clear |
清除 hash_map 的所有元素。
|
count |
返回 hash_map 中其键与指定为参数的键匹配的元素数量。
|
crbegin |
返回一个 const 迭代器,用于寻址反向 hash_map 中的第一个元素。
|
crend |
返回一个 const 迭代器,用于寻址反向 hash_map 中最后一个元素之后的位置。
|
emplace |
将就地构造的元素插入到 hash_map 。
|
emplace_hint |
将就地构造的元素插入到 hash_map ,附带位置提示。
|
empty |
测试 hash_map 是否为空。
|
end |
返回一个迭代器,此迭代器用于发现 hash_map 中最后一个元素之后的位置。
|
equal_range |
返回一对迭代器,这两个迭代器分别用于发现 hash_map 中其键大于指定键的第一个元素,以及 hash_map 中其键等于或大于指定键的第一个元素。
|
erase |
从指定位置删除 hash_map 中的一个元素或一系列元素
|
find |
返回一个迭代器,此迭代器用于发现 hash_map 中其键与指定键等效的元素的位置。
|
get_allocator |
返回用于构造 allocator 的 hash_map 对象的副本。
|
insert |
将一个元素或元素范围插入到 hash_map 。
|
key_comp |
返回一个迭代器,此迭代器指向 hash_map 中其键值等于或大于指定键的键值的第一个元素。
|
lower_bound |
返回一个迭代器,此迭代器指向 hash_map 中其键值等于或大于指定键的键值的第一个元素。
|
max_size |
返回 hash_map 的最大长度。
|
rbegin |
返回一个迭代器,此迭代器用于发现反向 hash_map 中的第一个元素。
|
rend |
返回一个迭代器,此迭代器用于发现反向 hash_map 中最后一个元素之后的位置。
|
size |
返回 hash_map 中的元素数量。
|
swap |
交换两个 hash_map 的元素。
|
upper_bound |
返回一个迭代器,此迭代器指向 hash_map 中其键值大于指定键的键值的第一个元素。
|
value_comp |
检索用于对 hash_map 中的元素值进行排序的比较对象副本。
|
四、算法
4.1 概述
STL提供在各种容器中通用的算法大约有70种,如插入、删除、查找、排序等,大部分算法都在algorithm库中定义,numeric中也有一些算法。
算法通过对容器中提供的迭代器,实现对容器内数据的操作。
4.2 copy
介绍
copy是STL里面使用非常频繁的函数,所以为了提高效率,copy可以称为是STL的精妙代表之一。
实现
首先是copy的对外调用接口,主要有三种:
template <class InputIterator, class OutputIterator> // 迭代器泛化版本
inline OutputIterator copy(InputIterator first, InputIterator last, OutputIterator result) {return __copy_dispatch<InputIterator,OutputIterator>()(first, last, result);
}inline char* copy(const char* first, const char* last, char* result) { // 针对原生指针的重载memmove(result, first, last - first);return result + (last - first);
}inline wchar_t* copy(const wchar_t* first, const wchar_t* last, // 针对原生指针的重载wchar_t* result) {memmove(result, first, sizeof(wchar_t) * (last - first));return result + (last - first);
}
上面有一个咱平常倒不是很常用的原生指针类型wchar_t
,这是用于保存unicode编码的字符的类型。同时在代码中可以看到,如果传入的迭代器是字符型(char/wchar_t)的原生指针,那么直接使用底层的memmove拷贝,效率非常高,但如果是普通的迭代器,则需要进一步分析。
所以更进一步分析,我们再来看__copy_dispatch函数,其中包括一个泛化版本和两个偏特化版本:
template <class InputIterator, class OutputIterator> // 泛化版本
struct __copy_dispatch {OutputIterator operator()(InputIterator first, InputIterator last,OutputIterator result) {return __copy(first, last, result, iterator_category(first));}
};template <class T>
struct __copy_dispatch<T*, T*> { // 特化版本T* operator()(T* first, T* last, T* result) {typedef typename __type_traits<T>::has_trivial_assignment_operator t; return __copy_t(first, last, result, t());}
};template <class T>
struct __copy_dispatch<const T*, T*> { // 特化版本T* operator()(const T* first, const T* last, T* result) {typedef typename __type_traits<T>::has_trivial_assignment_operator t; return __copy_t(first, last, result, t());}
};
本人对泛化与特化不是特别了解,所以在此补充一下内容:
- 特化是对C++模版选择时,进行优先判断的对象,满足则使用,不满足则判断其他特化直到最后选择泛化;
- 这里的特化版本使用的是指针类型的偏特化,简单来说就是指针就选特化,不是就泛化。
举例说明:
xxx<string> obj1
会使用泛化版本;xxx<string*> obj2
则会使用特化版本。
泛化版本——如果迭代器是普通的迭代器类型,则会调用泛化版本,并且将会根据迭代器类型选择调用不同的函数:
template <class InputIterator, class OutputIterator>
inline OutputIterator __copy(InputIterator first, InputIterator last,OutputIterator result, input_iterator_tag) { // 输入迭代器// 输入迭代器的移动只能靠operator++,所以采用逐个赋值for ( ; first != last; ++result, ++first)*result = *first;return result;
}template <class RandomAccessIterator, class OutputIterator>
inline OutputIterator __copy(RandomAccessIterator first, RandomAccessIterator last,OutputIterator result, random_access_iterator_tag) { // 随机迭代器return __copy_d(first, last, result, distance_type(first));
}
上文这里能进行重载时根据最后一个参数判断的迭代器类型。
如果是随机迭代器,则继续往下调用:
template <class RandomAccessIterator, class OutputIterator, class Distance>
inline OutputIterator __copy_d(RandomAccessIterator first, RandomAccessIterator last,OutputIterator result, Distance*) {for (Distance n = last - first; n > 0; --n, ++result, ++first) *result = *first;return result;
}
这里为什么要单独列出一个函数,不像输入迭代器一样直接写在__copy里面呢?主要是因为特化版本也会用到啦。
为了进一步的时间优化,将first != last
这个条件优化成了n > 0
,作为比较条件,这样当然会减少时间成本,效率更高。
以上已经讲完了泛化版本,然后是两个特化版本,调用__copy_t,它的第三个参数是用来判断指针所指类型是否真的需要用复制操作符来一个个复制,也就是判断是trivial还是non-trivial,判断工作就交给了类型萃取器__type_traits来完成。
我并不是很懂trivial还是non-trivial的意思,所以在此补充:
如果指针所指对象拥有的是trivial assignment operator(拥有自定义的赋值构造),复制操作可以不通过构造函数,我们可以直接用memmove来完成拷贝。如果对于原生指针,它指向的对象拥有的是non-trivial assignment operator(自己没有定义赋值),我们就只能使用for循环来慢慢拷贝。
根据是否需要单独复制可以把__copy_t分成两个版本:
template <class T> // trivial
inline T* __copy_t(const T* first, const T* last, T* result, __true_type) {memmove(result, first, sizeof(T) * (last - first));return result + (last - first);
}template <class T> // non-trivial
inline T* __copy_t(const T* first, const T* last, T* result, __false_type) {return __copy_d(first, last, result, (ptrdiff_t*) 0);
}
其中trivial版本的很容易,直接memmove,不需要做过多的复制动作。而non-trivial版本的拷贝则需要逐一进行。由于原生指针属于随机迭代器,所以它可以直接调用刚才介绍的__copy_d函数。
总结一下:
copy通过泛化与特化的形式,区分可以采用memmove赋值与手动循环赋值的情况,使程序在特定情况下可以获得当前情况最优的时间消耗。就此实现了支持泛化并拥有极高效率的copy函数。
还有一点是在
__copy_d
的判断条件优化中,可以看到STL对常用函数在细节上尽所能及的优化。
4.3 partial_sort
介绍
- partial_sort(部分排序)可以将数组中(最大/最小)的n个数有序排列在数组头部;
- partial_sort是通过堆实现的;
- 输入值包含first、middle、last,其中[first, middle)为有序数列,[middle, last]无序并且与初始相对位置不一定相同。
实现
首先介绍一下partial_sort的算法思想(以升序为例):
- 首先对容器内区间为[first, middle)的元素执行make_heap()操作构造一个最大堆,此时[first, middle)中的数据呈堆序;
- 然后循环遍历[middle, last)中的元素,和first(堆最大值)进行比较。如果当前元素小于first,则将first弹出并放入该元素位置,该元素插入堆(这一步的目的是为了剔除堆中最大的元素,使得堆内最后为最小的n个数);
- 最后将堆内所有数据弹出至[first, middle)中,使得[first, middle)中数据升序。
下面分析STL的源码。partial_sort有两个版本,一个默认以小于作为比较规则,出来的顺序为递增排列。另一个可以传入一个仿函数,即自定义比较规则,本文分析前者。
接下来看一下partial_sort的外层代码:
template <class RandomAccessIterator>
inline void partial_sort(RandomAccessIterator first,RandomAccessIterator middle,RandomAccessIterator last) {__partial_sort(first, middle, last, value_type(first));
}
继续看__partial_sort代码:
template <class RandomAccessIterator, class T>
void __partial_sort(RandomAccessIterator first, RandomAccessIterator middle,RandomAccessIterator last, T*) {make_heap(first, middle); // 将[first, middle)区间构造成heapfor (RandomAccessIterator i = middle; i < last; ++i)if (*i < *first) // 当前遍历到的元素比堆中最大的元素小__pop_heap(first, middle, i, T(*i), distance_type(first)); // first值放i中,i的原值融入heap并调整sort_heap(first, middle); // 将数据弹出至[first, middle)中
}
下面是__pop_heap函数,实现了遍历的元素与最大值的位置互换(first移到i的位置上,i进堆):
template <class RandomAccessIterator, class T, class Distance>
inline void __pop_heap(RandomAccessIterator first, RandomAccessIterator last,RandomAccessIterator result, T value, Distance*) {*result = *first; // 弹出元素放到遍历元素的位置__adjust_heap(first, Distance(0), Distance(last - first), value); // 弹出first并插入value
}
这里的__adjust_heap主要内容就是弹出first、插入value。
接下来继续看sort_heap:
template <class RandomAccessIterator>
void sort_heap(RandomAccessIterator first, RandomAccessIterator last) {while (last - first > 1) pop_heap(first, last--);
}
我们在pop_heap中的主要操作就是将heap中的最大元素弹出到数组的尾部(弹出后正好尾部会空出一个空间,直接放进去),结束后数组自然就变成升序了。
同时如果想直接对数组进行堆排,就直接输入partial_sort(first, last, last);
就好了,这样也会自动跳过__pop_heap阶段(后面的快排会用到),非常的nice。
4.4 sort
介绍
- 容器的迭代器必须是随机迭代器;
- sort的基本策略为:数据量大时采用快速排序,数据量小时采用插入排序(对快排常用的一种优化策略),递归层次过深改用堆排序。
实现
首先解释插入排序,平均和最坏时间复杂度都为O(N²),量级小于千时插入排序还是一个不错的选择。SGI STL的插入排序默认以增序排列,另外还可以传入一个仿函数作为比较规则。
template <class RandomAccessIterator>
void __insertion_sort(RandomAccessIterator first, RandomAccessIterator last) {if (first == last) return; // 容器为空,直接返回for (RandomAccessIterator i = first + 1; i != last; ++i) // 确定排序范围__linear_insert(first, i, value_type(first));
}
接下来是__linear_insert函数:
template <class RandomAccessIterator, class T>
inline void __linear_insert(RandomAccessIterator first, RandomAccessIterator last, T*) {T value = *last; // 插入元素值if (value < *first) { // 如果比已排序区间的第一个元素还要小copy_backward(first, last, last + 1); // 使已排序区间整体后移一个位置*first = value; // 新元素放在开头位置}else__unguarded_linear_insert(last, value);
}
除去头插判断,就是__unguarded_linear_insert函数了:
template <class RandomAccessIterator, class T>
void __unguarded_linear_insert(RandomAccessIterator last, T value) {RandomAccessIterator next = last; // 指向新插入元素--next; // 指向之前一个元素while (value < *next) { // 发现逆转对(需要前移)*last = *next; // 较大的元素往后走last = next;--next;}*last = value; // 新元素放入终点位置
}
插入方法采用从后向前遍历已排序区间。同时可以发现这里没有临界情况判断,这就是在__linear_insert函数中进行头插判断的原因了。
如果数据量大,STL改用快速排序,平均效率为O(NlogN),最坏效率为O(N²)。
STL快排采用的方法是选取前、中、后三点的中值作为枢轴。然后是分割,遍历整个迭代器,小于枢轴的放左边,大于枢轴的放右边。具体分割代码如下:
template <class RandomAccessIterator, class T> // 快排的分割函数
RandomAccessIterator __unguarded_partition(RandomAccessIterator first, RandomAccessIterator last, T pivot) {while (true) {while (*first < pivot) ++first; // 向后移动直到*first大于或等于枢轴--last;while (pivot < *last) --last; // 向前移动直到*last小于或等于枢轴if (!(first < last)) return first; // 交叉后停止,返回的迭代器作为右子区间的开头iter_swap(first, last); // 交换两个迭代器所指元素++first;}
}
这里只写分割代码是因为,快排并非是一种单独情况下调用的函数,而是用来减小需要排序数组长度的手段。递归被嵌入到综合排序函数里面了。
递归层数过高则咱们会使用堆排序,可以直接调用partial_sort对整个数组进行排序
所以现在我们可以来看一下sort的外层代码了。
首先是sort的对外接口:
template <class RandomAccessIterator>
inline void sort(RandomAccessIterator first, RandomAccessIterator last) {if (first != last) {__introsort_loop(first, last, value_type(first), __lg(last - first) * 2);__final_insertion_sort(first, last);}
}
接下来我们继续一个一个解释,首先是__introsort_loop:
wiki对__introsort_loop的解释:
内省排序(英语:Introsort)是由David Musser在1997年设计的排序算法。这个排序算法首先从快速排序开始,当递归深度超过一定深度(深度为排序元素数量的对数值)后转为堆排序。采用这个方法,内省排序既能在常规数据集上实现快速排序的高性能,又能在最坏情况下仍保持O(nlogn)的时间复杂度。
其中需要判断的“超过一定深度”,在上面代码中使用了__lg(last - first) * 2
根据元素个数计算深度阈值进行判断。
template <class Size>
inline Size __lg(Size n) { // 控制分割恶化的情况,求2^k <= n的最大k值Size k;for (k = 0; n != 1; n >>= 1) ++k;return k;
}
__introsort_loop的代码如下:
template <class RandomAccessIterator, class T, class Size>
void __introsort_loop(RandomAccessIterator first,RandomAccessIterator last, T*,Size depth_limit) {while (last - first > __stl_threshold) { // 大于某个值才进行快速排序,这里__stl_threshold = 16if (depth_limit == 0) {partial_sort(first, last, last); // 分割恶化,采用堆排序return;}--depth_limit;// 采用三点中值作为枢轴进行快速排序RandomAccessIterator cut = __unguarded_partition(first, last, T(__median(*first, *(first + (last - first)/2), *(last - 1))));__introsort_loop(cut, last, value_type(first), depth_limit); // 对右半部分递归进行排序last = cut; // 调整last位置,下一次while循环将对左半部分排序}
}
其实这个函数相对于普通的快排只有两个特点,但这两个特点使得快排更加稳定:
- 确定快排的最小范围(设定__stl_threshold值),到了下界阈值后交给插排;
- 确定快排的最大范围(计算depth_limit值),超过上届阈值直接堆排。
实现完内省排序(introsort_loop)后,我们的数组经过有下界的快排已经大差不差的变成一个已经比较有序的数列了(如果堆排已经结束了),那么__final_insertion_sort显而易见,就是把剩下的数组进行一个分段插排就好:
template <class RandomAccessIterator>
void __final_insertion_sort(RandomAccessIterator first, RandomAccessIterator last) {if (last - first > __stl_threshold) { // 超过16个元素,则分割为两段__insertion_sort(first, first + __stl_threshold); // 前段使用插入排序__unguarded_insertion_sort(first + __stl_threshold, last); // 剩余元素逐个插入}else__insertion_sort(first, last); // 元素个数不超过16,直接使用插入排序
}
这个操作是不是很熟悉,为什么要先对前16(__stl_threshold)个数进行插入排序呢?我们继续往下看。
下面解释__unguarded_insertion_sort:
template <class RandomAccessIterator>
inline void __unguarded_insertion_sort(RandomAccessIterator first, RandomAccessIterator last) {__unguarded_insertion_sort_aux(first, last, value_type(first));
}
其中套用的__unguarded_insertion_sort_aux为:
template <class RandomAccessIterator, class T>
void __unguarded_insertion_sort_aux(RandomAccessIterator first, RandomAccessIterator last, T*) {for (RandomAccessIterator i = first; i != last; ++i)__unguarded_linear_insert(i, T(*i)); // 将i向前遍历进行插入
}
从__unguarded_insertion_sort_aux的循环中,我们不难看出,现在就是在套用之前已经学过的__unguarded_linear_insert函数进行前向遍历插入,因为整个数组是分段有序的,所以不难得出结论:每个数据向前移动距离不会超过__stl_threshold(在这里就是16)。
那么这里又为什么不需要进行临界判断呢?这就是在__final_insertion_sort中先做一遍插入排序的原因了。
小结一下
sort也是STL中非常经典的算法了,在sort中我们也能看出STL也节省时间上有着极大的特点。这一点在最后的插入排序上尤为突出,因为数据已经大致有序,所以每个数据向前移动的距离必定不超过__stl_threshold。使得最后的插入排序优势突出。
STL原理与构建——阅读笔记相关推荐
- word2vec原理_word2vec论文阅读笔记
word2vec算是NLP中的经典算法,之前在课程中简单的学过,但面试时经不起深问.痛定思痛,参考Jack(@没搜出来)的总结,笔者重点阅读了Mikolov的原始论文[1]和Xin Rong的详细推导 ...
- 无线通信原理及应用--阅读笔记一
第二章:关于蜂窝的概念: 系统设计的基础 1. 蜂窝技术解决的问题: 在有限的频谱上提供大的容量.空分复用的典型应用. 2. 基站, 小区, 扇区, 簇的比较 G和W有不同区分方式, 大意 ...
- 《游戏设计的100个原理》阅读笔记——游戏创新、创作、平衡与解决问题的方法论【转】...
<游戏设计的100个原理([美]Wendy Despain)>整合了众多游戏设计秘籍,它概括并阐释了100条重要的游戏设计领域的方法.原理和设计哲学,分4篇向读者讲述了游戏创新.创作.平衡 ...
- 大型网站技术架构:核心原理与案例分析阅读笔记二
大型网站技术架构:核心原理与案例分析阅读笔记二 网站架构设计时可能会存在误区,其实不必一味追随大公司的解决方案,也不必为了技术而技术,要根据本公司的实际情况,制定适合本公司发展的网站架构设计,否则会变 ...
- 构建之法阅读笔记之三
构建之法阅读笔记之三 本章为团队和流程,主要介绍了典型的软件团队模式和开发流程以及它们的优缺点.TSP.MVP.MBP.RUP 团队:并不是几个人凑到一起就叫团队,称之为团队 1.应该有一致的集体目标 ...
- 关于《构建之法》阅读笔记 的致歉博客
在暑假的时候,我阅读了<构建之法>,并写了三篇阅读笔记.在几天前,我在百度上看到一篇关于<构建之法>的阅读笔记,我仔细的阅读了这个同学写的阅读笔记,觉得他的阅读笔记写的很好.我 ...
- 构建之法阅读笔记(二)
离上次写阅读笔记很长时间了,罪恶感涌来.. 这次主要写一下我对创新的感受,这部分是在<构建之法>的第16章-IT行业的创新. 最近几年我经常能够听到"创新"这个词,总以 ...
- 嵌入式实时操作系统ucosii原理及应用(任哲)-- --阅读笔记2
本文是<嵌入式实时操作系统ucosii原理及应用(任哲)>一书第三章的阅读笔记,知识点多为摘录,若希望深入了解,请购买该书认真研读.由于一些知识比较零散,记起来不大方便,又习惯画图辅助记忆 ...
- MCS-51单片机原理与接口技术--阅读笔记(原创)
MCS-51单片机原理与接口技术--阅读笔记(原创) 由 王宇 原创并发布 : 第1章 单片机概述 MSC-51美国Intel公司 第2章 基本机构 制造工艺为HMOS MSC-5 ...
最新文章
- python列表切片口诀-python学习之“切片操作从入门到精通”
- opus android编译,Mac系统opus Android编译集成
- 不擅长面试可以怎么准备面试?
- android studio turn off hyperv,Android Studio 无法运行模拟器
- python语言中的单行注释语句_Python 1基础语法一(注释、行与缩进、多行语句、空行和代码组)...
- JPDA 架构研究5 - Agent利用环境指针访问VM (内存管理篇)
- Linux内核自旋锁
- Linux crontab定时执行任务 命令格式与详细例子
- vue饼图组件_vue写一个图表组件(1)----饼图
- mysql 数据库备份与恢复_mysql 数据库备份与还原
- 一种FIFO实现原理
- hibernate一级缓存的源码初窥
- 算法竞赛入门经典—C++入门
- Chrome debugger调试技巧
- ARP报文抓包解析学习
- Autoit+selenium+python实现文件上传功能
- 【渝粤题库】陕西师范大学165210 国际人力资源管理 作业(专升本)
- Excel PivotTable 使用心得手顺分享(三)
- 回味无穷:历史名人的幽默隽语[转自人民网]
- 微信小程序开发笔记6——小程序添加并使用外部字体(亲测可用)
热门文章
- 创建一个Date类,具有三个整型成员变量year,month,day,具有三个成员方法setDate,isLeapYear,print
- python 列表嵌套字典 添加修改删除_python3--字典,字典的嵌套,概念:分别赋值
- 博导谈寒门子弟上大学:要相信双一流大学没有“废物”!
- Ubuntu 14.04.2安装内核源码树以及编译
- 神经网络模型量化论文小结
- Buffon投针实验:究竟为什么是pi?
- 启动计算机时听到嘀嘀声,为何电脑开机时会有滴滴的声音?
- 【按键精灵源码】一个稍微复杂点的脚本界面
- 小米手机升级后便签内容没了如何找回
- python爬虫某招聘数据进行可视化