【问题标题】:Efficient way to calculate average value over disjoint subranges of STL map计算 STL 映射不相交子范围平均值的有效方法
【发布时间】:2010-11-03 17:46:29
【问题描述】:

我正在将算法从 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 知识是有限的,所以我很难选择一个解决方案,特别是因为有很多不同的意见。如果有人可以通过比较此处发布的解决方案来帮助我做出决定,那就太好了。为您提供更多背景信息:

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

【问题讨论】:

  • 您的哪些部分有问题?
  • @gregg 我认为答案应该是使用来自 STL 的 。
  • 使用地图计算两个平均值。我可以遍历所有值并计算平均值,但我真的怀疑这是最好的解决方案。
  • 这里有一个链接,你可以在这里提高你的C++/STL知识cplusplus.com/reference/stl/map

标签: c++ stl map average


【解决方案1】:

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

【讨论】:

  • 如果只选择索引在特定范围内的项目,这将如何工作?
  • 使用std::map::lower_bound 获取您关心的值的迭代器,然后将这些迭代器传递给std::accumulate。对于索引小于x的值:std::accumulate(m.begin(),m.lower_bound(x)),其中m是映射,对于索引大于或等于x的值:std::accumulate(m.lower_bound(x),m.end())。
  • 如果要将小于更改为小于或等于,或者大于或等于严格大于,请使用upper_bound。另外,我认为有一个必需的init 参数我忘了传递给accumulate,它应该是0。
  • Accumulate 不提供先转换值的选项。您可以使用自定义函数,但您仍然需要处理 std::pair 的集合。 boost::transform_iterator 或类似的东西首先需要从迭代地图时获得的对中提取第二个元素。
【解决方案2】:

编辑:一次性地图累加器 - 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;
};

旧版本:

这对我有用。在从map::upper_bound 检索的范围上使用accumulate 是有问题的,因为许多STL 操作要求最终迭代器可以从范围内的第一个开始访问。这里有点作弊 - 假设 map 值 >= 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;
};

【讨论】:

  • 我会尝试您的新解决方案并发布结果。
  • @xsl - 很好,我认为这将是最快的(而且肯定是最少的内存),但请告诉我们。也可以使boundary 成为静态并节省空间而没有任何副作用。
  • @xsl - 是的,static in boundary 在这里工作。更新代码。
  • 我需要上下边界的平均值。例如。所有的键值 750。我正在更新你的代码以获取这些值,但如果你愿意,你也可以编辑你的帖子,这样我就可以确保我做的一切都是正确的。
  • 我明白了。这不是一个艰难的改变。我认为这意味着您想将键划分为 X。
【解决方案3】:
  • 您使用 std::lower_bound 和 std::upper_bound 找到您的范围,不同之处在于,lower_bound 包含您的值,因此将给出第一个迭代器 >= 您的值,而 upper_bound 将给出第一个迭代器 >你的价值。如果您的值不在地图中,它们将返回相同的迭代器。

  • 您可以使用累积,但不能只将 std::pairs 添加在一起,因此您需要在此处使用自定义函子,或者使用 boost::transform_iterator,或者在找到边界后循环。循环并不像某些人所说的那么邪恶(累积实际上是最可怕的算法之一)。

【讨论】:

  • 积累有什么可怕的?
  • 感谢您的回答。如果我理解正确,您建议使用 std::lower_bound 和 std::upper_bound 查找范围并循环查找平均值。我不明白关于积累是可怕的部分。 STL 实现是可怕的还是使用自定义函子不好?
  • @xsl - accumulate 不适用于没有自定义函子来执行累积的 map,因为 std::pair(map 元素)没有默认的 operator+。因为你有两个范围要累积,所以我找不到一个很好的方法来完成单次通过。也许提供一个有状态的仿函数,它根据给定的映射键在两个地方累积,即。 pair&lt;int,int&gt;.first。我通过将您的地图分成两个vectors 来做到这一点,然后简单地使用accumulate。
  • @Steve_M accumulate 使用 operator+ 和 x=x+y 构造,因此如果您使用自定义对象,它将在每次迭代时复制该对象。您提供一个初始对象,结果是您的对象处于完成状态。您可以放入一个自定义运算符,通过引用获取您的对象并欺骗您的模板使用引用,但您的代码可能看起来很模糊。
【解决方案4】:

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&lt;...&gt;::const_reverse_iterator)、map.rend() 和&gt;。

edit:STL 算法可能会使代码更短(即在哪里使用)。比如计算value以下key的平均值。

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);

