如何在Rust中使用带有f64的HashMap作为键?

pix*_*nil 8 floating-point hashmap rust

我想使用a HashMap<f64, f64>,用于将已知x和y的点的距离保存到另一个点.f64因为价值在这里无关紧要,重点应放在关键上.

let mut map = HashMap<f64, f64>::new();
map.insert(0.4, f64::hypot(4.2, 50.0));
map.insert(1.8, f64::hypot(2.6, 50.0));
...
let a = map.get(&0.4).unwrap();
Run Code Online (Sandbox Code Playgroud)

由于f64既不是Eq也不是Hash,只是PartialEq,f64不足以作为重点.我需要先保存距离,但也要稍后通过y访问距离.y的类型需要是浮点精度,但如果不起作用f64,我将使用i64带有已知指数的.

我通过使用自己的方法尝试了一些hacks struct Dimension(f64),然后Hash通过将float转换为a String然后对其进行哈希来实现.

#[derive(PartialEq, Eq)]
struct DimensionKey(f64);

impl Hash for DimensionKey {
    fn hash<H: Hasher>(&self, state: &mut H) {
        format!("{}", self.0).hash(state);
    }
}
Run Code Online (Sandbox Code Playgroud)

这似乎非常糟糕,两个解决方案,我自己的struct或float作为带有base和exponent的整数似乎对于一个键来说非常复杂.

更新:我可以保证我的密钥永远不会是NaN,或者是无限的价值.此外,我不会计算我的密钥,只是迭代它们并使用它们.因此,已知错误应该没有错误0.1 + 0.2 ? 0.3. 如何在Vec of Floats上进行二进制搜索?并且这个问题的共同点是实现浮点数的总排序和相等,不同之处仅在于散列或迭代.

lje*_*drz 7

您可以将 拆分f64为整数部分和小数部分,并按以下方式将它们存储在结构中:

#[derive(Hash, Eq, PartialEq)]
struct Distance {
    integral: u64,
    fractional: u64
}
Run Code Online (Sandbox Code Playgroud)

剩下的很简单:

use std::collections::HashMap;

#[derive(Hash, Eq, PartialEq)]
struct Distance {
    integral: u64,
    fractional: u64
}

impl Distance {
    fn new(i: u64, f: u64) -> Distance {
        Distance {
            integral: i,
            fractional: f
        }
    }
}

fn main() {
    let mut map: HashMap<Distance, f64> = HashMap::new();

    map.insert(Distance::new(0, 4), f64::hypot(4.2, 50.0));
    map.insert(Distance::new(1, 8), f64::hypot(2.6, 50.0));

    assert_eq!(map.get(&Distance::new(0, 4)), Some(&f64::hypot(4.2, 50.0)));
}
Run Code Online (Sandbox Code Playgroud)

编辑:正如 Veedrac 所说,更通用和有效的选择是将 解构f64为尾数-指数-符号三元组。可以执行此操作的函数在 中integer_decode()已弃用std,但可以在Rust GitHub 中轻松找到。

integer_decode()函数可以定义如下:

use std::mem;

fn integer_decode(val: f64) -> (u64, i16, i8) {
    let bits: u64 = unsafe { mem::transmute(val) };
    let sign: i8 = if bits >> 63 == 0 { 1 } else { -1 };
    let mut exponent: i16 = ((bits >> 52) & 0x7ff) as i16;
    let mantissa = if exponent == 0 {
        (bits & 0xfffffffffffff) << 1
    } else {
        (bits & 0xfffffffffffff) | 0x10000000000000
    };

    exponent -= 1023 + 52;
    (mantissa, exponent, sign)
}
Run Code Online (Sandbox Code Playgroud)

的定义Distance可以是:

#[derive(Hash, Eq, PartialEq)]
struct Distance((u64, i16, i8));

impl Distance {
    fn new(val: f64) -> Distance {
        Distance(integer_decode(val))
    }
}
Run Code Online (Sandbox Code Playgroud)

此变体也更易于使用:

fn main() {
    let mut map: HashMap<Distance, f64> = HashMap::new();

    map.insert(Distance::new(0.4), f64::hypot(4.2, 50.0));
    map.insert(Distance::new(1.8), f64::hypot(2.6, 50.0));

    assert_eq!(map.get(&Distance::new(0.4)), Some(&f64::hypot(4.2, 50.0)));
}
Run Code Online (Sandbox Code Playgroud)

  • 为什么有人会使用“符号-指数-尾数”拆分版本而不是简单的 `impl`-ing `Eq` 和 `Hash` 来处理 `Distance(f64)`,因为它遇到了 `f64` 具有的相同问题 (`0.3 ` != `0.1 + 0.2` 是三元组形式还是`f64`)? (2认同)
  • @John `f64` 没有哈希实现的唯一原因是 `NaN` 不等于自身,因此不能具有哈希值。使用 Shepmaster 的解决方案而不是这个很好(尽管那个违反了`Hash` 的合同,并且更难保证安全),但我不明白为什么人们认为四舍五入可以解决任何问题。不分析域的舍入只会使问题变得更糟。 (2认同)

