哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
在这里会简单模拟哈希表,并说明几个解决冲突的方法。先来模拟一下哈希表,然后一步一步完善它。 先定义一个类,姑且叫信息类吧
package hashTable;
public class Info {
private int key;
private String name;
public Info(int key,String name){
this.key = key;
this.name = name;
}
public int getKey() {
return key;
}
public void setKey(int key) {
this.key = key;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
很简单的一个类,不多说。请注意这里的key,接下来就要对这个key来做文章了。
接下来定义一个HashTable类
package hashTable;
public class HashTable {
private Info[] arr;
public HashTable(){
this.arr = new Info[100];
}
public HashTable(int maxSize){
this.arr = new Info[maxSize];
}
/**
* 插入
* @param info
*/
public void insert(Info info){
arr[info.getKey()] = info;
}
public Info find(int key){
return arr[key];
}
}
大家看到了,其实就是将Info对象的key作为HashTable对象数据的key,而整个info对象作为HashTable对象的value。
测试类
package hashTable;
public class TestHashTable {
public static void main(String[] args) {
HashTable ht = new HashTable();
ht.insert(new Info(12,"张三"));
ht.insert(new Info(20,"李四"));
System.out.println(ht.find(20).getName());
}
}
最后输出的是李四。但是如果key有需求并不是数字,而是字符串呢?所以我们应该Info这个类的key的类型都改为String,但是我们就没有办法根据索引进行插入和查找了,所以需要定义一个方法public int hashCode(String key),根据key计算出一个数字,然后将这个数字作为HashTable数据中的key。
如何根据一个字符串计算一个int类型的key呢?这里简单有三种:
1、将字母转换成ASCII码,然后进行相加
2、幂的连乘
3、压缩可选值
首先来看第一个
public int hashCode(String key){
int hashVal = 0;
for(int i=key.length()-1;i>=0;i--){
int letter = key.charAt(i) - 96;
hashVal += letter;
}
return hashVal;
}
上面就是用了
将字母转换成ASCII码,然后进行相加的办法,但是这里有个问题,如果测试数据是
ht.insert(new Info("abc","张三"));
ht.insert(new Info("bca","李四"));
Sysout.out.println(ht.find("abc").getName());
Sysout.out.println(ht.find("bca").getName());
输出的两个值都是李四,这说明hashCode返回了一样的数值,而很明显abc和cba是两个不相同的字符串,为了解决这个问题,可以使用幂的连乘
设字符串的长度为n,则幂的连乘公式 1*27的n-1次方+2*27的n-2次方+....+n*27的0次方,这样就肯定不会出现上述重复的现象。
public int hashCode(String key){
int hashVal = 0;
int pow27 = 1;
for(int i=key.length()-1;i>=0;i--){
int letter = key.charAt(i) - 96;
hashVal += letter * pow27;
pow27 *= 27;
}
return hashVal;
}
但是考虑到连乘会导致返回的hashVal可能是一个很大很大数字,既占用空间又容易出现问题,所以我们考虑第三种方式,就是
压缩可选值,其实就是给
hashVal取模,用数组的长度取模。将此方法的最后一句话话改为:return hashVal % arr.length;
ok,运行报错,原来是hashVal是int类型,当key的长度很长的时候,连乘是一个很大的数字会超过int的范围,所以我们这里应该使用BigInteger
public int hashCode(String key){
BigInteger hashVal = new BigInteger("0");
BigInteger pow27 = new BigInteger("1");
for(int i=key.length()-1;i>=0;i--){
int letter = key.charAt(i) - 96;
BigInteger letterB = new BigInteger(String.valueOf(letter));
hashVal = hashVal.add(letterB.multiply(pow27));
pow27 = pow27.multiply(new BigInteger(String.valueOf(27)));
}
return hashVal.mod(new BigInteger(String.valueOf(arr.length))).intValue();
}
做完这些,又发现在压缩后还是可能出现上面提到过的重复的情况,这里有几种办法。
1、开放地址法:当发生冲突时,通过查找数组的一个空位,并将数据填入,而不在用哈希函数得到的数组下标,这个方法就叫做开放地址法,说的简单点就是,假设字符串a和ct经过哈希函数产生的hashCode一样,那么相当于在存ct所对应的info对象时把a所对应的info对象给挤掉了,也就是a和ct所对应的地址其实是一个,开发地址法的意思就是在计算a的hashcode后计算ct的hashCode,发现ct和a的hashCode冲突,那么就找个数组的空的位置来存放ct对应的info对象,而不是替换掉原来的。具体看代码吧。
public void insert(Info info){
String key = info.getKey();
int hashVal = hashCode(key);
//加上arr[hashVal].getName() != null 是因为删除的时候会把name属性置为空
//这里需要未被删除的数据
while(arr[hashVal] != null && arr[hashVal].getName() != null){
++hashVal;
//为了防止数组长度-1 的数字+1后超过数组界限,所以再进行取余操作
hashVal %= arr.length;
}
arr[hashVal] = info;
}
public Info find(String key){
int hashVal = hashCode(key);
while(arr[hashVal] != null){
if(arr[hashVal].getKey().equals(key)){
return arr[hashVal];
}
++hashVal;
hashVal %= arr.length;
}
return null;
}
/**
* 删除,为了方便调试看对象的内容,这里删除方法返回Info对象
* @param key
* @return
*/
public Info delete(String key){
int hashVal = hashCode(key);
while(arr[hashVal] != null){
if(arr[hashVal].getKey().equals(key)){
Info tmp = arr[hashVal];
tmp.setName(null);
return tmp;
}
++hashVal;
hashVal %= arr.length;
}
return null;
}
2、链地址法
上个方法思想是当发现数组里面有与本次计算的hashCode相同的key,那么就在下一个空的位置插入进去,而在这里介绍的链地址法的思想是,在数组中每一个元素是一个链表,当发现有与本次计算的hashCode相同的key,则存放在对应元素的链表中。举个例子,a和ct计算的hashCode相同都为1,那么插入a的时候,在数组中的第二个元素(对应一个链表)中插入a对应的info对象。再细说一点就是每个数组中每个元素是链表,链表中每个元素是info对象。这样我们就可以通过链表的查找,删除,插入才进行hashTable的查找,删除,插入。请看代码:
链表的模拟请看前几篇博客,以下与前几篇介绍的类似,只是更改了很小一部分。
节点类:
package hashTable1;
/**
* 节点类
* @author Administrator
*/
public class Node {
Info info;
Node next;
public Node(Info info){
this.info = info;
}
}
package hashTable1;
public class LinkList {
private Node first;
/**
* 插入一个节点
*/
public void insertNode(Info info){
Node node = new Node(info);
node.next = first;
first = node;
}
/**
* 删除一个节点
*/
public void deleteFirst(){
Node tmp = first;
first = tmp.next;
}
/**
* 查找节点
*/
public Node find(String key){
Node current = first;
while(!key.equals(current.info.getKey())){
if(current.next == null){
return null;
}
current = current.next;
}
return current;
}
/**
* 根据值删除节点
*/
public Node delete(String key){
Node current = first;
Node tmp = first;
while(!key.equals(current.info.getKey())){
if(current.next == null){
return null;
}
tmp = current;
current = current.next;
}
if(current == first){
first = current.next;
}else{
tmp.next = current.next;
}
return current;
}
}
package hashTable1;
import java.math.BigInteger;
public class HashTable {
private LinkList[] arr;
public HashTable(){
this.arr = new LinkList[100];
}
public HashTable(int maxSize){
this.arr = new LinkList[maxSize];
}
/**
* 插入
* @param info
*/
public void insert(Info info){
String key = info.getKey();
int hashVal = hashCode(key);
if(arr[hashVal] == null){
arr[hashVal] = new LinkList();
}
arr[hashVal].insertNode(info);
}
/*public Info find(String key){
return arr[hashCode(key)];
}*/
public Info find(String key){
int hashVal = hashCode(key);
Node node = null;
if(arr[hashVal] != null){
node = arr[hashVal].find(key);
}
if(node != null){
return node.info;
}
return null;
}
/**
* 删除,为了方便调试看对象的内容,这里删除方法返回Info对象
* @param key
* @return
*/
public Info delete(String key){
int hashVal = hashCode(key);
arr[hashVal].delete(key);
return null;
}
public int hashCode(String key){
BigInteger hashVal = new BigInteger("0");
BigInteger pow27 = new BigInteger("1");
for(int i=key.length()-1;i>=0;i--){
int letter = key.charAt(i) - 96;
BigInteger letterB = new BigInteger(String.valueOf(letter));
hashVal = hashVal.add(letterB.multiply(pow27));
pow27 = pow27.multiply(new BigInteger(String.valueOf(27)));
}
return hashVal.mod(new BigInteger(String.valueOf(arr.length))).intValue();
}
}