реализация структуры данных, подобной хэш-таблице, с ключами с плавающей запятой, где значения в пределах допуска объединяются - PullRequest
2 голосов
/ 08 ноября 2019

Мне нужна ассоциативная структура данных с ключами с плавающей запятой, в которой ключи с почти равными значениями объединяются вместе. Я работаю в C ++, но язык на самом деле не имеет значения.

В основном моя текущая стратегия состоит в том, чтобы

  1. обрабатывать только числа с плавающей запятой одинарной точности

  2. использовать unordered_map с пользовательским типом ключа

  3. определить хэш-функцию для типа ключа как

    a. для заданного числа с плавающей запятой v разделить v на некоторый допуск, например 0,0005, с двойной точностью, получив k.

    b. приведите k к 64-битному целому числу, получая ki

    c. return std :: hash of ki.

Прежде всего, существует ли стандартная именованная структура данных, которая делает что-то подобное? Если нет, то есть ли лучший способ сделать это, чем мой общий подход?

Главное, что мне не нравится в следующей реализации, это то, что мне не понятно, какие значения с плавающей запятой будут объединены вместе;Я справляюсь с этим, имея общее представление о том, какие значения в моих входных данных я хочу считать одним и тем же значением, и просто проверяю различные допуски, но было бы неплохо, если бы вы добавили 12.0453 в контейнер, тогда значения 12.0453 +/- 0.0005 были бысчитается равным, если параметр допуска равен 0,0005, но это не так - я даже не думаю, что такое поведение было бы возможно поверх unordered_map, потому что я думаю, что хеш-функция будет зависеть от значений в таблице.

По сути, моя реализация делит числовую линию на одномерную сетку, в которой каждая ячейка сетки имеет ширину в эпсилон-единицы, а затем присваивает значения с плавающей запятой нулевому индексу ячейки сетки, в которую они попадают. У меня вопрос, есть ли лучший способ реализовать ассоциативный контейнер значений с плавающей запятой с допуском, который также равен O (1)? и есть ли проблемы с реализацией ниже?

    template<typename V, int P=4>
    class float_map
    {
    private:
        struct key {
        public:
            long long val;

            static constexpr double epsilon(int digits_of_precision)
            {
                return (digits_of_precision == 1) ? 0.5 : 0.1 * epsilon(digits_of_precision - 1);
            }

            static constexpr double eps = epsilon(P);

            key(float fval) : val(static_cast<long long>( fval / eps))
            {}

            bool operator==(key k) const {
                return val == k.val;
            }
        };

        struct key_hash
        {
            std::size_t operator()(key k) const {
                return std::hash<long long>{}(k.val);
            }
        };

        std::unordered_map<key, V, key_hash> impl_;

    public:
        V& operator[](float f) {
            return impl_[key(f)];
        }

        const V& at(float f) const {
            return impl_.at(key(f));
        }

        bool contains(float f) const {
            return impl_.find(f) != impl_.end();
        }

        double epsilon() const {
            return key::eps;
        }
    };

    int main()
    {
        float_map<std::string> test;

        test[12.0453f] = "yes";

        std::cout << "epsilon = " << test.epsilon() << std::endl;                             // 0.0005

        std::cout << "12.0446f => " << (test.contains(12.0446f) ? "yes" : "no") << std::endl; // no
        std::cout << "12.0447f => " << (test.contains(12.0447f) ? "yes" : "no") << std::endl; // no
        std::cout << "12.0448f => " << (test.contains(12.0448f) ? "yes" : "no") << std::endl; // no
        std::cout << "12.0449f => " << (test.contains(12.0449f) ? "yes" : "no") << std::endl; // no
        std::cout << "12.0450f => " << (test.contains(12.0450f) ? "yes" : "no") << std::endl; // yes
        std::cout << "12.0451f => " << (test.contains(12.0451f) ? "yes" : "no") << std::endl; // yes
        std::cout << "12.0452f => " << (test.contains(12.0452f) ? "yes" : "no") << std::endl; // yes
        std::cout << "12.0453f => " << (test.contains(12.0453f) ? "yes" : "no") << std::endl; // yes
        std::cout << "12.0454f => " << (test.contains(12.0454f) ? "yes" : "no") << std::endl; // yes
        std::cout << "12.0455f => " << (test.contains(12.0455f) ? "yes" : "no") << std::endl; // yes
        std::cout << "12.0456f => " << (test.contains(12.0456f) ? "yes" : "no") << std::endl; // no
        std::cout << "12.0457f => " << (test.contains(12.0457f) ? "yes" : "no") << std::endl; // no
        std::cout << "12.0458f => " << (test.contains(12.0458f) ? "yes" : "no") << std::endl; // no
        std::cout << "12.0459f => " << (test.contains(12.0459f) ? "yes" : "no") << std::endl; // no
        std::cout << "12.0460f => " << (test.contains(12.0460f) ? "yes" : "no") << std::endl; // no

    }

