目录

迭代器

for_each

排序算法

查找算法


在 C++ 里,算法的地位非常高,甚至有一个专门的“算法库”。早期,它是泛型编程的示范和应用,而在 C++ 引入 lambda 表达式后,它又成了函数式编程的具体实践,所以,学习掌握算法能够很好地训练你的编程思维,帮你开辟出面向对象之外的新天地。在代码里普遍应用 vector、set、map,但几乎从来不用任何算法,聊起算法这个话题,也是“一问三不知”,这的确是一个比较奇怪的现象。

C++ 里的算法,指的是工作在容器上的一些泛型函数,会对容器内的元素实施的各种操作。

比如说 count 算法,它的功能非常简单,就是统计某个元素的出现次数,完全可以用 range-for 来实现同样的功能:

vector<int> v = {1,3,1,7,5};    // vector容器auto n1 = std::count(          // count算法计算元素的数量 begin(v), end(v), 1        // begin()、end()获取容器的范围
);  int n2 = 0;
for(auto x : v) {              // 手写for循环if (x == 1) {              // 判断条件,然后统计n2++;}
}  

用算法加上 lambda 表达式,就可以初步体验函数式编程的感觉(即函数套函数):

auto n = std::count_if(      // count_if算法计算元素的数量begin(v), end(v),       // begin()、end()获取容器的范围[](auto x) {            // 定义一个lambda表达式return x > 2;       // 判断条件}
);                          // 大函数里面套了三个小函数

迭代器

迭代器(iterator)相当于算法的“手脚”。算法操作容器,但实际上它看到的并不是容器,而是指向起始位置和结束位置的迭代器,算法只能通过迭代器去“间接”访问容器以及元素,算法的能力是由迭代器决定的。

这种间接的方式有什么好处呢?

这就是泛型编程的理念,与面向对象正好相反,分离了数据和操作。算法可以不关心容器的内部结构,以一致的方式去操作元素,适用范围更广,用起来也更灵活。

当然万事无绝对,这种方式也有弊端。因为算法是通用的,免不了对有的数据结构虽然可行但效率比较低。所以,对于 merge、sort、unique 等一些特别的算法,容器就提供了专门的替代成员函数(相当于特化),这个稍后再提一下。

C++ 里的迭代器也有很多种,比如输入迭代器、输出迭代器、双向迭代器、随机访问迭代器,等等,概念解释起来不太容易。不过,你也没有必要把它们搞得太清楚,因为常用的迭代器用法都是差不多的。你可以把它简单地理解为另一种形式的“智能指针”,只是它强调的是对数据的访问,而不是生命周期管理。

容器一般都会提供 begin()、end() 成员函数,调用它们就可以得到表示两个端点的迭代器,具体类型最好用 auto 自动推导,不要过分关心:

vector<int> v = {1,2,3,4,5};    // vector容器auto iter1 = v.begin();        // 成员函数获取迭代器,自动类型推导
auto iter2 = v.end();

建议使用更加通用的全局函数 begin()、end(),虽然效果是一样的,但写起来比较方便,看起来也更清楚(另外还有 cbegin()、cend() 函数,返回的是常量迭代器):

auto iter3 = std::begin(v);   // 全局函数获取迭代器,自动类型推导
auto iter4 = std::end(v);

迭代器和指针类似,也可以前进和后退,但你不能假设它一定支持“++”“--”操作符,最好也要用函数来操作,常用的有这么几个:

  • distance(),计算两个迭代器之间的距离;
  • advance(),前进或者后退 N 步;
  • next()/prev(),计算迭代器前后的某个位置。

你可以参考下面的示例代码快速了解它们的作用:

array<int, 5> arr = {0,1,2,3,4};  // array静态数组容器auto b = begin(arr);          // 全局函数获取迭代器,首端
auto e = end(arr);            // 全局函数获取迭代器,末端assert(distance(b, e) == 5);  // 迭代器的距离auto p = next(b);              // 获取“下一个”位置
assert(distance(b, p) == 1);    // 迭代器的距离
assert(distance(p, b) == -1);  // 反向计算迭代器的距离advance(p, 2);                // 迭代器前进两个位置,指向元素'3'
assert(*p == 3);
assert(p == prev(e, 2));     // 是末端迭代器的前两个位置

for_each

首先,我带你来认识一个最基本的算法 for_each,它是手写 for 循环的真正替代品。or_each 在逻辑和形式上与 for 循环几乎完全相同:

vector<int> v = {3,5,1,7,10};   // vector容器for(const auto& x : v) {        // range for循环cout << x << ",";
}auto print = [](const auto& x)  // 定义一个lambda表达式
{cout << x << ",";
};
for_each(cbegin(v), cend(v), print);// for_each算法for_each(                      // for_each算法,内部定义lambda表达式cbegin(v), cend(v),        // 获取常量迭代器[](const auto& x)          // 匿名lambda表达式{cout << x << ",";}
);

