【问题标题】:How can I use a HashMap with f64 as key in Rust?如何在 Rust 中使用 f64 作为键的 HashMap?
【发布时间】:2017-01-30 23:47:39
【问题描述】:

我想使用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),然后通过将浮点数转换为String,然后对其进行散列来实现Hash

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

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

这似乎很糟糕,两种解决方案,我自己的结构或浮点数作为具有基数和指数的整数似乎都非常复杂。

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

【问题讨论】:

  • 您真的需要按精确距离获取对象吗?使用浮点数作为键与测试两个是否相等(确实会发生舍入错误)一样糟糕。
  • @Shepmaster:这里可能存在f64 没有实现Eq 的问题,但我认为问题更深层次=> 即使您排除NaN,比较两个浮点数是否相等只是自找麻烦。
  • 您希望您的密钥有任何重复值吗?是否有必要通过哈希映射对它们进行重复数据删除?

标签: floating-point hashmap rust


【解决方案1】:

您可以将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)));
}

【讨论】:

  • 这似乎比无损(并且更节省空间)符号-指数-尾数三元组更糟糕。
  • 你说得对,我完全忘记了这一点。我将添加此信息。
  • 当遇到与@987654338 相同的问题时,为什么有人会使用“符号-指数-尾数”拆分版本而不是简单的impl-ing EqHash for Distance(f64) @有(0.3!=0.1 + 0.2是三元组还是f64)?
  • @John MattieuM 的回答涉及四舍五入和不精确,并涉及每次比较的算术。相比之下,这个是无损的。
  • @John f64 没有哈希实现的唯一原因是NaN 不等于它自己,所以不能有哈希值。使用 Shepmaster 的解决方案而不是这个很好(尽管它违反了与 Hash 的合同,并且更难保证安全),但我不明白为什么人们认为四舍五入可以解决任何问题。不分析域的舍入只会使问题变得更糟。
【解决方案2】:

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

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

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

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

【讨论】:

  • 请注意,如果NaN 的奇怪结果有问题,您可以随时在构造函数中将它们过滤掉。
【解决方案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 并且您可以接受精度损失,那么一个简单的解决方案是先规范化,然后再规范化根据规范值定义相等/散列:

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) 动态范围是预先知道的,因为该因子是静态的。幸运的是,这适用于许多常见问题,但需要记录和测试......

【讨论】:

  • 最好将canonicalize 转换为f32,而不是乘以一个常数并转换为一个整数,因为1e-122e-15 都将映射到当前的0方案,但 f32 中的值不同。它也解决了精度问题,因为它只在比较期间进行演员表。
  • @John:也许,也许不是。这一切都取决于你想认为什么是平等的。对于以米为单位的距离测量,1e-12 是 1 皮米:如果 1 皮米的差异与任何类型的地理跟踪相关(例如),我会感到非常惊讶。这确实是一个领域建模决策。如果您希望保持更高的精度,那么哈希映射查找是有缺陷的,您将需要边界卷、KD-Trees 等...
  • 我不喜欢这个解决方案;它增加了不必要的不​​精确性,并且似乎不能很好地映射到任何域。如果像这样的舍入就足够了,那么您一开始就不应该使用浮点数。
  • 我看不出如果f32添加没有产生准确的预期结果,散列应该以任何方式受到影响的推理的相关性。毕竟,没有“法律”,它需要x1 + x2 == x3 -> x1.hash() + x2.hash() == x3.hash()。如果舍入错误对应用程序来说是个问题,则不要使用浮点数,但不要声明浮点值由于舍入而无法散列。使用 bignums 或该部门提供的任何锈迹。这里唯一可以接受的理由是NaN 问题。
  • @BitTickler:散列浮点数很容易,您可以将它们重新解释为整数并散列整数。它满足两个浮点数相等的要求,它们的哈希值相等(因为NaN 不等于任何东西)。不过,这里要指出的是,该方案依赖于精确相等(按位相等),而浮点本质上具有舍入误差,因此在在容差阈值内相等。这就是散列失败的地方:它无法处理这个容差阈值
猜你喜欢
  • 2016-10-16
  • 2015-05-11
  • 1970-01-01
  • 1970-01-01
  • 2017-07-06
  • 1970-01-01
  • 2019-01-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多