繁体   English   中英

了解std :: accumulate

[英]Understanding std::accumulate

我想知道为什么需要std::accumulate (aka reduce)第3个参数。 对于那些不知道accumulate是什么的人,它的使用方式如下:

vector<int> V{1,2,3};  
int sum = accumulate(V.begin(), V.end(), 0);
// sum == 6

召集accumulate相当于:

sum = 0;  // 0 - value of 3rd param
for (auto x : V)  sum += x;

还有可选的第4个参数,允许用任何其他操作替换添加。

我听说过的理由是,如果你需要说不加,而是乘以向量的元素,我们需要其他(非零)初始值:

vector<int> V{1,2,3};
int product = accumulate(V.begin(), V.end(), 1, multiplies<int>());

但是为什么不像Python一样 - 为V.begin()设置初始值,并使用从V.begin()+1开始的范围。 像这样的东西:

int sum = accumulate(V.begin()+1, V.end(), V.begin());

这适用于任何操作。 为什么需要第三个参数?

你犯了一个错误的假设:类型TInputIterator类型相同。

但是std::accumulate是通用的,允许所有不同类型的创意累积和减少。

示例#1:在员工之间累积薪水

这是一个简单的例子:一个Employee类,包含许多数据字段。

class Employee {
/** All kinds of data: name, ID number, phone, email address... */
public:
 int monthlyPay() const;
};

你无法有意义地“积累”一组员工。 这是没有意义的; 这是未定义的。 但是,您可以定义有关员工的积累。 比方说,我们要总结全体员工所有的月薪。 std::accumulate可以做到这一点:

/** Simple class defining how to add a single Employee's
 *  monthly pay to our existing tally */
auto accumulate_func = [](int accumulator, const Employee& emp) {
   return accumulator + emp.monthlyPay();
 };

// And here's how you call the actual calculation:
int TotalMonthlyPayrollCost(const vector<Employee>& V)
{
 return std::accumulate(V.begin(), V.end(), 0, accumulate_func);
}

因此,在此示例中,我们在Employee对象的集合上累积了一个int值。 在这里,累加和是不一样类型的变量,我们实际上是求和。

示例#2:累积平均值

您也可以将accumulate用于更复杂的累积类型 - 可能希望将值附加到向量; 也许你有一些你在输入中跟踪的神秘统计数据; 等你积累了什么并不一定只是一个号码; 它可能是更复杂的东西。

例如,这是一个使用accumulate来计算整数向量平均值的简单示例:

// This time our accumulator isn't an int -- it's a structure that lets us
// accumulate an average.
struct average_accumulate_t
{
    int sum;
    size_t n;
    double GetAverage() const { return ((double)sum)/n; }
};

// Here's HOW we add a value to the average:
auto func_accumulate_average = 
    [](average_accumulate_t accAverage, int value) {
        return average_accumulate_t(
            {accAverage.sum+value, // value is added to the total sum
            accAverage.n+1});      // increment number of values seen
    };

double CalculateAverage(const vector<int>& V)
{
    average_accumulate_t res =
        std::accumulate(V.begin(), V.end(), average_accumulate_t({0,0}), func_accumulate_average)
    return res.GetAverage();
}

示例#3:累计运行平均值

您需要初始值的另一个原因是因为该值并非总是您正在进行的计算的默认/中性值。

让我们以我们已经看到的平均例子为基础。 但是现在,我们想要一个可以保持平均运行的类 - 也就是说,我们可以继续提供新的值,并检查到目前为止的多个调用的平均值。

class RunningAverage
{
    average_accumulate_t _avg;
public:
    RunningAverage():_avg({0,0}){} // initialize to empty average

    double AverageSoFar() const { return _avg.GetAverage(); }

    void AddValues(const vector<int>& v)
    {
        _avg = std::accumulate(v.begin(), v.end(), 
            _avg, // NOT the default initial {0,0}!
            func_accumulate_average);
    }

};

int main()
{
    RunningAverage r;
    r.AddValues(vector<int>({1,1,1}));
    std::cout << "Running Average: " << r.AverageSoFar() << std::endl; // 1.0
    r.AddValues(vector<int>({-1,-1,-1}));
    std::cout << "Running Average: " << r.AverageSoFar() << std::endl; // 0.0
}

这是我们完全依赖于能够为std::accumulate设置初始值的情况 - 我们需要能够从不同的起点初始化累积。


总之, std::accumulate适用于您在输入范围内迭代并在该范围内构建一个结果的任何时间。 但结果不需要与范围相同的类型,并且您不能对使用的初始值做任何假设 - 这就是为什么您必须有一个初始实例用作累积结果。

事情的方式,对于确保范围不是空的并且想要从范围的第一个元素开始累积的代码来说,这很烦人。 根据用于累积的操作,使用的“零”值并不总是很明显。

另一方面,如果您只提供需要非空范围的版本,那么对于不确定其范围不为空的呼叫者来说这很烦人。 给他们带来了额外的负担。

一个观点是两个世界中最好的当然是提供两种功能。 例如,Haskell提供foldl1foldr1 (需要非空列表)以及foldlfoldr (镜像std::transform )。

另一个观点是,因为一个可以用另一个平凡的变换来实现(如你所示: std::transform(std::next(b), e, *b, f) - std::next是C ++ 11,但重点仍然存在),最好尽可能减少界面,尽管没有真正的表达能力损失。

如果你想accumulate(V.begin()+1, V.end(), V.begin())你就可以写出来。 但是如果你认为v.begin()可能是v.end()(即v为空)怎么办? 如果没有实现v.begin() + 1会怎么样(因为v只实现++,而不是生成加法)? 如果累加器的类型不是元素的类型怎么办? 例如。

std::accumulate(v.begin(), v.end(), 0, [](long count, char c){
   return isalpha(c) ? count + 1 : count
});

因为标准库算法应该适用于(兼容的)迭代器的任意范围。 因此, accumulate的第一个参数不必是begin() ,它可以是begin()end()之前的任何迭代器。 它也可以使用反向迭代器。

整个想法是将算法与数据分离。 如果我理解正确,您的建议需要数据中的某种结构。

确实不需要它。 我们的代码库有2个和3个参数的重载,它们使用T{}值。

但是, std::accumulate很老了; 它来自原始的STL。 我们的代码库具有花哨的std::enable_if逻辑,用于区分“2迭代器和初始值”以及“2迭代器和缩减运算符”。 这需要C ++ 11。 我们的代码还使用尾随返回类型( auto accumulate(...) -> ... )来计算返回类型,另一个C ++ 11特性。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM