18-3 标准库算法介绍
新手程序员通常会花费大量时间编写自定义循环来执行相对简单的任务,例如排序、计数或搜索数组。这类循环存在诸多问题:既容易出错,又难以维护——因为循环结构本身往往晦涩难懂。
鉴于搜索、计数和排序是如此常见的操作,C++标准库提供了大量函数,仅需几行代码即可完成这些任务。此外,这些标准库函数经过预先测试,运行高效,支持多种容器类型,且多数支持并行化(即调用多个CPU线程处理同一任务以提升效率)。
算法库提供的功能主要分为三大类:
- 检查器
Inspectors——用于查看(但不修改)容器中的数据。例如搜索和计数。 - 修改器
Mutators——用于修改容器中的数据。例如排序和打乱顺序。 - 辅助器
Facilitators——用于根据数据成员的值生成结果。例如对值进行乘法的对象,或确定元素对排序顺序的对象。
这些算法存在于算法库中。本节课我们将探讨部分常用算法——但还有更多算法,建议您查阅相关链接的参考资料以了解全部可用功能!
注意:所有这些算法都使用迭代器,若您不熟悉基础迭代器,请复习第18.2节——迭代器介绍。
使用 std::find 按值查找元素
std::find 在容器中搜索某个值的首次出现位置。std::find 接受三个参数:指向序列起始元素的迭代器、指向序列结束元素的迭代器,以及要搜索的值。它返回指向该元素的迭代器(若找到)或容器末尾的迭代器(若未找到)。
例如:
#include <algorithm>
#include <array>
#include <iostream>
int main()
{
std::array arr{ 13, 90, 99, 5, 40, 80 };
std::cout << "Enter a value to search for and replace with: ";
int search{};
int replace{};
std::cin >> search >> replace;
// Input validation omitted
// std::find returns an iterator pointing to the found element (or the end of the container)
// we'll store it in a variable, using type inference to deduce the type of
// the iterator (since we don't care)
auto found{ std::find(arr.begin(), arr.end(), search) };
// Algorithms that don't find what they were looking for return the end iterator.
// We can access it by using the end() member function.
if (found == arr.end())
{
std::cout << "Could not find " << search << '\n';
}
else
{
// Override the found element.
*found = replace;
}
for (int i : arr)
{
std::cout << i << ' ';
}
std::cout << '\n';
return 0;
}
当元素被找到时执行示例运行

当元素未找到时的示例运行

使用 std::find_if 查找满足特定条件的元素
有时我们需要检查容器中是否存在满足特定条件的值(例如包含特定子字符串的字符串),而非精确匹配的值。此时 std::find_if 堪称完美解决方案。
std::find_if 的工作原理与 std::find 类似,但它不直接传入具体值进行搜索,而是传入可调用对象(如函数指针或后文将介绍的lambda表达式)。迭代遍历每个元素时,std::find_if 会调用该函数(将元素作为参数传递),函数在找到匹配项时返回 true,否则返回 false。
以下示例演示如何使用 std::find_if 检查元素是否包含子字符串 “nut”:
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
// Our function will return true if the element matches
bool containsNut(std::string_view str)
{
// std::string_view::find returns std::string_view::npos if it doesn't find
// the substring. Otherwise it returns the index where the substring occurs
// in str.
return str.find("nut") != std::string_view::npos;
}
int main()
{
std::array<std::string_view, 4> arr{ "apple", "banana", "walnut", "lemon" };
// Scan our array to see if any elements contain the "nut" substring
auto found{ std::find_if(arr.begin(), arr.end(), containsNut) };
if (found == arr.end())
{
std::cout << "No nuts\n";
}
else
{
std::cout << "Found " << *found << '\n';
}
return 0;
}
输出

若要手动编写上述示例,至少需要三个循环(一个遍历数组,两个匹配子字符串)。而标准库函数让我们仅用几行代码就能实现相同功能!
使用 std::count 和 std::count_if 统计元素出现次数
std::count 和 std::count_if 可搜索某个元素或满足特定条件的元素的所有出现位置。
在下面的示例中,我们将统计包含子字符串 “nut” 的元素数量:
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
bool containsNut(std::string_view str)
{
return str.find("nut") != std::string_view::npos;
}
int main()
{
std::array<std::string_view, 5> arr{ "apple", "banana", "walnut", "lemon", "peanut" };
auto nuts{ std::count_if(arr.begin(), arr.end(), containsNut) };
std::cout << "Counted " << nuts << " nut(s)\n";
return 0;
}
输出

