哈希表与哈希值

哈希表是一种数据结构,它可以提供快速的插入和查找操作。不管哈希表中有多少数据,插入和删除时间都是接近常量的时间:即O(1)。
缺点是:它是基于数组的,数组创建后难以扩展,哈希表被基本填满时,性能下降严重,而且不能够顺序遍历数据。

由于哈希表是基于数组的,所以就要把关键字转化成数组下标。
哈希函数负责把大范围的数字(由关键字计算得出)转换成小范围的数字。这个小范围的数字对应着数组下标。

转化时会碰到一个问题:不是数组每个位置都有值,也不是每个位置都保证只有一个值。
当冲突发生时,一个方法是通过系统的方法找到数组的一个空位,把这个数据填入,而不再用哈希函数得到的数组下标,这个方法叫做开放地址法
第二种方法是创建一个存放数据的链表的数组,这样当冲突发生时新的数据项直接接到数组下标所指的链表中。这种方法叫链地址法


java中object类的hashCode方法,默认返回的一般不是对象的地址,具体hashCode与jvm实现有关(C++实现的ObjectSynchronizer::FastHashCode调用get_next_hash方法计算hash值)。
大多数对象默认的hashCode意义不大,比如整数的hash值就是整数本身。
如果重写了对象的equals方法,一般也要重写hashCode方法,保证相等。
推测:保证对象相等时哈希值一致的目的,是为了hashSet,hashMap,hashtable等hash体系的数据结构具有高效的性能。

hash值需要遵守的约定:
  • 一致性(consistent),在程序的一次执行过程中,对同一个对象必须一致地返回同一个整数。
  • 如果两个对象通过equals(Object)比较,结果相等,那么对这两个对象分别调用hashCode方法应该产生相同的整数结果。
  • 如果两个对象通过java.lang.Object.equals(java.lang.Ojbect)比较,结果不相等,不必保证对这两个对象分别调用hashCode也返回两个不相同的整数。

开放地址法

在开放地址法中,若数据不能直接放在由哈希函数计算出来的数据下标所指的单元时,就需要寻找数组的其它位置。下面介绍探索开放地址的三种方法:线性探测、二次探测和再哈希法。

线性探测

如果542是要插入的位置,而它已经被占用了,那么就插入543,依次递增。
查找时,先根据关键字计算出哈希值对应的索引,如果有元素,检查是不是符合要求的,如果不是则去下一个索引找。
一种实现方式:

public class HashRelative {
    int arraySize;
    DataItem[] hashArray;
    public DataItem find(int key){
        int hashValu=hashFunc(key);

        while (hashArray[hashValu]!=null){
            if (hashArray[hashValu].key==key){
                return hashArray[hashValu];
            }
            hashValu++;
            hashValu %=arraySize;
        }
        return null;
    }

    public void insert(DataItem item){
        int key=item.key;
        int hashVal=hashFunc(key);

        while (hashArray[hashVal]!=null){
            ++hashVal;
            hashVal %= arraySize;
        }
        hashArray[hashVal]=item;
    }

    private int hashFunc(int key) {
        return key % arraySize;
    }
}

hash表的元素数量不能超过总容量的一半,当哈希表变得太满时,一个选择是扩展数组。
创建一个新的数组时,不能直接复制原数组的位置到新数组,而是要重新计算索引,这也叫做重新哈希化。
扩展后的数组容量通常是原来的2倍,实际上,数组容量应该是一个质数。
下面的算法可以帮助计算新数组的容量:

 private int getPrime(int min){
        for (int j=min+1;true;j++){
            if (isPrime(j)){
                return j;
            }
        }
    }

    private boolean isPrime(int n) {
        for (int j=2;j*j<=n;j++){
            if (n%j==0){
                return false;
            }
        }
        return true;
    }
二次探测

在开放地址法的线性探测中会发生聚集,一旦聚集形成,新的数据项就更容易发生移动,导致聚集增长变快。
装填因子:总元素个数/总容量。
装填因子太大时,会降低哈希表的性能。
二次探测是防止聚集产生的一种尝试。思想是探测相隔较远的单元,而不是相邻的单元。
二次探测步长为 hash+11,hash+22,hahs+3*3…
二次探测的问题是可能产生二次聚集,每有一个关键字映射到某个地址,就需要更长的步长探测。

再哈希法

为了消除原始聚集和二次聚集,有一种更好的方法再哈希法。
再哈希法是产生一种依赖关键字的探测序列,专家发现下面形式的哈希函数效果很好:
stepSize=constant-(key % constant);
其中constant是质数,且少于数组容量。

 public void insertData(DataItem item){
        int key=item.key;
        int hashVal=hashFunc(key);
        int step=hashFunc2(key);
        while (hashArray[hashVal]!=null){
            hashVal+=step;
            hashVal %= arraySize;
        }
        hashArray[hashVal]=item;
    }
     private int hashFunc(int key) {
        return key % arraySize;
    }
    public int hashFunc2(int key){
        return 5-key % 5;
    }

使用质数作为容量,是因为任何数都不会被整除,因此探测序列最终会检查所有单元。
使用开放地址策略时,探测序列通常用再哈希法生成。


链地址法

链地址法在哈希表每个单元中设置链表,这样就不需要寻找空位。
链地址法更加健壮,装填因子可以比1大,且对性能影响不大。

public void insert(Link item){
        int key=item.iData;
        int hashVal=hashFunc(key);
        
        links[hashVal].insert(item);
    }

public void insert(Link theLink){
        int key=theLink.iData;

        Link previous=null;
        Link current=first;
        while (current!=null && key >current.iData){
            previous=current;
            current=current.next;
        }
        if (previous==null){
            first=theLink;
        }else {
            previous.next=theLink;
        }
        theLink.next=current;
    }

哈希函数的核心是找到简单又快的哈希函数,而且去掉关键字的无用数据,并且尽可能的使用所有的数据。

对于字符串的hash值,可以采用下面一种取法
取32作为每一位的基数,使用位操作提高性能,提前取余防止溢出。

public static int hashFun(String key){
        int hashval=0;
        for (int j=0;j<key.length();j++){
            int letter=key.charAt(j)-96;
            hashval=(hashval<<5+letter)%size;
        }
        return hashval;
    }

对于固定长度length的数据源,可以考虑分组折叠法,根据数组容量设计分组策略,比如容量1000的数组,那么就是3个一组,length%3一组,共分为length/3+1组,把每组的值加起来就是哈希值了。容量100的数组,那么就是2个一组,length%2一组,共分为length/2+1组。

哈希化的效率
插入和搜索都可以达到O(1)级别,发生哈希值冲突时,插入和搜索时间与探测长度成正比。探测长度又取决于装填因子。

对于容量不变,内存充足,装填因子低于0.5时,线性探测简单好用。
二次探测和再哈希法性能相当,一般优于线性探测。小型的哈希表,再哈希法性能一般好一些。

当要保存的数据项数目未知时,优先考虑链地址法。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值