Ответы [ 3 ]

1 голос
/ 08 ноября 2019

Лучший способ сделать это - использовать арифметику с фиксированной точкой.

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

В любом случае, более ясный способ думать об этой проблеме - не пытаться использовать понятие «допуск», а вместо этого использовать понятие «значимые цифры ". Относитесь только к n основанию из 10 цифр справа от десятичного знака как к значащему и параметризуйте это n. По сути, это приводит к использованию значений с фиксированной запятой в качестве ключей, а не значений с плавающей запятой;в приведенной выше реализации это похоже на использование эпсилона 0,0001 вместо 0,0005.

Но вместо того, чтобы просто модифицировать эпсилон в исходном коде, теперь нет причин не просто делать значения с фиксированной точкой общедоступным типом и использовать этот тип в качестве ключа unordered_map, предоставляемого пользователю. Ранее мы хотели скрыть тип ключа, обернув unordered_map реализации в пользовательскую структуру данных, потому что в этом случае ключи были непрозрачными, не имели интуитивного значения. Использование ключей с фиксированной запятой в обычном unordered_map дает дополнительное преимущество, заключающееся в том, что нам не нужно реализовывать методы-оболочки для всех стандартных вызовов std :: unordered_map, поскольку пользователю теперь предоставляется фактическое unordered_map.

код ниже:

template<int P=4>
class fixed_point_value
{
    static constexpr double calc_scaling_factor(int digits_of_precision)
    {
        return (digits_of_precision == 1) ? 10.0 : 10.0 * calc_scaling_factor(digits_of_precision - 1);
    }

    static constexpr double scaling_factor = calc_scaling_factor(P);

    template<int P>
    friend struct fixed_point_hash;

public:
    fixed_point_value(float val) : 
        impl_(static_cast<long long>(std::llround(scaling_factor * val)))
    {}

    bool operator==(fixed_point_value<P> fpv) const 
    {
        return impl_ == fpv.impl_;
    }

    float to_float() const
    {
        return static_cast<float>(impl_ / scaling_factor);
    }

private:
    long long impl_;
};

template<int P = 4>
struct fixed_point_hash
{
    std::size_t operator()(fixed_point_value<P> key) const {
        return std::hash<long long>{}(key.impl_);
    }
};

template<typename V, int P = 4>
using fixed_point_table = std::unordered_map<fixed_point_value<P>, V, fixed_point_hash<P>>;

