【问题标题】:How to use two numbers as a Map key如何使用两个数字作为 Map 键
【发布时间】:2010-11-28 16:26:40
【问题描述】:

我有两个数字,我想将它们一起用作Map 中的键。目前,我正在连接他们的字符串表示。例如,假设键号是 4 和 12。我使用:

String key = 4 + "," + 12;

地图声明为Map<String, Object>

我觉得这太糟糕了!我喜欢使用 String 以外的其他东西作为键!我想要创建这些密钥的最快方法。

谁有好主意?

【问题讨论】:

  • 我认为逗号分隔的字符串是个好主意。我一直使用这种方法。

标签: java performance data-structures collections key


【解决方案1】:

您应该使用 java.awt.Dimension 作为您的密钥。

维度键 = new Dimension(4, 12);

Dimension 有一个非常好的 hashCode() 方法,它为每对正整数生成不同的 hashCode,因此 (4, 12) 和 (12, 4) 的 hashCode 不同。所以这些可以快速实例化并制作非常好的哈希码。

我真希望他们使类不可变,但您可以根据 Dimension 创建自己的不可变类。

这是一个表格,显示了不同宽度和高度值的 hashCode:

     0   1   2   3   4  <-- width
  +--------------------
0 |  0   2   5   9  14
1 |  1   4   8  13
2 |  3   7  12
3 |  6  11
4 | 10

^
|
height

如果您按照从 0 到 14 的顺序执行 hashCodes,您会看到该模式。

这是产生这个 hashCode 的代码:

public int hashCode() {
    int sum = width + height;
    return sum * (sum + 1)/2 + width;
}

您可能会在最后一行中认出三角数的公式。这就是表格的第一列包含所有三角数的原因。

为了速度,你应该在构造函数中计算 hashCode。所以你的整个班级可能看起来像这样:

public class PairHash {
  private final int hash;
  public PairHash(int a, int b) {
    int sum = a+b;
    hash = sum * (sum+1)/2 + a;
  }
  public int hashCode() { return hash; }
}

当然,如果你可能需要一个 equals 方法,但你限制自己使用不会溢出的正整数,你可以添加一个非常快的方法:

public class PairHash {
  // PAIR_LIMIT is 23170
  // Keeping the inputs below this level prevents overflow, and guarantees
  // the hash will be unique for each pair of positive integers. This
  // lets you use the hashCode in the equals method.
  public static final int PAIR_LIMIT = (int) (Math.sqrt(Integer.MAX_VALUE))/2;
  private final int hash;

  public PairHash(int a, int b) {
    assert a >= 0;
    assert b >= 0;
    assert a < PAIR_LIMIT;
    assert b < PAIR_LIMIT;
    int sum = a + b;
    hash = sum * (sum + 1) / 2 + a;
  }

  public int hashCode() { return hash; }

  public boolean equals(Object other) {
    if (other instanceof PairHash){
      return hash == ((PairHash) other).hash;
    }
    return false;
  }
}

我们将此限制为正值,因为负值会产生一些重复的哈希码。但是有了这个限制,这些是可以编写的最快的 hashCode() 和 equals() 方法。 (当然,您可以通过在构造函数中计算 hashCode,在任何不可变类中以同样快的速度编写 hashCode。)

如果你不能忍受这些限制,你只需要保存参数。

public class PairHash {
  private final int a, b, hash;
  public PairHash(int a, int b) {
    this.a = a;
    this.b = b;
    int sum = a+b;
    hash = sum * (sum+1)/2 + a;
  }
  public int hashCode() { return hash; }
  public boolean equals(Object other) {
    if (other instanceof PairHash) {
      PairHash otherPair = (PairHash)other;
      return a == otherPair.a && b == otherPair.b;
    }
    return false;
}

但这是最重要的。你根本不需要这门课。由于该公式为每对数字提供了一个唯一的整数,因此您可以使用该整数作为映射键。 Integer 类有自己的快速 equals() 和 hashCode 方法,它们可以正常工作。此方法将从两个短值生成哈希键。限制是您的输入必须是正短值。这保证不会溢出,并且通过将中间和转换为 long,它比以前的方法具有更广泛的范围:它适用于所有正的 short 值。

static int hashKeyFromPair(short a, short b) {
  assert a >= 0;
  assert b >= 0;
  long sum = (long) a + (long) b;
  return (int) (sum * (sum + 1) / 2) + a;
}

【讨论】:

【解决方案2】:

这个问题的实际答案是:

hashCode = a + b * 17;

... 其中 a、b 和 hashCode 都是整数。 17 只是一个任意的素数。您的哈希不会是唯一的,但没关系。 Java 标准库中到处都在使用这种东西。

【讨论】:

    【解决方案3】:

