最近学习了C++
标准库中的algorithms
(算法)和adaptors
(适配器)后,决定写一篇文章整理经常使用的算法以及适配器。
使用这些算法和适配器可以更好的提升代码的可读性。
本文将介绍以下内容:
标准库中的算法被包含在algorithm
头文件中,而在GNU
的实现中,其实现则是包含在stl_algo.h
(位于include/c++/9/bits/stl_algo.h
中),其中以双下划线__
开头的函数不属于标准库的内容,而是属于用于实现标准库内容会用到的算法,不建议使用双下划线开头的方法,而是使用以非双下划线开头的算法,即标准文档要求实现的算法。
直接查看代码的定义可以让我们学习其实现,而对于只是需要了解怎么使用,其效率怎么样,借助官方的文档是一个不错的选择:Algorithms library cppreference。
注:个人建议,尽量不要使用编译器中定义的非标准的函数、方法、类等,如果需要类似的功能,应该使用第三方的库进行实现,因为编译器不会保证之后的版本不会移除之前的代码,而对于库作者而言,其通常需要考虑向前兼容。
算法库的中的函数多种多样,但是其命名规则隐藏着一些共性,而正式因为这些命名的共性,才能够很好的利用算法库提升代码的可读性。当然,使用算法库除了能够提升代码的可读性,还能够保证程序的效率,算法库中的代码对于不同类型的参数会提供不同的实现方式以确保程序高效地执行,因此使用算法库也能够保证程序执行的效率。
下面给出算法库中算法的一些共性:
_n
结尾,那么其通常需要接收两个迭代器,及对应两个起止迭代器:begin
和end
,对应的操作区间均为左闭右开:[begin, end)
,例如std::sort
用于排序。std::find_first_of
用于查找第二个序列在第一个序列第一次出现的位置。_n
结尾的算法往往只需要一个开始迭代器和一个
n
n
n来指明元素个数,例如std::fill_n
用于将从传入的迭代器起的n
的元素填入指定值,std::fill
则是将指定的左闭右开区间的元素填上指定值。_if
结尾的算法,往往需要传入一个仿函数(或者函数,通常被称为predicate
即谓语),这个仿函数往往需要返回一个bool
类型的值,这类算法会对返回值为真的进行操作,例如:std::count_if(begin, end, [] (int x) { return x > 0; })
会统计大于0
的元素个数。_if_not
结尾的算法,其在谓语为假是进行相关的操作。std::find_first_of
查找第一个std::find_end
则是查找最后一个,而std::binary_search
一眼便能看出使用二分查找。std::copy/std::copy_if
:接收三个参数(if
版本接收四个参数)分别为源的起止迭代器,目的的起始迭代器,表现行为我将源范围拷贝到目的范围,不会做范围检查,需要保证目的位置有足够的空间容纳数据。当目的的起始迭代器在[sourceFirst, sourceEnd)
时,行为未定义,此时应该调用std::copy_backward
。std::copy_backward
:同样接收三个参数,第三个参数为目的的终止迭代器(不包含),只是该算法能够处理目的起始迭代器在源起止迭代器范围内的情况。当目的终止迭代器在(sourceFirst, sourceEnd)
时,行为未定义,此时应该调用std::copy
。std::move
:有两个不同语义的std::move
一个用于将变量转换为将亡值,一个用于移动赋值,在使用std::copy
的时候会调用复制赋值运算符,而使用std::move
则会使用移动赋值运算符。如果你还不了解C++11
的移动语义,那么这篇文章会做一个较为详细的介绍左值、右值、移动、完备转发等介绍。std::move_backward
:对应std::copy_backward
。std::for_each/std::for_each_n
:接收三个参数,第三个参数为接收一个变量的仿函数,该算法将范围内的每一个元素作为函数的参数调用传入的仿函数。std::count/std::count_if
:计数或者记录满足使传入仿函数的返回值为真的元素个数。std::swap
:交换两个变量。std::iter_swap
:用于交换迭代器指向的数据。std::fill/std::fill_n
:填充指定的值。std::generate/std::generate_n
:将传入函数的返回值赋值给指定的范围。std::unique
:对有序序列进行去重。std::reverse
:翻转序列。std::sort/std::stable_sort
:排序、稳定排序(默认不降序),支持传入指定的比较器。std::is_sorted
:判断序列是否升序,,支持传入指定的比较器。std::lower_bound/std::upper_bound
:std::lower_bound/std::upper_bound详讲。std::min/std::max/std::minmax
:获取最小值,最大值,最小最大值(以pair
进行存储,first
为min
,second
为max
)。对于非迭代器类型,可以使用初始化列表,例如:std::min({a, b, c, d})
,可以获取四个变量中的最小值。std::min_element/std::max_element/std::minmax_element
:获取下标。std::next_permutation/std::prev_permutation
:获取下一个/上一个排列(按字典序获取),返回值表示是否还有排列(即是否已经降序/升序)。std::partial_sum
:接收三个迭代器,源的起止迭代器和结果保存位置,默认用于求前缀和。可以指定第四个参数为自定义操作函数(默认是std::plus
)。std::accmulate
:接收两个迭代器和一个用于保存结果的变量,如果希望保存结果的变量能够被更新,那么应该传入引用,否则应该将返回值作为结果。默认行为是求和,可以传入谓语。例如:std::vector<int> v{1, 2, 3, 4};
int res = std::accumulate(v.begin(), v.end(), 0); // return 1 + 2 + 3 + 4;
res = 0;
int &resConference = res;
std::accumulate(v.begin(), v.end(), resConference); // still return 10, and res will be 10, too.
对于能够完全通过算法库实现的操作,其往往拥有良好的可读性,例如:std::count_if(begin, end, std::bind(std::less<T>(), std::placeholders::_1, 40))
表示统计小于40
的元素个数。
对于不能通过算法库实现的代码,这里指的是需要自己书写仿函数的代码,例如要对所有的元素进行自增操作,使用Lambda
表达式当然可以实现:
vector<int> v{1, 2, 3, 4, 5};
std::for_each(v.begin(), v.end(), [] (int &x) { x++; });
上面的代码可读性也不差,但是如果需要进行多次的自增操作,那么同一个Lambda
可能需要使用多次,此时就需要保存下这个Lambda
函数或者自定义个一个自增函数。这里同样提供一种使用仿函数的方式实现:
template<class T>
struct increment : public std::unary_function<T, void> {
void operator()(T &x) { ++x; }
};
std::for_each(v.begin(), v.end(), increment<int>());
上面的代码好处在于可以通过模板指定类型(当然使用模板Lambda
函数也能实现类似的功能),而且不用重复书写Lambda
函数,且具有更好的可读性。上面的代码继承std::unary_function
是为了更好的融入标准库。你完全可以放心这种继承,这种继承并不会引入额外的数据,在std::unary_function
中只是定义了函数的参数类型和返回值类型。
适配器这一个词在设计模式中也有提到:适配器模式。适配器实际上指的是能够改变接口使其符合新的规范的东西。例如现在的很多手机而言其只有type-c
的接口,没有3.5mm
的耳机接口,而如果此时你没有一个type-c
接口的耳机(当然你可以买一个 )而只有一个3.5mm
的耳机时,你可以选择够买一根转接线,将type-c
母口转换成3.5mm
的母口这样你就能够继续使用你的3.5mm
的耳机了,而这个转接线就被称为适配器:adaptor
。
标准库提供了std::deque
的实现。而std::queue
和std::stack
的默认实现是内含一个std::deque
实现,而这两者便可以称为适配器。
在介绍的算法库中,很多算法需要接收一个谓语,为了更好的解释,我们这里以std::sort
为例,在不传入谓语(比较器)的时候,std::sort
的内部通过<
符号进行元素的比较,越小的元素会被安排在前面。对于我们自定义的对象,就拿一个学生成绩类来举例,我们如果直接传入std::sort
会发生SF
(Substitution Failure),提示我们找不到<
的定义,因此此时我们需要进行运算符重载。
我们自然可以进行运算符重载,但是使用重载的方式意味着比较方式将永远固定,例如我们现在按照学生的分数的大小方式进行了重载(重载小于符号),此时又需要按照学生分数升序排序,那么此时我们必须修改重载使其按照新的要求工作。实际上我们可以重载大于符号然后通过std::greater<StudentScore>()
传入比较器(此时std::greater
就是一个适配器)实现。
因为我们重载的>
符号不能符合std::sort
的标准,而通过std::greater
将会重载operator()(const T& a, const T &b)
以实现两个元素的大小比较。
类似的还有:
std::plus
std::minus
std::multiplies
std::divides
std::logical_and
std::logical_or
等等,这些类均是模板类,而对于应的操作大多数可以通过重载运算符的方式实现。
有的时候我们并不希望重载运算符,这时我们可以通过特化模板类来实现,例如:
template<> struct std::greater<StudentScore> :
public std::binary_function<StudentScore, StudentScore, bool> {
bool operator()(const StudentScore &a, const StudentScore &b) {
return a.score > b.score;
}
}
std::sort(v.begin(), v.end(), std::greater<StudentScore>);
为了更好的符合标准库的规范,我们查看源代码发现std::greater
会继承std::binary_function
通过这样方式特化的模板,能够很好的融入到标准库中。上面的代码我们便不再要求重载StudentScore
的>
符号。
对于我们自定义的谓语,我们可以使其命名符合其意义,同时使其继承std::binary_function
或者std::unary_function
使其能够更好的融入到标准库中。