CS61B - HW3 - Hashing - 溢出问题


HW3主要做了两件事情:重写equals和hashCode方法。有两个值得注意的点。

equals方法需要满足的条件

在这里插入图片描述
所以在重写equals方法时,必须额外考虑null, 其它class这两种特殊情况。

@Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (o == null) return false;
        if (o.getClass() != this.getClass()) return false;
        SimpleOomage that = (SimpleOomage) o;
        return (this.blue == that.blue) && (this.red == that.red) && (this.green == that.green);
    }

hashCode中的溢出问题

HW3最后要解决这样一个问题:

计算ComplexOomages的hashCode方法给出,如下:

@Override
    public int hashCode() {
        int total = 0;
        for (int x : params) {
            total = total * 256;
            total = total + x;
        }
        return total;
    }

其中,params是ComplexOomages中一个List属性,每个元素是Integer类型,取值为0~255,List的size不限。
这个方法完美地通过了hashCode的spread测试(hashTable同一格的元素在N / 50和N / 2.5之间),但是是有bug的,要求我们写一个测试方法使测试fail掉。

想到了溢出问题,但是一直是把params的前4个元素设置成相同的int(256在四次方的时候就会溢出),根本没用。也是很平常的思路,4次方时溢出成0,之后后面的元素就一直是0了。

完全不对,还是对溢出了解不深入。其实课上的PPT已经把答案说了,但是当时其实也没看懂,没有细想。
在这里插入图片描述
其实应该把最后4位设置成相同。因为看一下hashCode计算代码,其实第一个元素是最高位,第一个进去的元素一直在乘以256。所以其实是最后四位比较重要。

测试随机生成一个ComplexOomage的方法如下,params结尾4个数字是相同的。

public static ComplexOomage randomComplexOomageEndsSame4() {
        int N = StdRandom.uniform(4, 10);
        ArrayList<Integer> params = new ArrayList<>(N);
        for (int i = 0; i < N - 4; i += 1) {
            params.add(StdRandom.uniform(0, 255));
        }
        for (int i = N - 4; i < N; i++) {
            params.add(9);
        }
        return new ComplexOomage(params);
    }

溢出详解

我开始设置的前四位数字相同,都为9。但是奇怪的是,在无效之后我进行了debug,但是并没有碰见任何,计算hashCode为0的时候。这就很奇怪了,最高位数远远超过了4位,应该早就溢出才对。其实做完之后还是有点懵逼,所以还是把溢出这事好好掰扯一下,才能知道这里面究竟发生了什么。

把测试的每个对象的hashCode计算过程用2进制打印出来,就很直观。可以看到是一个压栈堆栈的过程,满了之后就把高的八位pop,最后就剩下后4位数。

// params长度为4~10随机,后四位都是9
199 120 175 118 9 9 9 9 
11000111 // 0 + 199
1100011101111000 //199 * 256 + 120
110001110111100010101111 
11000111011110001010111101110110
11110001010111101110110000010010
10101111011101100000100100001001
11101100000100100001001000010010
10010000100100001001000010010000

47 201 141 48 72 9 9 9 9 
101111
10111111001001
1011111100100110001101
101111110010011000110100110000
11001001100011010011000001001000
10001101001100000100100000001001
11000001001000000010010000100100
10010000000100100001001000010010
10010000100100001001000010010000

63 68 176 9 9 9 9 
111111
11111101000100
1111110100010010110000
111111010001001011000000001001
1000100101100000000100100001001
10110000000010010000100100001001
10010000100100001001000010010000

61 191 184 225 87 9 9 9 9 
111101
11110110111111
1111011011111110111000
111101101111111011100011100001
10111111101110001110000101010111
10111000111000010101011100001001
11100001010101110000100100001001
10101110000100100001001000010010
10010000100100001001000010010000

222 22 217 230 9 9 9 9 
11011110
1101111000010110
110111100001011011011001
11011110000101101101100111100110
10110110110011110011000001001000
11011001111001100000100100001001
11100110000010010000100100001001
10010000100100001001000010010000

解决方法

把256换成31就行了。跟String类型的hashCode计算方法相同。

一个有意思的事

在做溢出实验的时候发现了几组有趣的结果。

// 1
1<<31 = -2147483648; 1<<32 = 1;
// 2
256<<23 = -2147483648; 256<<24 = 0; ... 256<<31 = 0; 256<<32 = 256;
// 3
1<<31 - 1 = 1073741824

分析之前先整理几个运算技巧:

  • 左移几位,就相当于右边填几个零
  • 一个数是2的几次方,就相当于1左移几位(二进制)

先单独看1。1左移31位,等于1后面有31个零。我们知道int类型是32位的,范围是 -2 ^ 32 ~ 2 ^ 32 - 1。以及计算机中是以补码表示数的,第32位是符号位,所以当32位为1的时候,表示的其实是负数的最小值—— -2 ^ 32也就是-2147483648。
然后1左移32位,也就是循环了一遍,1回到了第一位,等于1,没什么问题。

再看2。256 = 2^8 = 1<<8,所以256<<23相当于1<<31,输出了一样的结果。
但是当256<<24,相当于1<<32,却输出的是0。直到256<<31,输出仍然是0,然后256<<32就变成了256,完成了一次循环。

到这里我感觉“处理中心”中发生了这样的事:
在256最高位的1溢出32位范围之后,它还是被保存着,而计算机仍然显示原有的32位数字。左移24位时,原有32位数字所有位都是0,等于0;由于256有8位0,所以知道左移31位时,输出都是0,至左移32位,256的9位完全移出原32位数字时,显示了新的数字,也就是256。

做了其他数字的实验,是相同的结果:

8<<29 = 0; 8<<30 = 0; 8<<31 = 0; 8<<32 = 8;...

再看一组实验:

(1<<31) * 26 = 0(1<<31) * 2 = 0; (1<<31) * 52 = 0;
(1<<30) * 7 = -1073741824; (-1<<30) * 3 = 1073741824;
1<<31) * 13 = -2147483648;

所以乘法溢出有以下规律:

  1. 除去(+/-1<<31),乘以奇数时,绝对值不变,符号改变
  2. (+/-1<<31),乘以奇数时,保持不变
  3. 乘以偶数,视为先乘奇数,然后移位

这解释了为什么hashCode的进制最好用奇数。奇数相对偶数溢出时会保留信息,偶数会有移位操作。
至于为什么用质数,据说是因为传统。

再具体,为什么用31,因为它很好运算

31 * i == (i << 5) - i
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值