map中的浮点键

Floating point keys in std:map

本文关键字:map      更新时间:2023-10-16

下面的代码应该在存在的std::map中找到键3.0。但是由于浮点精度,它将无法被找到。

map<double, double> mymap;
mymap[3.0] = 1.0;
double t = 0.0;
for(int i = 0; i < 31; i++)
{
  t += 0.1;
  bool contains = (mymap.count(t) > 0);
}

在上面的例子中,contains总是false。我目前的解决方法是将t乘以0.1,而不是添加0.1,如下所示:

for(int i = 0; i < 31; i++)
{
  t = 0.1 * i;
  bool contains = (mymap.count(t) > 0);
}

现在的问题是:

是否有一种方法来引入一个模糊比较std::map,如果我使用double键?浮点数比较的常见解决方案通常是a-b < epsilon。但我没有看到一个简单的方法来做到这一点与std::map。我真的必须在类中封装double类型并覆盖operator<(...)来实现此功能吗?

所以在std::map中使用double作为键存在一些问题。

首先,NaN的比较值小于其本身,这是一个问题。如果有可能插入NaN,使用:

struct safe_double_less {
  bool operator()(double left, double right) const {
    bool leftNaN = std::isnan(left);
    bool rightNaN = std::isnan(right);
    if (leftNaN != rightNaN)
      return leftNaN<rightNaN;
    return left<right;
  }
};

但这可能过于偏执了。不要,我重复一遍,不要在传递给std::set或类似的比较运算符中包含epsilon阈值:这将违反容器的排序要求,并导致不可预测的未定义行为。

(在我的排序中,我把NaN放在比所有double s(包括+inf)都大的位置,没有什么好的理由。小于所有double s也可以)。

所以要么使用默认的operator<,或者上面的safe_double_less,或者类似的东西。

接下来,我建议使用std::multimapstd::multiset,因为您应该期望每次查找有多个值。您不妨让内容管理成为日常事务,而不是一个角落案例,以增加代码的测试覆盖率。(我很少推荐这些容器)加上这个阻塞operator[],当你使用浮点键时不建议使用。

需要使用epsilon的地方是在查询容器时。与其使用直接接口,不如创建一个这样的辅助函数:

// works on both `const` and non-`const` associative containers:
template<class Container>
auto my_equal_range( Container&& container, double target, double epsilon = 0.00001 )
-> decltype( container.equal_range(target) )
{
  auto lower = container.lower_bound( target-epsilon );
  auto upper = container.upper_bound( target+epsilon );
  return std::make_pair(lower, upper);
}

适用于std::mapstd::set(和multi版本)。

(在更现代的代码库中,我希望range<?>对象从equal_range函数返回是更好的事情。但是现在,我将使它与equal_range兼容。

这将找到一个键值"足够接近"您所请求的对象的范围,而容器内部保持其排序保证,并且不会执行未定义行为。

要测试是否存在一个键,可以这样做:

template<typename Container>
bool key_exists( Container const& container, double target, double epsilon = 0.00001 ) {
  auto range = my_equal_range(container, target, epsilon);
  return range.first != range.second;
}

,如果你想删除/替换条目,你应该处理可能有多个条目被击中的可能性。

简短的回答是"不要使用浮点值作为std::setstd::map的键",因为这有点麻烦。

如果您确实对std::setstd::map使用浮点键,几乎可以肯定的是,永远不要对它们执行.find[],因为这极有可能成为bug的来源。您可以将其用于自动排序的内容集合,只要精确的顺序无关紧要(例如,一个特定的1.0在前面或后面,或者与另一个1.0完全在同一位置)。即便如此,我还是会选择multimap/multiset,因为依赖碰撞或缺乏碰撞并不是我所依赖的东西。

关于IEEE浮点值的确切值的推理是困难的,并且依赖于它的代码的脆弱性是常见的。

下面是一个使用软比较(又名epsilon或几乎相等)如何导致问题的简化示例。

为简单起见,设为epsilon = 2。将14放入map中。现在可能看起来像这样:

1
 
  4

所以1是树的根。

现在按顺序输入数字2, 3, 4。每个都将替换根,因为它比较等于它。然后是

4
 
  4

已经坏了。(假设没有尝试重新平衡树。)我们可以继续用5, 6, 7:

7
 
  4

,这就更糟糕了,因为现在如果我们问4是否在那里,它会说"否",如果我们要求一个小于7的迭代器,它不会包括4

虽然我必须说我在过去多次使用基于这个有缺陷的模糊比较运算符的map,每当我发现一个错误时,它从来都不是由于这个。这是因为在我的应用领域的数据集从来没有真正达到压力测试这个问题。

正如Naszta所说,您可以实现自己的比较函数。他漏掉的是让它工作的关键——你必须确保函数总是对于任何在你的等价容错范围内的值返回false

return (abs(left - right) > epsilon) && (left < right);

编辑:正如对这个答案和其他答案的许多评论所指出的那样,如果您提供的值是任意分布的,则有可能出现这种情况,因为您不能保证!(a<b)!(b<c)会导致!(a<c)。这个在问题中不会有问题,因为所讨论的数字以0.1的增量聚集;只要你的epsilon足够大,足以解释所有可能的舍入误差,但小于0.05,它就是可靠的。至关重要的是,映射的键之间的距离不能小于2*。

您可以实现自己的比较函数

#include <functional>
class own_double_less : public std::binary_function<double,double,bool>
{
public:
  own_double_less( double arg_ = 1e-7 ) : epsilon(arg_) {}
  bool operator()( const double &left, const double &right  ) const
  {
    // you can choose other way to make decision
    // (The original version is: return left < right;) 
    return (abs(left - right) > epsilon) && (left < right);
  }
  double epsilon;
};
// your map:
map<double,double,own_double_less> mymap;

更新:见Effective STL中的第40项!根据建议更新。

使用double作为键是没有用的。一旦对键进行了任何运算,您就无法确定它们的确切值,因此无法使用它们对映射进行索引。唯一合理的用法是键保持不变。

相关文章:
  • 没有找到相关文章