C++学习:六个月从基础到就业——STL算法(四)数值算法(下)与集合算法
本文是我C++学习之旅系列的第二十八篇技术文章,也是第二阶段"C++进阶特性"的第六篇,主要介绍C++ STL算法库中的数值算法(下部分)和集合算法。查看完整系列目录了解更多内容。
引言
在上一篇文章中,我们讨论了STL中的基本数值算法,包括accumulate
、inner_product
、partial_sum
、adjacent_difference
和iota
。本文将继续探索更多数值算法的高级应用,并深入介绍STL中的集合算法,这些算法主要用于处理有序序列的集合操作,如并集、交集、差集等。
集合算法是STL中一类非常实用但可能被忽略的算法,它们可以高效地执行集合论中的基本操作,帮助我们处理各种有序数据集合。这些算法包含在<algorithm>
头文件中,与排序和查找算法并列。
本文将通过详细介绍和实际示例,帮助你掌握这些强大的工具,并了解如何在实际编程中应用它们来解决问题。
高级数值算法应用
在进入集合算法之前,让我们先探索一些数值算法的高级应用场景。
数值积分:梯形法则与辛普森法则
数值积分是科学计算中常见的任务,我们可以使用STL算法实现常见的数值积分方法:
#include <iostream>
#include <vector>
#include <cmath>
#include <numeric>
#include <functional>
#include <iomanip>
// 使用partial_sum和adjacent_difference实现梯形法则积分
double trapezoidalIntegration(const std::function<double(double)>& f,
double a, double b, int n) {
if (n <= 0) return 0;
std::vector<double> x(n+1);
std::vector<double> y(n+1);
// 生成x值(等间距点)
double h = (b - a) / n;
for (int i = 0; i <= n; ++i) {
x[i] = a + i * h;
y[i] = f(x[i]);
}
// 应用梯形法则: (h/2) * (f(a) + 2*f(a+h) + 2*f(a+2h) + ... + 2*f(b-h) + f(b))
double sum = y[0] + y[n]; // 首尾项
for (int i = 1; i < n; ++i) {
sum += 2 * y[i];
}
return (h / 2) * sum;
}
// 使用更复杂的accumulate实现辛普森法则
double simpsonIntegration(const std::function<double(double)>& f,
double a, double b, int n) {
if (n <= 0 || n % 2 != 0) return 0; // n必须是偶数
double h = (b - a) / n;
std::vector<double> y(n+1);
for (int i = 0; i <= n; ++i) {
y[i] = f(a + i * h);
}
// 使用辛普森公式: (h/3) * (f(a) + 4*f(a+h) + 2*f(a+2h) + 4*f(a+3h) + ... + 4*f(b-h) + f(b))
double sum = y[0] + y[n]; // 首尾项
// 使用accumulate和自定义操作
sum += std::accumulate(y.begin() + 1, y.end() - 1, 0.0,
[&](double acc, double val) {
static int idx = 1;
// 根据索引决定系数(4或2)
double coef = (idx++ % 2 == 0) ? 2.0 : 4.0;
return acc + coef * val;
});
return (h / 3) * sum;
}
int main() {
// 测试函数:∫sin(x)dx从0到π
auto f = [](double x) { return std::sin(x); };
double exactResult = 2.0; // 解析解是2
std::cout << std::fixed << std::setprecision(10);
std::cout << "Numerical Integration of sin(x) from 0 to π:" << std::endl;
std::cout << "Exact result: " << exactResult << std::endl;
// 测试不同精度的梯形法则
for (int n : {10, 100, 1000, 10000}) {
double result = trapezoidalIntegration(f, 0, M_PI, n);
double error = std::abs(result - exactResult);
std::cout << "Trapezoidal rule with n = " << std::setw(6) << n
<< ": " << result << ", error: " << error << std::endl;
}
// 测试不同精度的辛普森法则
for (int n : {10, 100, 1000, 10000}) {
double result = simpsonIntegration(f, 0, M_PI, n * 2); // 确保n是偶数
double error = std::abs(result - exactResult);
std::cout << "Simpson's rule with n = " << std::setw(6) << n * 2
<< ": " << result << ", error: " << error << std::endl;
}
return 0;
}
这个例子展示了如何使用STL算法实现两种常见的数值积分方法:梯形法则和辛普森法则。可以看到,辛普森法则通常比梯形法则具有更高的精度。
数值微分:中心差分法
类似地,我们可以使用adjacent_difference
来实现数值微分:
#include <iostream>
#include <vector>
#include <functional>
#include <numeric>
#include <iomanip>
#include <cmath>
// 使用前向差分计算数值导数
std::vector<double> numericalDerivative(const std::function<double(double)>& f,
double a, double b, int n) {
std::vector<double> x(n+1);
std::vector<double> y(n+1);
std::vector<double> dydx(n+1);
// 计算步长
double h = (b - a) / n;
// 计算函数值
for (int i = 0; i <= n; ++i) {
x[i] = a + i * h;
y[i] = f(x[i]);
}
// 使用中心差分法计算导数
// 对于内部点: (f(x+h) - f(x-h)) / (2h)
for (int i = 1; i < n; ++i) {
dydx[i] = (y[i+1] - y[i-1]) / (2 * h);
}
// 对于边界使用前向/后向差分
dydx[0] = (y[1] - y[0]) / h; // 前向差分
dydx[n] = (y[n] - y[n-1]) / h; // 后向差分
return dydx;
}
// 使用adjacent_difference实现二阶导数计算
std::vector<double> secondDerivative(const std::function<double(double)>& f,
double a, double b, int n) {
std::vector<double> x(n+1);
std::vector<double> y(n+1);
std::vector<double> d2ydx2(n+1);
// 计算步长
double h = (b - a) / n;
// 计算函数值
for (int i = 0; i <= n; ++i) {
x[i] = a + i * h;
y[i] = f(x[i]);
}
// 使用中心差分法计算二阶导数
// (f(x+h) - 2f(x) + f(x-h)) / h^2
for (int i = 1; i < n; ++i) {
d2ydx2[i] = (y[i+1] - 2*y[i] + y[i-1]) / (h * h);
}
// 对于边界,使用前向/后向差分(精度较低)
d2ydx2[0] = (y[2] - 2*y[1] + y[0]) / (h * h);
d2ydx2[n] = (y[n] - 2*y[n-1] + y[n-2]) / (h * h);
return d2ydx2;
}
int main() {
// 测试函数: f(x) = sin(x),其导数是cos(x),二阶导数是-sin(x)
auto f = [](double x) { return std::sin(x); };
auto df = [](double x) { return std::cos(x); }; // 解析导数
auto d2f = [](double x) { return -std::sin(x); }; // 解析二阶导数
// 计算区间[0, 2π]上的导数
double a = 0.0, b = 2 * M_PI;
int n = 100; // 分割数
// 生成x值
std::vector<double> x(n+1);
double h = (b - a) / n;
for (int i = 0; i <= n; ++i) {
x[i] = a + i * h;
}
// 计算数值导数和二阶导数
auto numerical_dy = numericalDerivative(f, a, b, n);
auto numerical_d2y = secondDerivative(f, a, b, n);
// 打印结果比较
std::cout << std::fixed << std::setprecision(6);
std::cout << "Comparing numerical derivatives with analytical solutions:" << std::endl;
std::cout << std::setw(10) << "x"
<< std::setw(15) << "f(x)"
<< std::setw(15) << "f'(x) num"
<< std::setw(15) << "f'(x) exact"
<< std::setw(15) << "f''(x) num"
<< std::setw(15) << "f''(x) exact" << std::endl;
// 选择几个点进行比较
for (int i = 0; i <= n; i += 10) {
std::cout << std::setw(10) << x[i]
<< std::setw(15) << f(x[i])
<< std::setw(15) << numerical_dy[i]
<< std::setw(15) << df(x[i])
<< std::setw(15) << numerical_d2y[i]
<< std::setw(15) << d2f(x[i]) << std::endl;
}
return 0;
}
这个例子演示了如何使用中心差分法计算函数的数值导数和二阶导数,并与解析解进行比较。
统计分析:移动平均和滑动窗口
移动平均是数据分析中常用的一种平滑技术,可以使用partial_sum
和滑动窗口来实现:
#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>
#include <iomanip>
#include <random>
// 使用partial_sum和滑动窗口计算移动平均
std::vector<double> movingAverage(const std::vector<double>& data, int windowSize) {
int n = data.size();
if (n < windowSize || windowSize <= 0) {
return {}; // 返回空向量
}
// 计算前缀和
std::vector<double> prefixSum(n + 1, 0);
std::partial_sum(data.begin(), data.end(), prefixSum.begin() + 1);
// 计算移动平均
std::vector<double> result(n - windowSize + 1);
for (int i = 0; i <= n - windowSize; ++i) {
result[i] = (prefixSum[i + windowSize] - prefixSum[i]) / windowSize;
}
return result;
}
// 使用accumulate和滑动窗口计算指数移动平均
std::vector<double> exponentialMovingAverage(const std::vector<double>& data, double alpha) {
if (data.empty() || alpha < 0 || alpha > 1) {
return {}; // 返回空向量
}
std::vector<double> result(data.size());
result[0] = data[0]; // 第一个值就是初始EMA
// 计算指数移动平均: EMA_today = alpha * price_today + (1 - alpha) * EMA_yesterday
for (size_t i = 1; i < data.size(); ++i) {
result[i] = alpha * data[i] + (1 - alpha) * result[i-1];
}
return result;
}
// 生成模拟股票数据
std::vector<double> generateStockData(int days, double initialPrice,
double dailyVolatility, unsigned seed) {
std::mt19937 gen(seed);
std::normal_distribution<> distr(0, dailyVolatility);
std::vector<double> prices(days);
prices[0] = initialPrice;
for (int i = 1; i < days; ++i) {
// 随机游走模型
double change = distr(gen);
prices[i] = prices[i-1] * (1 + change);
// 确保价格不会变为负数
if (prices[i] < 0) prices[i] = prices[i-1] / 2;
}
return prices;
}
int main() {
// 生成模拟股票数据
auto stockPrices = generateStockData(100, 100.0, 0.02, 42);
// 计算不同窗口大小的简单移动平均
auto sma10 = movingAverage(stockPrices, 10); // 10天移动平均
auto sma20 = movingAverage(stockPrices, 20); // 20天移动平均
// 计算不同参数的指数移动平均
auto ema12 = exponentialMovingAverage(stockPrices, 2.0 / (12 + 1));
auto ema26 = exponentialMovingAverage(stockPrices, 2.0 / (26 + 1));
// 打印部分结果
std::cout << std::fixed << std::setprecision(2);
std::cout << "Stock Price Analysis:" << std::endl;
std::cout << std::setw(6) << "Day"
<< std::setw(10) << "Price"
<< std::setw(10) << "SMA(10)"
<< std::setw(10) << "SMA(20)"
<< std::setw(10) << "EMA(12)"
<< std::setw(10) << "EMA(26)" << std::endl;
// 显示每5天的数据
for (int day = 0; day < 100; day += 5) {
std::cout << std::setw(6) << day + 1
<< std::setw(10) << stockPrices[day];
// SMA值(只有当有足够的数据时才显示)
if (day < sma10.size()) {
std::cout << std::setw(10) << sma10[day];
} else {
std::cout << std::setw(10) << "-";
}
if (day < sma20.size()) {
std::cout << std::setw(10) << sma20[day];
} else {
std::cout << std::setw(10) << "-";
}
// EMA值
std::cout << std::setw(10) << ema12[day]
<< std::setw(10) << ema26[day]
<< std::endl;
}
// 计算MACD (Moving Average Convergence Divergence)
std::vector<double> macd(stockPrices.size());
std::transform(ema12.begin(), ema12.end(), ema26.begin(), macd.begin(),
[](double a, double b) { return a - b; });
std::cout << "\nMACD Analysis (EMA12 - EMA26):" << std::endl;
for (int day = 0; day < 100; day += 10) {
std::cout << "Day " << day + 1 << ": " << macd[day] << std::endl;
}
return 0;
}
这个例子演示了如何使用partial_sum
和滑动窗口计算股票价格的简单移动平均,以及如何使用累积计算来实现指数移动平均。这些技术在金融数据分析中非常常用。
集合算法详解
现在,让我们转向STL中的集合算法,它们主要用于对有序序列执行集合操作。这些算法要求输入序列已经有序,它们的输出也是有序的。
std::set_union - 集合并集
set_union
算法计算两个有序序列的并集,即包含所有出现在任一序列中的元素。
#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>
void demoSetUnion() {
// 两个已排序的数组
std::vector<int> v1 = {1, 3, 5, 7, 9, 11};
std::vector<int> v2 = {2, 3, 5, 7, 8};
std::cout << "集合并集示例 (set_union):" << std::endl;
std::cout << "v1: ";
for (int n : v1) std::cout << n << " ";
std::cout << std::endl;
std::cout << "v2: ";
for (int n : v2) std::cout << n << " ";
std::cout << std::endl;
// 计算并集
std::vector<int> result(v1.size() + v2.size()); // 最大可能大小
auto it = std::set_union(v1.begin(), v1.end(), v2.begin(), v2.end(), result.begin());
// 调整结果大小
result.resize(it - result.begin());
std::cout << "并集: ";
for (int n : result) std::cout << n << " ";
std::cout << std::endl;
// 使用自定义比较函数
std::vector<int> v3 = {9, 7, 5, 3, 1}; // 降序排列
std::vector<int> v4 = {8, 7, 5, 3, 2}; // 降序排列
std::vector<int> result2(v3.size() + v4.size());
auto it2 = std::set_union(v3.begin(), v3.end(), v4.begin(), v4.end(),
result2.begin(), std::greater<int>());
result2.resize(it2 - result2.begin());
std::cout << "降序并集: ";
for (int n : result2) std::cout << n << " ";
std::cout << std::endl;
// 直接输出到标准输出流
std::cout << "直接输出并集: ";
std::set_union(v1.begin(), v1.end(), v2.begin(), v2.end(),
std::ostream_iterator<int>(std::cout, " "));
std::cout << std::endl;
}
int main() {
demoSetUnion();
return 0;
}
输出:
集合并集示例 (set_union):
v1: 1 3 5 7 9 11
v2: 2 3 5 7 8
并集: 1 2 3 5 7 8 9 11
降序并集: 9 8 7 5 3 2 1
直接输出并集: 1 2 3 5 7 8 9 11
set_union
算法的时间复杂度是O(n+m),其中n和m是两个输入序列的长度。
std::set_intersection - 集合交集
set_intersection
算法计算两个有序序列的交集,即同时出现在两个序列中的元素。
#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>
#include <string>
void demoSetIntersection() {
std::vector<int> v1 = {1, 3, 5, 7, 9, 11};
std::vector<int> v2 = {2, 3, 5, 7, 8};
std::cout << "集合交集示例 (set_intersection):" << std::endl;
std::cout << "v1: ";
for (int n : v1) std::cout << n << " ";
std::cout << std::endl;
std::cout << "v2: ";
for (int n : v2) std::cout << n << " ";
std::cout << std::endl;
// 计算交集
std::vector<int> result(std::min(v1.size(), v2.size())); // 最大可能大小
auto it = std::set_intersection(v1.begin(), v1.end(), v2.begin(), v2.end(), result.begin());
// 调整结果大小
result.resize(it - result.begin());
std::cout << "交集: ";
for (int n : result) std::cout << n << " ";
std::cout << std::endl;
// 字符串的交集
std::vector<std::string> fruits1 = {"apple", "banana", "orange", "pear", "strawberry"};
std::vector<std::string> fruits2 = {"apple", "grape", "orange", "watermelon"};
std::vector<std::string> commonFruits(std::min(fruits1.size(), fruits2.size()));
auto fruitIt = std::set_intersection(
fruits1.begin(), fruits1.end(),
fruits2.begin(), fruits2.end(),
commonFruits.begin()
);
commonFruits.resize(fruitIt - commonFruits.begin());
std::cout << "共同水果: ";
for (const auto& fruit : commonFruits) std::cout << fruit << " ";
std::cout << std::endl;
}
int main() {
demoSetIntersection();
return 0;
}
输出:
集合交集示例 (set_intersection):
v1: 1 3 5 7 9 11
v2: 2 3 5 7 8
交集: 3 5 7
共同水果: apple orange
set_intersection
算法的时间复杂度也是O(n+m)。
std::set_difference - 集合差集
set_difference
算法计算两个有序序列的差集,即出现在第一个序列但不在第二个序列中的元素。
#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>
#include <set>
void demoSetDifference() {
std::vector<int> v1 = {1, 3, 5, 7, 9, 11};
std::vector<int> v2 = {2, 3, 5, 7, 8};
std::cout << "集合差集示例 (set_difference):" << std::endl;
std::cout << "v1: ";
for (int n : v1) std::cout << n << " ";
std::cout << std::endl;
std::cout << "v2: ";
for (int n : v2) std::cout << n << " ";
std::cout << std::endl;
// 计算v1 - v2
std::vector<int> diff1(v1.size());
auto it1 = std::set_difference(v1.begin(), v1.end(), v2.begin(), v2.end(), diff1.begin());
diff1.resize(it1 - diff1.begin());
std::cout << "v1 - v2: ";
for (int n : diff1) std::cout << n << " ";
std::cout << std::endl;
// 计算v2 - v1
std::vector<int> diff2(v2.size());
auto it2 = std::set_difference(v2.begin(), v2.end(), v1.begin(), v1.end(), diff2.begin());
diff2.resize(it2 - diff2.begin());
std::cout << "v2 - v1: ";
for (int n : diff2) std::cout << n << " ";
std::cout << std::endl;
// 使用STL集合类型
std::set<char> set1 = {'a', 'b', 'c', 'd', 'e'};
std::set<char> set2 = {'b', 'd', 'f', 'g'};
std::cout << "set1: ";
for (char c : set1) std::cout << c << " ";
std::cout << std::endl;
std::cout << "set2: ";
for (char c : set2) std::cout << c << " ";
std::cout << std::endl;
std::cout << "set1 - set2: ";
std::set_difference(
set1.begin(), set1.end(),
set2.begin(), set2.end(),
std::ostream_iterator<char>(std::cout, " ")
);
std::cout << std::endl;
}
int main() {
demoSetDifference();
return 0;
}
输出:
集合差集示例 (set_difference):
v1: 1 3 5 7 9 11
v2: 2 3 5 7 8
v1 - v2: 1 9 11
v2 - v1: 2 8
set1: a b c d e
set2: b d f g
set1 - set2: a c e
set_difference
算法的时间复杂度是O(n+m)。
std::set_symmetric_difference - 对称差集
set_symmetric_difference
算法计算两个有序序列的对称差集,即出现在其中一个序列但不同时出现在两个序列中的元素。
#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>
void demoSymmetricDifference() {
std::vector<int> v1 = {1, 3, 5, 7, 9, 11};
std::vector<int> v2 = {2, 3, 5, 7, 8};
std::cout << "对称差集示例 (set_symmetric_difference):" << std::endl;
std::cout << "v1: ";
for (int n : v1) std::cout << n << " ";
std::cout << std::endl;
std::cout << "v2: ";
for (int n : v2) std::cout << n << " ";
std::cout << std::endl;
// 计算对称差集
std::vector<int> symDiff(v1.size() + v2.size());
auto it = std::set_symmetric_difference(
v1.begin(), v1.end(),
v2.begin(), v2.end(),
symDiff.begin()
);
symDiff.resize(it - symDiff.begin());
std::cout << "对称差集: ";
for (int n : symDiff) std::cout << n << " ";
std::cout << std::endl;
// 使用对称差集实现XOR操作
std::vector<bool> bits1 = {true, false, true, false, true};
std::vector<bool> bits2 = {false, false, true, true, false};
std::cout << "bits1: ";
for (bool b : bits1) std::cout << (b ? "1" : "0") << " ";
std::cout << std::endl;
std::cout << "bits2: ";
for (bool b : bits2) std::cout << (b ? "1" : "0") << " ";
std::cout << std::endl;
// 注意:vector<bool>是特殊的,不能直接使用set_symmetric_difference
// 这里只是概念演示,实际应用中应该使用不同的方法
std::vector<int> bitsResult(5);
std::transform(bits1.begin(), bits1.end(), bits2.begin(), bitsResult.begin(),
[](bool a, bool b) { return a != b; });
std::cout << "XOR结果: ";
for (int b : bitsResult) std::cout << b << " ";
std::cout << std::endl;
}
int main() {
demoSymmetricDifference();
return 0;
}
输出:
对称差集示例 (set_symmetric_difference):
v1: 1 3 5 7 9 11
v2: 2 3 5 7 8
对称差集: 1 2 8 9 11
bits1: 1 0 1 0 1
bits2: 0 0 1 1 0
XOR结果: 1 0 0 1 1
set_symmetric_difference
算法的时间复杂度是O(n+m)。
std::includes - 子集检查
includes
算法检查第一个有序序列是否包含第二个有序序列的所有元素,即第二个序列是否是第一个序列的子集。
#include <iostream>
#include <vector>
#include <algorithm>
#include <set>
void demoIncludes() {
std::vector<int> v1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::vector<int> v2 = {2, 4, 6};
std::vector<int> v3 = {2, 4, 11};
std::cout << "子集检查示例 (includes):" << std::endl;
std::cout << "v1: ";
for (int n : v1) std::cout << n << " ";
std::cout << std::endl;
std::cout << "v2: ";
for (int n : v2) std::cout << n << " ";
std::cout << std::endl;
std::cout << "v3: ";
for (int n : v3) std::cout << n << " ";
std::cout << std::endl;
// 检查v2是否是v1的子集
bool v2IsSubset = std::includes(v1.begin(), v1.end(), v2.begin(), v2.end());
std::cout << "v2是v1的子集? " << (v2IsSubset ? "是" : "否") << std::endl;
// 检查v3是否是v1的子集
bool v3IsSubset = std::includes(v1.begin(), v1.end(), v3.begin(), v3.end());
std::cout << "v3是v1的子集? " << (v3IsSubset ? "是" : "否") << std::endl;
// 使用集合类型
std::set<char> set1 = {'a', 'b', 'c', 'd', 'e', 'f', 'g'};
std::set<char> set2 = {'a', 'c', 'e'};
std::set<char> set3 = {'a', 'h'};
bool set2IsSubset = std::includes(set1.begin(), set1.end(), set2.begin(), set2.end());
bool set3IsSubset = std::includes(set1.begin(), set1.end(), set3.begin(), set3.end());
std::cout << "set2是set1的子集? " << (set2IsSubset ? "是" : "否") << std::endl;
std::cout << "set3是set1的子集? " << (set3IsSubset ? "是" : "否") << std::endl;
// 多重集合(允许重复元素)
std::multiset<int> ms1 = {1, 1, 2, 2, 3, 3, 4, 4};
std::multiset<int> ms2 = {1, 2, 3};
std::multiset<int> ms3 = {1, 1, 3};
bool ms2IsSubset = std::includes(ms1.begin(), ms1.end(), ms2.begin(), ms2.end());
bool ms3IsSubset = std::includes(ms1.begin(), ms1.end(), ms3.begin(), ms3.end());
std::cout << "ms2是ms1的子集? " << (ms2IsSubset ? "是" : "否") << std::endl;
std::cout << "ms3是ms1的子集? " << (ms3IsSubset ? "是" : "否") << std::endl;
}
int main() {
demoIncludes();
return 0;
}
输出:
子集检查示例 (includes):
v1: 1 2 3 4 5 6 7 8 9 10
v2: 2 4 6
v3: 2 4 11
v2是v1的子集? 是
v3是v1的子集? 否
set2是set1的子集? 是
set3是set1的子集? 否
ms2是ms1的子集? 是
ms3是ms1的子集? 是
includes
算法的时间复杂度是O(n+m)。
std::merge - 合并有序序列
merge
算法将两个有序序列合并成一个有序序列。与set_union
不同,merge
保留重复元素。
#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>
#include <functional>
void demoMerge() {
std::vector<int> v1 = {1, 3, 5, 7, 9};
std::vector<int> v2 = {2, 4, 6, 8, 10};
std::cout << "合并有序序列示例 (merge):" << std::endl;
std::cout << "v1: ";
for (int n : v1) std::cout << n << " ";
std::cout << std::endl;
std::cout << "v2: ";
for (int n : v2) std::cout << n << " ";
std::cout << std::endl;
// 合并两个序列
std::vector<int> result(v1.size() + v2.size());
std::merge(v1.begin(), v1.end(), v2.begin(), v2.end(), result.begin());
std::cout << "合并结果: ";
for (int n : result) std::cout << n << " ";
std::cout << std::endl;
// 降序合并
std::vector<int> v3 = {9, 7, 5, 3, 1};
std::vector<int> v4 = {10, 8, 6, 4, 2};
std::vector<int> resultDesc(v3.size() + v4.size());
std::merge(v3.begin(), v3.end(), v4.begin(), v4.end(),
resultDesc.begin(), std::greater<int>());
std::cout << "降序合并: ";
for (int n : resultDesc) std::cout << n << " ";
std::cout << std::endl;
// 合并带有重复元素的序列
std::vector<int> v5 = {1, 3, 3, 5, 7, 9, 9};
std::vector<int> v6 = {2, 3, 5, 5, 8};
std::vector<int> resultWithDuplicates(v5.size() + v6.size());
std::merge(v5.begin(), v5.end(), v6.begin(), v6.end(), resultWithDuplicates.begin());
std::cout << "带重复元素的合并: ";
for (int n : resultWithDuplicates) std::cout << n << " ";
std::cout << std::endl;
// 与set_union的区别
std::vector<int> unionResult(v5.size() + v6.size());
auto unionEnd = std::set_union(v5.begin(), v5.end(), v6.begin(), v6.end(), unionResult.begin());
unionResult.resize(unionEnd - unionResult.begin());
std::cout << "使用set_union的结果: ";
for (int n : unionResult) std::cout << n << " ";
std::cout << std::endl;
}
int main() {
demoMerge();
return 0;
}
输出:
合并有序序列示例 (merge):
v1: 1 3 5 7 9
v2: 2 4 6 8 10
合并结果: 1 2 3 4 5 6 7 8 9 10
降序合并: 10 9 8 7 6 5 4 3 2 1
带重复元素的合并: 1 2 3 3 3 5 5 5 7 8 9 9
使用set_union的结果: 1 2 3 5 7 8 9
merge
算法的时间复杂度是O(n+m)。
实际应用示例
现在,让我们看一些集合算法的实际应用例子。
数据分析:共同兴趣发现
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
#include <map>
#include <set>
// 用户兴趣分析
void analyzeCommonInterests() {
// 用户和他们的兴趣
std::map<std::string, std::set<std::string>> userInterests = {
{"Alice", {"读书", "旅行", "摄影", "音乐", "电影"}},
{"Bob", {"足球", "篮球", "旅行", "电影", "编程"}},
{"Charlie", {"摄影", "旅行", "编程", "徒步", "音乐"}},
{"David", {"电影", "音乐", "烹饪", "绘画", "旅行"}}
};
std::cout << "用户兴趣分析:" << std::endl;
// 打印每个用户的兴趣
for (const auto& [user, interests] : userInterests) {
std::cout << user << "的兴趣: ";
for (const auto& interest : interests) {
std::cout << interest << " ";
}
std::cout << std::endl;
}
// 分析每对用户的共同兴趣
for (auto it1 = userInterests.begin(); it1 != userInterests.end(); ++it1) {
auto it2 = it1;
++it2;
for (; it2 != userInterests.end(); ++it2) {
const std::string& user1 = it1->first;
const std::string& user2 = it2->first;
const auto& interests1 = it1->second;
const auto& interests2 = it2->second;
std::vector<std::string> commonInterests;
std::set_intersection(
interests1.begin(), interests1.end(),
interests2.begin(), interests2.end(),
std::back_inserter(commonInterests)
);
std::cout << user1 << "和" << user2 << "的共同兴趣(" << commonInterests.size() << "个): ";
for (const auto& interest : commonInterests) {
std::cout << interest << " ";
}
std::cout << std::endl;
}
}
// 找出所有用户都有的兴趣
std::set<std::string> commonToAll = userInterests.begin()->second;
for (const auto& [user, interests] : userInterests) {
std::set<std::string> temp;
std::set_intersection(
commonToAll.begin(), commonToAll.end(),
interests.begin(), interests.end(),
std::inserter(temp, temp.begin())
);
commonToAll = temp;
}
std::cout << "\n所有用户共有的兴趣: ";
if (commonToAll.empty()) {
std::cout << "无";
} else {
for (const auto& interest : commonToAll) {
std::cout << interest << " ";
}
}
std::cout << std::endl;
// 找出每个用户独有的兴趣
for (const auto& [user, interests] : userInterests) {
std::set<std::string> uniqueInterests = interests;
for (const auto& [otherUser, otherInterests] : userInterests) {
if (user != otherUser) {
std::set<std::string> temp;
std::set_difference(
uniqueInterests.begin(), uniqueInterests.end(),
otherInterests.begin(), otherInterests.end(),
std::inserter(temp, temp.begin())
);
uniqueInterests = temp;
}
}
std::cout << user << "的独有兴趣: ";
if (uniqueInterests.empty()) {
std::cout << "无";
} else {
for (const auto& interest : uniqueInterests) {
std::cout << interest << " ";
}
}
std::cout << std::endl;
}
}
int main() {
analyzeCommonInterests();
return 0;
}
这个例子展示了如何使用集合算法分析用户的共同兴趣和独有兴趣,这在推荐系统和社交网络分析中非常有用。
文本处理:共同词汇分析
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
#include <set>
#include <sstream>
#include <map>
#include <iomanip>
// 分词函数
std::set<std::string> tokenize(const std::string& text) {
std::set<std::string> tokens;
std::istringstream iss(text);
std::string token;
while (iss >> token) {
// 简单处理:转为小写,去除标点
std::transform(token.begin(), token.end(), token.begin(),
[](unsigned char c) { return std::tolower(c); });
token.erase(std::remove_if(token.begin(), token.end(),
[](unsigned char c) { return std::ispunct(c); }),
token.end());
if (!token.empty()) {
tokens.insert(token);
}
}
return tokens;
}
void analyzeTextSimilarity() {
std::map<std::string, std::string> documents = {
{"文档1", "机器学习是人工智能的一个子领域,它使用统计技术让计算机系统能够学习和自我改进。"},
{"文档2", "深度学习是机器学习的一个分支,它使用人工神经网络模型进行模式识别和决策。"},
{"文档3", "自然语言处理是人工智能的一个分支,它使计算机能够理解、解释和生成人类语言。"},
{"文档4", "计算机视觉是人工智能的一个领域,它使计算机能够理解和处理数字图像。"}
};
// 对每个文档进行分词
std::map<std::string, std::set<std::string>> tokenizedDocs;
for (const auto& [name, text] : documents) {
tokenizedDocs[name] = tokenize(text);
std::cout << name << "的词汇: ";
for (const auto& token : tokenizedDocs[name]) {
std::cout << token << " ";
}
std::cout << std::endl;
}
std::cout << "\n文档相似度分析 (基于共享词汇):" << std::endl;
// 计算每对文档的词汇重叠度
for (auto it1 = tokenizedDocs.begin(); it1 != tokenizedDocs.end(); ++it1) {
auto it2 = it1;
++it2;
for (; it2 != tokenizedDocs.end(); ++it2) {
const std::string& doc1Name = it1->first;
const std::string& doc2Name = it2->first;
const auto& words1 = it1->second;
const auto& words2 = it2->second;
// 计算共同词汇
std::vector<std::string> intersection;
std::set_intersection(
words1.begin(), words1.end(),
words2.begin(), words2.end(),
std::back_inserter(intersection)
);
// 计算并集大小
std::vector<std::string> union_;
std::set_union(
words1.begin(), words1.end(),
words2.begin(), words2.end(),
std::back_inserter(union_)
);
// 计算Jaccard相似度:交集大小 / 并集大小
double similarity = static_cast<double>(intersection.size()) / union_.size();
std::cout << doc1Name << "和" << doc2Name << "的相似度: "
<< std::fixed << std::setprecision(3) << similarity
<< " (共享词汇: ";
for (const auto& word : intersection) {
std::cout << word << " ";
}
std::cout << ")" << std::endl;
}
}
// 找出所有文档共有的词汇
auto it = tokenizedDocs.begin();
std::set<std::string> commonWords = it->second;
++it;
for (; it != tokenizedDocs.end(); ++it) {
std::set<std::string> temp;
std::set_intersection(
commonWords.begin(), commonWords.end(),
it->second.begin(), it->second.end(),
std::inserter(temp, temp.begin())
);
commonWords = temp;
}
std::cout << "\n所有文档共有的词汇: ";
if (commonWords.empty()) {
std::cout << "无";
} else {
for (const auto& word : commonWords) {
std::cout << word << " ";
}
}
std::cout << std::endl;
}
int main() {
analyzeTextSimilarity();
return 0;
}
这个例子展示了如何使用集合算法分析文本相似度,计算文档间的共同词汇和Jaccard相似度,这在信息检索和文本分类中很有用。
最佳实践与性能考量
集合算法的前提条件
- 输入序列必须有序:所有集合算法都假设输入序列已经排序。如果输入序列未排序,先使用
std::sort
排序。 - 输出容器应有足够空间:预先调整输出容器的大小以适应结果,或使用插入迭代器(如
std::back_inserter
)。 - 使用相同的排序标准:如果使用自定义比较函数,确保相同的比较函数用于排序和集合操作。
性能优化技巧
- 预先分配空间:合理预估结果大小,避免动态扩展容器。
- 选择适当的容器:对于大型集合,考虑使用
std::set
或std::map
而不是向量。 - 处理重复元素:了解各算法如何处理重复元素(
merge
保留所有重复,set_union
只保留一次)。 - 组合算法:有时组合多个简单集合操作比使用更复杂的算法更高效。
常见陷阱
- 输入未排序:将未排序的数据传递给集合算法会产生不正确的结果。
- 比较器不一致:在排序和集合操作中使用不同的比较函数可能导致错误结果。
- 输出空间不足:如果输出容器空间不足且未使用插入迭代器,将导致内存破坏。
- 迭代器失效:修改输入集合可能会使迭代器失效。
性能比较:集合算法与STL容器
#include <iostream>
#include <vector>
#include <set>
#include <algorithm>
#include <chrono>
#include <random>
#include <functional>
#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();
}
void performanceComparison() {
const size_t size = 100000;
// 生成随机数据
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(1, size * 2);
std::vector<int> v1(size);
std::vector<int> v2(size);
for (size_t i = 0; i < size; ++i) {
v1[i] = dis(gen);
v2[i] = dis(gen);
}
// 排序数据(集合算法要求)
std::sort(v1.begin(), v1.end());
std::sort(v2.begin(), v2.end());
// 创建对应的set
std::set<int> s1(v1.begin(), v1.end());
std::set<int> s2(v2.begin(), v2.end());
std::cout << "性能比较 (数据量: " << size << ")" << std::endl;
// 测试并集性能
long long unionVectorTime = measureTime([&]() {
std::vector<int> result(v1.size() + v2.size());
auto it = std::set_union(v1.begin(), v1.end(), v2.begin(), v2.end(), result.begin());
result.resize(it - result.begin());
});
long long unionSetTime = measureTime([&]() {
std::set<int> result;
result.insert(s1.begin(), s1.end());
result.insert(s2.begin(), s2.end());
});
// 测试交集性能
long long intersectionVectorTime = measureTime([&]() {
std::vector<int> result(std::min(v1.size(), v2.size()));
auto it = std::set_intersection(v1.begin(), v1.end(), v2.begin(), v2.end(), result.begin());
result.resize(it - result.begin());
});
long long intersectionSetTime = measureTime([&]() {
std::set<int> result;
for (int val : s1) {
if (s2.count(val) > 0) {
result.insert(val);
}
}
});
// 测试差集性能
long long differenceVectorTime = measureTime([&]() {
std::vector<int> result(v1.size());
auto it = std::set_difference(v1.begin(), v1.end(), v2.begin(), v2.end(), result.begin());
result.resize(it - result.begin());
});
long long differenceSetTime = measureTime([&]() {
std::set<int> result;
for (int val : s1) {
if (s2.count(val) == 0) {
result.insert(val);
}
}
});
// 打印结果
std::cout << std::left << std::setw(20) << "操作"
<< std::setw(20) << "集合算法 (μs)"
<< std::setw(20) << "STL容器 (μs)"
<< std::setw(15) << "比率" << std::endl;
std::cout << std::string(75, '-') << std::endl;
auto printRow = [](const std::string& op, long long algoTime, long long containerTime) {
double ratio = static_cast<double>(containerTime) / algoTime;
std::cout << std::left << std::setw(20) << op
<< std::setw(20) << algoTime
<< std::setw(20) << containerTime
<< std::fixed << std::setprecision(2) << ratio << "x" << std::endl;
};
printRow("并集", unionVectorTime, unionSetTime);
printRow("交集", intersectionVectorTime, intersectionSetTime);
printRow("差集", differenceVectorTime, differenceSetTime);
}
int main() {
performanceComparison();
return 0;
}
这个示例比较了使用STL集合算法和标准容器操作的性能差异。通常,对于大量数据,使用排序好的向量和集合算法比直接在std::set
上操作更高效。
总结
在这两篇文章中,我们详细探讨了STL中的数值算法和集合算法,包括:
- 数值算法基础:
accumulate
、inner_product
、partial_sum
、adjacent_difference
和iota
。 - 高级数值应用:数值积分、数值微分和移动平均。
- 集合算法详解:
set_union
、set_intersection
、set_difference
、set_symmetric_difference
、includes
和merge
。 - 实际应用示例:用户兴趣分析和文本相似度计算。
- 最佳实践与性能考量。
这些算法为处理数值数据和有序集合提供了强大的工具,掌握它们可以帮助你编写更简洁、高效的代码,避免重新发明轮子。
在下一篇文章中,我们将探讨STL算法的实际应用案例,展示如何将这些算法组合起来解决复杂的实际问题。
参考资源
- C++ Reference - STL算法文档
- 《C++标准库》by Nicolai M. Josuttis
- 《Effective STL》by Scott Meyers
- 《C++ Cookbook》by D. Ryan Stephens, Christopher Diggins, et al.
- 《C++17 STL Cookbook》by Jacek Galowicz
这是我C++学习之旅系列的第二十八篇技术文章。查看完整系列目录了解更多内容。
如有任何问题或建议,欢迎在评论区留言交流!