Java-取模操作中的&和(length-1)

0.背景

在哈希表相关的操作中,有一个典型的问题:

将n个元素放置到长度为k的数组中

现在,我们假定数组的长度为8,元素个数为10个。

1、2、3、4、5、6、7、8、9、10

如果我们拥有一个理想的哈希函数,可以将其中8个元素均匀的放置到数组的8个位置上。

不过由于有10个元素,所以必然会出现冲突的可能。

1.元素放置

现在,我们模拟下上面的操作,选择怎么设计一个哈希函数。

由于值是已知的,很显然能想到一个办法,直接取模。

    @Test
    void name2() {
        for (int i = 1; i < 11; i++) {
            log.info("k为: {},取模[8]: {}", i, i % 8);
        }
    }

运行结果

2023-06-17 16:56:34.381 -- [main] INFO  cn.yang37.basic.HashCodeTest.name2 - k为: 1,取模[8]: 1
2023-06-17 16:56:34.382 -- [main] INFO  cn.yang37.basic.HashCodeTest.name2 - k为: 2,取模[8]: 2
2023-06-17 16:56:34.383 -- [main] INFO  cn.yang37.basic.HashCodeTest.name2 - k为: 3,取模[8]: 3
2023-06-17 16:56:34.383 -- [main] INFO  cn.yang37.basic.HashCodeTest.name2 - k为: 4,取模[8]: 4
2023-06-17 16:56:34.383 -- [main] INFO  cn.yang37.basic.HashCodeTest.name2 - k为: 5,取模[8]: 5
2023-06-17 16:56:34.383 -- [main] INFO  cn.yang37.basic.HashCodeTest.name2 - k为: 6,取模[8]: 6
2023-06-17 16:56:34.383 -- [main] INFO  cn.yang37.basic.HashCodeTest.name2 - k为: 7,取模[8]: 7
2023-06-17 16:56:34.384 -- [main] INFO  cn.yang37.basic.HashCodeTest.name2 - k为: 8,取模[8]: 0
2023-06-17 16:56:34.384 -- [main] INFO  cn.yang37.basic.HashCodeTest.name2 - k为: 9,取模[8]: 1
2023-06-17 16:56:34.384 -- [main] INFO  cn.yang37.basic.HashCodeTest.name2 - k为: 10,取模[8]: 2

现在,给定的元素都能有序的放置到数组中。

2.修改输入

上面的例子比较简单,我们换一下,假定给出的元素是。

sads、sadsa、wewq、sadd、hfhg、erter、tytr、cxfcx、dkfpdfs、dsfds

嗯,现在思路就乱了,没有任何规律呀?

所以,我们可以想到使用jdk自带的哈希函数,先来打印一下哈希值。

    @Test
    void name4() {
        String[] array = {"sads", "sadsa", "wewq", "sadd", "hfhg", "erter", "tytr", "cxfcx", "dkfpdfs", "dsfds"};
        Arrays.stream(array).forEach(e-> System.out.println(e.hashCode()));
    }

运行结果

3522397
109194404
3645992
3522382
3199613
96786516
3575747
95104710
1717009152
95879302

为了放置进去开始的8个数组,我们容易想到,还是取模这个老办法。

@Test
void name4() {
    String[] array = {"sads", "sadsa", "wewq", "sadd", "hfhg", "erter", "tytr", "cxfcx", "dkfpdfs", "dsfds"};
    Arrays.stream(array).forEach(e -> {
        int hashCode = e.hashCode();
        log.info("k为: {},hashCode: {},取模[8]: {}", e, hashCode, hashCode % 8);
    });
}

运行结果

2023-06-17 17:06:12.389 -- [main] INFO  cn.yang37.basic.HashCodeTest.lambda$name4$0 - k为: sads,hashCode: 3522397,取模[8]: 5
2023-06-17 17:06:12.391 -- [main] INFO  cn.yang37.basic.HashCodeTest.lambda$name4$0 - k为: sadsa,hashCode: 109194404,取模[8]: 4
2023-06-17 17:06:12.391 -- [main] INFO  cn.yang37.basic.HashCodeTest.lambda$name4$0 - k为: wewq,hashCode: 3645992,取模[8]: 0
2023-06-17 17:06:12.391 -- [main] INFO  cn.yang37.basic.HashCodeTest.lambda$name4$0 - k为: sadd,hashCode: 3522382,取模[8]: 6
2023-06-17 17:06:12.391 -- [main] INFO  cn.yang37.basic.HashCodeTest.lambda$name4$0 - k为: hfhg,hashCode: 3199613,取模[8]: 5
2023-06-17 17:06:12.391 -- [main] INFO  cn.yang37.basic.HashCodeTest.lambda$name4$0 - k为: erter,hashCode: 96786516,取模[8]: 4
2023-06-17 17:06:12.391 -- [main] INFO  cn.yang37.basic.HashCodeTest.lambda$name4$0 - k为: tytr,hashCode: 3575747,取模[8]: 3
2023-06-17 17:06:12.392 -- [main] INFO  cn.yang37.basic.HashCodeTest.lambda$name4$0 - k为: cxfcx,hashCode: 95104710,取模[8]: 6
2023-06-17 17:06:12.392 -- [main] INFO  cn.yang37.basic.HashCodeTest.lambda$name4$0 - k为: dkfpdfs,hashCode: 1717009152,取模[8]: 0
2023-06-17 17:06:12.392 -- [main] INFO  cn.yang37.basic.HashCodeTest.lambda$name4$0 - k为: dsfds,hashCode: 95879302,取模[8]: 6