初看上去 for_each 算法显得有些累赘,既要指定容器的范围,又要写 lambda 表达式,没有 range-for 那么简单明了。对于很简单的 for 循环来说,确实是如此,我也不建议你对这么简单的事情用 for_each 算法。

但更多的时候,for 循环体里会做很多事情,会由 if-else、break、continue 等语句组成很复杂的逻辑。而单纯的 for 是“无意义”的,你必须去查看注释或者代码,才能知道它到底做了什么,回想一下曾经被巨大的 for 循环支配的“恐惧”吧。

for_each 算法的价值就体现在这里,它把要做的事情分成了两部分,也就是两个函数:一个遍历容器元素,另一个操纵容器元素,而且名字的含义更明确,代码也有更好的封装。我自己是很喜欢用 for_each 算法的,我也建议你尽量多用 for_each 来替代 for,因为它能够促使我们更多地以“函数式编程”来思考,使用 lambda 来封装逻辑,得到更干净、更安全的代码。

排序算法

for_each 是 for 的等价替代,还不能完全体现出算法的优越性。但对于“排序”这个计算机科学里的经典问题,你是绝对没有必要自己写 for 循环的,必须坚决地选择标准算法。在求职面试的时候,你也许手写过不少排序算法吧,像选择排序、插入排序、冒泡排序,等等,但标准库里的算法绝对要比你所能写出的任何实现都要好。

说到排序,你脑海里跳出的第一个词可能就是 sort(),它是经典的快排算法,通常用它准没错。

auto print = [](const auto& x)  // lambda表达式输出元素
{cout << x << ",";
};std::sort(begin(v), end(v));         // 快速排序
for_each(cbegin(v), cend(v), print); // for_each算法

不过,排序也有多种不同的应用场景,sort() 虽然快,但它是不稳定的,而且是全排所有元素。很多时候,这样做的成本比较高,比如 TopN、中位数、最大最小值等,我们只关心一部分数据,如果你用 sort(),就相当于“杀鸡用牛刀”,是一种浪费。C++ 为此准备了多种不同的算法,不过它们的名字不全叫 sort,所以你要认真理解它们的含义。

一些常见问题对应的算法:

  • 要求排序后仍然保持元素的相对顺序,应该用 stable_sort,它是稳定的;
  • 选出前几名(TopN),应该用 partial_sort;
  • 选出前几名,但不要求再排出名次(BestN),应该用 nth_element;
  • 中位数(Median)、百分位数(Percentile),还是用 nth_element;
  • 按照某种规则把元素划分成两组,用 partition;
  • 第一名和最后一名,用 minmax_element。

下面的代码使用 vector 容器示范了这些算法,注意它们“函数套函数”的形式:

// top3
std::partial_sort(begin(v), next(begin(v), 3), end(v));  // 取前3名// best3
std::nth_element(begin(v), next(begin(v), 3), end(v));  // 最好的3个// Median
auto mid_iter =                            // 中位数的位置next(begin(v), v.size()/2);
std::nth_element( begin(v), mid_iter, end(v));// 排序得到中位数
cout << "median is " << *mid_iter << endl;// partition
auto pos = std::partition(                // 找出所有大于9的数begin(v), end(v),[](const auto& x)                    // 定义一个lambda表达式{return x > 9;}
);
for_each(begin(v), pos, print);         // 输出分组后的数据  // min/max
auto value = std::minmax_element(        //找出第一名和倒数第一cbegin(v), cend(v)
);

在使用这些排序算法时,还要注意一点,它们对迭代器要求比较高,通常都是随机访问迭代器(minmax_element 除外),所以最好在顺序容器 array/vector 上调用。如果是 list 容器,应该调用成员函数 sort(),它对链表结构做了特别的优化。有序容器 set/map 本身就已经排好序了,直接对迭代器做运算就可以得到结果。而对无序容器,则不要调用排序算法,原因你应该不难想到(散列表结构的特殊性质,导致迭代器不满足要求、元素无法交换位置)。

查找算法

排序算法的目标是让元素有序,这样就可以快速查找,节约时间。算法 binary_search,顾名思义,就是在已经排好序的区间里执行二分查找。但糟糕的是,它只返回一个 bool 值,告知元素是否存在,而更多的时候,我们是想定位到那个元素,所以 binary_search 几乎没什么用。

vector<int> v = {3,5,1,7,10,99,42};  // vector容器
std::sort(begin(v), end(v));        // 快速排序auto found = binary_search(         // 二分查找,只能确定元素在不在cbegin(v), cend(v), 7
); 

想要在已序容器上执行二分查找,要用到一个名字比较怪的算法:lower_bound,它返回第一个“大于或等于”值的位置:

decltype(cend(v)) pos;            // 声明一个迭代器,使用decltypepos = std::lower_bound(          // 找到第一个>=7的位置cbegin(v), cend(v), 7
);
found = (pos != cend(v)) && (*pos == 7); // 可能找不到,所以必须要判断
assert(found);                          // 7在容器里pos = std::lower_bound(               // 找到第一个>=9的位置cbegin(v), cend(v), 9
);
found = (pos != cend(v)) && (*pos == 9); // 可能找不到,所以必须要判断
assert(!found);                          // 9不在容器里

lower_bound 的返回值是一个迭代器,所以就要做一点判断工作,才能知道是否真的找到了。判断的条件有两个,一个是迭代器是否有效,另一个是迭代器的值是不是要找的值。注意 lower_bound 的查找条件是“大于等于”,而不是“等于”,所以它的真正含义是“大于等于值的第一个位置”。相应的也就有“大于等于值的最后一个位置”,算法叫 upper_bound,返回的是第一个“大于”值的元素。

pos = std::upper_bound(             // 找到第一个>9的位置cbegin(v), cend(v), 9
);

因为这两个算法不是简单的判断相等,作用有点“绕”,不太好掌握,我来给你解释一下。它俩的返回值构成一个区间,这个区间往前就是所有比被查找值小的元素,往后就是所有比被查找值大的元素,可以写成一个简单的不等式:

begin <    x <= lower_bound < upper_bound     < end

比如,在刚才的这个例子里,对数字 9 执行 lower_bound 和 upper_bound,就会返回[10,10]这样的区间。对于有序容器 set/map,就不需要调用这三个算法了,它们有等价的成员函数 find/lower_bound/upper_bound,效果是一样的。不过,你要注意 find 与 binary_search 不同,它的返回值不是 bool 而是迭代器,可以参考下面的示例代码:

multiset<int> s = {3,5,1,7,7,7,10,99,42};  // multiset,允许重复auto pos = s.find(7);                      // 二分查找,返回迭代器
assert(pos != s.end());                   // 与end()比较才能知道是否找到auto lower_pos = s.lower_bound(7);       // 获取区间的左端点
auto upper_pos = s.upper_bound(7);       // 获取区间的右端点for_each(                                // for_each算法lower_pos, upper_pos, print          // 输出7,7,7
);

除了 binary_search、lower_bound 和 upper_bound,标准库里还有一些查找算法可以用于未排序的容器,虽然肯定没有排序后的二分查找速度快,但也正因为不需要排序,所以适应范围更广。这些算法以 find 和 search 命名,不过可能是当时制定标准时的疏忽,名称有点混乱,其中用于查找区间的 find_first_of/find_end,或许更应该叫作 search_first/search_last。

这几个算法调用形式都是差不多的,用起来也很简单:

vector<int> v = {1,9,11,3,5,7};  // vector容器decltype(v.end()) pos;          // 声明一个迭代器,使用decltypepos = std::find(                 // 查找算法,找到第一个出现的位置begin(v), end(v), 3
);
assert(pos != end(v));         // 与end()比较才能知道是否找到pos = std::find_if(            // 查找算法,用lambda判断条件begin(v), end(v),[](auto x) {              // 定义一个lambda表达式return x % 2 == 0;    // 判断是否偶数}
);
assert(pos == end(v));        // 与end()比较才能知道是否找到array<int, 2> arr = {3,5};    // array容器
pos = std::find_first_of(      // 查找一个子区间begin(v), end(v),begin(arr), end(arr)
);
assert(pos != end(v));       // 与end()比较才能知道是否找到

C++ 里的算法像是一个大宝库,非常值得你去发掘。比如类似 memcpy 的 copy/move 算法(搭配插入迭代器)、检查元素的 all_of/any_of 算法,用好了都可以替代很多手写 for 循环。https://en.cppreference.com/w/cpp/algorithm

  • 算法是专门操作容器的函数,是一种“智能 for 循环”,它的最佳搭档是 lambda 表达式;
  • 算法通过迭代器来间接操作容器,使用两个端点指定操作范围,迭代器决定了算法的能力;
  • for_each 算法是 for 的替代品,以函数式编程替代了面向过程编程;
  • 有多种排序算法,最基本的是 sort,但应该根据实际情况选择其他更合适的算法,避免浪费;
  • 在已序容器上可以执行二分查找,应该使用的算法是 lower_bound;
  • list/set/map 提供了等价的排序、查找函数,更适应自己的数据结构;
  • find/search 是通用的查找算法,效率不高,但不必排序也能使用。