用 std::sort 实现自定义排序
我们之前曾使用 std::sort 对数组进行升序排序,但 std::sort 的功能远不止于此。存在一种带第三参数的 std::sort 版本,该参数为比较函数,可实现任意排序逻辑。该函数接收两个比较参数,若第一个参数应排在第二个之前则返回 true。默认情况下,std::sort 按升序排序元素。
现在我们使用自定义比较函数 greater 对数组进行逆序排序:
#include <algorithm>
#include <array>
#include <iostream>
bool greater(int a, int b)
{
// Order @a before @b if @a is greater than @b.
return (a > b);
}
int main()
{
std::array arr{ 13, 90, 99, 5, 40, 80 };
// Pass greater to std::sort
std::sort(arr.begin(), arr.end(), greater);
for (int i : arr)
{
std::cout << i << ' ';
}
std::cout << '\n';
return 0;
}
输出

再次证明,无需编写自定义循环函数,我们只需几行代码就能随心所欲地排序数组!
我们的大于比较函数需要两个参数,但我们并未传递任何参数,那么参数从何而来?当我们不使用括号()调用函数时,它仅作为函数指针存在,而非实际调用。你可能还记得,当我们尝试打印无括号函数时,std::cout会输出“1”。std::sort正是利用这个指针,用数组中任意两个元素调用实际的 greater 函数。我们无法预知 greater 会被调用处理哪些元素,因为 std::sort 内部使用的排序算法并未明确定义。关于函数指针的更多内容,我们将在后续章节中详细探讨。
提示:
由于降序排序非常常见,C++为此也提供了一个自定义类型(名为 std::greater,属于函数式头文件的一部分)。在上例中,我们可以将:
std::sort(arr.begin(), arr.end(), greater); // call our custom greater function
替换为:std::sort(arr.begin(), arr.end(), std::greater{}); // use the standard library greater comparison // Before C++17, we had to specify the element type when we create std::greater std::sort(arr.begin(), arr.end(), std::greater<int>{}); // use the standard library greater comparison请注意,std::greater{} 需要大括号,因为它不是可调用的函数。它是一种类型,要使用它,我们需要实例化该类型的对象。大括号会实例化该类型的匿名对象(该对象随后会被作为参数传递给 std::sort)。
面向高级读者:
为进一步阐释std::sort如何运用比较函数,我们需回溯至第18.1节——使用选择排序对数组进行排序中选择排序示例的修改版本。
#include <iostream>
#include <iterator>
#include <utility>
void sort(int* begin, int* end)
{
for (auto startElement{ begin }; startElement != end-1; ++startElement)
{
auto smallestElement{ startElement };
// std::next returns a pointer to the next element, just like (startElement + 1) would.
for (auto currentElement{ std::next(startElement) }; currentElement != end; ++currentElement)
{
if (*currentElement < *smallestElement)
{
smallestElement = currentElement;
}
}
std::swap(*startElement, *smallestElement);
}
}
int main()
{
int array[]{ 2, 1, 9, 4, 5 };
sort(std::begin(array), std::end(array));
for (auto i : array)
{
std::cout << i << ' ';
}
std::cout << '\n';
return 0;
}
到目前为止,这没什么新奇之处,排序总是将元素从低到高排列。要添加比较函数,我们必须使用新类型
std::function<bool(int, int)>来存储一个函数,该函数接受两个整数参数并返回布尔值。暂时将此类型视为魔法,我们将在第20章进行详细说明。
void sort(int* begin, int* end, std::function<bool(int, int)> compare)
现在我们可以传递一个比较函数(如 greater)来排序,但排序函数如何使用它呢?我们只需替换以下代码行:
if (*currentElement < *smallestElement)
替换为:
if (compare(*currentElement, *smallestElement))
现在,该排序的调用者可以选择如何比较两个元素。
#include <functional> // std::function
#include <iostream>
#include <iterator>
#include <utility>
// sort accepts a comparison function
void sort(int* begin, int* end, std::function<bool(int, int)> compare)
{
for (auto startElement{ begin }; startElement != end-1; ++startElement)
{
auto smallestElement{ startElement };
for (auto currentElement{ std::next(startElement) }; currentElement != end; ++currentElement)
{
// the comparison function is used to check if the current element should be ordered
// before the currently "smallest" element.
if (compare(*currentElement, *smallestElement))
{
smallestElement = currentElement;
}
}
std::swap(*startElement, *smallestElement);
}
}
int main()
{
int array[]{ 2, 1, 9, 4, 5 };
// use std::greater to sort in descending order
// (We have to use the global namespace selector to prevent a collision
// between our sort function and std::sort.)
::sort(std::begin(array), std::end(array), std::greater{});
for (auto i : array)
{
std::cout << i << ' ';
}
std::cout << '\n';
return 0;
}
使用 std::for_each 对容器中的所有元素执行操作
std::for_each 接受列表作为输入,并对每个元素应用自定义函数。当需要对列表中的每个元素执行相同操作时,此功能非常实用。
以下示例演示如何使用 std::for_each 将数组中的所有数字翻倍:
#include <algorithm>
#include <array>
#include <iostream>
void doubleNumber(int& i)
{
i *= 2;
}
int main()
{
std::array arr{ 1, 2, 3, 4 };
std::for_each(arr.begin(), arr.end(), doubleNumber);
for (int i : arr)
{
std::cout << i << ' ';
}
std::cout << '\n';
return 0;
}
输出:

