【问题标题】:On integer multiplication, overflow, and information loss关于整数乘法、溢出和信息丢失
【发布时间】:2012-01-24 13:01:16
【问题描述】:

我正在阅读 Joshua Bloch 的 Effective Java 的 Chapter 3。在Item 8: Always override hashCode when you override equals中,作者在他的哈希函数中使用了以下组合步骤:

result = 37 * result + c;

然后他解释了为什么选择 37(强调):

选择乘数 37 是因为它是一个奇素数。 如果它是偶数并且 乘法溢出,信息会丢失,因为乘法 乘二相当于移位。 使用素数的优点较少 很清楚,但传统上为此目的使用素数。

我的问题是为什么组合因子 (37) 奇数 很重要?无论因子是奇数还是偶数,乘法溢出不会导致信息丢失吗?

【问题讨论】:

    标签: java overflow hashcode multiplication


    【解决方案1】:

    质数并不是确保多样性所必需的;必要的是该因子与模数互质。

    由于二进制算术的模数始终是 2 的幂,因此任何奇数都是互质数,就足够了。但是,如果您要取除溢出以外的模数,则质数将继续确保多样性(假设您没有选择相同的质数...)。

    【讨论】:

      【解决方案2】:

      为什么...的非数学简单版本...

      质数用于散列以保持多样性。

      也许由于 Set 和 Map 的实现,多样性更重要。这些实现使用对象哈希数的最后一位来索引内部条目数组。

      例如,在具有内部表(数组)的 HashMap 中,对于大小为 8 的条目,它将使用哈希数字的最后 3 位来寻址表条目。

      静态 int indexFor(int h, int length) { 返回 h & (length-1); }

      事实上不是,但如果 Integer 对象会有

      哈希 = 4 * 数字;

      大部分表格元素是空的,但有些会包含太多条目。这将导致在搜索特定条目时进行额外的迭代和比较操作。

      我猜 Joshua Bloch 的主要关注点是尽可能均匀地分布散列整数,通过在 Maps 和 Sets 中均匀分布对象来优化集合的性能。素数直觉上似乎是一个很好的分布因素。

      【讨论】:

        【解决方案3】:

        解决方案在于数论以及乘数和模数的Lowest common denominator。

        一个例子可能会有所帮助。假设您只有 2 位来表示一个数字,而不是 32 位。所以你有4个数字(类)。 0、1、2和3

        CPU 中的溢出与模运算相同

        Class - x2 - mod 4 - x2 - mod 4
        
        0       0      0     0     0
        
        1       2      2     4     0
        
        2       4      0     0     0
        
        3       6      2     4     0
        

        经过 2 次操作后,您只剩下 1 个可能的数字(类别)。所以你已经“丢失”了信息。

        Class - x3 - mod 4 - x3 - mod 4 ...
        
        0       0      0     0     0
        
        1       3      3     9     1
        
        2       6      2     6     2
        
        3       9      1     3     3
        

        这可以永远持续下去,您仍然拥有所有 4 个课程。所以你不会丢失信息。

        关键是,您的乘数和模类的 LCD 为 1。这适用于所有奇数,因为您的模数目前始终是 2 的幂。它们不必是素数,也不必是具体为 37 岁。但是信息丢失只是选择 37 的一个标准,其他标准是价值分布等。

        【讨论】:

          【解决方案4】:

          hashCode 的目的是根据输入获得随机位(尤其是低位,因为这些位经常使用得更多)

          当你乘以 2 时,最低位只能为 0,缺乏随机性。如果你乘以一个奇数,最低位可以是奇数或偶数。


          一个类似的问题是你在这里得到什么

          public static void main(String... args) {
              System.out.println(factorial(66));
          }
          
          public static long factorial(int n) {
              long product = 1;
              for (; n > 1; n--)
                  product *= n;
              return product;
          }
          

          打印

          0
          

          每隔一个数是偶数,每隔一个数是 4 的倍数,以此类推。

          【讨论】:

          • 可爱,你可以用手证明它溢出到 0。所以没有阶乘作为哈希函数......我不会这样做。
          • 部分诀窍在于找出为什么 66 是第一个为 0 的阶乘。而不是 128,例如,它有 64 个偶数因子。
          【解决方案5】:

          考虑在以 2 为底的表示中将正值重复乘以 2 时会发生什么 - 所有设置的位最终都会离开末尾,留下零。

          偶数乘数会导致散列码的多样性降低。

          另一方面,奇数可能会导致溢出,但不会损失多样性。

          【讨论】:

          • 啊,所以我们担心的不仅仅是溢出导致的一点点信息丢失,而是您可以从中获取的信息完全丢失将结果归零?
          • @BilltheLizard:实际上,它是来自多个相互模拟的属性的数据。假设三个属性 a、b 和 c 使用上述算法 result = 2 * (2 * a + b) + c,您可以看到在许多可能常见的 a,b,c 集合中会有重复。如果您使用奇数作为常数,则具有相同哈希值的集合的可能性会大大降低。
          • 甚至在您将结果完全归零之前,问题就已经显现。考虑将 8 位哈希乘以 2 的乘数一次 - 它以 256 个可能值开始,以 128 个可能值结束。
          猜你喜欢
          • 1970-01-01
          • 2015-03-30
          • 2020-06-12
          • 1970-01-01
          • 1970-01-01
          • 2020-03-27
          • 1970-01-01
          • 1970-01-01
          • 2012-01-21
          相关资源
          最近更新 更多