写在前面的话:
- 参考资料:《漫画算法》52页散列表
- 所需知识点:单链表的增删改查
- 本章知识难点:多个链表的增删改查
- IDE:IntelliJ IDEA 2021.2.1
- JDK:Java8
目录
散列表结构图:(代码根据该结构图制作出来)
项目结构:
1.散列表
MyHashTable.java
package ex01;
/*
散列表:
1.将传入的Key通过哈希函数转换成索引值
2.当第1步出现哈希冲突时,考虑使用拉链法(链表法)来解决问题
确定是否进行扩容的问题:
当size >= capacity * loadFactor时,需要进行扩容。
1.扩容:
数组长度变为原来的两倍。
2.重新Hash:
将原先在散列表中的值,根据哈希函数规则再次放入新的散列表中
*/
public class MyHashTable<Key, Value> {
private int size;//存放元素的个数
//这里体现了多个链表的使用
private Node<Key, Value>[] head;//头节点,含有多个链表【头节点也有多个】
private Node<Key, Value>[] tail;//尾结点,跟头节点对应
private float capacity;//散列表的容量 ==> 散列表的长度【与array.length相等】
private final float loadFactor = 0.75f;//负载因子 ==> 通常设定为0.75f
//仓库中相当于1个链表
private Node storeHouse_H;//放入插入散列表中的所有节点的头【相当于一个仓库】
private Node storeHouse_T;//放入插入散列表中的所有节点的尾【相当于一个仓库】
/**
* 链表的节点
*/
private class Node<Key, Value> {
//数据
Value data;
//关键key
Key key;
//下一个节点
Node next;
//创建一个节点时,将数据放进去
public Node(Key key, Value data) {
this.data = data;
this.key = key;
}
public Node() {
}
}
//创建散列表时需要用到
public MyHashTable() {
this(10);//未指定时,初始值就是10
this.capacity = 10;//创建完,更新散列表容量(capacity)
}
/**
* 实例化一个散列表时需要
*
* @param capacity 初始散列表的容量
*/
public MyHashTable(int capacity) {
if (capacity > 0) {
//创建一个存储空间为capacity的数组
head = (Node<Key, Value>[]) new Node[capacity];
tail = (Node<Key, Value>[]) new Node[capacity];
//初始化链表数组
for (int i = 0; i < capacity; i++) {
head[i] = new Node();
tail[i] = new Node();
}
} else {
//手动抛出异常
throw new IllegalArgumentException("参数capacity错误!capacity应不小于或等于0:" + capacity);
}
this.capacity = capacity;//创建完,更新散列表容量(capacity)
}
/**
* 通过哈希算法转换过来的索引值
*
* @param x 传入的Key值
* @return 返回一个索引值
*/
private int getHash(Key x) {
//确保索引值是正数
return (x.hashCode() & 0x7fffffff) % head.length;
}
/**
* 将数据插入散列表中
*
* @param key
* @param value
*/
public void insert(Key key, Value value) {
//判断数组是否需要扩容
if (size >= capacity * loadFactor) {
expandHashMap();
}
//当散列表中有多个数据,才会出现key相同的情况
if (size > 0) {
//判断参数key是否存在
if (isExist(key)) {
//若存在,则抛出异常
throw new IllegalArgumentException("参数key错误!参数key:" + key + "已存在!");
}
}
//先创建一个(将要插入的)节点
Node<Key, Value> insertedNode = new Node<>(key, value);
int index = getHash(key);
if (size == 0) {
//当散列表中还未有数据时,仓库为0
storeHouse_H = insertedNode;
storeHouse_T = insertedNode;
}
if (head[index].key == null) {
//此索引值无节点【还未放入元素】
//将插入节点设置成该索引值的头节点和尾节点
head[index] = insertedNode;//该索引值的头节点
} else {
//在该索引值位置是一个链表,将其插入到该索引值的链表尾部
tail[index].next = insertedNode;
}
tail[index] = insertedNode;//该索引值的尾节点
if (size != 0) {
storeHouse_T.next = insertedNode;//将节点插入到仓库当中进行保存
storeHouse_T = insertedNode;
}
size++;//元素个数增加
}
/**
* 从散列表中获取数据
*
* @param key 通过关键key来获取数据
* @return 返回获取到的数据
*/
public Value getValue(Key key) {
/*
思路:
1.通过哈希函数将参数key转变成索引值
2.查看当前索引值的key是否与参数key相等,若相等,返回数据,否则进入第3步
3.依次遍历当前索引值的整条链表,直至为空
*/
int index = getHash(key);//获取通过哈希函数转变成的索引值
Node<Key, Value> node = head[index];//从头节点开始遍历【当前索引值的链表的头节点】
if (node == null) {
//该索引值未放元素
return null;
}
for (; node.key != null; node = node.next) {
//判断key是否相等
if (node.key.equals(key)) {
return node.data;//若相等,则返回数据
} else {
node = node.next;//遍历下一个节点
}
}
return null;//遍历完,没有遇到匹配值,散列表中无该数据
}
/**
* 通过key来修改散列表
*
* @param key 关键key,定位到要修改的节点
* @param newValue 修改的新值
*/
public void change(Key key, Value newValue) {
if (size <= 0) {
throw new IllegalArgumentException("参数key错误!该散列表没有数据!");
}
//判断key是否存在
if (isExist(key)) {
//从此处开始,与查询相似【public Value getValue(Key key)】
int index = getHash(key);//通过哈希算法得到一个索引值
Node<Key, Value> node = head[index];//从头节点开始遍历【当前索引值的链表的头节点】
while (node.key != null) {
//判断key是否相等
if (node.key.equals(key)) {
node.data = newValue;//将新的值赋值给data
break;//修改后,跳出循环【没有这句,将成为死循环】
} else {
node = node.next;//遍历下一个节点
}
}
} else {
throw new IllegalArgumentException("参数key错误!未找到key:" + key);
}
}
/*
删除思路:
1.从仓库中删除掉该节点
2.获取索引值(通过哈希函数把key转换),找到对应链表
3.利用链表删除的思路(头节点删除、中间删除、尾部删除)
*/
public void delete(Key key) {
if (size <= 0) {
throw new IllegalArgumentException("参数key错误!该散列表没有数据!");
}
//判断key是否存在【存在才能够删除】
if (isExist(key)) {
int index = getHash(key);//通过哈希算法得到一个索引值
Node<Key, Value> node = head[index];//从头节点开始遍历【当前索引值的链表的头节点】
Node<Key, Value> storeNode = storeHouse_H;//仓库节点
Node<Key, Value> preStoreNode = null;//获取要删除仓库中节点前的一个节点
Node<Key, Value> preNode = null;//获取前一个节点
/*
先将简单的把仓库中的节点删除
*/
//遍历整个仓库中的节点,发现key相同的节点删除它【插入时已经判断key要唯一】
while (storeNode != null) {
if (storeNode.key.equals(key)) {
//发现与参数key相同
if (storeHouse_H.key.equals(key)) {//当要删除的仓库的头节点时
//这里与单个链表不同,需特别注意:这是由多个链表【size==1一定等于头节点,但size != 1不一定不是头节点】
if(size == 1){//当仓库中国只有1个元素时
size = 0;//元素个数为0
}else{
//当仓库中元素不只有1个,但是该节点是其中一个链表的头节点时,需特别注意!
if(storeNode.next == null){
//要删除的节点是刚加入的【要删除的节点为尾节点】
preStoreNode.next = null;//将前一个仓库节点的后一个节点设置为null
storeHouse_T = preStoreNode;//并将该节点设置为尾部节点
break;//删除后,跳出循环
}
storeHouse_H = storeNode.next;//移动仓库的头节点
}
break;//删除后,跳出循环
} else if (storeNode.next == null) {
//要删除的节点是刚加入的【要删除的节点为尾节点】
preStoreNode.next = null;//将前一个仓库节点的后一个节点设置为null
storeHouse_T = preStoreNode;//并将该节点设置为尾部节点
break;//删除后,跳出循环
} else {
//从中间删除的情况
preStoreNode.next = storeNode.next;//前一个节点的下一个 ==> 当前节点的下一个【删除成功】
}
}
//将此节点记录下来,作为下一节点的前一个节点
preStoreNode = storeNode;
storeNode = storeNode.next;
}
/*
从散列表中删除节点
*/
while (node != null) {
if (node.key.equals(key)) {
//发现与参数key相同
if (node.key.equals(key)) {
//此时相当于该索引值处的节点==>就是要删除的节点
node.key = null;//将key删掉,等同于删除这个节点【插入时,是判断有无key的】
break;//删除后,跳出循环
} else if (node.next == null) {
//要删除的节点是刚加入的【要删除的节点为尾节点】
preNode.next = null;//将前一个仓库节点的后一个节点设置为null
tail[index] = preNode;//并将该节点设置为尾部节点
break;//删除后,跳出循环
} else {
//从中间删除的情况
preNode.next = node.next;//前一个节点的下一个 ==> 当前节点的下一个【删除成功】
}
}
//将此节点记录下来,作为下一节点的前一个节点
preNode = node;
node = node.next;
}
} else {
throw new IllegalArgumentException("参数key错误!未找到key:" + key);
}
size--;//删除后,元素个数减一
}
/**
* 散列表进行扩容
*/
private void expandHashMap() {
//将整个头节点数组扩大成2倍
Node<Key, Value>[] newHead = new Node[head.length * 2];
head = newHead;
Node<Key, Value> node = storeHouse_H;
//重新hash
for (int i = 0; i < size; i++) {
insert(node.key, node.data);
node = node.next;
}
}
/**
* 判断插入的key是否在散列表中存在
*
* @param key 传入要插入的key
* @return 返回true -> 存在 false -> 不存在
*/
private boolean isExist(Key key) {
boolean flag = false;
Node node = storeHouse_H;//从仓库中头节点开始遍历,寻找可能存在的参数key
while (node != null) {
//判断是否存在参数key
if (node.key.equals(key)) {
flag = true;
}
node = node.next;
}
return flag;
}
@Override
public int hashCode() {
return super.hashCode();
}
}
2.测试类
Test.java
package ex01;
public class Test {
public static void main(String[] args) {
MyHashTable<Integer,String> myHashTable = new MyHashTable<Integer,String>(20);
/*
增加数据
*/
myHashTable.insert(1001,"张三");
myHashTable.insert(1002,"李四");
myHashTable.insert(1003,"王五");
myHashTable.insert(1004,"刘一");
myHashTable.insert(1005,"熊大");
myHashTable.insert(1006,"熊二");
myHashTable.insert(1007,"光头强");
myHashTable.insert(1008,"喜羊羊");
myHashTable.insert(1009,"孙悟空");
myHashTable.insert(1010,"kitty");
for(int i = 1001;i <= 1010;i++){
String name = myHashTable.getValue(i);
System.out.println(i + " " + name);
}
System.out.println("---------------------------add");
/*
修改数据
*/
myHashTable.change(1002,"甲乙");
for(int i = 1001;i <= 1010;i++){
String name = myHashTable.getValue(i);
System.out.println(i + " " + name);
}
System.out.println("---------------------------update");
/*
删除数据
*/
myHashTable.delete(1004);
myHashTable.delete(1005);
myHashTable.delete(1001);
myHashTable.delete(1010);
for(int i = 1001;i <= 1010;i++){
String name = myHashTable.getValue(i);
System.out.println(i + " " + name);
}
System.out.println("---------------------------delete");
/*
再次添加
*/
myHashTable.insert(1004,"灰太狼");
for(int i = 1001;i <= 1010;i++){
String name = myHashTable.getValue(i);
System.out.println(i + " " + name);
}
}
}
3.运行截图
增加部分:
修改1002的值改为”甲乙“:
删除部分:
删除之后,能够添加: