原文地址:《Hash地址冲突解决之开放定址法》
1、什么是hash冲突
hash函数也被称为散列函数,就是把任意长度的输入,通过散列算法,变成固定长度的输出,该输出就是散列值。
这种转换是一种压缩映射,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值,也就是不同的输入值可能得到相同的输出值。当在使用数组存储对象时,如果使用对象的hash值来分配对象在数组中的位置,当多个不同的对象出现相同的hash值时,它们在数组中存储的位置出现了冲突,这种由于hash值相同造成的冲突也被称为hash冲突。
2、什么是开放定址法
接着hash冲突来说,在实际项目中,出现了hash冲突肯定是需要解决的,开放地址法就是解决hash冲突的一种方式。它是使用一种 探测方式在整个数组中找到另一个可以存储值的地方。
它基本思想是:当发生地址冲突时,按照某种方法继续探测哈希表中的其他存储单元,直到找到空位置为止。这个过程可用下式描述:
H i ( key ) = ( H ( key )+ d i ) mod m ( i = 1,2,…… , k ( k ≤ m – 1))
其中: H ( key ) 为关键字 key 的直接哈希地址, m 为哈希表的长度, di 为每次再探测时的地址增量。
采用这种方法时,首先计算出元素的直接哈希地址 H ( key ) ,如果该存储单元已被其他元素占用,则继续查看地址为 H ( key ) + d 2 的存储单元,如此重复直至找到某个存储单元为空时,将关键字为 key 的数据元素存放到该单元。
增量 d 可以有不同的取法,并根据其取法有不同的称呼:
( 1 ) d i = 1 , 2 , 3 , …… 线性探测再散列;
( 2 ) d i = 1^2 ,- 1^2 , 2^2 ,- 2^2 , k^2, -k^2…… 二次探测再散列;
( 3 ) d i = 伪随机序列 伪随机再散列;
3、开放地址法实例
在代码实例中的开放定址法使用的是线性探测的方法直到找到一个可以存放元素的位置位置。由于开放定址法处理hash冲突的方式原因,在删除数组的一个元素时,需要对其都续的元素进行处理判断,以免中断数据存储的连续性造成后续无法获取元素。
其基本的添加元素操作如下:
如图所示,NodeD最终会被存放在6的位置。整个集合操作的部分代码如下:
public class TestSet {
private Node[] nodes;
public TestSet(){
this(16);
}
public TestSet(int capacity){
nodes = new Node[capacity];
}
public void add(Node node){
if(node == null){
return;
}
int index = node.hashCode();
for(;;){
if(nodes[index] == null){
nodes[index] = node;
break;
}
index++;
}
}
public void remove(Node node){
int index = node.hashCode();
for (;;){
if(nodes[index] == node) {
nodes[index] = null;
break;
}
if(nodes[index] == null){
break;
}
index++;
}
index = index+1;
for(;;){
if(nodes[index] != null){
Node node1 = nodes[index];
nodes[index] = null;
add(node1);
}else{
break;
}
index++;
}
}
private int nextIndex(int index){
return (index+1)>=nodes.length?0:(index+1);
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
for(int i = 0;i<nodes.length;i++){
Node node = nodes[i];
if(node == null){
continue;
}
builder.append("index = ").append(i).append(", ").append(node.toString()).append("\n");
}
return builder.toString();
}
}
节点对象定义代码如下。重写对象hashcode生成方法,制造hash冲突:
public class Node {
private int value ;
private String name;
public Node() {
}
public Node(int value, String name) {
this.value = value;
this.name = name;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public int hashCode() {
return value/5;
}
@Override
public boolean equals(Object obj) {
return super.equals(obj);
}
@Override
public String toString() {
return "[ name = "+name+" , value = "+value+" , hashCode = "+hashCode()+" ]";
}
}
移除数组中的元素大致流程如下:
在删除NodeA之后需要对NodeA后续的元素进行处理,如果没有进行处理,那个将无法根据对象的hashCode找到NodeD,NodeD在没有重新分配位置时还一直占据着6的位置。具体的代码在上图已有展示。
public class HashApplication {
static Random random = new Random();
public static void main(String[] args){
TestSet set = new TestSet(16);
List<Node> nodeList = new ArrayList<>();
for(int i = 0;i<10;i++){
Node node = new Node(random.nextInt(64),"name"+i);
set.add(node);
nodeList.add(node);
}
System.out.println("添加元素:");
System.out.println(set.toString());
set.remove(nodeList.get(3));
System.out.println("删除元素:"+nodeList.get(3).toString());
System.out.println(set.toString());
set.remove(nodeList.get(7));
System.out.println("删除元素:"+nodeList.get(7).toString());
System.out.println(set.toString());
}
}
4、代码测试结果
添加元素:
index = 2, [ name = name0 , value = 12 , hashCode = 2 ]
index = 6, [ name = name6 , value = 32 , hashCode = 6 ]
index = 7, [ name = name2 , value = 39 , hashCode = 7 ]
index = 8, [ name = name5 , value = 37 , hashCode = 7 ]
index = 9, [ name = name9 , value = 34 , hashCode = 6 ]
index = 10, [ name = name4 , value = 50 , hashCode = 10 ]
index = 11, [ name = name1 , value = 56 , hashCode = 11 ]
index = 12, [ name = name3 , value = 62 , hashCode = 12 ]
index = 13, [ name = name7 , value = 56 , hashCode = 11 ]
index = 14, [ name = name8 , value = 55 , hashCode = 11 ]
删除元素:[ name = name3 , value = 62 , hashCode = 12 ]
index = 2, [ name = name0 , value = 12 , hashCode = 2 ]
index = 6, [ name = name6 , value = 32 , hashCode = 6 ]
index = 7, [ name = name2 , value = 39 , hashCode = 7 ]
index = 8, [ name = name5 , value = 37 , hashCode = 7 ]
index = 9, [ name = name9 , value = 34 , hashCode = 6 ]
index = 10, [ name = name4 , value = 50 , hashCode = 10 ]
index = 11, [ name = name1 , value = 56 , hashCode = 11 ]
index = 12, [ name = name7 , value = 56 , hashCode = 11 ]
index = 13, [ name = name8 , value = 55 , hashCode = 11 ]
删除元素:[ name = name7 , value = 56 , hashCode = 11 ]
index = 2, [ name = name0 , value = 12 , hashCode = 2 ]
index = 6, [ name = name6 , value = 32 , hashCode = 6 ]
index = 7, [ name = name2 , value = 39 , hashCode = 7 ]
index = 8, [ name = name5 , value = 37 , hashCode = 7 ]
index = 9, [ name = name9 , value = 34 , hashCode = 6 ]
index = 10, [ name = name4 , value = 50 , hashCode = 10 ]
index = 11, [ name = name1 , value = 56 , hashCode = 11 ]
index = 12, [ name = name8 , value = 55 , hashCode = 11 ]
根据代码的运行结果来看,可以对hashcode冲突的对象寻址到另一个地址去进行存储,在删除元素时,由于对后续元素都进行了重新的地址分配。运行结果基本上解释了开放定址法的整个定址、元素的添加和删除流程。
这里也可以看出开放地址法的弊端,首先在大数据集合、hash冲突经常发生的场合,如果使用开放定址法会造成在删除是重新为后续连续元素重新计算地址并分配。而且由于元素的添加顺序不一致,会导致在寻找一个元素时需要跨越多个hashcode值才能找到。如上述结果,如果需要查找value为34的元素,初始定位在下标为6的位置,通过比对需要一直比对到下标为9的位置才能正确找到元素。不利于集合的快速操作。