对新手开发者而言,这似乎是最不必要的算法,因为使用基于范围的 for 循环编写的等效代码更简洁易懂。但 std::for_each 确实有其优势。让我们对比 std::for_each 与基于范围的 for 循环。
std::ranges::for_each(arr, doubleNumber); // Since C++20, we don't have to use begin() and end().
// std::for_each(arr.begin(), arr.end(), doubleNumber); // Before C++20
for (auto& i : arr)
{
doubleNumber(i);
}
使用 std::for_each 时,我们的意图很明确:对数组 arr 的每个元素调用 doubleNumber。而在基于范围的 for 循环中,我们必须新增一个变量 i。这会导致程序员在疲惫或分心时可能犯下若干错误。例如,若未使用 auto 声明,可能发生隐式转换。可能遗漏&符号导致doubleNumber无法作用于数组;也可能误将i以外的变量传递给doubleNumber。这些错误在使用std::for_each时完全不会发生。
此外,std::for_each支持跳过容器首尾元素——例如要跳过arr的首个元素,可通过std::next将begin推进至下一个元素。
std::for_each(std::next(arr.begin()), arr.end(), doubleNumber);
// Now arr is [1, 4, 6, 8]. The first element wasn't doubled.
基于范围的 for 循环无法实现这一点。
与许多算法类似,std::for_each 可并行化以实现更快的处理速度,因此相较于基于范围的 for 循环,它更适合大型项目和大数据场景。
性能与执行顺序
算法库中的许多算法对其执行方式都提供某种保证。通常这些保证分为两类:性能保证或执行顺序保证。例如,std::for_each 保证每个元素仅被访问一次,且元素按正向顺序依次访问。
虽然多数算法提供某种性能保证,但执行顺序保证的算法较少。对于此类算法,我们需谨慎避免对元素访问或处理顺序作出预设。
例如,若使用标准库算法将第一个值乘以1、第二个值乘以2、第三个值乘以3……则必须规避任何无法保证正向顺序执行的算法!
以下算法保证顺序执行:std::for_each、std::copy、std::copy_backward、std::move 和 std::move_backward。许多其他算法(特别是使用前向迭代器的算法)因前向迭代器要求而隐含顺序性。
最佳实践:
使用特定算法前,请确保其性能和执行顺序保证符合您的具体用例需求。
范围Ranges(C++20)
每次向算法显式传递arr.begin()和arr.end()确实有些烦人。但别担心——C++20引入了范围概念,让我们只需传递arr即可。这将使代码更简洁、更易读。
结论Conclusion
算法库提供了大量实用功能,能让您的代码更简洁、更健壮。本节课仅介绍了其中一小部分,但由于这些函数大多工作原理相似,掌握其中几个的用法后,您就能灵活运用绝大多数功能。
顺带一提……:
这段视频很好地简明阐释了库中各类算法。
最佳实践:
优先使用算法库中的函数,而非自行编写实现相同功能的代码。

浙公网安备 33010602011771号