C++学习:六个月从基础到就业——STL算法(三)—— 数值算法(上)
本文是我C++学习之旅系列的第二十七篇技术文章,也是第二阶段"C++进阶特性"的第五篇,主要介绍C++ STL算法库中的数值算法(上部分)。查看完整系列目录了解更多内容。
引言
在前两篇文章中,我们分别介绍了STL算法库中的基础查找算法以及排序和变序算法。本文作为系列的第三篇,将重点探讨STL中的数值算法,这些算法主要定义在<numeric>
头文件中,用于执行各种数值计算操作。
数值算法是科学计算、数据分析和统计处理中不可或缺的工具。STL提供的数值算法虽然在数量上不及其他类型的算法丰富,但它们提供了基础的数学运算功能,可以方便地处理数值序列,执行累加、乘积、差分、前缀和等常见数值操作。
本文将详细介绍这些算法的用法、特性和应用场景,并通过实际的代码示例,帮助你更好地理解和应用这些算法。
STL数值算法概述
STL的数值算法主要定义在<numeric>
头文件中,它们提供了一系列用于数值计算的函数模板。与其他STL算法一样,这些数值算法也是通过迭代器操作容器元素,因此可以应用于任何满足迭代器要求的容器。
主要的数值算法包括:
std::accumulate
- 计算序列元素的累加值或通过指定操作的累积值std::inner_product
- 计算两个序列的内积或通过指定操作的组合值std::partial_sum
- 计算序列的部分和(即前缀和)std::adjacent_difference
- 计算序列中相邻元素的差值std::iota
- 用连续递增的值填充序列
此外,C++17新增了几个并行版本的数值算法,如std::reduce
、std::transform_reduce
等,它们提供了更高的性能和灵活性。
接下来,我们将逐一介绍这些算法的用法和特点。
std::accumulate - 累加与累积
std::accumulate
是最常用的数值算法之一,用于计算序列元素的总和或者通过某种操作的累积结果。
基本用法
accumulate
算法有两种形式:
- 使用默认的加法操作:
accumulate(first, last, init)
- 使用自定义二元操作:
accumulate(first, last, init, binary_op)
其中,first
和last
是定义序列范围的迭代器,init
是累加的初始值,binary_op
是可选的二元操作函数。
让我们来看一些基本示例:
#include <iostream>
#include <vector>
#include <numeric>
#include <functional>
#include <string>
int main() {
// 简单整数序列的累加
std::vector<int> numbers = {1, 2, 3, 4, 5};
// 计算总和(初始值为0)
int sum = std::accumulate(numbers.begin(), numbers.end(), 0);
std::cout << "Sum: " << sum << std::endl; // 输出: 15
// 计算总和(初始值为10)
int sumWith10 = std::accumulate(numbers.begin(), numbers.end(), 10);
std::cout << "Sum with initial value 10: " << sumWith10 << std::endl; // 输出: 25
// 使用自定义操作(乘法)计算乘积
int product = std::accumulate(numbers.begin(), numbers.end(), 1, std::multiplies<int>());
std::cout << "Product: " << product << std::endl; // 输出: 120 (1*2*3*4*5)
// 使用lambda表达式计算平方和
int squareSum = std::accumulate(numbers.begin(), numbers.end(), 0,
[](int acc, int val) { return acc + val * val; });
std::cout << "Sum of squares: " << squareSum << std::endl; // 输出: 55 (1^2+2^2+3^2+4^2+5^2)
return 0;
}
这个例子展示了如何使用accumulate
计算序列的总和、乘积以及更复杂的运算如平方和。
处理不同类型的数据
accumulate
不仅可以处理基本数值类型,还可以处理任意可以应用指定操作的类型。下面是一些例子:
#include <iostream>
#include <vector>
#include <numeric>
#include <string>
int main() {
// 字符串连接
std::vector<std::string> words = {"Hello", ", ", "world", "!"};
std::string concatenated = std::accumulate(words.begin(), words.end(), std::string(""));
std::cout << "Concatenated string: " << concatenated << std::endl; // 输出: Hello, world!
// 注意初始值类型的重要性
// 如果使用const char*作为初始值,结果会不正确
// 错误的做法:
// std::string wrong = std::accumulate(words.begin(), words.end(), ""); // 不要这样做
// 浮点数计算(注意初始值的类型)
std::vector<int> values = {1, 2, 3, 4, 5};
double average = std::accumulate(values.begin(), values.end(), 0.0) / values.size();
std::cout << "Average: " << average << std::endl; // 输出: 3.0
// 使用自定义结构
struct Point {
int x, y;
Point(int x = 0, int y = 0) : x(x), y(y) {}
};
std::vector<Point> points = {{1, 2}, {3, 4}, {5, 6}, {7, 8}};
// 计算所有点的x和y坐标总和
Point sum = std::accumulate(points.begin(), points.end(), Point(),
[](const Point& acc, const Point& p) {
return Point(acc.x + p.x, acc.y + p.y);
});
std::cout << "Sum of points: (" << sum.x << ", " << sum.y << ")" << std::endl; // 输出: (16, 20)
return 0;
}
这个例子展示了如何使用accumulate
处理字符串连接、浮点数计算以及自定义结构的累积操作。
常见应用场景
accumulate
在实际编程中有许多应用场景,以下是一些常见的例子:
- 计算统计指标:总和、平均值、方差等
- 字符串拼接:将多个字符串按特定格式连接
- 向量运算:计算向量的范数、点积等
- 数据转换:将一个容器的数据转换为另一种表示形式
下面是一个更复杂的例子,展示了如何使用accumulate
实现一些常见的统计计算:
#include <iostream>
#include <vector>
#include <numeric>
#include <cmath>
#include <iomanip>
#include <functional>
// 计算统计指标的函数
struct StatisticsResult {
double sum;
double mean;
double variance;
double stdDev;
double max;
double min;
};
StatisticsResult calculateStatistics(const std::vector<double>& data) {
StatisticsResult result{};
if (data.empty()) return result;
// 计算总和
result.sum = std::accumulate(data.begin(), data.end(), 0.0);
// 计算平均值
result.mean = result.sum / data.size();
// 计算方差
result.variance = std::accumulate(data.begin(), data.end(), 0.0,
[mean = result.mean](double acc, double val) {
return acc + std::pow(val - mean, 2);
}) / data.size();
// 计算标准差
result.stdDev = std::sqrt(result.variance);
// 找出最大值
result.max = *std::max_element(data.begin(), data.end());
// 找出最小值
result.min = *std::min_element(data.begin(), data.end());
return result;
}
int main() {
// 一组样本数据
std::vector<double> measurements = {
10.2, 15.6, 9.8, 12.3, 14.5, 13.7, 11.2, 10.9, 12.8, 13.4
};
// 计算统计指标
StatisticsResult stats = calculateStatistics(measurements);
// 打印结果
std::cout << std::fixed << std::setprecision(2);
std::cout << "Statistical Analysis Results:" << std::endl;
std::cout << "---------------------------" << std::endl;
std::cout << "Sum: " << stats.sum << std::endl;
std::cout << "Mean: " << stats.mean << std::endl;
std::cout << "Variance: " << stats.variance << std::endl;
std::cout << "Standard Deviation: " << stats.stdDev << std::endl;
std::cout << "Maximum: " << stats.max << std::endl;
std::cout << "Minimum: " << stats.min << std::endl;
// 使用accumulate连接字符串生成格式化报告
std::vector<std::pair<std::string, double>> statItems = {
{"Sum", stats.sum},
{"Mean", stats.mean},
{"Variance", stats.variance},
{"Standard Deviation", stats.stdDev},
{"Maximum", stats.max},
{"Minimum", stats.min}
};
std::string report = std::accumulate(statItems.begin(), statItems.end(), std::string("Statistical Report:\n"),
[](const std::string& acc, const std::pair<std::string, double>& item) {
std::stringstream ss;
ss << acc << "- " << std::left << std::setw(20) << item.first
<< ": " << std::fixed << std::setprecision(2) << item.second << "\n";
return ss.str();
});
std::cout << "\n" << report << std::endl;
return 0;
}
这个示例展示了如何使用accumulate
计算一组数据的统计指标,包括总和、平均值、方差和标准差,以及如何用accumulate
生成格式化的统计报告。
std::inner_product - 内积与组合操作
std::inner_product
用于计算两个序列的内积或者通过自定义操作组合两个序列的元素。
基本用法
inner_product
算法有两种形式:
- 使用默认的乘法和加法操作:
inner_product(first1, last1, first2, init)
- 使用自定义操作:
inner_product(first1, last1, first2, init, binary_op1, binary_op2)
其中,first1
和last1
定义第一个序列的范围,first2
是第二个序列的开始迭代器,init
是初始累积值,binary_op1
和binary_op2
是可选的二元操作函数。
让我们来看一些基本示例:
#include <iostream>
#include <vector>
#include <numeric>
#include <functional>
int main() {
// 两个整数向量
std::vector<int> v1 = {1, 2, 3, 4, 5};
std::vector<int> v2 = {10, 20, 30, 40, 50};
// 计算内积(相应元素相乘然后相加)
int dotProduct = std::inner_product(v1.begin(), v1.end(), v2.begin(), 0);
std::cout << "Dot product: " << dotProduct << std::endl; // 输出: 550 (1*10 + 2*20 + 3*30 + 4*40 + 5*50)
// 使用自定义操作:加法替代乘法,乘法替代加法
int customResult = std::inner_product(v1.begin(), v1.end(), v2.begin(), 1,
std::multiplies<int>(), std::plus<int>());
std::cout << "Custom result: " << customResult << std::endl; // 输出: 65610 (1 * (1+10) * (2+20) * (3+30) * (4+40) * (5+50))
// 检查两个向量是否相等
std::vector<int> v3 = {1, 2, 3, 4, 5};
bool areEqual = std::inner_product(v1.begin(), v1.end(), v3.begin(), true,
std::logical_and<bool>(),
[](int a, int b) { return a == b; });
std::cout << "v1 and v3 are " << (areEqual ? "equal" : "not equal") << std::endl; // 输出: v1 and v3 are equal
return 0;
}
这个例子展示了如何使用inner_product
计算两个向量的内积以及使用自定义操作实现其他功能,如检查两个序列是否相等。
常见应用场景
inner_product
在多个领域有广泛的应用,例如:
- 向量代数:计算向量的点积、余弦相似度等
- 矩阵运算:矩阵乘法的基本操作
- 信号处理:信号的相关性计算
- 相似度度量:计算两个数据集的相似度
下面是一个使用inner_product
计算向量余弦相似度的例子:
#include <iostream>
#include <vector>
#include <numeric>
#include <cmath>
#include <iomanip>
// 计算两个向量的余弦相似度
double cosineSimilarity(const std::vector<double>& v1, const std::vector<double>& v2) {
if (v1.size() != v2.size() || v1.empty()) {
return 0.0; // 无效输入
}
// 计算点积
double dotProduct = std::inner_product(v1.begin(), v1.end(), v2.begin(), 0.0);
// 计算v1的模长
double magnitude1 = std::sqrt(std::inner_product(v1.begin(), v1.end(), v1.begin(), 0.0));
// 计算v2的模长
double magnitude2 = std::sqrt(std::inner_product(v2.begin(), v2.end(), v2.begin(), 0.0));
// 计算余弦相似度
if (magnitude1 == 0.0 || magnitude2 == 0.0) {
return 0.0; // 避免除以零
}
return dotProduct / (magnitude1 * magnitude2);
}
int main() {
// 两个文档的词频向量(TF-IDF值)
std::vector<double> doc1 = {0.5, 0.8, 0.3, 0, 0.2, 0.7, 0, 0.4};
std::vector<double> doc2 = {0.9, 0.4, 0.2, 0.1, 0, 0.6, 0.3, 0.2};
// 计算文档相似度
double similarity = cosineSimilarity(doc1, doc2);
std::cout << "Cosine similarity between two documents: "
<< std::fixed << std::setprecision(4) << similarity << std::endl;
// 相似度解释
std::cout << "Interpretation: ";
if (similarity > 0.8) {
std::cout << "Very similar";
} else if (similarity > 0.5) {
std::cout << "Moderately similar";
} else if (similarity > 0.2) {
std::cout << "Slightly similar";
} else {
std::cout << "Not similar";
}
std::cout << std::endl;
return 0;
}
这个例子演示了如何使用inner_product
计算两个文档向量的余弦相似度,这是文本挖掘和信息检索中常用的相似度度量方法。
std::partial_sum - 部分和(前缀和)
std::partial_sum
用于计算序列的部分和,也称为前缀和或累积和,即输出序列的每个元素是输入序列中对应位置及之前所有元素的和。
基本用法
partial_sum
算法有两种形式:
- 使用默认的加法操作:
partial_sum(first, last, d_first)
- 使用自定义的二元操作:
partial_sum(first, last, d_first, binary_op)
其中,first
和last
定义输入序列的范围,d_first
是输出序列的起始迭代器,binary_op
是可选的二元操作函数。
以下是一些基本示例:
#include <iostream>
#include <vector>
#include <numeric>
#include <iterator>
#include <functional>
// 辅助函数,打印向量内容
template<typename T>
void printVector(const std::vector<T>& vec, const std::string& name) {
std::cout << name << ": ";
for (const T& item : vec) {
std::cout << item << " ";
}
std::cout << std::endl;
}
int main() {
// 整数序列
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::vector<int> sums(numbers.size());
// 计算前缀和
std::partial_sum(numbers.begin(), numbers.end(), sums.begin());
printVector(numbers, "Original"); // 输出: 1 2 3 4 5
printVector(sums, "Prefix sums"); // 输出: 1 3 6 10 15
// 使用自定义操作(乘法)计算部分积
std::vector<int> products(numbers.size());
std::partial_sum(numbers.begin(), numbers.end(), products.begin(), std::multiplies<int>());
printVector(products, "Prefix products"); // 输出: 1 2 6 24 120
// 原地计算(覆盖原始数据)
std::partial_sum(numbers.begin(), numbers.end(), numbers.begin());
printVector(numbers, "In-place prefix sums"); // 输出: 1 3 6 10 15
return 0;
}
这个例子展示了如何使用partial_sum
计算一个序列的前缀和以及如何使用自定义操作计算前缀积。
常见应用场景
partial_sum
在多种场景下都非常有用,例如:
- 前缀和查询:快速计算数组任意区间的和
- 累积统计:计算随时间累积的值
- 积分近似:数值积分的梯形法则
- 概率分布:计算累积分布函数(CDF)
下面是一个使用partial_sum
解决实际问题的例子:
#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>
#include <iomanip>
#include <random>
// 使用前缀和快速计算区间和
class RangeSum {
private:
std::vector<int> prefixSums;
public:
RangeSum(const std::vector<int>& arr) {
// 计算前缀和数组
prefixSums.resize(arr.size() + 1); // 多一个元素,使索引更直观
prefixSums[0] = 0; // 前缀和的第一个元素为0
std::partial_sum(arr.begin(), arr.end(), prefixSums.begin() + 1);
}
// 计算区间[start, end)的和
int query(int start, int end) const {
if (start < 0 || end > static_cast<int>(prefixSums.size()) - 1 || start >= end) {
return 0; // 无效区间
}
return prefixSums[end] - prefixSums[start];
}
};
// 计算股票的移动平均价格
void calculateMovingAverage(const std::vector<double>& prices, int windowSize) {
if (prices.empty() || windowSize <= 0) return;
std::vector<double> movingSum(prices.size() + 1);
movingSum[0] = 0.0;
std::partial_sum(prices.begin(), prices.end(), movingSum.begin() + 1);
std::cout << "Moving Average (window size " << windowSize << "):" << std::endl;
std::cout << std::setw(10) << "Day" << std::setw(10) << "Price" << std::setw(15) << "Moving Avg" << std::endl;
std::cout << std::string(35, '-') << std::endl;
for (size_t i = 0; i < prices.size(); ++i) {
std::cout << std::fixed << std::setprecision(2);
std::cout << std::setw(10) << i + 1 << std::setw(10) << prices[i];
// 计算移动平均
if (i + 1 >= static_cast<size_t>(windowSize)) {
int start = i + 1 - windowSize;
double avg = (movingSum[i + 1] - movingSum[start]) / windowSize;
std::cout << std::setw(15) << avg;
} else {
std::cout << std::setw(15) << "-";
}
std::cout << std::endl;
}
}
int main() {
// 示例1:使用前缀和进行高效区间查询
std::vector<int> data = {3, 7, 2, 5, 8, 9, 1, 6};
RangeSum rangeSum(data);
std::cout << "Original array: ";
for (int num : data) std::cout << num << " ";
std::cout << std::endl;
// 查询示例
std::cout << "Sum of range [1, 4): " << rangeSum.query(1, 4) << std::endl; // 7+2+5=14
std::cout << "Sum of range [2, 6): " << rangeSum.query(2, 6) << std::endl; // 2+5+8+9=24
std::cout << "Sum of range [0, 8): " << rangeSum.query(0, 8) << std::endl; // 整个数组的和=41
// 示例2:计算股票的移动平均价格
std::vector<double> stockPrices = {
45.67, 46.23, 46.01, 45.89, 47.05, 46.78, 47.30, 48.15, 47.95, 48.87
};
calculateMovingAverage(stockPrices, 5);
return 0;
}
这个例子展示了两种partial_sum
的实际应用:
- 使用前缀和实现高效的区间和查询
- 计算股票价格的移动平均线
std::adjacent_difference - 相邻差值
std::adjacent_difference
用于计算序列中相邻元素的差值,即输出序列的每个元素是输入序列中当前元素与前一个元素的差。
基本用法
adjacent_difference
算法有两种形式:
- 使用默认的减法操作:
adjacent_difference(first, last, d_first)
- 使用自定义的二元操作:
adjacent_difference(first, last, d_first, binary_op)
其中,first
和last
定义输入序列的范围,d_first
是输出序列的起始迭代器,binary_op
是可选的二元操作函数。
以下是一些基本示例:
#include <iostream>
#include <vector>
#include <numeric>
#include <functional>
template<typename T>
void printVector(const std::vector<T>& vec, const std::string& name) {
std::cout << name << ": ";
for (const T& item : vec) {
std::cout << item << " ";
}
std::cout << std::endl;
}
int main() {
// 整数序列
std::vector<int> numbers = {1, 3, 6, 10, 15};
std::vector<int> diffs(numbers.size());
// 计算相邻差值
std::adjacent_difference(numbers.begin(), numbers.end(), diffs.begin());
printVector(numbers, "Original"); // 输出: 1 3 6 10 15
printVector(diffs, "Differences"); // 输出: 1 2 3 4 5 (注意第一个元素保持不变)
// 使用自定义操作(加法而非减法)
std::vector<int> sums(numbers.size());
std::adjacent_difference(numbers.begin(), numbers.end(), sums.begin(), std::plus<int>());
printVector(sums, "Adjacent sums"); // 输出: 1 4 9 16 25
// 原地计算
std::adjacent_difference(numbers.begin(), numbers.end(), numbers.begin());
printVector(numbers, "In-place differences"); // 输出: 1 2 3 4 5
return 0;
}
这个例子展示了如何使用adjacent_difference
计算序列的相邻元素差值,以及如何使用自定义操作代替默认的减法。
还原前缀和
有趣的是,adjacent_difference
可以看作是partial_sum
的逆操作。如果我们有一个前缀和序列,使用adjacent_difference
可以还原原始序列:
#include <iostream>
#include <vector>
#include <numeric>
template<typename T>
void printVector(const std::vector<T>& vec, const std::string& name) {
std::cout << name << ": ";
for (const T& item : vec) {
std::cout << item << " ";
}
std::cout << std::endl;
}
int main() {
// 原始序列
std::vector<int> original = {3, 1, 4, 1, 5, 9};
printVector(original, "Original");
// 计算前缀和
std::vector<int> prefixSum(original.size());
std::partial_sum(original.begin(), original.end(), prefixSum.begin());
printVector(prefixSum, "Prefix sum");
// 使用adjacent_difference还原原始序列
std::vector<int> restored(prefixSum.size());
std::adjacent_difference(prefixSum.begin(), prefixSum.end(), restored.begin());
printVector(restored, "Restored");
return 0;
}
输出:
Original: 3 1 4 1 5 9
Prefix sum: 3 4 8 9 14 23
Restored: 3 1 4 1 5 9
常见应用场景
adjacent_difference
在多种场景下有用,例如:
- 差分数组:求解区间更新问题
- 速度计算:从位置序列计算速度
- 加速度计算:从速度序列计算加速度
- 连续变化率:计算数据的变化速率
下面是一个使用adjacent_difference
解决实际问题的例子:
#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>
#include <iomanip>
#include <cmath>
// 通过位置数据计算速度和加速度
void calculateKinematics(const std::vector<double>& positions, double timeStep) {
if (positions.size() < 3) return;
std::vector<double> velocities(positions.size());
std::vector<double> accelerations(positions.size());
// 计算速度(一阶差分)
std::adjacent_difference(positions.begin(), positions.end(), velocities.begin());
// 将速度除以时间步长
std::transform(velocities.begin() + 1, velocities.end(), velocities.begin() + 1,
[timeStep](double diff) { return diff / timeStep; });
// 计算加速度(二阶差分)
std::adjacent_difference(velocities.begin(), velocities.end(), accelerations.begin());
// 将加速度除以时间步长
std::transform(accelerations.begin() + 1, accelerations.end(), accelerations.begin() + 1,
[timeStep](double diff) { return diff / timeStep; });
std::cout << std::fixed << std::setprecision(3);
std::cout << std::setw(10) << "Time" << std::setw(12) << "Position"
<< std::setw(12) << "Velocity" << std::setw(15) << "Acceleration" << std::endl;
std::cout << std::string(49, '-') << std::endl;
for (size_t i = 0; i < positions.size(); ++i) {
std::cout << std::setw(10) << i * timeStep
<< std::setw(12) << positions[i]
<< std::setw(12) << (i > 0 ? velocities[i] : 0)
<< std::setw(15) << (i > 1 ? accelerations[i] : 0)
<< std::endl;
}
}
// 使用差分数组进行区间更新和查询
class RangeUpdateQuery {
private:
std::vector<int> arr; // 原始数组
std::vector<int> diff; // 差分数组
public:
RangeUpdateQuery(const std::vector<int>& initial) : arr(initial) {
diff.resize(arr.size());
std::adjacent_difference(arr.begin(), arr.end(), diff.begin());
}
// 区间[start, end]增加val
void rangeUpdate(int start, int end, int val) {
if (start < 0 || end >= static_cast<int>(arr.size()) || start > end) return;
diff[start] += val;
if (end + 1 < static_cast<int>(arr.size())) {
diff[end + 1] -= val;
}
// 更新原始数组
std::partial_sum(diff.begin(), diff.end(), arr.begin());
}
// 获取当前数组
const std::vector<int>& getArray() const {
return arr;
}
};
int main() {
// 示例1:计算运动学参数
std::cout << "Kinematics Example:" << std::endl;
// 简单的抛物线运动:x = x0 + v0*t + 0.5*a*t^2
double x0 = 0.0; // 初始位置
double v0 = 10.0; // 初始速度
double a = -9.8; // 加速度(重力)
double dt = 0.5; // 时间步长
std::vector<double> positions;
for (int i = 0; i < 10; ++i) {
double t = i * dt;
double x = x0 + v0 * t + 0.5 * a * t * t;
positions.push_back(x);
}
calculateKinematics(positions, dt);
// 示例2:使用差分数组进行区间更新
std::cout << "\nRange Update Example:" << std::endl;
std::vector<int> initial(10, 0); // 初始全为0的数组
RangeUpdateQuery ruq(initial);
std::cout << "Initial array: ";
for (int val : ruq.getArray()) std::cout << val << " ";
std::cout << std::endl;
// 进行区间更新
ruq.rangeUpdate(2, 6, 3); // 区间[2,6]增加3
ruq.rangeUpdate(0, 4, 2); // 区间[0,4]增加2
ruq.rangeUpdate(5, 9, 1); // 区间[5,9]增加1
std::cout << "After updates: ";
for (int val : ruq.getArray()) std::cout << val << " ";
std::cout << std::endl;
return 0;
}
这个例子展示了两个adjacent_difference
的实际应用:
- 根据物体位置序列计算速度和加速度
- 使用差分数组高效处理区间更新问题
std::iota - 递增序列生成
std::iota
用于以递增顺序填充序列,它生成连续的递增值,从指定的起始值开始。这个算法的名称来源于APL编程语言中的一个函数,而不是英文缩写。
基本用法
iota
算法的格式为:iota(first, last, value)
其中,first
和last
定义要填充的序列范围,value
是起始值。
以下是一些基本示例:
#include <iostream>
#include <vector>
#include <list>
#include <numeric>
#include <iomanip>
template<typename T>
void printContainer(const T& container, const std::string& name) {
std::cout << name << ": ";
for (const auto& item : container) {
std::cout << item << " ";
}
std::cout << std::endl;
}
int main() {
// 填充向量
std::vector<int> vec(10);
std::iota(vec.begin(), vec.end(), 1); // 从1开始递增
printContainer(vec, "Vector"); // 输出: 1 2 3 4 5 6 7 8 9 10
// 填充列表
std::list<int> lst(5);
std::iota(lst.begin(), lst.end(), 100); // 从100开始递增
printContainer(lst, "List"); // 输出: 100 101 102 103 104
// 使用浮点数
std::vector<double> doubles(8);
std::iota(doubles.begin(), doubles.end(), -3.5); // 从-3.5开始递增
std::cout << "Doubles: ";
for (double d : doubles) {
std::cout << std::fixed << std::setprecision(1) << d << " ";
}
std::cout << std::endl; // 输出: -3.5 -2.5 -1.5 -0.5 0.5 1.5 2.5 3.5
// 使用字符
std::vector<char> chars(26);
std::iota(chars.begin(), chars.end(), 'a'); // 从'a'开始递增
printContainer(chars, "Chars"); // 输出: a b c ... z
return 0;
}
这个例子展示了如何使用iota
填充各种容器,生成不同类型的递增序列。
常见应用场景
iota
在创建索引序列、生成测试数据、初始化容器等方面非常有用,下面是一些常见的应用:
- 创建索引数组:用于间接排序或重新排列元素
- 生成测试序列:快速创建具有特定模式的测试数据
- 数值范围生成:创建特定范围内的数值序列
- 初始化循环变量:为循环或迭代准备初始值
下面是一些实际应用的例子:
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
#include <iomanip>
#include <random>
// 间接排序示例
void indirectSortExample() {
std::cout << "Indirect Sort Example:" << std::endl;
// 原始数据
std::vector<std::string> names = {
"Alice", "Bob", "Charlie", "David", "Eve", "Frank"
};
std::vector<double> scores = {85.5, 92.0, 78.5, 95.5, 88.0, 71.5};
// 创建索引数组
std::vector<int> indices(scores.size());
std::iota(indices.begin(), indices.end(), 0); // 填充0,1,2,...
// 按分数排序索引
std::sort(indices.begin(), indices.end(),
[&scores](int a, int b) { return scores[a] > scores[b]; });
// 打印排序结果
std::cout << "Rank | Name | Score" << std::endl;
std::cout << "---------------------------" << std::endl;
for (size_t i = 0; i < indices.size(); ++i) {
int idx = indices[i];
std::cout << std::setw(4) << i + 1 << " | "
<< std::left << std::setw(10) << names[idx] << " | "
<< std::fixed << std::setprecision(1) << scores[idx]
<< std::endl;
}
}
// 生成随机排列
std::vector<int> generateRandomPermutation(int n) {
std::vector<int> perm(n);
std::iota(perm.begin(), perm.end(), 0); // 填充0,1,2,...,n-1
// 随机打乱
std::random_device rd;
std::mt19937 g(rd());
std::shuffle(perm.begin(), perm.end(), g);
return perm;
}
// 使用iota生成离散区间
template<typename T>
std::vector<T> linearSpace(T start, T end, size_t num) {
std::vector<T> result(num);
T step = (end - start) / static_cast<T>(num - 1);
// 使用transform和iota生成线性空间
std::vector<size_t> indices(num);
std::iota(indices.begin(), indices.end(), 0);
std::transform(indices.begin(), indices.end(), result.begin(),
[start, step](size_t i) { return start + step * static_cast<T>(i); });
return result;
}
int main() {
// 间接排序示例
indirectSortExample();
// 生成随机排列
std::cout << "\nRandom Permutation Example:" << std::endl;
auto perm = generateRandomPermutation(10);
std::cout << "Random permutation: ";
for (int p : perm) std::cout << p << " ";
std::cout << std::endl;
// 生成线性空间
std::cout << "\nLinear Space Example:" << std::endl;
auto linspace = linearSpace<double>(0.0, 1.0, 11);
std::cout << "Linear space [0,1] with 11 points: ";
for (double x : linspace) {
std::cout << std::fixed << std::setprecision(2) << x << " ";
}
std::cout << std::endl;
return 0;
}
这个例子展示了iota
的三种实际应用:
- 创建索引数组用于间接排序,保留原始数据不变
- 生成随机排列,用于洗牌或随机采样
- 结合
transform
生成线性等分的数值序列
C++17的并行算法
C++17引入了并行版本的STL算法,包括数值算法,允许在多核环境中实现更高的性能。
并行版本的数值算法
C++17新增了以下并行数值算法:
std::reduce
-accumulate
的并行版本,允许非顺序计算std::transform_reduce
- 结合transform
和reduce
的功能std::inclusive_scan
-partial_sum
的并行版本std::exclusive_scan
- 类似inclusive_scan
,但不包括当前元素std::transform_inclusive_scan
- 结合transform和inclusive_scanstd::transform_exclusive_scan
- 结合transform和exclusive_scan
下面是这些并行算法的基本使用示例:
#include <iostream>
#include <vector>
#include <numeric>
#include <execution>
#include <chrono>
#include <random>
#include <iomanip>
// 计时辅助函数
template<typename Func>
long long measureTime(Func func) {
auto start = std::chrono::high_resolution_clock::now();
func();
auto end = std::chrono::high_resolution_clock::now();
return std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
}
int main() {
// 创建大数组进行测试
constexpr size_t size = 50'000'000;
std::vector<double> data(size);
// 用随机数填充
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<> dis(1.0, 100.0);
for (auto& val : data) {
val = dis(gen);
}
double sum1 = 0, sum2 = 0, sum3 = 0, sum4 = 0;
// 使用普通accumulate
auto time1 = measureTime([&] {
sum1 = std::accumulate(data.begin(), data.end(), 0.0);
});
// 使用顺序reduce (std::execution::seq)
auto time2 = measureTime([&] {
sum2 = std::reduce(std::execution::seq, data.begin(), data.end(), 0.0);
});
// 使用并行reduce (std::execution::par)
auto time3 = measureTime([&] {
sum3 = std::reduce(std::execution::par, data.begin(), data.end(), 0.0);
});
// 使用并行向量化reduce (std::execution::par_unseq)
auto time4 = measureTime([&] {
sum4 = std::reduce(std::execution::par_unseq, data.begin(), data.end(), 0.0);
});
std::cout << std::fixed << std::setprecision(1);
std::cout << "Sum (accumulate): " << sum1 << std::endl;
std::cout << "Sum (seq reduce): " << sum2 << std::endl;
std::cout << "Sum (par reduce): " << sum3 << std::endl;
std::cout << "Sum (par_unseq): " << sum4 << std::endl;
std::cout << "\nPerformance Comparison:" << std::endl;
std::cout << "accumulate: " << time1 << " microseconds" << std::endl;
std::cout << "sequential reduce: " << time2 << " microseconds" << std::endl;
std::cout << "parallel reduce: " << time3 << " microseconds" << std::endl;
std::cout << "parallel_unsequenced reduce: " << time4 << " microseconds" << std::endl;
// transform_reduce示例 - 计算平方和
double squareSum1 = 0, squareSum2 = 0;
// 使用transform和reduce组合
auto time5 = measureTime([&] {
std::vector<double> squares(size);
std::transform(data.begin(), data.end(), squares.begin(),
[](double x) { return x * x; });
squareSum1 = std::reduce(squares.begin(), squares.end(), 0.0);
});
// 使用transform_reduce
auto time6 = measureTime([&] {
squareSum2 = std::transform_reduce(
std::execution::par,
data.begin(), data.end(),
0.0,
std::plus<double>(),
[](double x) { return x * x; }
);
});
std::cout << "\nSquare Sum (transform + reduce): " << squareSum1 << std::endl;
std::cout << "Square Sum (transform_reduce): " << squareSum2 << std::endl;
std::cout << "Time (transform + reduce): " << time5 << " microseconds" << std::endl;
std::cout << "Time (transform_reduce): " << time6 << " microseconds" << std::endl;
return 0;
}
注意:要使用C++17的并行算法,需要支持C++17的编译器,并根据编译器要求链接适当的并行库(如Intel TBB、OpenMP等)。
总结
在这篇文章中,我们详细探讨了STL中的数值算法,包括:
- std::accumulate - 计算序列元素的总和或通过某种操作的累积结果
- std::inner_product - 计算两个序列的内积或组合结果
- std::partial_sum - 计算序列的部分和(前缀和)
- std::adjacent_difference - 计算序列中相邻元素的差值
- std::iota - 用递增值填充序列
我们还简要介绍了C++17引入的并行版本数值算法,如std::reduce
和std::transform_reduce
等。
这些数值算法在科学计算、数据分析、统计处理、信号处理等领域有广泛的应用。掌握这些算法可以帮助你更高效地处理数值数据,编写更简洁、更可读的代码。
在下一篇文章中,我们将继续探讨数值算法的高级应用以及STL中的集合算法,如std::set_union
、std::set_intersection
等,这些算法用于对有序序列执行集合操作。
参考资源
- C++ Reference - 详细的STL数值算法文档
- 《C++标准库》by Nicolai M. Josuttis
- 《Effective STL》by Scott Meyers
- 《C++17 STL Cookbook》by Jacek Galowicz
这是我C++学习之旅系列的第二十七篇技术文章。查看完整系列目录了解更多内容。
如有任何问题或建议,欢迎在评论区留言交流!