Hash 浅谈:计算与碰撞

起因

最近我使用 Doris 的 Bitmap 类型做用户统计时,发现 Doris 字符串转数字的 Hash 函数并不能很好地满足需求。因此,我想寻找一种更合适的 Hash 函数。我问同事:“你知道有没有一种 Hash 函数,既能够生成只有 32 位的数字,又能将碰撞率控制在千万分之一以下?” 他听完后说:“如果你真的找到了这样的函数,请务必再帮我找找有没有又大又便宜的房子,重点是大,而且便宜。” 我听完只能哈哈大笑,觉得这似乎是不可能的。但是,经过一番研究,我发现,事情并没有那么简单,这样的函数是可以存在的。

什么是 Hash

Hash 这个词对我来说真的是既熟悉又陌生,一直都知道 Hash 算法,但从来不知道它究竟是怎么做到的。

Hash 这个单词的含义是:
n. 剁碎的食物;混杂,拼凑;重新表述
vt. 搞糟,把……弄乱;切细;推敲

Hash 函数是一种将任意长度的输入数据映射为固定长度输出数据的函数。

怎么 Hash

我们以 MurmurHash 算法生成 32 位整型为例讲一下 Hash 算法具体的计算过程。

MurmurHash 是一种非加密哈希函数,广泛用于基于哈希的查找。它由 Austin Appleby 于 2008 年创建,旨在快速高效,同时仍能提供良好的分布和低碰撞率。它的名字来自于它内部循环中使用的两个基本操作:乘法(MU)和旋转(R)。

算法输入数据的种子和长度将散列值 h 初始化为随机值。然后以 4 字节块处理输入数据,应用一系列按位操作,并通过常量 m 对每个块进行乘法、通过常量 r 对每个块进行旋转,以生成哈希值 k。然后使用 k 更新哈希值 h。在处理所有 4 字节块后,该函数处理输入数据的剩余字节(如果有的话),并对散列值 h 应用额外的按位运算和 m 乘法。最后,该函数返回散列值 h。下面是具体的算法:

public static int hash32(final byte[] data, int length, int seed) {
    // 'm' and 'r' are mixing constants generated offline.
    final int m = 0x5bd1e995;
    final int r = 24;

    // Initialize the hash to a random value
    int h = seed^length;
    int length4 = length/4;

    for (int i=0; i<length4; i++) {
        final int i4 = i*4;
        int k = (data[i4+0]&0xff) +((data[i4+1]&0xff)<<8)
                +((data[i4+2]&0xff)<<16) +((data[i4+3]&0xff)<<24);
        k *= m;
        k ^= k >>> r;
        k *= m;
        h *= m;
        h ^= k;
    }
    
    // Handle the last few bytes of the input array
    switch (length%4) {
    case 3: h ^= (data[(length&~3) +2]&0xff) << 16;
    case 2: h ^= (data[(length&~3) +1]&0xff) << 8;
    case 1: h ^= (data[length&~3]&0xff);
            h *= m;
    }

    h ^= h >>> 13;
    h *= m;
    h ^= h >>> 15;

    return h;
}

一些计算细节

mr 分别用于进行乘法和旋转运算,也是 MurMurHash 名字的来源,它们的值是开发者调试出来的、认为效果比较好的值。

&0xff 操作用于确保字节值被视为无符号值。 & 是与运算,0xff 是十进制的255,二进制的 11111111。本质上做完与运算后,所有的1和0都是不变的。但是在 Java 中,bytes 是有符号的,&0xff 操作后,Java 会把它当作一个无符号的数字看待,因此可以实现转为无符号值

length&~3 等同于将 length 向下舍入到 4 的倍数,具体逻辑如下:

假设 length=5,用二进制表示是 101,将3的二进制表示取反,得到~3的二进制表示为:11111111111111111111111111111100

将 5 的二进制表示和~3 进行按位与操作,即:

101     (5的二进制表示)
& 11111111111111111111111111111100   (~3的二进制表示)
----------------------------------
100     (结果,二进制表示)

按位与的操作规则是若相应位都为1,则结果该位为1,否则该位为0,因此,只有在5的二进制表示的前2位与 ~3 的二进制表示的前 2 位都为 1 时才会得到 1,其他位都是 0。因此,所得到的结果为 100,转化为十进制就是 4

length&~3 等同于将 length 的二进制表示的最后两位全部清零,相当于将 length 向下舍入到 4的倍数。

Hash 碰撞率

了解了 Hash 计算的过程,我们再来看看怎么计算 Hash 碰撞率。

Hash 碰撞是指两个不同的输入数据,经过 Hash 函数计算后,得到相同的哈希值的情况。Hash 函数的设计目的是尽可能地减少碰撞率,即使输入数据发生微小的变化,也能够得到截然不同的哈希值。然而,由于哈希值的长度是固定的,而输入数据的长度是不确定的,因此,Hash 碰撞是不可避免的。

Hash 计算的过程是根据输入在一个给定的空间中寻找一个数字,碰撞率就是找到相同数字的概率。为了方便计算 Hash 碰撞率,可以将其转换为这样一个问题:

假设有 k 个随机生成的值,其中每个值都是小于 N 的非负整数,则至少有两个值相等的概率是多少?

计算两个值相等的概率,可以通过计算其补集,然后用 1 减去就好了。给定一个包含 N 个可能哈希值的空间,假设你已经选择了一个单一的值。之后,剩下 N-1 个值与第一个值不同。因此,随机生成两个互不相同的整数的概率是 N − 1 N \frac{N-1}{N} NN1。之后,有 N − 2 N-2 N2 剩余值与前两个值是唯一的,这意味着随机生成三个都是唯一的整数的概率是 N − 1 N × N − 2 N \frac{N-1}{N}\times\frac{N-2}{N} NN1×NN2

以此类推可以得到:

N − 1 N × N − 2 N × ⋯ × N − ( k − 2 ) N × N − ( k − 1 ) N \frac{N-1}{N}\times\frac{N-2}{N}\times\dots\times\frac{N-(k-2)}{N}\times\frac{N-(k-1)}{N} NN1×NN2××NN(k2)×NN(k1)

随着 N 的增大,上面的式子约等于下面的式子:

e − k ( k − 1 ) 2 N e^{\frac{-k(k-1)}{2N}} e2Nk(k1)

因此碰撞概率计算公式如下,其中 k 是生成的个数,N 是总的空间

1 − e − k ( k − 1 ) 2 N 1 - e^{\frac{-k(k-1)}{2N}} 1e2Nk(k1)

对应 1 − e − X 1 - e^{-X} 1eX 形式的式子,当 X 比较小的时候, 1 − e − X ≈ X 1 - e^{-X} \approx X 1eXXX 小意味着生成的个数 k 比较小,或者总的空间 N 比较大,因此,对于小碰撞概率,我们可以使用简化表达式:

k ( k − 1 ) 2 N \frac{k(k-1)}{2N} 2Nk(k1)

由于 k(k - 1) k 2 k^2 k2 之间的差别不是很大,可以进一步将其简化为如下形式:

P ( A ) = k 2 2 N P(A)=\frac{k^2}{2N} P(A)=2Nk2

这样,我们就得到了一个十分简洁的计算 Hash 碰撞率的式子。回到文章开头的那个问题:有没有一种 Hash 函数,既能够生成只有 32 位的数字,又能将碰撞率控制在千万分之一以下?

我们计算一下,代入式子:

1 10000000 = k 2 2 ∗ 2 32 \frac{1}{10000000}=\frac{k^2}{2*2^{32}} 100000001=2232k2

k ≈ 29 k \approx 29 k29

换句话说,如果仅生成 29 个数字,那么 32 位数字的 Hash 值碰撞率可以在千万分之一以下。虽然这个答案可能有点鸡肋,但至少回答了一个不知道的问题。此外,这也学到了不少东西,这才是重要的。

参考资料

  • https://github.com/tnm/murmurhash-java
  • https://preshing.com/20110504/hash-collision-probabilities

我正在使用免费的纯真社区版IP库。纯真(CZ88.NET)自2005年起一直为广大社区用户提供社区版IP地址库,只要获得纯真的授权就能免费使用,并不断获取后续更新的版本。
纯真除了免费的社区版IP库外,还提供数据更加准确、服务更加周全的商业版IP地址查询数据。纯真围绕IP地址,基于 网络空间拓扑测绘 + 移动位置大数据 方案,对IP地址定位、IP网络风险、IP使用场景、IP网络类型、秒拨侦测、VPN侦测、代理侦测、爬虫侦测、真人度等均有近20年丰富的数据沉淀。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值