3.取模

取模操作是一个常用的例子,翻看各种工程代码,我们会发现,取模,它并不是这样写的。

int mod = k % (arr.length);

而是

int mod = k & (arr.length - 1);

嗯,一个奇怪的写法,不妨先试试。

@Test
void name4() {
    String[] array = {"sads", "sadsa", "wewq", "sadd", "hfhg", "erter", "tytr", "cxfcx", "dkfpdfs", "dsfds"};
    Arrays.stream(array).forEach(e -> {
        int hashCode = e.hashCode();
        log.info("k为: {},hashCode: {},取模[8]: {}", e, hashCode, hashCode & (8 - 1));
    });
}

注意,我将hashCode % 8换成了hashCode & (8 - 1),再看下运行结果。

2023-06-17 17:10:16.230 -- [main] INFO  cn.yang37.basic.HashCodeTest.lambda$name4$0 - k为: sads,hashCode: 3522397,取模[8]: 5
2023-06-17 17:10:16.232 -- [main] INFO  cn.yang37.basic.HashCodeTest.lambda$name4$0 - k为: sadsa,hashCode: 109194404,取模[8]: 4
2023-06-17 17:10:16.232 -- [main] INFO  cn.yang37.basic.HashCodeTest.lambda$name4$0 - k为: wewq,hashCode: 3645992,取模[8]: 0
2023-06-17 17:10:16.232 -- [main] INFO  cn.yang37.basic.HashCodeTest.lambda$name4$0 - k为: sadd,hashCode: 3522382,取模[8]: 6
2023-06-17 17:10:16.232 -- [main] INFO  cn.yang37.basic.HashCodeTest.lambda$name4$0 - k为: hfhg,hashCode: 3199613,取模[8]: 5
2023-06-17 17:10:16.232 -- [main] INFO  cn.yang37.basic.HashCodeTest.lambda$name4$0 - k为: erter,hashCode: 96786516,取模[8]: 4
2023-06-17 17:10:16.232 -- [main] INFO  cn.yang37.basic.HashCodeTest.lambda$name4$0 - k为: tytr,hashCode: 3575747,取模[8]: 3
2023-06-17 17:10:16.233 -- [main] INFO  cn.yang37.basic.HashCodeTest.lambda$name4$0 - k为: cxfcx,hashCode: 95104710,取模[8]: 6
2023-06-17 17:10:16.233 -- [main] INFO  cn.yang37.basic.HashCodeTest.lambda$name4$0 - k为: dkfpdfs,hashCode: 1717009152,取模[8]: 0
2023-06-17 17:10:16.233 -- [main] INFO  cn.yang37.basic.HashCodeTest.lambda$name4$0 - k为: dsfds,hashCode: 95879302,取模[8]: 6

嗯,结果还是一样。

通过位运算进行快速取模

4.分析

通过位运算进行快速取模,使用的前提是数组长度是2的幂次方

当数组的长度是2的幂次方时,arr.length - 1 的二进制表示中所有位都是1,例如:

  • 数组长度为4时,二进制表示为100,arr.length - 1 的二进制表示为011
  • 数组长度为8时,二进制表示为1000,arr.length - 1 的二进制表示为0111
  • 数组长度为16时,二进制表示为10000,arr.length - 1 的二进制表示为01111
  • 以此类推。

现在,看一下&操作的原理。位运算&是对元素进行且的操作,即串联

有假则假,全真为真。

0 & 0 = 0
0 & 1 = 0
1 & 0 = 0
1 & 1 = 1

我们再来看下上面的arr.length - 1,结果固定是0和n个1。

x & 0 = 0
x & 1 = x

取模是在干什么,就是找到那个余数。

当取余的时候,不管参与运算的数有多大,(arr.length-1)的前面都是0。

# 10的二进制
1010
# 7(arr.leng-1)的二进制
0111
# 10 & 7
(10) 1010
(7)  0111
   = 0010

# 再用150(10010110)做个例子,结果为6
(150) 10010110
(7)   00000111
    = 00000110

所以,当参与&运算时,前面的x位数字都可以忽略掉了(结果必定是0),我们实际要关心的只有(arr.length-1)的这个窗口。

由于(arr.length-1)后面几位都是111...,所以结果只会受输入的影响,值是什么结果就是什么即,前面提到的。

x & 1 = x

拿150举例,相当于把某倍数的arr.length-1(8)扔掉,即10010(144),扔掉,取出余数6。

5.总结

当数组(槽)长度是2n时,元素k的取模操作可替换为位运算以提高效率。

# 取模
m = k % arr.length
# 可替换为
m = k & (arr.length-1)

简单抽取一个方法.

    /**
     * 根据hashCode()计算数组中索引位置
     *
     * @param key str
     * @param arr arr
     * @return idx
     */
    private static int calculateIdx(String key, String[] arr) {
        // 2^n
        if (arr.length % 2 == 0) {
            return key.hashCode() & (arr.length - 1);
        }

        // !(2^n)
        return key.hashCode() % arr.length;
    }

6.附

哪些地方用到了?

6.1 HashMap

HashMap 的取模公式为e.hash & (capacity - 1)

这里 capacity 是 HashMap 数组结构的大小,约定为 2 的 n 次幂,记为 capacity = 2n。
对于节点 e,它的哈希值用 e.hash 表示。

posted @ 2023-06-17 18:09  羊37  阅读(66)  评论(1编辑  收藏  举报