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

时间:2016-09-22 11:55:16

标签: floating-point hashmap rust

我想使用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();

由于f64既不是Eq也不是Hash,但只有PartialEqf64不足以作为密钥。我需要先保存距离,然后再用y来访问距离。 y的类型需要是浮点精度,但如果不能与f64一起使用,我将使用具有已知指数的i64

我尝试使用自己的struct Dimension(f64),然后通过将浮动转换为Hash然后对其进行散列来实现String来尝试一些黑客攻击。

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

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

看起来非常糟糕,只有一个键,两个解决方案,我自己的struct或float作为带有base和exponent的整数似乎都非常复杂。

更新: 我可以保证我的密钥永远不会是NaN或无限值。此外,我不会计算我的密钥,只是迭代它们并使用它们。因此,0.1 + 0.2 ≠ 0.3的已知错误不会出错。 How to do a binary search on a Vec of floats?这个问题的共同点是实现浮点数的总排序和相等,差别仅在于散列或迭代。

3 个答案:

答案 0 :(得分:3)

不幸的是,浮动类型相等是hard and counter-intuitive

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

// Prints: 0.30000000000000004 0.3 false

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

如果在你的情况下,你有足够小的范围来适应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

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")

警告:重申一下,只有当(a)动态值范围小到足以在i64(19位数)中捕获并且如果(b)时,此策略才有效由于因子是静态的,因此事先知道动态范围。幸运的是,这适用于许多常见问题,但需要记录和测试......

答案 1 :(得分:3)

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

use std::hash;
use std::collections::HashMap;

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

impl DontUseThisUnlessYouUnderstandTheDangers {
    fn key(&self) -> u64 {
        unsafe { mem::transmute(self.0) }
    }
}

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));
}

基本上,如果你想将f64视为没有意义的一组位,那么我们可以将它们视为一个相当大小的位包,知道如何被哈希和按位比较。

16 million NaN values doesn't equal another one之一,不要感到惊讶。

答案 2 :(得分:2)

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

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

其余的很简单:

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)));
}

编辑:正如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)
}

Distance的定义可能是:

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

impl Distance {
    fn new(val: f64) -> Distance {
        Distance(integer_decode(val))
    }
}

此变体也更易于使用:

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)));
}