散列(hash)算法——Java表示

本文介绍了散列算法的基本概念、特点和常见类型,包括除余法、乘法、平方取中法、直接寻址法、数字分析法和折叠法。还探讨了Java中hashCode()和equals()的关系,以及Hash冲突的解决方法,如开放地址法、再哈希、链地址和建立公共溢出区。文章以JDK中的String类为例,展示了hashCode()的实现,并提到了HashMap中的冲突解决策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Hash,一般翻译做散列、杂凑,或音译为哈希,是把任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值。

百度百科给出的散列算法的解释如上,个人理解散列算法就是给个输入通过散列算法后输出得道固定长度的输出。
输入—>散列算法—>输出(固定长度)
第一个接触的散列算法应该是MD5吧…学JavaWeb的时候做密码加密。虽然现在已经不推荐使用了。
0.散列算法的特点
a.固定长度
b.不可逆
c.高速存储“空间换时间”
…先这些想到再加
1.常用散列算法
MD5,SHA这种的就不列了,我们说点基础的…
a.除余法
除余法就是用关键码a % M,并取余数作为散列地址。除余法几乎是最简单的散列方法,散列函数为: h(x) = x % M。

//取余法
    public int remainderHash(int a)
    {
        int b = 0;
        b = a % 10;
        return b;
    }

b.乘法
这种类型的Hash函数利用了乘法的不相关性(乘法的这种性质,最有名的莫过于平方取头尾的随机数生成算法,虽然这种算法效果并不好)

static int bernsteinHash(String key)
{
     int hash = 0;
     int i;
     for (i=0; i<key.length(); ++i) {
         hash = 33 * hash + key.charAt(i);
     }
     return hash;
}

jdk5.0里面的String类的hashCode()方法也使用乘法Hash。不过,它使用的乘数是31。推荐的乘数还有:131, 1313, 13131, 131313等等。(上面这个不是很典型…返回值长度可能不一致,理解意思)写个一样长的。

static int RSHash(String str)
    {
        int b    = 378551;
        int a    = 63689;
        int hash = 0;

        for(int i = 0; i < str.length(); i++)
        {
            hash = hash * a + str.charAt(i);
            a    = a * b;
        }
        return (hash & 0x7FFFFFFF);
    }

c.平方取中法
由于整数相除的运行速度通常比相乘要慢,所以有意识地避免使用除余法运算可以提高散列算法的运行时间。平方取中法的具体实现是:先通过求关键码的平方值,从而扩大相近数的差别,然后根据表长度取中间的几位数(往往取二进制的比特位)作为散列函数值。因为一个乘积的中间几位数与乘数的每一数位都相关,所以由此产生的散列地址较为均匀。
d. 直接寻址法
取keyword或keyword的某个线性函数值为散列地址。即H(key)=key或H(key) = a•key + b,当中a和b为常数(这样的散列函数叫做自身函数)。
e. 数字分析法
分析一组数据,比方一组员工的出生年月日,这时我们发现出生年月日的前几位数字大体同样,这种话,出现冲突的几率就会非常大,可是我们发现年月日的后几位表示月份和详细日期的数字区别非常大,假设用后面的数字来构成散列地址,则冲突的几率会明显减少。因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。
f.折叠法
将keyword切割成位数同样的几部分,最后一部分位数能够不同,然后取这几部分的叠加和(去除进位)作为散列地址。
2.hashCode与equals
对hashCode重写就必须重写equals,这事必须的…
我们看看String类中
String中hashCode与equals
hashCode
以31为权,每一位为字符的ASCII值进行运算,用自然溢出来等效取模,达到了目的——只要字符串的内容相同,返回的哈希码也相同。

public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];//看到了上面提到了31
            }
            hash = h;
        }
        return h;
    }

equals
equals方法包含了"==",双等号比较的是地址,存储地址相同,内容则相同。当地址不同的时候,先验证了比较对象是否为String,接着比较了两个字符串的长度,最后才循环比较每个字符是否相等。

public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
   }

不得不说人这JDK写的是好…
3.Hash冲突
比如用余数法上面我给出的A%10的例子中,1,11,21…的hash值都一样,这就造成了hash冲突,在算法上要尽量避免hash冲突,但是毕竟固定长度…出现冲突在所难免,所以需要对此解决。
a.开放地址
b.再哈希
c.链地址
d.建立公共溢出区

a.开放地址:开放地址法处理冲突的基本原则就是出现冲突后按照一定算法查找一个空位置存放。

1)线性探测再散列,即依次向后查找。
2)二次探测再散列,即依次向前后查找,增量为1、2、3的二次方。
3)伪随机探测再散列,伪随机,顾名思义就是随机产生一个增量位移。

b.再哈希:出现冲突后采用其他的哈希函数计算,直到不再冲突为止。
c.链地址:链接地址法不同与前两种方法,他是在出现冲突的地方存储一个链表,所有的同义词记录都存在其中。形象点说就行像是在出现冲突的地方直接把后续的值摞上去。
这也是HashMap中的解决办法(JDK8中当链表过长(>8)就会变成红黑树)
d.建立公共溢出区
设哈希函数的值域是[1,m-1],则设向量HashTable[0…m-1]为基本表,每个分量存放一个记录,另外设向量OverTable[0…v]为溢出表,所有关键字和基本表中关键字为同义词的记录,不管它们由哈希函数得到的哈希地址是什么,一旦发生冲突,都填入溢出表。

参考:https://www.jianshu.com/p/f9239c9377c5
https://segmentfault.com/a/1190000012201011

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值