【讨论】:

  • 感谢您的回答。我的解决方案与您发布的第一个代码段非常相似。使用 STL 的优缺点是什么?
  • 您使用的是 STL,如果您使用的是地图(即 std::map)。 STL 算法有时可能会更清楚代码在做什么,但在这种情况下差别不大(for 循环版本可能会快一点)
  • 感谢您的快速回复。所以基本上我可以选择循环遍历地图两次,这更有效或使用上限和下限,自定义函数和累积速度较慢,但​​会使代码更短。我没听错吗?
  • 请注意,for 循环不会遍历整个映射 - 仅通过小于(或大于)value 的键(查看条件)。但除此之外,是的,这里就是这种情况。
  • 显然这个答案被否决了。如果有人能告诉我原因,我会很高兴。
【解决方案5】:

如果谓词是地图的比较函数,您最好使用std::map&lt;&gt;::lower_bound() 和std::map&lt;&gt;::upper_bound()。获取指向相关边界的迭代器,并将其与来自&lt;numeric&gt; 的std::accumulate() 一起使用。因为您使用的是关联容器,所以在取平均值时需要进行调整,以便使用 second 值而不是 std::pair&lt;&gt;。

如果您的谓词可能会更改为其他内容,那么您可以使用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;

您将需要&lt;vector&gt;、&lt;algorithm&gt;、&lt;numeric&gt;、&lt;iterator&gt; 和 &lt;functional&gt; 标头。

【讨论】:

  • @Steve Townsend:这是您推荐的解决方案吗?
  • @xsl - 如果空间非常宝贵,我会研究使用自定义函子来进行单次累积(您必须计算 elts 并将它们相加,所以总共有三个或四个状态变量) - 换句话说,避免使用 temp vectors。否则,这对我来说是有意义的,不应该表现得很糟糕。您是否对这里的其他选项感到满意?
  • @xsl 我建议使用std::map&lt;&gt;::upper_bound() 和std::map&lt;&gt;::lower_bound(),因为这意味着您第一次遍历字典时,您只能按照2*log n 元素的顺序遍历。这也意味着谓词必须是映射比较器的绑定。但是,如果您发现需要更改谓词,则对映射进行分区允许任何谓词。那么第一次遍历map是按照n运行时间的顺序。
  • @Steve Townsend:我还没有真正决定。整个迭代器主题对我来说是新的,所以我很难理解所有的答案。 eq-的答案非常简单,也是我唯一完全理解的答案。您和 wilhelmtell 似乎对该主题了解很多,而且你们俩可能还测试了您提交的代码,这很棒。所以基本上我在你的解决方案,wilhelmtell's 和 eq-'s 之间做出决定。
  • 修复了一个愚蠢的错误:划分分区的大小,而不是整个容器的大小。
【解决方案6】:

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

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

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 找到第一部分的结束迭代器没有任何意义。无论如何,您都会在地图上走来走去,所以您不妨边走边检查。映射迭代不是免费的,并且可能会在内存中跳跃一下——与此相比,每次迭代的额外比较应该不明显。

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

【讨论】:

  • 调试构建的两个明显变化,如果它太慢的话: 1. 在第二个循环中使用for 循环(因为你知道还有多少项目)并避免调用@ 987654324@。 2. 对于第一个循环,在查看之前获取指向*it 的指针并避免(实际上)调用std::map&lt;int,int&gt;::const_iterator::operator-&gt;。
【解决方案7】:

好的,对于那些喜欢使用累积来减轻痛苦的人来说,这是我的大纲。让我们创建一个名为 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;
  }
};

以上内容适用于任何映射类型,无论键类型如何,以及任何自动转换为双精度的值类型,即使它实际上不是双精度。

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

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

统计数据将准备好进行分析。请注意,您可以自定义统计信息以供以后在其构造函数中使用,因此如果您不希望它计算偏度和峰度,您可以设置标志以不计算立方/四次方(甚至不计算平方,如果您不关心方差)。

【讨论】:

    【解决方案8】:

    大概:

    • 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; } 
     }
    

    您可以通过累加来一次性收集计数和总和。


    [编辑]根据评论,这是优化的基本原理:

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

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

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

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

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

    【讨论】:

    • 针对循环进行基准测试,看看它是否“优化”。我曾经有一个非常相似的问题,也写了一个累加器。但我也存储了值的平方,所以如果我愿意,我也可以找到方差/标准差。嘿,为什么不也存储立方体和 4 次方,我们可以计算偏度和峰度。
    • 不。第一个测试是简单的实现是否足够快。除非你相信你的编译器会折叠这两个循环(我不相信),或者期待一场重大的硬件革命,否则这只是 N 和你的客户缓存大小的问题。
    • 足够快可能已经足够好了,但是当您在一段代码之前特别写下注释“For optimization:”时,我想知道您为什么认为那段代码用于优化目的。顺便说一句,我曾经实现自己的算法,并实现了一个名为 accum22 的算法,它使用 += 或自定义仿函数/函数,它接受一个左值和一个右值并修改左值。计算平均值需要您存储 2 个数字,即总和和计数。您的课程也不是 const 正确的。
    猜你喜欢
    • 2020-01-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-02-14
    • 1970-01-01
    • 2018-02-28
    相关资源
    最近更新 更多