Java实现Hash表
前言
数组加链表的形式实现的Hash表
一、Hash表是什么?
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。
二、代码
package com.cxf.study.hash;
import java.util.Iterator;
/**
* @Description: java实现hash表
* @Author: xiaowang
* */
public class MyHashMap<K, V> implements Iterable<MyHashMap.Node<K, V>> {
// 存数据的数组
private Node<K, V>[] table;
//键值对个数
private int size = 0;
// 数组默认长度
private static final int DEFAULT_LENGTH = 16;
// 默认负载因子大小
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 负载因子(负载因子*当前数组长度=当前数组存储数据的阈值)
private final float LOAD_FACTOR;
// hash表数组最大长度
private static final int MAXIMUM_LENGTH = 1 << 30;
// 当前数组存储数据的阈值,一旦size大于这个值就要进行resize()
private int threshold;
/**
* 构造器
*/
public MyHashMap() {
LOAD_FACTOR = DEFAULT_LOAD_FACTOR;
}
public MyHashMap(float load) {
if (load <= 0) {
throw new RuntimeException("负载因子大小不能为负且不能为0");
}
LOAD_FACTOR = load;
}
public MyHashMap(int initSize) {
if (initSize <= 0) {
throw new RuntimeException("初始化大小不能为负且不能为0");
}
if (initSize > MAXIMUM_LENGTH) {
initSize = MAXIMUM_LENGTH;
}
LOAD_FACTOR = DEFAULT_LOAD_FACTOR;
threshold = tableSizeFor(initSize);
}
public MyHashMap(int initSize, float load) {
if (initSize <= 0) {
throw new RuntimeException("初始化大小不能为负且不能为0");
}
if (load <= 0) {
throw new RuntimeException("负载因子大小不能为负且不能为0");
}
if (initSize > MAXIMUM_LENGTH) {
initSize = MAXIMUM_LENGTH;
}
LOAD_FACTOR = load;
threshold = tableSizeFor(initSize);
}
/**
* 添加元素,key不允许为null
*/
public void put(K key, V value) {
// 判空
if (key == null) {
throw new NullPointerException("key must not be null!");
}
// 首次添加,需要去初始化散列表
if (table == null || table.length == 0) {
initTable();
}
// 新节点
Node<K, V> newNode = new Node<>(key.hashCode(), key, value, null);
// 获取下标
int i = indexByHash(key,table);
// 该下标已经存在的节点
Node<K, V> existedNode = table[i];
if (existedNode == null) {
table[i] = newNode;
// 计数器
size++;
} else {
// 寻找key相同的节点
if (isSame(key, existedNode)) {
existedNode.value = value;
} else {
Node<K, V> temp = existedNode.next;
while (temp != null && !isSame(key, temp)) {
existedNode = existedNode.next;
temp = existedNode.next;
}
if (temp == null) {
existedNode.next = newNode;
// 计数器
size++;
} else {
temp.value = value;
}
}
}
// 判断size是否大于阈值
if (size > threshold) {
resize();
}
}
/**
* 根据key获取值
*/
public V get(K key) {
return getNodeByKey(key) == null ? null : getNodeByKey(key).value;
}
/**
* 移除元素
*/
public V remove(K key) {
if (table == null || table.length == 0 || size == 0) {
return null;
}
if (key == null) {
return null;
}
// 获取下标
int i = indexByHash(key,table);
// 该下标已经存在的节点
Node<K, V> existedNode = table[i];
if (existedNode != null) {
if (isSame(key, existedNode)) {
if (existedNode.next == null) {
table[i] = null;
} else {
table[i] = existedNode.next;
}
size--;
return existedNode.value;
} else {
Node<K, V> temp = existedNode.next;
while (temp != null && !isSame(key, temp)) {
existedNode = existedNode.next;
temp = existedNode.next;
}
if (temp != null) {
// 重新按链
existedNode.next = temp.next;
size--;
return temp.value;
} else {
return null;
}
}
} else {
return null;
}
}
/**
* size
* */
public int size(){
return size;
}
/**
* 清空hash表
* */
public void clear(){
Node<K,V>[] tab = table;
if (tab != null && size > 0){
for (int i = 0; i < tab.length; i++) {
tab[i] = null;
}
size = 0;
}
}
/**
* toString
* */
public String toString() {
if (table == null || table.length == 0 || size == 0) {
return "{}";
}
StringBuilder str = new StringBuilder();
str.append("{");
Node<K, V> node = getFirstNode();
for (int i = 0; i < size; ) {
if (i == size - 1) {
str.append(node).append("}");
} else {
str.append(node).append(",");
}
node = getAfterNode(node);
i++;
}
return str.toString();
}
/**
* 获取数组的首个节点
**/
private Node<K, V> getFirstNode() {
if (table != null) {
Node<K, V> t;
for (int i = 0; i < table.length; i++) {
if (table[i] != null) {
t = table[i];
return t;
}
}
}
return null;
}
/**
* 获取特殊意义上的下一个节点(如果该节点在链表上有next节点,
* 则下一个节点为next节点,如果没有,则根据下标遍历数组)
*/
private Node<K, V> getAfterNode(Node<K, V> node) {
Node<K, V> nextNode = null;
// 如果节点有next
if (node.next != null) {
nextNode = node.next;
} else {
for (int i = indexByHash(node.key,table) + 1; i < table.length; i++) {
if (table[i] != null) {
nextNode = table[i];
break;
}
}
}
return nextNode;
}
/**
* 初始化数组
*/
private void initTable() {
if (threshold > 0) {
table = new Node[threshold];
} else {
table = new Node[DEFAULT_LENGTH];
threshold = (int) (DEFAULT_LENGTH * LOAD_FACTOR);
}
}
/**
* 一个key对应相同的节点
*/
private boolean isSame(K key, Node<K, V> node) {
return node.key == key || (key.hashCode() == node.key.hashCode() && key.equals(node.key));
}
/**
* 根据key获取节点
*/
private Node<K, V> getNodeByKey(K key) {
if (table == null || table.length == 0 || size == 0) {
return null;
}
if (key == null) {
return null;
}
// 下标
int index = indexByHash(key,table);
Node<K, V> temp = table[index];
while (temp != null && !isSame(key, temp)) {
temp = temp.next;
}
return temp;
}
/**
* 简易哈希算法(用hashCode值作为hash值),
* 通过key的hashCode值对数组长度取余算出下标(效率底下,
* 建议采用HashMap的高16位算法)
*/
private int indexByHash(K key,Node<K,V>[] tab) {
return key.hashCode() % tab.length;
}
/**
* 当散列表的元素个数大于阈值的时候,进行重新编排
* (也就是把当前数组的元素一个一个拿出来,重新算出在新数组
* 的下标,然后放进新数组里)
* */
private void resize() {
// 老数组
Node<K, V>[] oldTable = table;
// 老数组长度
int oldLength = oldTable.length;
// 新数组长度
int newLength = oldLength << 1;
// 新数组
Node<K, V>[] newTable = (Node<K, V>[])new Node[newLength];
// 新阈值
int newThreshold = (int) (newLength * LOAD_FACTOR);
for (int i = 0; i < oldLength; i++) {
Node<K, V> oldNode = oldTable[i];
if (oldNode != null) {
// 声明一个中间变量
Node<K, V> tempNode;
do {
tempNode = oldNode.next;
// 重新编排的下标
int index = indexByHash(oldNode.key,newTable);
// 新数组的位置的元素
Node<K, V> existed = newTable[index];
// 新数组下标指向老节点
newTable[index] = oldNode;
// 头插法插入
oldNode.next = existed;
// 向下走一格
oldNode = tempNode;
} while (oldNode != null);
}
}
// 新阈值赋值
threshold = newThreshold;
// table指向新数组
table = newTable;
}
/**
* 获取刚好大于或等于a的2的n次方,也
* 可以简写为 a |= a >>> 1
* a |= a >>> 2
* ...
* */
private int tableSizeFor(int a) {
a = a - 1;
a = a | a >>> 1;
a = a | a >>> 2;
a = a | a >>> 4;
a = a | a >>> 8;
a = a | a >>> 16;
return a < 0 ? 1 : a > MAXIMUM_LENGTH ? MAXIMUM_LENGTH : a + 1;
}
/**
* 匿名内部类实现迭代器
* */
@Override
public Iterator<Node<K, V>> iterator() {
Iterator iterator = new Iterator<Node<K,V>>() {
// 当前编号 初始化为0
private int index = 0;
// 上一个节点
private Node<K, V> lastNode;
// 当前节点 初始化为首个节点
private Node<K, V> theNode = getFirstNode();
@Override
public boolean hasNext() {
return index < size;
}
@Override
public Node<K, V> next() {
if (!hasNext()) {
throw new RuntimeException("NoSuchElementException");
}
lastNode = theNode;
theNode = getAfterNode(lastNode);
index++;
return lastNode;
}
};
return iterator;
}
/**
* 内部类:节点
* */
public static class Node<K, V> {
final int hash;
final K key;
V value;
Node<K, V> next;
Node(int hash, K key, V value, Node<K, V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
@Override
public String toString() {
return key + "=" + value;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
}
}
三、测试代码
package com.cxf.study.hash;
import java.util.Scanner;
public class TestHash {
public static void main(String[] args) {
MyHashMap<Integer,String> map = new MyHashMap<>(2,2.0f);
Scanner scanner = new Scanner(System.in);
Scanner scanner2 = new Scanner(System.in);
boolean isCan = true;
while (isCan){
showWord();
int flag = scanner.nextInt();
switch (flag){
case 1 :
System.out.println("请输入要添加的key:");
Integer key1 = scanner.nextInt();
System.out.println("请输入要添加的value:");
String value1 = scanner2.nextLine();
map.put(key1,value1);
System.out.println("添加成功~~~");
break;
case 2 :
System.out.println("请输入key:");
Integer key2 = scanner.nextInt();
String value = map.get(key2);
if (value == null){
System.out.println("无数据~~~");
}else{
System.out.println("数据:"+value);
}
break;
case 3 :
System.out.println("哈希表数据:\n"+map.toString());
break;
case 4 :
System.out.println("遍历hash表开始");
for (MyHashMap.Node<Integer,String> node:map){
System.out.println(node.getKey()+"->"+node.getValue());
}
System.out.println("遍历结束");
break;
case 5 :
System.out.println("哈希表键值对的个数为:"+map.size());
break;
case 6 :
map.clear();
System.out.println("链表已经清空...");
break;
case 7 :
System.out.println("请输入要删除的key:");
int key7 = scanner.nextInt();
String oldData = map.remove(key7);
System.out.println("数据:"+oldData+" 已删除...");
break;
default:
isCan = false;
System.out.println("已退出...");
break;
}
}
}
private static void showWord(){
System.out.println("欢迎来到键值记录系统,"+
"退出:0;"+
"添加键值对:1;"+
"根据key获取:2;"+
"展示hash表:3;"+
"foreach遍历:4;"+
"查看键值对个数:5;"+
"清空hash表:6;"+
"删除键值对:7;");
System.out.println("请选择功能:");
}
}
总结
以上就是今天要讲的内容,本文只是简单介绍了散列表的实现以及测试。
笔者在这里记录几个问题:
1.当数组水平扩容时没有加限制,达到一定数据量(2的30次方*0.75)会报错
2.数组加链表的形式固然能满足解决hash冲突的问题,但链表也有可能太长,会使查询效率大打折扣,jdk1.8采用红黑树进行优化
3.为了方便测试,笔者采用两个参数的有参构造,测试强度不够
4.在对节点分配下标采用的是hashcode%table.length,建议大家参考JDK1.8的HashMap的hash方法
其他同学肯定有更好的实现,有写的不妥的地方还请指出。上面代码没有进行特别详细的测试,如果有发现bug的童鞋希望能及时提出来,共同学习,一起进步。