Mat*_* M. 7

不幸的是,浮点类型相等是困难且违反直觉的

fn main() {
    println!("{} {} {}", 0.1 + 0.2, 0.3, 0.1 + 0.2 == 0.3);
}

// Prints: 0.30000000000000004 0.3 false
Run Code Online (Sandbox Code Playgroud)

因此散列也很困难,因为相等值的散列应该相等。


如果在你的情况下,你有一个足够小的范围来适应 a 中的数字,i64 并且你可以接受精度的损失,那么一个简单的解决方案是首先规范化,然后根据规范值定义 equal/hash :

use std::cmp::Eq;

#[derive(Debug)]
struct Distance(f64);

impl Distance {
    fn canonicalize(&self) -> i64 {
        (self.0 * 1024.0 * 1024.0).round() as i64
    }
}

impl PartialEq for Distance {
    fn eq(&self, other: &Distance) -> bool {
        self.canonicalize() == other.canonicalize()
    }
}

impl Eq for Distance {}

fn main() {
    let d = Distance(0.1 + 0.2);
    let e = Distance(0.3);

    println!("{:?} {:?} {:?}", d, e, d == e);
}

// Prints: Distance(0.30000000000000004) Distance(0.3) true
Run Code Online (Sandbox Code Playgroud)

Hash如下所示,从那时起您就可以用作Distance哈希映射中的键:

impl Hash for Distance {
    fn hash<H>(&self, state: &mut H) where H: Hasher {
        self.canonicalize().hash(state);
    }
}

fn main() {
    let d = Distance(0.1 + 0.2);
    let e = Distance(0.3);

    let mut m = HashMap::new();
    m.insert(d, "Hello");

    println!("{:?}", m.get(&e));
}

// Prints: Some("Hello")
Run Code Online (Sandbox Code Playgroud)

警告:重申一下,此策略仅在以下情况下才有效:(a) 值的动态范围足够小,可以在 a i64(19 位)中捕获,并且 (b) 由于因子是静态的,动态范围提前已知。幸运的是,这适用于许多常见问题,但需要记录和测试......

  • 我不认为如果“f32”添加没有产生精确的预期结果,则散列应该受到任何影响的推理的相关性。毕竟,不存在“法则”,即“x1 + x2 == x3” -&gt; “x1.hash() + x2.hash() == x3.hash()”。如果舍入错误对应用程序来说是一个问题,则不要使用浮点,但不要声明浮点值由于舍入而无法进行哈希处理。使用 bignums 或 Rust 在该部门提供的任何东西。这里唯一可接受的推理是“NaN”问题。 (3认同)
  • @BitTickler:散列浮点数很容易,您只需将它们重新解释为整数并对整数进行散列即可。它满足两个浮点数相等则其哈希值相等的要求(因为“NaN”不等于任何值)。不过,这里要指出的是,该方案依赖于精确相等(按位相等),而本质上浮点具有舍入误差,因此在容差阈值内相等。这就是散列失败的地方:它无法处理这个“容忍阈值”。 (3认同)

opt*_*evo 7

您可以使用ordered_float板条箱来为您完成此操作。


She*_*ter 6

除了阅读所有其他评论和答案之外,没有任何评论以了解您可能不想这样做的原因

use std::{collections::HashMap, hash};

#[derive(Debug, Copy, Clone)]
struct DontUseThisUnlessYouUnderstandTheDangers(f64);

impl DontUseThisUnlessYouUnderstandTheDangers {
    fn key(&self) -> u64 {
        self.0.to_bits()
    }
}

impl hash::Hash for DontUseThisUnlessYouUnderstandTheDangers {
    fn hash<H>(&self, state: &mut H)
    where
        H: hash::Hasher,
    {
        self.key().hash(state)
    }
}

impl PartialEq for DontUseThisUnlessYouUnderstandTheDangers {
    fn eq(&self, other: &DontUseThisUnlessYouUnderstandTheDangers) -> bool {
        self.key() == other.key()
    }
}

impl Eq for DontUseThisUnlessYouUnderstandTheDangers {}

fn main() {
    let a = DontUseThisUnlessYouUnderstandTheDangers(0.1);
    let b = DontUseThisUnlessYouUnderstandTheDangers(0.2);
    let c = DontUseThisUnlessYouUnderstandTheDangers(0.3);

    let mut map = HashMap::new();
    map.insert(a, 1);
    map.insert(b, 2);

    println!("{:?}", map.get(&a));
    println!("{:?}", map.get(&b));
    println!("{:?}", map.get(&c));
}
Run Code Online (Sandbox Code Playgroud)

基本上,如果您想将 af64视为一组没有意义的位,那么我们可以将它们视为知道如何进行散列和按位比较的等效大小的位包。

1600 万个NaN值之一不等于另一个值时,请不要感到惊讶。

  • 请注意,如果带有“NaN”的奇怪结果有问题,您可以随时在构造函数中将它们过滤掉。 (2认同)