在STL图的不相交子范围上计算平均值的有效方法

时间:2010-11-03 17:46:29

标签: c++ stl map average

我正在将算法从C#转换为C ++。该算法的一小部分是计算字典中某些区域的平均值。

字典中的数据按以下方式存储:

Index     Value
1         10
3         28
290       78
1110      90

我需要计算索引小于某个数字且所有索引值都大于某个数字的所有值的平均值。在C#中,我按照以下方式进行:

if (dictionary.Where(x => x.Key < areaWidth).Count() > 0)
{
    avgValue = (int) dictionary.Where(x => x.Key < areaWidth).Average(
        x => x.Value);
}

for (var i = 0; i < line.Length; i++)
{
    if (i == areaWidth)
    {
        avgValue = -1;
        i = line.Length - areaWidth;
        var rightBorder = i - areaWidth;

        if (dictionary.Where(x => x.Key > (rightBorder)).Count() > 0)
        {
            avgValue = (int) dictionary.Where(
                x => x.Key > (rightBorder)).Average(
                                x => x.Value);
        }
    }

    if (line[i] < avgValue * 0.8)
    {
        reallyImportantValue += (avgValue - line[i]);
    }
}

我知道这不是非常有效且非常糟糕的代码,但我知道无论如何我必须完全重写C ++中的这部分算法,所以我决定快速而肮脏地实现它。

无论如何,我现在将其移植到C ++,因为它将在移动平台上运行,性能非常重要。凭借我有限的C ++ / STL知识,我很可能完成工作,但结果可能比C#代码更糟糕。

因此,如果您知道在C ++中完成此任务的有效方法,请告诉我。


编辑:谢谢你的所有答案。正如我在帖子中提到的,我的STL知识是有限的,所以我很难选择一个解决方案,特别是因为有很多不同的意见。如果有人能够通过比较这里发布的解决方案来帮助我做出决定,那将是很棒的。为您提供更多背景信息:

该函数将在地图中使用1000个值调用大约500次。最重要的方面是稳定性,性能是第二重要的。

8 个答案:

答案 0 :(得分:3)

您可以使用std::accumulate计算值的总和,然后除以元素的数量。以下是如何使用STL计算均值和其他统计数据的examples

答案 1 :(得分:3)

编辑:一遍地图累加器 - result2包含您需要的信息:

#include <map>
#include <algorithm>
#include <numeric>

typedef map<const unsigned int, unsigned int> Values;

struct averageMap
{
    averageMap() : lowerCount(0), lowerSum(0), upperSum(0) {}
    averageMap operator()(const averageMap& input, 
           const Values::value_type& current)
    {
        if (current.first > boundary)
        {
            upperSum += current.second;
        }
        else
        {
            lowerSum += current.second;
            ++lowerCount;
        }
        return *this;
    }

    static size_t boundary;
    size_t lowerCount;
    unsigned int lowerSum;
    unsigned int upperSum;
};

size_t averageMap::boundary(0);

struct averageRange
{
    averageRange() : count(0), sum(0) {}
    averageRange operator()(const averageRange& input, 
        const Values::value_type& current)
    {
        sum += current.second;
        ++count;

        return *this;
    }

    size_t count;
    unsigned int sum;
};


int main()
{
    Values values;

    values[1] = 10;
    values[3] = 28;
    values[290] = 78;
    values[1110] = 110;

    averageMap::boundary = 100;
    averageMap result = accumulate(values.begin(), values.end(), 
        averageMap(boundary), averageMap(boundary));

averageRange result2 = accumulate(values.lower_bound(2), values.upper_bound(300), 
    averageRange(), averageRange());

    return 0;
};

OLD VERSION:

这对我有用。在accumulate检索的范围上使用map::upper_bound是有问题的,因为许多STL操作要求最终的迭代器可以从第一个范围内到达。这里有一点作弊 - 假设map值为&gt; = 0。

#include <map>
#include <algorithm>
#include <numeric>
#include <vector>

using namespace std;

typedef map<unsigned int, unsigned int> Values;

