1、HashTable的概述
从基本层面讲,数据结构有数组与链表两种。数组具有查找快,但插入耗时的特点;链表具有插入快,但查找费时的特点。有没有可能在查找与插入之间取得平衡呢?哈希表的诞生回答了这个问题。
构建一个好的哈希表,主要得考量哈希函数与解决冲突这两面。不管是哈希函数,还是解决冲突,往深里挖都是可以走得很远很远。
2、实现哈希表的思路
直接上思维导图,思维上建立了,写个哈希表就不那么费力了。
public class HashTable<K, V> { private int size;//元素个数 private static int initialCapacity=16;//HashTable的初始容量 private Entry<K,V> table[];//实际存储数据的数组对象 private static float loadFactor=0.75f;//加载因子 private int threshold;//阀值,能存的最大的数max=initialCapacity*loadFactor //构造指定容量和加载因子的构造器 public HashTable(int initialCapacity,float loadFactor){ if(initialCapacity<0) throw new IllegalArgumentException("Illegal Capacity:"+initialCapacity); if(loadFactor<=0) throw new IllegalArgumentException("Illegal loadFactor:"+loadFactor); this.loadFactor=loadFactor; threshold=(int)(initialCapacity*loadFactor); table=new Entry[threshold]; } //使用默认参数的构造器 public HashTable(){ this(initialCapacity,loadFactor); } //放入元素 public boolean put(K key,V value){ //取得在数组中的索引值 int hash=key.hashCode(); Entry<K,V> temp=new Entry(key,value,hash); if(addEntry(temp,table)){ size++; return true; } return false; } //添加元素到指定索引处 private boolean addEntry(HashTable<K, V>.Entry<K, V> temp, HashTable<K, V>.Entry<K, V>[] table) { //1.取得索引值 int index=indexFor(temp.hash,table.length); //2.根据索引找到该位置的元素 Entry<K,V> entry=table[index]; //2.1非空,则遍历并进行比较 if(entry!=null){ while(entry!=null){ if((temp.key==entry.key||temp.key.equals(entry.key))&&temp.hash==entry.hash &&(temp.value==entry.value||temp.value.equals(entry.value))) return false; else if(temp.key!=entry.key&&temp.value!=entry.value){ if(entry.next==null) break; entry=entry.next; } } //2.2链接在该索引位置处最后一个元素上 addEntryLast(temp,entry); } //3.若空则直接放在该位置 setFirstEntry(temp,index,table); //4.插入成功,返回true return true; } //链接元素到指定索引处最后一个元素上 private void addEntryLast(HashTable<K, V>.Entry<K, V> temp, HashTable<K, V>.Entry<K, V> entry) { if(size>threshold) reSize(table.length*4); entry.next=temp; } //初始化索引处的元素值 private void setFirstEntry(HashTable<K, V>.Entry<K, V> temp, int index, HashTable<K, V>.Entry<K, V>[] table) { if(size>threshold) reSize(table.length*4); table[index]=temp; //注意指定其next元素,防止多次使用该哈希表时造成冲突 temp.next=null; } //扩容容量 private void reSize(int newSize) { Entry<K,V> newTable[]=new Entry[newSize]; threshold=(int) (loadFactor*newSize); for(int i=0;i<table.length;i++){ Entry<K,V> entry=table[i]; //数组中,实际上每个元素都是一个链表,所以要遍历添加 while(entry!=entry){ addEntry(entry,newTable); entry=entry.next; } } table=newTable; } //计算索引值 private int indexFor(int hash, int tableLength) { //通过逻辑与运算,得到一个比tableLength小的值 return hash&(tableLength-1); } //取得与key对应的value值 protected V get(K k){ Entry<K,V> entry; int hash=k.hashCode(); int index=indexFor(hash,table.length); entry=table[index]; if(entry==null) return null; while(entry!=null){ if(entry.key==k||entry.key.equals(k)) return entry.value; entry=entry.next; } return null; } //内部类,包装需要存在哈希表中的元素 class Entry<K,V>{ Entry<K,V> next; K key; V value; int hash; Entry(K k,V v,int hash){ this.key=k; this.value=v; this.hash=hash; } } }注:(1)本代码中采用的hash算法是:第一步采用JDK给出的hashcode()方法,计算加入对象的一个哈希值,其中hashcode()在Object类中定义为:public native int hashcode();说明这是一个本地方法,它的具体实现跟本地机器相关。第二步是通过hashcode&(table.lenth-1),返回一个比length小的值,即为索引值。 (2)该代码解决冲突的办法是采用的“挂链法”。 (3)代码中的加载因子loadFactor是参见HashMap的源码,据说0.75是一个耗时与占用内存的折中值。 (4)插入元素时,若索引处位置非空,要与已有元素进行对比,根据java规范,并不强制不相等的两个对象拥有不相等的hashcode值,因此还需进一步调用equals方法进行判断。 3、与数组与链表进行性能对比 数组直接采用的java.util.ArrayList; 链表直接采用的java.util.LinkedList; 代码如下:
public class Main { public static ArrayList<User> al; public static LinkedList<User> ll; public static void main(String[] args) { Main m = new Main(); al = new ArrayList<User>(); ll = new LinkedList<User>(); m.insert(); m.find(9999); } public void insert(){ long l; //测试数组队列插入时间 l = System.currentTimeMillis(); for(int i=0; i<1000000; i++){ User u = new User(i,"abc"+i); al.add(u); } l = System.currentTimeMillis() - l; System.out.println("ArrayList插入(用时):"+l); //测试链表插入时间 l = System.currentTimeMillis(); for(int i=0; i<1000000; i++){ User u = new User(i,"abc"+i); ll.add(u); } l = System.currentTimeMillis()-l; System.out.println("LinkedList插入(用时):"+l); } public void find(int id){ long l; //测试数组队列查找时间 l = System.currentTimeMillis(); for(int i=0; i<1000000; i++){ if(al.get(i).getId() == id){ System.out.println("find it!"); l = System.currentTimeMillis()-l; System.out.println("ArrayList查找时间:"+l); break; } } //测试链表查找时间 l = System.currentTimeMillis(); for(int i=0; i<1000000; i++){ if(ll.get(i).getId() == id){ System.out.println("find it!"); l = System.currentTimeMillis()-l; System.out.println("LinkedList查找时间:"+l); break; } } } //测试用的User类 class User{ private int id; private String name; public User(int id, String name){ this.id = id; this.name = name; } public int getId(){ return id; } } }运行结果:
现在来看看哈希表:
public class Manager { public static void main(String[] args) { HashTable<String,String> ht=new HashTable<String,String>(); long beginTime=System.currentTimeMillis(); for(int i=0;i<1000000;i++){ ht.put(i+"", "test"+i); } long endTime=System.currentTimeMillis(); System.out.println("The HashTable insert time is:"+(endTime-beginTime)); long beginTime2=System.currentTimeMillis(); ht.get(9999+""); long endTime2=System.currentTimeMillis(); System.out.println("The HashTable Search time is:"+(endTime2-beginTime2)); } }运行结果:
注:同是插入1000000数量级的元素,同是查找第9999个元素。其时间效率对比应该是很明显的。 4、总结及感言 不弄懂散列表怎敢说自己是学习了数据结构的? 上学期的债今天终于偿还了。这一学习过程,深深体会到散列表这一数组与链表的综合体,的确是非常奇妙的。当然哈希表只是一个开始,是数组与链表的一个进阶。后面更博大精深的是树与图。 这一个过程中,首先是找到了两篇很有质量的博客,对于我理解散列表起到了关键作用, 时常感慨互联网的伟大,就是每个互联网的参与者都贡献出自己的智慧,薪火相传,终究会酝酿出更大的智慧。 另一个就是查看源代码,才真的感受到 代码之美,源代码聚集了无数程序员的智慧,其精炼,其逻辑的严密性,是不看不知道,一看就好中意。看来要持续进阶学习,源代码算是一条康庄大道。 参考资料: JDK API Hashtable Hashtable源代码 深入Java集合学习系列:HashMap的实现原理