何为Hash

1.hash的意思

hash在英语中表示将…弄乱,在计算机中指将任意长度的输入通过hash算法变换成固定长度的输出,该输出值就是散列值,不同的输入可能会出现相同的输出。简单来说就是一种将任意长度的消息通过压缩到某一固定长度的消息摘要函数。还是很抽象是吧,下面通过一个例子来说明hash的工作原理

2.举例理解hash

我们都知道数组支持按照下标随机访问数据的属性,比方说我们创建一个字符串类型的数组来存储湖人队的球员信息,球员信息以JSON字符串的形式存储,数组下标表示球员号码,科比的号码24,Lakers[24] 我们就能以O(1)的时间复杂度来快速查询到科比的信息,这种方式有个问题,因为球员的号码不是连续的,一支NBA球队通常有15个球员,而NBA球衣号码一般在100以内,那为了存储球员信息我们需要创建一个长度为100的数组,但是极大部分的数组元素是被浪费的。为了解决这种浪费,我们就可以使用hash来实现,hash既能保持快速查询又能不浪费空间。
我们以Java中的HashMap为例来说明一下hash具体是怎么实现的。
在这里插入图片描述
如上图所示,我们定义了一个hashmap用来存储湖人队的球员信息,这个map的键是球员号码,value是球员的具体信息。
我们来通过源码来看看这个hashmap是怎么工作的,我们看这个key的hash是怎么计算的

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    
    public int hashCode() {
        return Integer.hashCode(value);
    }

    public static int hashCode(int value) {
        return value;
    }
    
    (n - 1) & hash
--------------------------

    

上面是Java中HashMap计算Keyhash涉及到的几个方法,我们可以看到对于Integer这个类型,先是取了Integer的hashCode,Integer的Hashcode就是基本类型int的值,然后将这个数向右移16位,因为Java中int类型是32位,向右移16位后就相当于将高16位的值移到了低16位,然后将移位后的值和原始值进行异或运算,这样运算后得到的结果就是,高16位保持原来的信息,低十六位为原来高十六位和低十六位的异或结果,这样做的目的是增加随机性。然后用(n - 1) & hash 的位运算来取模,这里的n就是当前map的数组的长度,减一正好让取模后的值都落在数组下标内。
下面总结一下hashMap计算key的hash算法。

Integer.hashCode(value)   取key的java计算的hashCode
(h = key.hashCode()) ^ (h >>> 16)   对Java计算的原始HashCode进行异或运算
 (n - 1) & hash    用位运算进行取模,取模的范围为数组下标
 当 HashMap 的数组长度是 2 的整次幂时,(n - 1) & hash与 hash % n计算的结果是等价的,因为位运算效率更高,但是限制数组容量为2的整次幂。

下面我们将Java中这个HashMap生成hash值的算法拿出来单独计算看看,我们下面来用这套算法来计算任意字符串的hash值

public static void main(String[] args) {
        System.out.println("恩比德--->"+indexForHash(myHash("恩比德")));
        System.out.println("西蒙斯--->"+indexForHash(myHash("西蒙斯")));
        System.out.println("维金斯--->"+indexForHash(myHash("维金斯")));
        System.out.println("邓肯--->"+indexForHash(myHash("邓肯")));
        System.out.println("杜德利--->"+indexForHash(myHash("杜德利")));
        System.out.println("海沃德--->"+indexForHash(myHash("海沃德")));
        System.out.println("沃尔--->"+indexForHash(myHash("沃尔")));
        System.out.println("戴维斯--->"+indexForHash(myHash("戴维斯")));
        System.out.println("伦纳德--->"+indexForHash(myHash("伦纳德")));
        System.out.println("杜兰特--->"+indexForHash(myHash("杜兰特")));
        System.out.println("考辛斯--->"+indexForHash(myHash("考辛斯")));
        System.out.println("哈登--->"+indexForHash(myHash("哈登")));
        System.out.println("约基奇--->"+indexForHash(myHash("约基奇")));
        System.out.println("利拉德--->"+indexForHash(myHash("利拉德")));
        System.out.println("麦克勒莫--->"+indexForHash(myHash("麦克勒莫")));
        System.out.println("哈里斯--->"+indexForHash(myHash("哈里斯")));
        System.out.println("乔治--->"+indexForHash(myHash("乔治")));
    }

    public static int myHash(Object key) {
        int h;
        return ((h = key.hashCode()) ^ (h >> 16));
    }

    public static int indexForHash(int hashValue) {
        return (16 - 1) & (hashValue);
    }


