Ключи с плавающей точкой в ​​std: map - PullRequest
29 голосов
/ 13 июля 2011

Следующий код должен найти ключ 3.0 в std::map, который существует. Но из-за точности с плавающей точкой он не будет найден.

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<(...) для реализации этой функции?

Ответы [ 5 ]

26 голосов
/ 14 ноября 2012

Таким образом, есть несколько проблем с использованием парных символов в качестве ключей в std::map.

Во-первых, 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;
  }
};

, но это может быть чрезмерно параноидальным.Не повторяю, не включайте порог epsilon в свой оператор сравнения, который вы передаете std::set или тому подобное: это нарушит требования заказа контейнера и приведет к непредсказуемому неопределенному поведению.

(Я поставил NaN больше, чем все double с, включая +inf, в моем порядке, без уважительной причины. Менее чем все double с тоже подойдут).

Так что используйте либопо умолчанию operator<, или выше safe_double_less, или что-то подобное.

Далее, я бы посоветовал использовать std::multimap или std::multiset, потому что вы должны ожидать нескольких значений для каждого поиска.С тем же успехом вы можете сделать управление контентом повседневным делом, а не угловым делом, чтобы расширить охват тестирования вашего кода.(Я бы редко рекомендовал эти контейнеры) Плюс к этому блоки operator[], которые не рекомендуется использовать при использовании ключей с плавающей запятой.

Точка, в которой вы хотите использовать эпсилон, - это когда вы запрашиваетеконтейнер.Вместо использования прямого интерфейса создайте вспомогательную функцию, подобную этой:

// 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::map, так и на std::setmulti версиях).

(Inболее современная кодовая база, я ожидаю, что объект 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::set и std::map», потому что это немного хлопотно.

Если вы используете ключи с плавающей запятой для std::set или std::mapпочти наверняка никогда делать .find или []на них, так как это весьма вероятно, будет источником ошибок.Вы можете использовать его для автоматически отсортированного набора вещей, если точный порядок не имеет значения (то есть, что один конкретный 1.0 находится впереди или позади или точно на том же месте, что и другой 1.0).Даже тогда я бы использовал мультикарту / мультимножество, так как полагаться на коллизии или их отсутствие - не то, на что я бы рассчитывал.

Рассуждать о точном значении значений с плавающей запятой IEEE сложно, а хрупкостькода, на который он опирается, распространено.

4 голосов
/ 12 мая 2014

Вот упрощенный пример того, как использование soft-сравнить (или epsilon или почти равно) может привести к проблемам.

Пусть epsilon = 2 для простоты. Положите 1 и 4 в ваш map. Теперь это может выглядеть так:

1
 \
  4

Итак, 1 - корень дерева.

Теперь введите числа 2, 3, 4 в указанном порядке. Каждый заменит корень, потому что он сравнивается равным ему Итак, у вас есть

4
 \
  4

который уже сломан. (Предположим, что никакая попытка восстановить баланс дерева не сделана.) Мы можем продолжать идти с 5, 6, 7:

7
 \
  4

и это еще более неправильно, потому что теперь, если мы спросим, ​​есть ли там 4, он скажет «нет», а если мы попросим итератор для значений меньше 7, он не будет включать 4.

Хотя я должен сказать, что я использовал map s на основе этого некорректного оператора нечеткого сравнения много раз в прошлом, и всякий раз, когда я обнаруживал ошибку, это никогда не происходило из-за этого. Это связано с тем, что наборы данных в моих областях применения никогда не сводятся к стресс-тестированию этой проблемы.

4 голосов
/ 13 июля 2011

Как говорит Настя , вы можете реализовать свою собственную функцию сравнения. То, что он пропускает, является ключом к тому, чтобы заставить его работать - вы должны убедиться, что функция всегда возвращает false для любых значений, которые находятся в пределах вашего допуска к эквивалентности.

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

Редактировать: как указывалось во многих комментариях к этому и другим ответам, существует вероятность того, что это плохо сработает, если значения, которые вы вводите, распределены произвольно, потому что вы не можете гарантировать, что !(a<b) и !(b<c) результаты в !(a<c). Это не будет проблемой в заданном вопросе , потому что рассматриваемые числа сгруппированы примерно в 0,1 приращения; если ваш эпсилон достаточно велик, чтобы учитывать все возможных ошибок округления, но меньше 0,05, он будет надежным. Жизненно важно, чтобы ключи на карте никогда не были ближе, чем 2 * эпсилон друг от друга.

4 голосов
/ 13 июля 2011

Вы можете реализовать собственную функцию сравнения.

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

Обновлено: см. Элемент 40 в Действующем STL !Обновлено на основе предложений.

0 голосов
/ 13 июля 2011

Использование пар в качестве ключей не полезно.Как только вы сделаете какую-либо арифметику с ключами, вы не будете уверены, какие именно значения они имеют, и, следовательно, не сможете использовать их для индексации карты.Единственное разумное использование - постоянные ключи.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...