int main()
{
    fixed_point_table<std::string, 4> t4;

    t4[12.0453f] = "yes";

    // these will all be "no" except 12.0453f because we have 4 base-10 digits of precision i.e.
    // 4 digits right of the decimal must be an exact match
    std::cout << "precision = 4" << std::endl;
    std::cout << "12.0446f => " << (t4.find(12.0446f) != t4.end() ? "yes" : "no") << std::endl;
    std::cout << "12.0447f => " << (t4.find(12.0447f) != t4.end() ? "yes" : "no") << std::endl;
    std::cout << "12.0448f => " << (t4.find(12.0448f) != t4.end() ? "yes" : "no") << std::endl;
    std::cout << "12.0449f => " << (t4.find(12.0449f) != t4.end() ? "yes" : "no") << std::endl;
    std::cout << "12.0450f => " << (t4.find(12.0450f) != t4.end() ? "yes" : "no") << std::endl;
    std::cout << "12.0451f => " << (t4.find(12.0451f) != t4.end() ? "yes" : "no") << std::endl;
    std::cout << "12.0452f => " << (t4.find(12.0452f) != t4.end() ? "yes" : "no") << std::endl;
    std::cout << "12.0453f => " << (t4.find(12.0453f) != t4.end() ? "yes" : "no") << std::endl;
    std::cout << "12.0454f => " << (t4.find(12.0454f) != t4.end() ? "yes" : "no") << std::endl;
    std::cout << "12.0455f => " << (t4.find(12.0455f) != t4.end() ? "yes" : "no") << std::endl;
    std::cout << "12.0456f => " << (t4.find(12.0456f) != t4.end() ? "yes" : "no") << std::endl;
    std::cout << "12.0457f => " << (t4.find(12.0457f) != t4.end() ? "yes" : "no") << std::endl;
    std::cout << "12.0458f => " << (t4.find(12.0458f) != t4.end() ? "yes" : "no") << std::endl;
    std::cout << "12.0459f => " << (t4.find(12.0459f) != t4.end() ? "yes" : "no") << std::endl;
    std::cout << "12.0460f => " << (t4.find(12.0460f) != t4.end() ? "yes" : "no") << std::endl;
    std::cout << "\n";

    fixed_point_table<std::string, 3> t3;
    t3[12.0453f] = "yes"; // 12.0453 will round to the fixed point value 12.045.
    std::cout << "precision = 4" << std::endl;
    std::cout << "12.0446f => " << (t3.find(12.0446f) != t3.end() ? "yes" : "no") << std::endl; // rounds to 12.0445 so yes;
    std::cout << "12.0447f => " << (t3.find(12.0447f) != t3.end() ? "yes" : "no") << std::endl; // rounds to 12.0445 so yes;
    std::cout << "12.0448f => " << (t3.find(12.0448f) != t3.end() ? "yes" : "no") << std::endl; // '
    std::cout << "12.0449f => " << (t3.find(12.0449f) != t3.end() ? "yes" : "no") << std::endl; // '
    std::cout << "12.0450f => " << (t3.find(12.0450f) != t3.end() ? "yes" : "no") << std::endl; // '
    std::cout << "12.0451f => " << (t3.find(12.0451f) != t3.end() ? "yes" : "no") << std::endl; // '
    std::cout << "12.0452f => " << (t3.find(12.0452f) != t3.end() ? "yes" : "no") << std::endl; // '
    std::cout << "12.0453f => " << (t3.find(12.0453f) != t3.end() ? "yes" : "no") << std::endl; // '
    std::cout << "12.0454f => " << (t3.find(12.0454f) != t3.end() ? "yes" : "no") << std::endl; // '
    std::cout << "12.0455f => " << (t3.find(12.0455f) != t3.end() ? "yes" : "no") << std::endl; // '
    std::cout << "12.0456f => " << (t3.find(12.0456f) != t3.end() ? "yes" : "no") << std::endl; // 12.0456f rounds to the 3 digits of precison fixed point value 12.0456 so no
    std::cout << "12.0457f => " << (t3.find(12.0457f) != t3.end() ? "yes" : "no") << std::endl; // '
    std::cout << "12.0458f => " << (t3.find(12.0458f) != t3.end() ? "yes" : "no") << std::endl; // '
    std::cout << "12.0459f => " << (t3.find(12.0459f) != t3.end() ? "yes" : "no") << std::endl; // '
    std::cout << "12.0460f => " << (t3.find(12.0460f) != t3.end() ? "yes" : "no") << std::endl; // '

}
1 голос
/ 08 ноября 2019

Хммм, может быть, вы могли бы использовать unordered_map с ключом целым числом и определить ключ с помощью чего-то вроде:

ключ = пол (значение / точность);

Это достаточно прозрачно,и ключ 0 будет содержать значения от 0,0 до 0,0005 (или независимо от вашей точности). Кроме того, отрицательные числа также будут работать логически в этом.

Если вы хотите сделать это с 2-мерными значениями, вы можете захотеть взглянуть на геохэш.

0 голосов
/ 13 ноября 2019

Простое объединение точек данных не может дать вам того, что вы хотите, потому что всегда будут точки, расположенные очень близко друг к другу по обе стороны от границы ячейки. Вам нужно использовать другой метод.

Например:

Допустим, вы делите свой домен на квадраты стороны epsilon. Затем вы можете построить std::map, который присваивает каждой точке данных квадрат;и учитывая произвольную точку P=(x,y), вы можете найти квадрат S(P), который содержит P. Теперь вам нужно посмотреть на все девять квадратов в сетке 3x3, содержащей S(P) в качестве центрального квадрата. Затем вы можете отсканировать эти девять блоков для поиска ближайшей точки данных к P.

Этот метод гарантированно найдет точку на расстоянии epsilon от (x,y), если она существует.

...