int main()
{
    Values values;

    values[1] = 10;
    values[3] = 28;
    values[290] = 78;
    values[1110] = 110;

    size_t boundary(100);
    Values::iterator iter = values.upper_bound(boundary);

    vector<int> lowerRange(values.size(), -1);

    transform(values.begin(), iter, lowerRange.begin(), 
        [](std::pair<unsigned int, unsigned int> p) 
                -> int { return p.second; });

    vector<int>::iterator invalid(find(lowerRange.begin(), 
        lowerRange.end(), -1));
    size_t lowerCount(distance(lowerRange.begin(), invalid));
    lowerRange.resize(lowerCount);

    vector<int> upperRange(values.size() - lowerCount);
    transform(iter, values.end(), upperRange.begin(), 
        [](std::pair<unsigned int, unsigned int> p) 
                -> int { return p.second; });

    size_t lowerAverage = accumulate(lowerRange.begin(), 
        lowerRange.end(), 0) / lowerRange.size();
    size_t upperAverage = accumulate(upperRange.begin(), 
        upperRange.end(), 0) / upperRange.size();

    return 0;
};

答案 2 :(得分:2)

  • 使用std :: lower_bound和std :: upper_bound找到你的范围,不同之处在于lower_bound包含你的值,因此会给第一个迭代器&gt; =你的值,而upper_bound会给你第一个迭代器&GT;你的价值。如果您的值不在地图中,则它们将返回相同的迭代器。

  • 你可以使用累积,但你不能只是将std :: pair添加到一起,所以你需要一个自定义函子,或者使用boost :: transform_iterator,或者只要找到你的边界就可以循环。循环并不像有些人那样邪恶(并且积累实际上是最可怕的算法之一)。

答案 3 :(得分:1)

std :: map中的键值对按键排序 - 即使使用for循环,也可以很容易地将键指向的值加到小于或大于某个值的值(如果您不想使用或学习使用STL)算法)。对于低于某些value的密钥:

std::map<int, int> map;
map[...] = ...;

int count = 0, sum = 0;
for (std::map<int, int>::const_iterator it = map.begin();
     it != map.end() && it->first < value; ++it, ++count)
{
    sum += it->second;
}
// check for count == 0
int avg = sum / count; // do note integer division, change if appropriate

对于大于值的键的平均值,请使用map.rbegin()(类型为std::map<...>::const_reverse_iterator),map.rend()>

编辑:STL算法可能会使代码更短(使用它的地方)。例如,要计算低于value的键的平均值。

int ipsum(int p1, const std::pair<int, int>& p2) {
    return p1 + p2.second;
}

...

std::map<int, int> map;
int sum = std::accumulate(map.begin(), map.lower_bound(value), 0, ipsum);

答案 4 :(得分:1)

如果谓词是地图的比较函数,那么最好使用std::map<>::lower_bound()std::map<>::upper_bound()。获取指向相关边界的迭代器,并将其与std::accumulate()中的<numeric>一起使用。由于您正在使用关联容器,因此您需要在获取平均值时进行调整,以便使用second值而不是std::pair<>

如果您的谓词可能会更改为其他内容,则可以使用std::partition()

// tmp container: should be fast with std::distance()
typedef std::vector<int> seq;

seq tmp(dict.size());
seq::iterator end(std::partition(dict.begin(), dict.end(),
                                 tmp.begin(),
                                 std::bind2nd(std::tmp(), UPPER_BOUND)));

// std::vector works well with std::distance()
seq::difference_type new_count = std::distance(tmp.begin(), end);
double lower_avg = std::accumulate(tmp.begin(), end, 0.0) / new_count;
seq::difference_type new_count = std::distance(end, tmp.end());
double higher_avg = std::accumulate(tmp.begin(), end, 0.0) / new_count;

此处您需要<vector><algorithm><numeric><iterator><functional>标题。

答案 5 :(得分:1)

假设您正在使用地图,最简单的解决方案是利用密钥的排序特性,就像其他人一样。遍历列表的第一部分,更新累加器和计数。然后遍历列表的第二部分,做同样的事情。两个循环,一个接一个,你可以从第一个部分的长度推断出第二个部分的长度。

非常简单的代码,乍一看应该很清楚,并且不会创建临时容器。出于这些原因,我个人更喜欢这种方法。事实上,如果我自己使用这种数据结构,这就是我写的代码。

int key = <whatever>;