    为什么要编写所有额外的代码来创建一个你不需要的完整类比使用简单的字符串更好?计算该类实例的哈希码会比计算字符串快得多吗?我不这么认为。

    除非您在计算能力极其有限的环境中运行,否则创建和散列字符串的开销不应明显大于实例化自定义类的开销。

    我想最快的方法是按照 ZZ Coder 的建议将 int 简单地打包到一个 Long 中,但无论如何,我认为速度提升不会很大。

    【讨论】:

      【解决方案4】:

      创建一个包含两个数字的对象并将其用作键。例如:

      class Coordinates {
      
        private int x;
        private int y;
      
        public Coordinates(int x, int y) {
           ...
        }
      
        // getters
      
        // equals and hashcode using x and y
      }
      
      Map<Coordinates, Location> locations = new HashMap<Coordinates, Location>();
      

      如果您更喜欢数学方法,请参阅this StackOverflow answer

      【讨论】:

      • 谢谢,你的方法很好,但我不想用“类”来解决问题。如何使用普通的数学方法来获取密钥?我只是问,“最快”
      • 好的。我已经添加了指向您想要的答案的链接:-)
      • 好的 - 所以这很明显是一个家庭作业问题。这里正确的解决方案是使用一个类。该类的 hashcode() 方法的实现是性能发挥作用的地方。
      • 如果您将 x 和 y 用作 HashMap 键,您真的希望 x 和 y 可变吗?
      • 另外,如果您需要从 Map 中查找内容,而不仅仅是对其进行迭代,您应该重写方法 hashCode() 和 equals()。这将允许您:A 在将来创建坐标对象并从您已填充的地图中检索内容。否则,即使您创建的坐标与用作 Map 中键的坐标具有相同的 x 和 y,也不会返回该值。 B 它将缩短从您的地图中查找的时间
      【解决方案5】:

      你可以这样存储两个整数,

         long n = (l << 32) | (r & 0XFFFFFFFFL);
      

      或者您可以使用以下Pair&lt;Integer, Integer&gt; 类,

      public class Pair<L, R> {
      
          private L l;
          private R r;
      
          public Pair() {
          }
      
          public Pair(L l, R r) {
              this.l = l;
              this.r = r;
          }
      
          public L getLeft() {
              return l;
          }
      
          public R getRight() {
              return r;
          }
      
          @Override
          public boolean equals(Object o) {
              if (!(o instanceof Pair)) {
                  return false;
              }
              Pair obj = (Pair) o;
              return l.equals(obj.l) && r.equals(obj.r);
          }
      
          @Override
          public int hashCode() {
              return l.hashCode() ^ r.hashCode();
          }
      } 
      

      【讨论】:

      • 我会赞成这个漂亮的 int -> long 解决方案,但我也会反对它使 Pair 可变。
      • 你不能使用原语作为泛型参数。
      • 不错的技巧,但是一旦它们存储在那里,我就无法从那个 long 中提取这两个整数。您可以使用哪些位运算来拉出这两个整数(r 和 l)?
      • (1,2)和(2,1)的hashCode相同
      【解决方案6】:

      另一种方法是使用嵌套地图:

      Map<Integer,Map<Integer,Object>>
      

      在这里您没有创建密钥的开销。但是,您需要更多开销来正确创建和检索条目,并且您始终需要映射访问以找到您正在寻找的对象。

      【讨论】:

        【解决方案7】:

        如果您使用对象解决方案,确保您的关键对象是不可变的

        否则,如果有人改变了该值,不仅它不再等于其他明显相同的值,而且存储在映射中的哈希码将不再与hashCode() 方法返回的哈希码匹配。那时你基本上是 SOL。

        例如,使用java.awt.Point -- 看起来, 在纸上,就像你想要的一样 -- 如下:

          public static void main(String[] args) {
            Map<Point, Object> map = new HashMap<Point, Object>();
        
            Point key = new Point(1, 3);
            Object val = new Object();
        
            map.put(key, val);
        
            System.out.println(map.containsKey(key));
            System.out.println(map.containsKey(new Point(1, 3)));
        
            // equivalent to setLeft() / setRight() in ZZCoder's solution,
            // or setX() / setY() in SingleShot's
            key.setLocation(2, 4);
        
            System.out.println(map.containsKey(key));
            System.out.println(map.containsKey(new Point(2, 4)));
            System.out.println(map.containsKey(new Point(1, 3)));
          }
        

        打印:

        true
        true
        false
        false
        false
        

        【讨论】:

          【解决方案8】:

          你需要编写正确的 eqauls 和 hashcode 方法,否则会产生一些错误。

          【讨论】:

            猜你喜欢
            • 2010-11-06
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2011-07-18
            • 2012-10-04
            • 1970-01-01
            相关资源
            最近更新 更多