因为标准算法的名字实在是太普通、太常见了,所以建议你一定要显式写出“std::”名字空间限定,这样看起来更加醒目,也避免了无意的名字冲突。

for_each 解读相关推荐

  1. Python Re 模块超全解读!详细

    内行必看!Python Re 模块超全解读! 2019.08.08 18:59:45字数 953阅读 121 re模块下的函数 compile(pattern):创建模式对象 > import ...

  2. Bert系列(二)——源码解读之模型主体

    本篇文章主要是解读模型主体代码modeling.py.在阅读这篇文章之前希望读者们对bert的相关理论有一定的了解,尤其是transformer的结构原理,网上的资料很多,本文内容对原理部分就不做过多 ...

  3. Bert系列(三)——源码解读之Pre-train

    https://www.jianshu.com/p/22e462f01d8c pre-train是迁移学习的基础,虽然Google已经发布了各种预训练好的模型,而且因为资源消耗巨大,自己再预训练也不现 ...

  4. NLP突破性成果 BERT 模型详细解读 bert参数微调

    https://zhuanlan.zhihu.com/p/46997268 NLP突破性成果 BERT 模型详细解读 章鱼小丸子 不懂算法的产品经理不是好的程序员 ​关注她 82 人赞了该文章 Goo ...

  5. 解读模拟摇杆原理及实验

    解读模拟摇杆原理及实验 Interpreting Analog Sticks 当游戏支持控制器时,玩家可能会一直使用模拟摇杆.在整个体验过程中,钉住输入处理可能会对质量产生重大影响.让来看一些核心概念 ...

  6. 自监督学习(Self-Supervised Learning)多篇论文解读(下)

    自监督学习(Self-Supervised Learning)多篇论文解读(下) 之前的研究思路主要是设计各种各样的pretext任务,比如patch相对位置预测.旋转预测.灰度图片上色.视频帧排序等 ...

  7. 自监督学习(Self-Supervised Learning)多篇论文解读(上)

    自监督学习(Self-Supervised Learning)多篇论文解读(上) 前言 Supervised deep learning由于需要大量标注信息,同时之前大量的研究已经解决了许多问题.所以 ...

  8. 可视化反投射:坍塌尺寸的概率恢复:ICCV9论文解读

    可视化反投射:坍塌尺寸的概率恢复:ICCV9论文解读 Visual Deprojection: Probabilistic Recovery of Collapsed Dimensions 论文链接: ...

  9. 从单一图像中提取文档图像:ICCV2019论文解读

    从单一图像中提取文档图像:ICCV2019论文解读 DewarpNet: Single-Image Document Unwarping With Stacked 3D and 2D Regressi ...

最新文章

  1. android 电量控件,Android实现显示电量的控件代码
  2. C#规范整理·集合和Linq
  3. C和C++结构体区别
  4. maven构建java web项目(idea开发)
  5. Java 微服务框架选型(Dubbo 和 Spring Cloud?),大厂 HR 如何面试
  6. 【java】java 命令 Unable to open socket file: target process not responding or HotSpot VM not loaded
  7. 【安卓的一个进程等级】
  8. php pdo 抛出异常模式,php实现的PDO异常处理操作分析
  9. Java家庭收支记账系统
  10. 东华大学python题库_2020尔雅纺纱学(东华大学)完整答案
  11. java中创建一个类
  12. 如何使用Nginx Ingress实现灰度发布和蓝绿发布?
  13. 第十篇:扩展SOUI的控件及绘图对象(ISkinObj)
  14. 断点续传 scp rsync
  15. Android studio 编译项目出现Keystore was tampered with, or password was incorrect
  16. 机器学习(三):一文读懂线性判别分析(LDA)
  17. Office365 Exchange Online系列之邮箱大管家视频课程-李远-专题视频课程
  18. 使用abel533大神的mybatis分页插件总结
  19. 图片如何缩小不降低清晰度?
  20. php高级教程Thinkphp电商项目实战开发

热门文章

  1. 【C++】C++ 知识点100题
  2. HTML box盒子模型练习校园风光木棉花
  3. 电脑蓝屏,睿频导致CPU温度过高解决方法
  4. C++定义结构体大小根堆的方法
  5. 计算机配件模拟,电脑装机模拟各配件跑分及计算公式分享
  6. tcp实时传输kafka数据_将物联网数据和MQTT消息流式传输到Apache Kafka
  7. 复旦2021计算机考研分数线,2021复旦大学考研录取分数线公布|附详情
  8. 职业自我认知的测试软件,职业生涯规划___自我认知测试.pdf
  9. 双显卡只用独显好吗_显卡有什么作用 独显和双显卡笔记本哪个好【详解】
  10. S/4 HANA标准表MARC增强字段