恩比德--->11
西蒙斯--->1
维金斯--->12
邓肯--->14
杜德利--->1
海沃德--->3
沃尔--->12
戴维斯--->15
伦纳德--->2
杜兰特--->11
考辛斯--->5
哈登--->9
约基奇--->5
利拉德--->7
麦克勒莫--->7
哈里斯--->11
乔治--->14

Process finished with exit code 0

上面一共测试了17个字符串经过HashMap中的hash算法得到的hash值,我们发现是有冲突的,可以看到虽然hash大法确实好,但是会出现多个key经过hash后得到的值是一样的,这个世界就是这样,任何事都是有利有弊的。为了解决这种冲突,hash算法提供了两个方式,一种是链表法,冲突的位置生成一个链表。还有一种是开放寻址法。

3.解决hash冲突

  • 链接法
    链接法很简单,当发现出现hash冲突后,在该位置生成一个链表,冲突的数据以链表的形式链接。类似下图这种
    在这里插入图片描述
    Java中的Hash就是采取这种方法,并且防止冲突数量太多,在1.8后,当冲突的数据达到8个还会将链表变成红黑树以增加查找效率。
  • 开放寻址法
    开放寻址法的核心思想就是如果发现当前坑被占了,那就去找一个没有被占的坑,最容易想到的就是从当前位置依次往后找,直到找到空闲位置然后插入数据,查找元素的时候,先根据hash找到下标然后比较元素是否相等如果不相等就继续往后找,如果找到空闲位置还没发现该元素说明该元素不在当前的hash表中,插入,查找都解决了,那么删除呢,由于我们查找元素时在发现有空位置之前还没有找到我们就认为这个元素不在这个hash表中,那万一这个空闲位置是因为删除元素导致的,而要查找的元素就在这个位置的后面就会出错了,为解决这个问题我们可以设置一个标记,删除元素的时候给那个位置一个delete标记,那么在查找元素的时候遇到delete标记的位置就会继续往下找。
    除了这种探测新空闲的方法外开放寻址法还提供了双重散列来解决冲突,双重散列就是使用多个hash算法,当用第一个hash算法发现有冲突后,切换采用第二个,以此类推。

4.负载因子

当hash表中的数据越来越多时,发生冲突的概率就会提高,负载因子就是一个用来代表这个hash表拥挤程度的指标,计算公式负载因子 = 填入hash表的个数/hash表的长度,当负载因子越大,表明出现冲突的可能性越高。

5.hash算法的实现方式

  • 除法散列法
    就是通过将要进行散列的关键字除以hash表的长度取余数,来映射到hash表的某个位置上。
    h(k) = k mod m (k为关键字,m为hash表的长度)
    应用除法散列法时要避免选择m的值,m不应为2的幂,因为如果m = 2^p,则h(k)就是k的p个最低位数字。在设计散列函数时,应该考虑关键字的所有位。
  • 乘法散列法
    第一步,用关键字k乘上常数A(0<A<1),并提取kA的小数部分。第二步,用m乘以这个值,再向下取整。
  • 全域散列法
    所谓全域散列法就是随机选择散列函数,解绑关键字和散列函数的关系。当然不是每次操作都选择一个哈希函数,而是构建一个哈希表的时候随机选一个,选定之后这个哈希表的所有操作都是基于这个哈希函数,这种方法可以防止竞争对手别有用心的设计一个键集,同时也能避免某些键集永远会导致较差的性能,如果是,那么重新建一个表就行!
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值