自定义哈希表
* 哈希表又称为散列表 是一种数据结构 最典型的可以是数组+链表,数组+二叉树的结构 java中HashMap就是对散列表的一种实现
* 以数据+链表举例
* 具体需要有三个主要部分 分为1. 数组 2. 链表 3. 散列表需要放置的元素
* 数组的作用:用来存放链表
* 链表的作用:用来存放元素
* 元素的作用:存放需要记录的信息(对象)
* 通过计算散列值来确认数组的下标,以此表示需要将元素加入到那个链表
* java中HashMap通过key的hashcode值来计算散列值,以此确认数组的下标
代码实现如下:
package test.hash;
import java.util.HashMap;
/**
*@ClassName HashTabTest
*@Description 哈希表实现
* 哈希表又称为散列表 是一种数据结构 最典型的可以是数组+链表,数组+二叉树的结构 java中HashMap就是对散列表的一种实现
* 以数据+链表举例
* 具体需要有三个主要部分 分为1. 数组 2. 链表 3. 散列表需要放置的元素
* 数组的作用:用来存放链表
* 链表的作用:用来存放元素
* 元素的作用:存放需要记录的信息(对象)
* 通过计算散列值来确认数组的下标,以此表示需要将元素加入到那个链表
* java中HashMap通过key的hashcode值来计算散列值,以此确认数组的下标
**/
public class HashTabTest {
public static void main(String[] args) {
//HashMap<String, String> hashMap = new HashMap<>();
//测试自定义的散列表
HashTab hashTab = new HashTab(4);
hashTab.put(new Stu(1,"学生1"));
hashTab.put(new Stu(2,"学生2"));
hashTab.put(new Stu(3,"学生3"));
hashTab.put(new Stu(4,"学生4"));
hashTab.put(new Stu(5,"学生5"));
hashTab.put(new Stu(9,"学生9"));
hashTab.print();
System.out.println(hashTab.get(10));
System.out.println(hashTab.get(1));
}
}
class HashTab {
//定义数组 数组中元素类型为链表
private StuList[] table;
//初始化数组的大小
private int size;
public HashTab(int size) {
this.size = size;
table = new StuList[size];
//给数组中每个stuList列表进行初始化
for (int i = 0; i < size; i++) {
table[i] = new StuList();
}
}
//添加数据
public void put(Stu stu) {
//先获取散列值
int hashNo = getHashNo(stu.id);
table[hashNo].add(stu);
}
//查找通过id查找stu信息
public Stu get(int id) {
//先计算散列值
int hashNo = getHashNo(id);
return table[hashNo].get(id);
}
//遍历散列表
public void print() {
for (int i = 0; i < size; i++) {
table[i].print();
}
}
//获取数据的散列值 暂时不适用hashCode
public int getHashNo (int id) {
return id % size;
}
}
class Stu { //类似于hashMap中的node
int id;
String name;
Stu next;
public Stu(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "Stu{" +
"id=" + id +
", name=" + name +
'}';
}
}
class StuList { //实现数组+链表结构中的学生链表 ,只存header 头结点
private Stu head;
public void add(Stu stu) { //向链表添加数据
if (head == null) {
head = stu;
return;
}
//查找到最后元素,将最后元素的next指向stu
Stu temp = head;
while (true) {
if (temp.next == null) { //找到了最后元素
break;
}
temp = temp.next;
}
temp.next = stu;
}
//遍历链表数据
public void print() {
Stu temp = head;
while (true) {
if (temp == null) break;
System.out.println(temp);
temp = temp.next;
}
}
//通过id查找链表中数据 找到就打印出来
public Stu get(int id) {
Stu temp = head;
while (true) {
if (temp == null) {
//System.out.println("未找到该元素");
return null;
}
if (temp.id == id) {
//System.out.println(temp);
return temp;
}
temp = temp.next;
}
}
}
打印如下:
Stu{id=4, name=学生4}
Stu{id=1, name=学生1}
Stu{id=5, name=学生5}
Stu{id=9, name=学生9}
Stu{id=2, name=学生2}
Stu{id=3, name=学生3}
null
Stu{id=1, name=学生1}
分析HashMap
实现哈希表结构需要数组和链表形成散列,新元素加入哈希表关键是完成取余操作,通过上述自定义哈希表的编写基础后,我们看HashMap源码中是如何完成的取余操作,然后分析分析HashMap中的数组,链表结构,最后分析put方法的源码;
1.理解HashMap中的位运算
通过分析HashMap源码,会看到源码中有如下两处位运算操作:
i = (n - 1) & hash // n 是 hashmap中数组的长度,此句代码是一句取余操作
(h = key.hashCode()) ^ (h >>> 16) //用于减少hash冲突
这两个位运算的基本知识是:
按位与
&
,只有两个操作数都是1,结果为1,否则为0。1 & 1 = 1,0 & 1 = 0,1 & 0 = 0,0 & 0 = 0异或
^
,当两个操作数相同结果为0,否则为1。1 ^ 1 = 0,0 ^ 1 = 1,1 ^ 0 = 1,0 ^ 0 = 0
问题1:首先,(n - 1) & hash 为什么是一步取余操作呢?
假如有 hash = 12345678,转换成二进制为 101111000110000101001110
此时如果数组长度为16,那个(n-1)= (16 -1)= 15的二进制为 1111
那么此时进行 (n - 1) & hash 操作,如下图:
12345678 % 16 = 14
最后得到的余数为 1110 = 14,此时会发现蓝色部分和结果的绿色部分拼接起来刚好等于原本的hash值
所以,源码此处的 (n - 1) & hash 和 hash % table.length 的结果一样都是取余操作
问题2:HashMap中数组长度为什么要是2的n次方?
根据上面的分析可以得知,只有2的n次方的高位只有一处为1,其余低位皆为0,例如:
16 = 10000,那么在进行 -1 操作以后,低位全变为1,此时 15 = 01111,此时进行 按位与操作,高位数据全部变为0,只为取到hash值的低位数据即是余数,如果不是(2^n -1),那么低位数据结果就不是余数了。
问题3:如何理解 (h = key.hashCode()) ^ (h >>> 16) 这一句位运算呢?
分析:为了尽可能减少hash冲突
int占有32位,如果不进行这一步运算,那么下一个key值高位发生变化而1110不发生变化的概率很大,因为在取余操作中,高位对取余结果不会产生影响,那么如果我们将hashcode的值无符号右移16位,然后再与hashcode按位异或,那么就会尽可能的让32的每一位就尽可能的参与取余操作,减少哈希冲突。
2.HashMap的结构分析
通过以上实现哈希表的思路,java中HashMap中肯定也是数组+链表的结构,再分析下其源码:
Entry是Map接口的一个内部接口,它提供了实现一个Key,Value数据结构需要的方法
再看HashMap中的Node
Node是继承了Entry接口,实现了getKey和getValue方法,另外还有key,value,hash,next(指向下一个元素)
HashMap中 数组是什么,链表是什么 ,元素是什么?
数组是一个Node类型的数组,
链表是一个Node类型结点(自带next属性形成链表)
元素是一个K,V类型的映射类型对象
3.put方法源码分析
以下为源码的分析注释:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true); //调用hash方法进行异或位运算
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0) //将table赋给tab,n为数组的长度,如果数组为空,进行初始化扩容
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null) //先进行取模运算,然后判断数组当前位置是否为null,如果为空直接将新元素放入作为head,将所在数组的元素赋值给p
tab[i] = newNode(hash, key, value, null);
else { //如果取模运算的当前数组位置不为null
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))) //p此时代表数组当前元素,即是链表的head,如果发生了hash冲突,并且key相等,则将p复制给e
e = p;
else if (p instanceof TreeNode) //链表超过8就会自动转换成红黑树,这里是在判断该链表是否已经是红黑树,红黑树添加的方法和链表不同
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { //如果都不是,先找到链表最后元素,再加入到链表的末尾,这里的操作是线程不安全的
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) { //表示找到最后节点
p.next = newNode(hash, key, value, null); //将新元素加入到链表的末尾
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 此处判断当前链表的元素是否超过7,超过7将链表转换成红黑树
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) //如果不等于null,且key的hash和equals都相等,返回当前e
break;
p = e;
}
}
if (e != null) { // existing mapping for key 如果e不为null表示出现了相同的key,hashcode和equals都相等
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null) //将改节点的value替换成最新的,且把原value返回
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount; //modCount 用来记录map结构被修改的次数
if (++size > threshold) //如果size大于 threshold(即将发生扩容的阈值) 此时执行扩容
resize(); //扩容方法 每次扩容一倍 保持2的n次方
afterNodeInsertion(evict);
return null;
}