如何使用float作为c#Dictionary中的键,自定义比较器舍入到最接近的.01?

时间:2018-02-07 02:25:00

标签: c# dictionary hashtable

我正在寻找一个IEqualityComparer类来存储和比较四舍五入到最接近的0.01的浮点键。特别是,我想确保正确实现GetHashCode方法。我想尽可能提高效率。我可以使用浮点值本身作为它自己的哈希吗?

我可以乘以100,转换为int并使用int作为键,但我很好奇是否可以使用浮动键来完成。

注意:我会将字典包装在一个类中,以确保只添加或比较四舍五入到.01的值。

跟进问题:如果我使用Decimal(保证总是四舍五入到.01)我可以在字典中使用Decimal的默认比较器和Decimal键吗?

我的第一个想法是试试这个实现。有任何陷阱吗?

class FloatEqualityComparer : IEqualityComparer<float>
{
    public bool Equals(float b1, float b2)
    {
        int i1 = (int)(b1 * 100);
        int i2 = (int)(b2 * 100);
        if(i1 == i2)
            return true;
        else
            return false;
    }

    public float GetHashCode(float x)
    {
        return x;
    }
}

3 个答案:

答案 0 :(得分:2)

问题是GetHashCode实施。如果两个浮动可能被认为是相等的,则它们必须产生相同的哈希码。

为什么不

sealed class FloatEqualityComparer : IEqualityComparer<float>
{
    public bool Equals(float x, float y) => Math.Round(x, 3) == Math.Round(y, 3);

    public int GetHashCode(float f) => Math.Round(f, 3).GetHashCode();
}

原因是如果两个哈希码不同,则不执行相等测试。这显着提高了性能,否则需要将每个值与每个其他值-> O(N 2 )进行比较。因此,如果两个值应相互比较以确保相等,则它们的哈希码必须发生冲突。

请注意,任何类型都可以用作IDictionary<TKey, TValue>

中的键

答案 1 :(得分:0)

浮点相等很麻烦。只是试图定义它的实际含义是混乱的。

首先让我们考虑一下四舍五入时会发生什么。

float x = 0.4999999;
float y = 0.5000000;
float z = 1.4999999;
Assert.Equals(false, Math.Round(x) == Math.Round(y));
Assert.Equals(true, Math.Round(y) == Math.Round(z));

如果您尝试模拟真实世界的过程,我希望 x 和 y 比 y 和 z 更相等。但是四舍五入迫使 y 和 z 进入同一个桶,而 x 进入不同的桶。

无论您选择什么比例的四舍五入,总会有任意靠近的数字被认为不同,而位于比例两端的数字被认为是相同的。如果您的数字是由某个任意过程生成的,您永远不知道应该被视为相等的两个数字是落在边界的同一侧还是相反侧。如果您选择四舍五入到最接近的 0.01,那么如果您只是将示例中的 x、y 和 z 乘以 0.01,则完全相同的示例也能工作。

假设您通过两个数字之间的距离来考虑相等性。

float x = 4.6;
float y = 5.0;
float z = 5.4;
Assert.Equals(true, Math.Abs(x - y) < 0.5);
Assert.Equals(true, Math.Abs(y - z) < 0.5);
Assert.Equals(false, Math.Abs(x - z) < 0.5);

现在,靠近在一起的数字总是被认为是相等的,但是您已经放弃了相等的传递属性。这意味着 x 和 y 被认为是相等的,y 和 z 被认为是相等的,但 x 和 z 被认为是不相等的。显然,如果没有传递等式,您就无法构建哈希集。

接下来要考虑的是,如果您在进行计算,则浮点数可能具有不同的精度,具体取决于它们的存储方式。由编译器决定它们的存储位置,它可以随时来回转换它们。计算将在寄存器中完成,当这些寄存器被复制到主内存时,以及当它们失去精度时,它会有所不同。这在代码中比较难演示,因为这真的取决于它的编译方式,所以让我们用一个假设的例子来说明。

float x = 4.49;
float y = Math.Round(x, 1); // equals 4.5
float z1 = Math.Round(x); // 4.49 rounds to 4
float z2 = Math.Round(y); // 4.5 rounds to 5
Assert.Equals(false, z1 == z2);

根据中间结果是否四舍五入,我在最终四舍五入时得到不同的结果。显然,寄存器 -> 内存不会四舍五入到 1 个十进制数字,但这说明了当您选择四舍五入时会影响结果的原则。如果您将 2 个应该相同的数字传递给相等函数,一个来自内存,另一个来自寄存器,您可能会得到以 2 种不同方式取整的结果。

编辑:要考虑的另一部分可能不会在这种情况下产生影响,即浮点数只有 24 位尾数。这意味着,一旦超过 2 的 24 次方,即 16,777,216,无论您认为四舍五入的精度如何,您认为不同的数字都会相等。

float x = 17000000;
float y = 17000001;
Assert.Equals(true, x == y);

因此,如果您对所有这些警告都满意,因为您想要的只是大部分时间都有效的东西,那么您可能可以尝试对浮点数进行散列。但是无论您如何尝试定义浮点相等,最终都会出现意外行为。

答案 2 :(得分:-1)

.NET文档中没有任何内容表明从Math.Round()返回的浮点值将通过相等比较,例如: 2.32应始终等于2.32,但如果任何一个值加上或减去float.Epsilon,则相等可能为false。这可能会为只有float.Epsilon移动的相同值创建2个键。我通过乘以和转换为int来处理舍入而不是调用Math.Round()来解决这个不太可能(虽然有问题)的问题。

sealed class FloatEqualityComparer : IEqualityComparer<float>
{
    int GetPreciseInt(float f)
    {
        int i1 = (int)(b1 * 100);
        int i2 = (int)(b2 * 100);
        return (i1 == i2);
    }

    public bool Equals(float f1, float f2) => GetPreciseInt(f1) == GetPreciseInt(f2);
    public int GetHashCode(float f) => GetPreciseInt(f).GetHashCode();
}

*我并不关心舍入有限精度浮点数的边缘情况,而是担心使用那些圆形不精确浮点数作为字典中的键。