哈希表是一种数据结构,它可以快速的进行插入、查找、删除操作,无论数据量有多大,它都能把插入、查找和删除操作的时间复杂度降为O(1)级别。
哈希表是基于数组+链表进行实现的,数组创建后难以拓展,所以当哈希表中存储的数据过多时,会有很严重的性能下降。此时,我们可以通过扩展哈希表数组的长度来解决问题,但是扩展哈希表的长度是一个费时的过程。因此哈希表的设计人员必须根据实际的数据量来定义合适的哈希表数组的长度。
如果我们知道一个数组的元素的索引,那么我们就可以通过索引在O(1)时间内获得元素。因此,如果我们能够将key对象与索引建立起一一对应的关系,那么我们就可以通过key对应的索引快速找到value值了,这个key对象转化为索引的算法就是哈希算法,这个索引叫做哈希码(散列码)。
int hashCode = (int)(key^(key>>32)); //^是按位异或操作符,例如11110000^00000011得到11110011
long bits = Double.doubleToLongBits(key); int hashCode = (int)(bits^(bits>>32));
当key位字符串类型时,我们有两种方法进行处理:
第一种方法比较直观,我们可以将所有字符串的Unicode码求和作为字符串的哈希码。(Unicode,也叫统一码、万国码、单一码,是计算机科学领域里的一项业界标准,包括字符集、编码方案等。Unicode 是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。)该方案只考虑了key的各个字母的个数,并没有考虑到字母的顺序。因此遇到具有相同字母的key值,将产生很多冲突,例如eat和ate。
因此产生了第二种方法,来将字母的顺序考虑在内。具体的方法为:
这里的S0为s.charAt(i)。这个表达式被称为多项式哈希码。使用针对多项式求值的Horner规则,可以如下高效的计算该多项式:
这个计算对于较长的字符串来说会导致溢出,但是Java会忽略算术溢出。应该选择一个合适的b值来最小化哈希冲突。查阅资料可以知道,b的较好取值为31,33,37,39,41,String类中,hashCode()被重写为采用b=31的多项式哈希码。
我们通过哈希算法计算出来的哈希码可能是一个很大的整数,超出了哈希表数组索引的范围,所以我们需要将它缩小到合适的范围。假设哈希表的数组长度为N。将一个整数缩小到0~N-1的方法通常是:
index = hashCode % N;
理论情况下,为了保证索引均匀展开,应该为N选择一个素数。然而,选择一个大的素数将很耗时。Java API中java.util.HashMap的实现,N采用16作为初始值,每次reHash将N扩大一倍。这样做具有合理性。当N为2的整数幂时,可以通过&运算来压缩哈希码,位运算比乘、除、取余操作要快很多,可以提高哈希表的性能,如下所示:
index = hashCode & (N-1);
&是一个按位AND操作符,如果两个二进制数的相同位都为1,那么结果中的对应位也为1。例如,假如N=4,hashCode = 7,那么index = hashCode & (N-1) = 0111 & 0011 = 0011 转换成十进制就是3,结果和7%4是一样的。
为了保证哈希表中的元素是均匀分布的,java.util.HashMap中为主哈希函数增加了一个补充的哈希函数,该函数定义为:
private static void supplementalHash(int h){ h ^= (h>>>20)^(h>>>12); return h ^ (h >>> 7) ^ (h >>> 4); }
所以完整的哈希函数如下定义:
h(hashCode) = supplementalHash(hashCode) & (N-1);
补充的哈希函数避免了两个低位相同的数之间的冲突,比如10101000 & 00001111 和 11111000 & 00001111所得到的结果都是1000。使用补充的哈希函数将减少这种冲突。
我们使用int类型作为key值,String类型作为value值,来实现<k,v>对的插入、查找和删除操作,以及reHash(哈希表的拓展)操作。
package dataStructure; public class MyHashMap { Node[] memo ; int count ; //当前哈希表中存储的<k,v>对的个数; public MyHashMap() { //无参构造方法 count = 0; memo = new Node[4]; //此处将MyHashMap默认的初始容量为4,并没有设置最大容量,所以当遇到整数溢出/内存不够时,会出错。 } /* * 添加一个新的<k,v>对,到哈希表中。 * int类型的数据通过哈希函数还是它本身,所以就直接用int值作为哈希码。 * 这里压缩哈希码使用了取模运算,此算法并不高效。当哈希表长度为2的整数次幂时,可以使用 哈希值 &(数组长度-1)来压缩哈希码。 * 当多个key值算出的索引相同时,使用链表来处理哈希冲突,将新插入的节点作为链表的头节点。 */ public void put(int k , String v) { int index = k % memo.length; Node node = new Node(k,v); node.next = memo[index]; memo[index ] = node; count++; if(count>memo.length*2) { reHash(); } } /* * 返回索引为k的value值 * */ public String get(int k) { int index = k % memo.length; Node node = memo[index]; while(node!=null) { if(node.data.k == k) { return node.data.v; } node = node.next; } return null; } /* * reHash方法:将哈希表的数组长度扩展为原来的2倍 * 新建一个长度为原来2倍的新数组,遍历老数组,将老数组中的所有节点逐个插入到新数组中。 */ private void reHash() { Node[] newMemo = new Node[memo.length*2]; for (int i = 0; i < memo.length; i++) { Node node = memo[i]; while(node!=null) { int index = node.data.k % newMemo.length; Node tNode = new Node(node.data); tNode.next = newMemo[index]; newMemo[index ] = tNode; node = node.next; } } memo = newMemo; } //返回哈希表中元素的个数 public int size() { return count; } /* * remove方法,将key=k的元素移出哈希表 * 计算出索引,遍历索引处的链表,逐个比对key值,找到相同的值时返回value值 */ public boolean remove(int k) { int index = k % memo.length; Node node = memo[index]; if(node.data.k==k) { //头节点为要移除的节点 if(memo[index].next!=null) { memo[index] = memo[index].next; }else { memo[index] = null; } count--; return true; } Node preNode = node; node = node.next; while(node!=null) { if(node.data.k == k) { preNode.next = node.next; count--; return true; } node = node.next; } return false; } //返回哈希表中数组的长度 public int getLength() { return memo.length; } } class Node{ Data data; Node next; public Node(Data data) { this.data = data; } public Node(int k , String v) { Data data = new Data(k, v); this.data=data; } public Data getData() { return data; } } /** * key值为int类型 * value为String */ class Data{ int k; String v; public Data(int k,String v) { this.k = k ; this.v = v; } }
package test; import dataStructure.MyHashMap; public class TsetMyHash { public static void main(String[] args) { MyHashMap myHashMap = new MyHashMap(); myHashMap.put(1,"value1"); myHashMap.put(2,"value2"); myHashMap.put(3,"value3"); myHashMap.put(4,"value4"); myHashMap.put(5,"value5"); myHashMap.put(6,"value6"); myHashMap.put(7,"value7"); myHashMap.put(8,"value8"); myHashMap.put(9,"value9"); myHashMap.put(10,"value10"); System.out.println("key为6,对应的value值为:"+ myHashMap.get(6)); boolean flag = myHashMap.remove(6); System.out.println("remove(6)的执行结果为: "+flag ); System.out.println("哈希表的长度为: " + myHashMap.getLength()); System.out.println("哈希表存储的元素个数为: " + myHashMap.size()); } }
执行结果如下: