HashMap
Hash这个名字对于学过计算机的人来说是一点都不陌生,接触过算法和数据结构的人应该也不陌生。哈希(Hash)那可真的是一个非常牛的人,不管是对计算机还是数学方面,我们这些后来者只能是默默的望着人家的背影,学习人家走过的路。
Hash在现在的计算机中已经不单单是指一个人的名字,更是一种计算机思想。如果没有他这个人的思想,起码人类在数据处理方面可能还要推迟几年时间,现在很多地方都用到了Hash,比如Hash编码。
1 什么是Hash
我们都知道计算机中数据的存储方式是二进制的方式,这些数据该怎么处理、怎么存放,在当时那个存储设备低下的时代是一个很大的问题。
Hash就是可以将任意长度的二进制数据通过某种映射规则装换成固定长度的二进制数据。
任意长度:比如基本数据类型,Java中的学生对象等等数据。value
映射规则:Hash算法,可以确定这种唯一映射关系。Hash函数
固定长度:其实说固定长度会让人产生误解,应该说是标识符更合理一些,比如学生对象的数据可以用学号来表示。key
2 Hash表
应该把Hash表想象成是数组,里面存放的应该是具体的数据和下标,注意这里的下标并不是key,而是通过key求解出来的下标值。
Hash表的作用就是:通过Hash函数求解key对应的下标值,通过该下标值对其相应的value值进行操作。
Hash表数据的存取操作的时间复杂度是O(1),这个和数组的类似,只需要知道key就可以求出下标,知道下标就可以获取value值。
3 Hash函数
上面所说的映射规则就是Hash函数,Hash函数并不是唯一的,可以有很多种,常见的有除留取余法。
通过Hash函数以及key可以算出value所对应的下标值。
除留取余法:就是取模运算,不过这个除数有要求,一般是小于数组长度的最大质数,比如数组长度为20,那么除数取19。
但是这样的Hash函数会出现一个问题:Hash冲突。
4 Hash冲突
Hash冲突是指通过hash函数算出来的下标值有相同的,但是他们的key和value并不相同。
解决Hash冲突的方法:线性探测法、链表形式。
线性探测法:如果某一个下标已经有值,那么就依次向后寻找没有值得下标,然后将其放入就行。
链表形式:将第一个插入该下标的值作为表尾元素,后面来的相同下标的元素就依次插入该下标位置,原来的就向后移。这个和头插法是一个意思,如下图。
5 HashMap
了解过Hash,那么就可以基本上知道HashMap的工作原理了,key-value模式就是上面介绍所说的,Java将映射规则给封装好了,Hash冲突也一起给解决了。Java中对HashMap的封装是比较好的。
通过观察源码可以发现,HashMap有几个关键的点:初始容量(Java默认是16)、负载因子(默认是0.75)、下标的求解方式、哈希冲突的解决方式。
知道了HashMap的基本原理,我们可以自己实现一个简单的HashMap,能够简单的进行存取操作,实现思路如下:
初始容量:16
负载因子:0.75
下标求解:除留取余法
Hash冲突:链表形式,Java中没有指针,所以只能用引用来指向下一个元素
实体类Entry:这是数组中存放的数据对象,指向链表下一个元素的引用、value、key
/**
*
* @author Administrator
* 数组中存放的实体类,包括Key、Value
* 哈希冲突用链表形式来解决,所以需要第三个字段,next指针
*/
public class Entry {
private Object key;
private Object value;
private Entry entry;
public Entry(Object key, Object value, Entry entry) {
super();
this.key = key;
this.value = value;
this.entry = entry;
}
public Entry() {
super();
}
public Object getKey() {
return key;
}
public void setKey(Object key) {
this.key = key;
}
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
public Entry getEntry() {
return entry;
}
public void setEntry(Entry entry) {
this.entry = entry;
}
}
/**
*
* @author Administrator
* 自定义HashMap集合,实现put和get方法
* 需要的基本属性:初始容量、负载因子、实际长度size
* 必须的属性:元素数组类型
*/
public class MyMap<K, V>{
//数组初始容量为16,规定是2的倍数
private static int defaultLength = 16;
//负载因子为0.75
private static double defaultLoad = 0.75;
//数组元素的长度
private static int size;
//数组元素
private Entry[] entry = null;
public MyMap(int defaultLength, double defaultLoad) {
this.defaultLength = defaultLength;
this.defaultLoad = defaultLoad;
this.entry = new Entry[defaultLength];
}
public MyMap() {
this(defaultLength, defaultLoad);
}
public void put(K key, V value) {
//1、根据key值获取哈希表中的value下标
int index = getIndex(key);
//获取该下标对应的value值
Entry indexEntry = entry[index];
//判断该下标的元素是否已存入元素
if(indexEntry == null) {
//元素不存在,那么就将value值得实体存入,并且是null
Entry newEntry = new Entry(key, value, null);
entry[index] = newEntry;
} else {
//元素存在,则用头插法插入新元素
Entry newEntry = new Entry(key, value, indexEntry);
entry[index] = newEntry;
}
}
public Object get(K key) {
int index = getIndex(key);
//没有考虑到同一个index里的链表结构的数据
return entry[index].getValue();
}
//根据key来获取数组下标,用除留取余法
public int getIndex(K key) {
//小于数组长度的最大质数
int divide = defaultLength - 1;
return key.hashCode() % divide;
}
}
测试类:
public static void main(String[] args) {
MyMap<String, Object> map = new MyMap<String, Object>();
map.put("one", "爱你一万年");
map.put("two", "月光宝盒");
System.out.println("Key is one, Value is " + map.get("one"));
System.out.println("Key is two, Value is " + map.get("two"));
}
说明:这个只是单纯的实现了基本的存取操作,存在某一些缺陷,读者可以自己进行补充。