哈希的入门指南

  近期胡老师讲解了java中和哈希有关的数据结构,自己也想趁这个机会谈谈自己对哈希的认知。

  我不喜欢用一些概念去描述,那我就基于我的经历来谈谈哈希。记得第一次听到哈希的时候是在ACM校队里听到学长说的,后来做题的时候碰到了哈希的题目才第一次去查资料,去解题。才明白哈希其实是用来解决一些散列的问题的,而且能更快更高效的去查找元素。那举个例子哈,也是当时做题的时候遇到的——来源POJ3349:

  题目的大体意思就是给出n个雪花,每个雪花有6个边,给出6个边的长度,如果n个雪花中有两个雪花的边是对应相等的(对应相等就是一个雪花可以通过顺时针或者逆时针的旋转到达另一个雪花的状态,比如1,2,3,4,5,6和2,3,4,5,6,1就是对应相等),就输出“Twin snowflakes found.”否则就输出“No two snowflakes are alike.”n的范围是0—100000,每条边的长度是0—10000000。时间限制4s,空间限制65535K。

  好了,拿到这个题目,不知道大家有什么想法没有,不对,是有什么正确的解题方法没有。大部分同学看到这个题目大概想法就是n^2的比较,这是最暴力的思路,而且肯定也是不可行的,因为时间不允许。既然这次是讲哈希的,那么肯定就和哈希有关是不是。这道题就是典型的数字哈希题目。在哈希里面有个很重要的东西叫做Hash值,也就是通过这个Hash值来找相应的元素。为了减少时间复杂度我们可以给这个题目单独设定一个Hash值,如果两个雪花是对应相等的,那么Hash值一定相等,这样就可以通过Hash值来寻找答案。那么还有一个问题,我们要让n个雪花分布的尽可能的分散,如果所有的雪花的Hash值都一样的话就都分到一个组了那不就是最暴力的方法了吗?所以这就是哈希的作用之处,给出一个正解,我们定义Key=(a1+a2+a3+a4+a5+a6)%prime;其中的ax就是对应的一个雪花的各个边的长度,总共6条边。prime是一个常数,那么算出来的Hash值就是该雪花在哈希结构里面的位置,这里的哈希结构就是一个数组。是不是觉得很抽象?没关系,我们慢慢说。通过计算每个雪花的Hash值给每个雪花分组,这样的好处就是如果两个雪花是对应相等的,那么肯定就是在一个组,但是在一个组内的雪花不一定就是对应相等的,通过Hash值的计算方法很好的理解,之前举得例子变一下顺序就可以了,1,2,3,4,5,6和5,6,4,3,2,1。到这里大家撇开细节不管应该知道这道题目的思路是什么了吧:通过分组的方式降低比较的复杂度。那么问题是这个prime是什么?为什么用(a1+a2+a3+a4+a5+a6)%prime这个公式来计算Hash值?提前透漏prime是小于等于100000的最大的素数。那为什么这样做,就要说一下哈希函数了。

  之前说过哈希解决的问题。通过Hash值来寻找元素,但是如果两个元素的Hash值是一样的怎么办?这就发生了冲突,如果冲突很多了那么就失去了哈希的作用,那么确定每个元素的Hash值就很重要,进来计算Hash值的方法就很重要了,我们管这个方法叫做哈希函数。怎么定义哈希函数呢?对于不同性质的元素集合,哈希函数是不同的,在这里介绍一下我经常用的哈希函数:

  1).平方取中法:这个方法就是将每个数进行平方,然后去这个平方数的中间几位。因为中间几位的数是和之前的数相关的,那么平方之后可以让每个元素的差异继续放大,比如:11,12。平方之后分别是121,144,那么取21,44就可以作为Hash值,这种方法是比较接近“随机化”的。

  2).减去法:将每个元素减去一个相同的数,将结果作为Hash值。

  3).基数转换法:这个方法就是将一个数转化成另一个进制的数,取其中几位再转化成原来进制的数,思想也是放大元素之间的差异。

  4).取余法:通过取模某个数,将剩下的余数作为Hash值。理论研究表明,除留余数法的模p取不大于表长且最接近表长m素数时效果最好。相应看到这里大家应该知道prime取值的原理了吧,当然还可以取很多其他的不同值。

  另外还有字符串数值哈希法等等,在这里就不在赘述了。看到这里应该对哈希有些比较具体的认识了吧。当时我也是通过做题才真正认识了哈希这种东东,后来学习了java,看到了HashMap,HashSet,Hashtable这样的类,正巧胡老师也在讲这些玩意,就简单的说一下,拿HashMap作为例子好了:

  拿到一个类,我们会去关心他的方法,看API文档可得:


  这里面对应每个方法有很具体的说明,但是个人对于put这个方法很感兴趣,因为之前说了哈希要解决的就是怎么高效率的存储,那么这个put方法的实现就可以为我们解答java中是怎么实现哈希的,以及哈希函数是什么样子的。看一下源代码(注释自己加上去的):

public V put(K key, V value) {
	    //这里是为了处理传入的Key值为空的情况
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);//计算Hash值,哈希函数是什么样子的待会再看
        int i = indexFor(hash, table.length);//通过hash值找到储存的位置
		/*之前说过,不同的元素的hash值有可能是一样的,从下面的代码可以看出
		  如果hash值是一样的,那么就替换掉之前旧的元素的值,所以可看出HashMap
		  的一个特性,对于一个Key,只有一个value与之对应,并且总是最新的那个
		*/
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }
  好了我们再看一下哈希函数是什么样子的:

    final int hash(Object k) {
        int h = 0;
        if (useAltHashing) {
            if (k instanceof String) {
                return sun.misc.Hashing.stringHash32((String) k);
            }
            h = hashSeed;
        }

        h ^= k.hashCode();

        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
  很复杂,至于为什么用这种公式会让元素散列的程度更好,有兴趣的可以去看看,我也在研究,等有点眉目了会和大家分享。
  


  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值