一、散列
散列是为了解决快速插入和快速查找的功能,它是基于数组+链表来实现的
1.散列结构
- 散列的节点:实际存储的节点
- 散列函数:hash函数,它通过一定的算法将所存储节点的key转化为一个整数,我们可以根据这个计算出来的整数值来确定它应该存储到散列表的什么位置
- 散列表:用于存放一系列散列节点的表
注:我们通过key计算出其hashCode值,但不同的key可能其hashCode值相同,这就是所谓的hash碰撞,处理hash碰撞就很多方式,比如重新hash,还有开放地址法等,我们这里采用链表的方式来处理哈希碰撞
2.利用链表来解决hash冲突
- 如果计算出的hash值,与原来的某个节点相同,而它应该存储的位置被另外一个节点占用了,此时,我们可以在存储位置的节点加上一个next指针,指向新的加入到这个位置的节点
二、动手实现一个散列结构
1.散列表的几个组成部分
- size : 散列表元素个数
- nodes : 存储节点的数组
- capacity:占用的空间
- Node : 节点定义
/**
* 散列表的大小
*/
private int size;
/**
* 散列表存储节点的数组
*/
private Node<K, V>[] nodes;
/**
* 容量
*/
private int capacity;
/*
* 散列表的节点
*/
private class Node<K, V> {
//键
private K key;
//值
private V value;
//键对应的hash码
private int hashCode;
//键的下一个节点指针
private Node<K, V> next;
}
2.添加元素(包括重置元素值)
- 新增的元素:该键不存在,查找到元素应该存储的位置,如果所有位置已经被占用,则在应该存储的位置的第后一个节点,通过next指针指向新增的节点
- 修改元素的值:该键已经存在,查找到元素修改值即可
/**
* 添加元素
* @param key
* @param value
* @return
*/
public boolean put(K key, V value) {
//1.新添加一个节点
Node<K, V> node = new Node<K, V>(key, value, null);
//2.计算出节点存储到数组中的位置
int index = getIndex(node.getHashCode());
//3.查找存储的位置上的节点
Node<K, V> indexNode = (Node<K, V>) nodes[index];
//4.定义一个是否为新节点的标识(默认为新增节点)
boolean newNode = true;
//5.如果插入的位置上节点不存在,那么直接将新增节点放入这个位置即可
if (indexNode == null) {
nodes[index] = node;
} else {
//否则说明该位置上有其他节点,定义一个当前指针
Node<K, V> current = indexNode;
//6.如果当前节点的下一个节点不为null,即这个位置有不少于2个节点的数据,一直遍历
while (current.getNext() != null) {
//7.如果插入的键已经存在
if (key.equals(current.getKey())) {
//插入的值存在的话,那么修改它的值,并且将newNode设置为false,即不是新增节点
current.setValue(value);
newNode = false;
break;
}
current = current.getNext();
}
//8.如果是新增节点,设置最后一个节点的next为新增的节点
if (newNode) {
current.setNext(node);
}
}
//9.如果是新增节点,则更新表节点的长度
if (newNode) {
this.size ++;
}
return true;
}
3.获取元素
- 先通过key计算出节点在数组中的存储位置
- 再在相应的位置上查找与key相等的节点即可
/**
* 获取元素值
* @param k
* @return
*/
public V get(K k) {
//1.获取键对应的索引位置
int index = getIndex(k.hashCode());
//2.获取该位置的节点
Node<K, V> node = nodes[index];
if (node != null) {
Node<K, V> current = node;
//遍历包含节点在内的链表,一直查找到与key相等的节点
while (current != null) {
if (k.equals(current.getKey())) {
return current.getValue();
}
current = current.getNext();
}
}
return null;
}
4.删除元素值
- 先通过key找到存储节点的位置
- 依次遍历位置上所有的节点,并记录遍历的节点的前一个节点,如果当前节点的key与删除的key相等
- 那么首先获取删除元素的值
- 如果删除元素的前一个元素为null,即删除的存储位置的第一个元素,那么让存储位置的元素指向当前删除元素的下一个元素
- 如果删除元素不是第一个元素,那么将删除元素的上一个元素的next指针指向删除元素的下一个元素
- 删除当前元素存储的值
/**
* 删除某个key对应的元素
* @param key
* @return
*/
public V remove(K key) {
//1.通过key获取在数组中的存储位置
int index = getIndex(key.hashCode());
//2.通过位置获取节点
Node<K, V> current = nodes[index];
//3.如果不存在,直接返回
if (current == null) {
return null;
}
//4.定义删除节点的前一个节点
Node<K, V> prev = null;
while (current != null) {
//5.如果查找到key对应的节点
if (current.getKey().equals(key)) {
//获取原值
V oldValue = current.getValue();
//如果上一个节点不为null,即删除的节点不是所在位置的第一个元素
if (prev != null) {
//设置前一个节点的next指向删除元素的下一个元素
prev.setNext(current.getNext());
} else {
//将数组对应的位置设置为当前元素的上一个元素(因为第一个元素已经被删除了,所以要更新数组中的对应的元素)
nodes[index] = current.getNext();
}
//清除删除元素
current = null;
//节点数更新
size --;
return oldValue;
}
//6.记录上一个节点
prev = current;
//7.当前节点下移
current = current.getNext();
}
return null;
}
5.散列的实现完整代码
package com.shixinke.demo.dataStruct;
public class MyHashMap<K,V> {
/**
* 散列表的大小
*/
private int size;
/**
* 散列表存储节点的数组
*/
private Node<K, V>[] nodes;
/**
* 容量
*/
private int capacity;
/**
* 默认的容量
*/
private static final int DEFAULT_CAPACITY = 5;
/**
* 不带参数的初始化,使用默认容量
*/
public MyHashMap() {
this.capacity = DEFAULT_CAPACITY;
clearMap();
}
/**
* 指定容量初始化
* @param capacity
*/
public MyHashMap(int capacity) {
if (capacity > DEFAULT_CAPACITY) {
this.capacity = capacity;
} else {
this.capacity = DEFAULT_CAPACITY;
}
clearMap();
}
/**
* 清除占用的空间
*/
public void clear() {
clearMap();
}
public void clearMap() {
this.size = 0;
this.nodes = new Node[this.capacity];
}
/**
* 添加元素
* @param key
* @param value
* @return
*/
public boolean add(K key, V value) {
return put(key, value);
}
/**
* 添加元素
* @param key
* @param value
* @return
*/
public boolean put(K key, V value) {
//1.新添加一个节点
Node<K, V> node = new Node<K, V>(key, value, null);
//2.计算出节点存储到数组中的位置
int index = getIndex(node.getHashCode());
//3.查找存储的位置上的节点
Node<K, V> indexNode = (Node<K, V>) nodes[index];
//4.定义一个是否为新节点的标识(默认为新增节点)
boolean newNode = true;
//5.如果插入的位置上节点不存在,那么直接将新增节点放入这个位置即可
if (indexNode == null) {
nodes[index] = node;
} else {
//否则说明该位置上有其他节点,定义一个当前指针
Node<K, V> current = indexNode;
//6.如果当前节点的下一个节点不为null,即这个位置有不少于2个节点的数据,一直遍历
while (current.getNext() != null) {
//7.如果插入的键已经存在
if (key.equals(current.getKey())) {
//插入的值存在的话,那么修改它的值,并且将newNode设置为false,即不是新增节点
current.setValue(value);
newNode = false;
break;
}
current = current.getNext();
}
//8.如果是新增节点,设置最后一个节点的next为新增的节点
if (newNode) {
current.setNext(node);
}
}
//9.如果是新增节点,则更新表节点的长度
if (newNode) {
this.size ++;
}
return true;
}
/**
* 获取元素值
* @param k
* @return
*/
public V get(K k) {
//1.获取键对应的索引位置
int index = getIndex(k.hashCode());
//2.获取该位置的节点
Node<K, V> node = nodes[index];
if (node != null) {
Node<K, V> current = node;
//遍历包含节点在内的链表,一直查找到与key相等的节点
while (current != null) {
if (k.equals(current.getKey())) {
return current.getValue();
}
current = current.getNext();
}
}
return null;
}
/**
* 删除某个key对应的元素
* @param key
* @return
*/
public V remove(K key) {
//1.通过key获取在数组中的存储位置
int index = getIndex(key.hashCode());
//2.通过位置获取节点
Node<K, V> current = nodes[index];
//3.如果不存在,直接返回
if (current == null) {
return null;
}
//4.定义删除节点的前一个节点
Node<K, V> prev = null;
while (current != null) {
//5.如果查找到key对应的节点
if (current.getKey().equals(key)) {
//获取原值
V oldValue = current.getValue();
//如果上一个节点不为null,即删除的节点不是所在位置的第一个元素
if (prev != null) {
//设置前一个节点的next指向删除元素的下一个元素
prev.setNext(current.getNext());
} else {
//将数组对应的位置设置为当前元素的上一个元素(因为第一个元素已经被删除了,所以要更新数组中的对应的元素)
nodes[index] = current.getNext();
}
//清除删除元素
current = null;
//节点数更新
size --;
return oldValue;
}
//6.记录上一个节点
prev = current;
//7.当前节点下移
current = current.getNext();
}
return null;
}
/**
* 获取表的长度
* @return
*/
public int size() {
return size;
}
/**
* 是否为空
* @return
*/
public boolean isEmpty() {
return size() == 0;
}
/**
* 是否包含某个key
* @param key
* @return
*/
public boolean containsKey(K key) {
//1.通过键获取键存储的位置
int index = getIndex(key.hashCode());
//2.通过索引查找到在数组中的节点
Node<K, V> current = nodes[index];
//3.如果位置上的节点为null,即不存在,直接返回
if (current == null) {
return false;
}
//4.遍历比较key与链表中的key是否一致
while (current != null) {
if (current.getKey().equals(key)) {
return true;
}
current = current.getNext();
}
return false;
}
/**
* 打印散列表数据
*/
public void print() {
Node<K, V> p = null;
System.out.print("{");
for (int i = 0; i < nodes.length; i++) {
p = nodes[i];
while ( p != null) {
System.out.print("["+i+"]:"+p.getKey()+"="+p.getValue()+",");
p = p.getNext();
}
}
System.out.println("}");
}
/**
* 获取某个值在数组中的对应的索引
* @param hashCode
* @return
*/
private int getIndex(int hashCode) {
return hashCode % this.capacity;
}
/**
* 散列表节点
* @param <K>
* @param <V>
*/
private class Node<K, V> {
//键
private K key;
//值
private V value;
//键对应的hash码
private int hashCode;
//当前键的下一个节点
private Node<K, V> next;
public Node(K key, V value, Node<K, V> next) {
this.key = key;
this.value = value;
this.hashCode = key.hashCode();
this.next = next;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
public int getHashCode() {
return hashCode;
}
public void setValue(V value) {
this.value = value;
}
public void setNext(Node<K, V> next) {
this.next = next;
}
public Node<K, V> getNext() {
return next;
}
}
}
6.例子
MyHashMap<String, Integer> testMap = new MyHashMap<>(3);
testMap.put("A", 1);
testMap.put("B", 2);
testMap.put("C", 3);
testMap.put("D", 4);
testMap.put("E", 5);
testMap.put("F", 6);
testMap.put("G", 7);
testMap.print(); //{[0]:A=1,[0]:F=6,[1]:B=2,[1]:G=7,[2]:C=3,[3]:D=4,[4]:E=5,}
System.out.println(testMap.containsKey("F")); //true
System.out.println(testMap.get("F")); //6
testMap.remove("F");
testMap.print(); //{[0]:A=1,[1]:B=2,[1]:G=7,[2]:C=3,[3]:D=4,[4]:E=5,}
testMap.remove("E");
testMap.print(); //{[0]:A=1,[1]:B=2,[1]:G=7,[2]:C=3,[3]:D=4,}