std::map<int, int>::const_iterator it = map.begin(), end = map.end();

size_t num1 = 0;
long total1 = 0;

while (it != end && it->first < key) {
    total1 += it->second;
    ++num1;
    ++it;
}

size_t num2 = map.size() - num1;
long total2 = 0;

while (it != end) {
    total2 += it->second;
    ++it;
}

int avg_less = num1 > 0 ? total1 / num1 : 0;
int avg_greater_equal = num2 > 0 ? total2 / num2 : 0;

在启动之前,我没有看到使用std::lower_bound查找第一部分的结束迭代器。无论如何,你都会在地图上走路,所以你也可以随时查看。地图迭代不是免费的,并且可能会在内存中跳过一点 - 与此相比,每次迭代的额外比较不应该是显而易见的。

(当然,我不得不说你应该测量一下,如果你想确定,因为你应该。这只是我对有关优化构建行为的有根据的猜测。)

答案 6 :(得分:1)

好的,这是我对那些喜欢使用积累使其稍微减轻痛苦的人的大纲。让我们创建一个名为StatsCollector的类。我不在乎它里面有什么,除非我们假设这是一个你将在你的代码的不同地方使用的类,它收集数字集合并给你信息。让我们松散地定义它。我将假设它将双精度值作为其值,但您可以将其模板化为value_type。

class StatsCollector
{
public:
   StatsCollector();

   void add(double val);

 // some stats you might want
   size_t count() const;
   double mean() const;
   double variance() const;
   double skewness() const;
   double kurtosis() const;
};

上面的目的是根据传入的数据计算统计矩。它是一个有用的类,不仅仅是为了避免使用循环而适合算法的黑客,希望你可以在很多地方使用它在你的代码中。

现在我将为我们的特定循环编写一个自定义仿函数(您可以使用一个函数)。我会指出上面的一个指针。 (引用的问题是std :: accumulate分配给它,所以它将复制不是我们想要的对象。它实际上将是一个自我赋值,但是自我指定我们的指针几乎是不-OP)

struct AddPairToStats
{
  template< typename T >
  StatsCollector * operator()( StatsCollector * stats, const T& value_type ) const
  { 
     stats->add( value_type.second );
     return stats;
  }
};

上述内容适用于任何地图类型,无论密钥类型如何,以及任何自动转换为double的值类型,即使它实际上不是double。

现在假设我们的地图中有迭代器范围,我们可以像这样使用累积:

StatsCollector stats;
std::accumuluate( iterStart, iterEnd, &stats, AddPairToStats() );

统计数据将随时可供分析。请注意,您可以自定义统计信息以供以后在其构造函数中使用,因此,如果您不希望它计算偏斜度和峰度,则可以设置标记以不计算立方体/第4次幂(如果不这样做,甚至不计算正方形)关心差异)。

答案 7 :(得分:0)

大致是:

  • map::upper_bound / lower_bound获取索引范围的迭代器
  • accumulate计算范围内的总和(简单),count计算元素

两次运行范围(不能很好地扩展)。为了优化:

 struct RunningAverage
 {
     double sum;
     int count;
     RunningAverage() { sum = 0; count = 0; }
     RunningAverage & operator+=(double value) 
     { sum += value; ++count; }

     RunningAverage operator+(double value) 
     { RunningAverage result = *this; result += value; return result; }

     double Avg() { return sum / count; } 
 }

你可以传递累积来一次性收集计数和总和。


[edit] 根据评论,以下是优化的基本原理:

  • O(N)算法,没有给出N
  • 的限制
  • 原语操作(节点遍历和添加)
  • 随机访问模式是可能的

在这些情况下,内存访问不再保证是缓存支持,因此与每元素操作(甚至超过该元素)相比,成本可能会变得很大。迭代两次会使内存访问的成本增加一倍。

本讨论中的“变量”仅取决于数据集和客户端计算机配置,而不是算法。

我更喜欢这种解决方案而不是自定义“累积”,因为扩展或修改其他操作很简单,而“累积”细节仍然是孤立的。它也可以与假设的accumulate_p方法一起使用,该方法可以并行访问(您还需要struct + struct运算符,但这很简单)。

哦,const正确性留给读者练习:)