哈希表,HashMap
HashMap = 数组 + 链表;
哈希表也可称为哈希映射。
设计哈希表的目的:快速查找
其实我这个类HashMap的实现方法并不复杂,其内部是一个容量很大的数组,通过在数组每个元素内挂一个链表来存储和查找数据的。
我们看一下官方HashMap的用法(Student类自备):
HashMap<String, Student> map = new HashMap<>();
map.put("1001", new Student(20191001, "张三"));
Student s = map.get("1001");
System.out.println(s);
控制台输出:
哈希表的设计
先实现一个最简单的哈希表:<Integer, Student>的映射
设计思想:建立一个超大的数组
Student[] a = new Student[100000000];
a[20191001] = ...
a[20191002] = ...
如图一,我们发现数组是不是太夸张了,且很多空间都比较浪费。所以优化一下取后四位数即可,如图二
Student[] a = new Student[10000];
a[1001]=...
a[1002]=...
这样,我们把数组设置10000的容量,通过数组下标就可以快速的查找到对应的数据。
具体代码实现:
public class MyHashMap
{
//创建一个容量为10000的数组
private Object[] buffer = new Object[10000];
//添加数据
public void put(Object key, Object value)
{
int index = key2index(key);
buffer[index] = value;
}
public Object get(Object key)
{
int index = key2index(key);
return buffer[index];
}
//映射key值(数组下标),将Key映射为数组下标Index
private int key2index(Object key)
{
//除10000取余
return (Integer)key % 10000;
}
}
关键步骤:Key2Index方法,将Key映射为数组下标Index
测试一下:
MyHashMap map = new MyHashMap();
map.put(20191001, new Student(20191001, "张三"));
map.put(20191002, new Student(20191002, "李四"));
map.put(20191003, new Student(20191003, "麻五"));
Student s = (Student)map.get(20191001);
System.out.println("结果:" + s);
控制台输出:
接下来我们发现一个新问题,如果新来一届新生20201001怎么办?
map.put(20201001, new Student(20201001, "小新"));
Student s = (Student)map.get(20191001);
System.out.println("结果:" + s);
不对啊,这不是我想要的结果啊。怎么回事呢?不难发现,我们存储数据时都是取后面四位数,2020级和2019级都是1001,后面添加的数据把前面的覆盖了,所以数组中只存在“小新”而没有“张三”。
这里就出现了位置冲突的问题
问题解决:
数组里不直接存储Student对象,而是挂一个链表!
位置冲突的处理:
- put():首先根据学号找到对应的位置,每个位置存放一个链表,把Student对象挂在链表上。
- get():首先根据学号找到对应的位置,每个位置存放着一个链表,遍历这个链表,找到相应的对象。
代码实现:
public class MyHashMap
{
//每个链表的节点
public static class Node
{
Object key;
Object value;
Node next;
}
//创建一个容量为10000的数组
private Node[] buffer = new Node[10000];
//这里使用有头链表实现
public MyHashMap()
{
//给所有数组元素添加一个头节点(空)
for(int i = 0; i < buffer.length; i ++)
buffer[i] = new Node();
}
//添加数据
public void put(Object key, Object value)
{
int index = key2index(key);
//创建一个链表,将键值对赋值
Node node = new Node();
node.key = key;
node.value = value;
//遍历节点找到尾节点,并将新节点附加
Node tail = buffer[index];
while(tail.next != null)
tail = tail.next;
tail.next = node;
}
public Object get(Object key)
{
int index = key2index(key);
Node tail = buffer[index];
//遍历节点找到key值所对应的节点,并返回
while(tail.next != null)
{
if(tail.next.key.equals(key))
return tail.next.value;
tail = tail.next;
}
return null;
}
//映射key值(数组下标),将Key映射为数组下标Index
private int key2index(Object key)
{
//除10000取余
return (Integer)key % 10000;
}
}
实现按名字检索(字符串查找)
此时,Key应该是字符串类型
我们需要把字符串映射为0-10000的 Index
解决方案:
- 取得字符串首字母,将其转化为整数,例如:“张三”取“首字母z”映射为109(参照ASCII码)
- 所有字母”z“开头的都挂在buffer[109]位置的链表下
代码实现:改造key2index方法
//映射key值(数组下标),将Key映射为数组下标Index
private int key2index(Object key)
{
//Key若是数字类型
if(key instanceof Integer || key instanceof Double)
{
return (Integer)key % 10000;
}
//Key若为字符串
else if(key instanceof String)
{
//取首字母转成字符
char n = ((String) key).charAt(0);
int p = n;
return p % 10000;
}
//直接取对象的HashCode(哈希码)
return key.hashCode() % 10000;
}
HashCode,Object下的方法,默认返回对象的内存地址(整型)
测试
map.put("zhangsan", new Student(20191001, "张三"));
map.put("zhangsi", new Student(20191002, "张四"));
Student s = (Student)map.get("zhangsi");
System.out.println("结果:" + s);
至此,HashMap的设计基本完成。
最后,再加上泛型的处理就OK啦!
上最终代码!
package my;
/**
* 作者:夏铭杰
* 修改于:2019-09-26
* */
public class MyHashMap<K, V>
{
//节点
public static class Node
{
Object key;
Object value;
Node next;
}
//数组缓冲区
private Node[] buffer = new Node[10000];
//初始化每个头节点
public MyHashMap()
{
for(int i = 0; i < buffer.length; i ++)
buffer[i] = new Node();
}
/*添加数据
* */
public void put(K key, V value)
{
int index = key2index(key);
Node node = new Node();
node.key = key;
node.value = value;
Node tail = buffer[index];
while(tail.next != null)
tail = tail.next;
tail.next = node;
}
/*取得数据
* */
public V get(K key)
{
int index = key2index(key);
Node tail = buffer[index];
while(tail.next != null)
{
if(tail.next.key.equals(key))
return (V)tail.next.value;
tail = tail.next;
}
return null;
}
/*删除
* */
public void remove(K key)
{
int index = key2index(key);
Node tail = buffer[index];
while(tail.next != null)
{
if(tail.next.key.equals(key))
tail.next = tail.next.next;
else
tail = tail.next;
}
}
//哈希函数
public Integer key2index(K key)
{
//Key若是数字类型
if(key instanceof Integer || key instanceof Double)
{
return (Integer)key % 10000;
}
//Key若为字符串
else if(key instanceof String)
{
//取首字母转成字符
char n = ((String) key).charAt(0);
int p = n;
return p % 10000;
}
//直接取对象的HashCode(哈希码)
return key.hashCode() % 10000;
}
}
测试
MyHashMap<String, Student> map = new MyHashMap<>();
map.put("1001", new Student(20191001, "张三"));
Student s = map.get("1001");
System.out.println(s);
结果
Key和Value可以是任意类型。这就实现了JDK中HashMap的基本功能了。