通过前面的学习以及使用,我们对 STL
已经有了一定的认识。
通俗说:STL
是 Standard Template Library
(标准模板库),是高效的 C++
程序库,其采用泛型编程思想对常见数据结构(顺序表,链表,栈和队列,堆,二叉树,哈希)和算法(查找、排序、集合、数值运算…)等进行封装,里面处处体现着泛型编程程序设计思想以及设计模式,已被集成到 C++
标准程序库中。具体说:STL
中包含了容器、适配器、算法、迭代器、仿函数以及空间配置器。
STL
设计理念:追求代码高复用性以及运行速度的高效率,在实现时使用了许多技术,因此熟悉 STL
不仅对我们正常使用有很大帮助,而且对自己的知识也有一定的提高。
STL
中的容器,可以划分为两大类:序列式容器和关联式容器。
必备技能:
算法:问题的求解步骤,以有限的步骤,解决数学或逻辑中的问题。 STL
中的算法主要分为两大类:与数据结构相关算法(容器中的成员函数)和通用算法(与数据结构不相干)。
STL
中通用算法总共有70多个,为我们提供了一个 algorithm
库,主要包含:排序,查找,排列组合,数据移动,拷贝,删除,比较组合,运算等。 以下只列出了部分常用的算法:
|
|
---|---|
accumulate | 元素统计 |
binary_search | 二分查找 |
copy | 拷贝 |
copy_backward | 逆向拷贝 |
copy_n | 拷贝n个元素 |
count | 计数 |
count_if | 在特定条件下计数 |
equal | 判断两个区间相等与否 |
fill | 填充元素 |
fill_n | 填充元素n次 |
find | 循环查找 |
find_if | 循环查找符合特定条件元素 |
find_end | 查找某个子序列的最后一次出现点 |
find_first_of | 查找某个元素首次出现点 |
for_each | 对区间内的每隔一元素实行某种操作 |
is_heap | 判断某区间是否为一个heap |
is_sorted | 判断某区间是否已排序 |
lexicographical_compare | 以字典顺序进行比较 |
max | 获取最大值 |
max_element | 最大值所在位置 |
merge | 合并两个序列 |
min | 获取最小值 |
min_element | 最小值所在位置 |
next_permutation | 获取下一个排列组合 |
pre_permutation | 获取前一个排列组合 |
partial_sort | 局部排序 |
partial_sum | 局部求和 |
partition | 分割 |
remove | 删除某类元素 |
remove_copy | 删除某类元素并将结果拷贝到另一个容器中 |
remove_if | 有条件的删除某类元素 |
replace | 替换某类元素 |
replace_if | 有条件的替换 |
reverse | 反转序列 |
sort | 排序(不稳定) |
stable_partition | 分割并保持元素的相对次序 |
stable_sort | 分割并保持相等元素的相对位置(稳定排序算法) |
unique | 取出重复性元素 |
make_heap | 创建堆 |
push_heap | 堆插入 |
pop_heap | 堆删除 |
sort_heap | 堆排序 |
在刷题过程中,sort、reverse、unique、remove、max、min、find
等函数用着是相当的舒服,甚至有些黑科技如 next_permutation、pre_permutation
排列组合函数能达到秒杀题目的效果。但在 STL
的算法库中这些仅仅是九牛一毛罢了,顶多就是比较常见而已。
就问一个简单的问题,STL
中最为常见的 sort
函数底层是怎么实现的?可能一部分人知道他是个混合排序,由快排和插排组成,但是再精确点呢?为什么我们写的排序没有 sort
快呢?这些都是 STL
算法封装的魅力所在,高效且优雅。
必备技能:
迭代器是一种设计模式,让用户通过特定的接口访问容器的数据,不需要了解容器内部的底层数据结构。
C++
中迭代器本质:是一个指针,让该指针按照具体的结构去操作容器中的数据。
通过前面算法的学习了解到:STL
中算法分为容器相关联与通用算法。所谓通用算法,即与具体的数据结构无关,比如:
参见代码如下:
template<class InputIterator, class T>
InputIterator find ( InputIterator first, InputIterator last, const T& value )
{
for ( ;first!=last; first++)
if ( *first==value )
break;
return first;
}
find
算法在查找时,与具体的数据结构无关,只要给出待查找数据集合的范围,find
就可在该范围中查找,找到返回该元素在区间中的位置,否则返回 end
。
来自大佬博文的图片: LLZK_ :STL迭代器的"特性萃取机"-----Traits
在这里也能够发现,上面的 InputIterator
即为只读迭代器,在这里做个引子,后面会详细介绍。
问题:对于 vector、list、deque、map、unordered_set
等容器,其底层数据结构均不相同,那 find
算法是怎么统一向后遍历呢?
++
算法,*
解引用算法,这是由它底层迭代器封装方式决定的。每个容器的底层结构都不同,为了降低算法使用容器时的复杂度,底层结构应该对于算法透明,迭代器就充当了算法与容器之间的转接层,因此:每个容器的迭代器应该由容器设计者负责提供,然后容器按照约定给出统一的接口即可。
参见代码如下:
// vector中:
typedef T* iterator;
iterator begin();
iterator end();
find(v.beign(), v.end(), 5);
// list中
typedef list_iterator<T, T&, T*> iterator;
iterator begin();
iterator end();
find(l.begin(), l.end(), 5);
容器底层结构不同,导致其实现原理不同,容器迭代器的设计,必须结合具体容器的底层数据结构。比如:
vector
vector
底层结构为一段连续空间,迭代器前后移动时比较容易实现,因此 vector
的迭代器实际是对原生态指针的封装,即:typedef T* iterator
。list
list
底层结构为带头结点的双向循环链表,迭代器在移动时,只能按照链表的结构前后依次移动,因此链表的迭代器需要对原生态的指针进行封装,因为当对迭代器 ++
时,应该通过节点中的 next
指针域找到下一个节点。可参考:[C++系列] 56. list深度剖析及模拟实现如果迭代器不能直接使用原生态指针操作底层数据时,必须要对指针进行封装,在封装时需要提供以下方法:
reference operator*()
/ pointer operator->()
self& operator++() / self operator++(int)
self& operator--() / self operator--(int)
(注意:有些容器不能向前移动,比如 forward_list
)bool operator!=(const self& it)const / bool operator==(const self& it)const
参见代码如下:
// 比如list:
template <class T, class Alloc = alloc>
class list
{
// ...
typedef __list_iterator<T, T&, T*> iterator;
// ...
};
参见代码如下:
template <class T, class Alloc = alloc>
class list
{
// ...
iterator begin(){ return (link_type)((*node).next);}
iterator end(){ return node;}
// ...
};
反向迭代器:正向迭代器的适配器,即正向迭代器 ++
往 end
方向移动,--
往 begin
方向移动,而反向迭代器 ++
则往 begin
方向移动,--
则向 end
方向移动。
可参考:[C++系列] 56. list深度剖析及模拟实现 中 list
的反向迭代器模拟实现。
可参考侯捷大佬的:《 STL
源码剖析》
可参考来自大佬博文: LLZK_ :STL迭代器的"特性萃取机"-----Traits 但是博文需要有前置知识,是默认了解类型萃取 Traits
原理机制。在此作简单介绍:
参见代码如下:
#include
#include
#include
using namespace std;
class A{};
// 希望
template <class T>
struct unknown_class
{
// 待萃取类型
typedef T return_type;
};
/****************************************/
// 容器萃取机
template <class unknown_class>
struct unknown_class_traits
{
// 在此必须加上typename
// 因为unknown_class不知道其为类型、函数、变量
// 更不知道return_value为类型、函数、变量
// 所以显示加上typename否则编译器不知道后面为类型
typedef typename unknown_class::return_type return_type;
// 萃取vector传入value_type即可
// typedef typename unknown_class::value_type return_type;
};
// 模板与模板函数
// 加入typename显示声明其为类型
template <class unknown_class>
typename unknown_class_traits<unknown_class>::return_type
func(unknown_class u)
{
typedef typename unknown_class_traits<unknown_class>::return_type return_type;
return return_type();
}
int main()
{
// 需要函数将unknown_class中的类型萃取出来定义变量
unknown_class<A> a;
// unknown_class a;
// vector a;
// 若b的类型为能与a相同那么类型萃取成功
auto b = func(a);
cout << typeid(b).name() << endl;
return 0;
}
适配器:又接着配接器,是一种设计模式,简单的说:需要的东西就在眼前,但是由于接口不对而无法使用,需要对其接口进行转化以方便使用。即:将一个类的接口转换成用户希望的另一个类的接口,使原本接口不兼容的类可以一起工作。
STL
中适配器总共有三种类型:
stack
和 queue
stack
的特性是后进先出,queue
的特性为先进先出,该种特性 deque
的接口完全满足,因此 stack
和 queue
在底层将 deque
容器中的接口进行了封装。参见代码如下:
template < class T, class Container = deque<T> >
class stack { ... };
template < class T, class Container = deque<T> >
class queue { ... };
++
和 --
操作刚好和正向迭代器相反,因此:反向迭代器只需将正向迭代器进行重新封装即可。仿函数:一种具有函数特征的对象,调用者可以像函数一样使用该对象 ,为了能够“行为类似函数”,该对象所在类必须自定义函数调用运算符 operator()
,重载该运算符后,就可在仿函数对象的后面加上一对小括号,以此调用仿函数所定义的operator()操作,就其行为而言,“仿函数”一次更切贴。
仿函数一般配合算法,作用就是:提高算法的灵活性。
参见代码如下:
#include
#include
class Mul2
{
public:
void operator()(int& data)
{ data <<= 1;}
};
void mul2(int & val)
{
val *= 2;
}
class Mod3
{
public:
bool operator()(int data)
{ return 0 == data % 3;}
};
int main()
{
// 给容器中每个元素乘2
vector<int> v{1,2,3,4,5,6,7,8,9,0};
// 临时对象
for_each(v.begin(), v.end(), Mul2());
// 也可显示定义对象
Mul2 m2;
for_each(v.begin(), v.end(), m2);
// 也可直接传函数
for_each(v.begin(), v.end(), mul2);
// 上三者等价,将扩大2*2*2=8倍
for (auto e : v)
cout << e << " ";
cout << endl;
// 删除容器中3的倍数
auto pos = remove_if(v.begin(), v.end(), Mod3());
v.erase(pos, v.end());
// 将容器中的元素打印出来
// 注意:对于功能简单的操作,可以使用C++11提供的lambda表达式来代替
// lambda表达式实现简单,其在底层与仿函数的原理相同,编译器会将lambda表达式转换为仿函数
for_each(v.begin(), v.end(), [](int data){cout << data << " "; });
cout << endl;
return 0;
}
参考博主上篇博文:[C++ 系列] 87. 简要剖析SGI-STL空间配置器