哈希表也称为散列表,是用来存储群体对象的集合类结构。
什么是哈希表
数组和向量都可以存储对象,但对象的存储位置是随机的,也就是说对象本身与其存储位置之间没有必然的联系。当要查找一个对象时,只能以某种顺序(如顺序查找或二分查找)与各个元素进行比较,当数组或向量中的元素数量很多时,查找的效率会明显的降低。
一种有效的存储方式,是不与其他元素进行比较,一次存取便能得到所需要的记录。这就需要在对象的存储位置和对象的关键属性(设为 k)之间建立一个特定的对应关系(设为 f),使每个对象与一个唯一的存储位置相对应。在查找时,只要根据待查对象的关键属性 k 计算f(k)的值即可。如果此对象在集合中,则必定在存储位置 f(k)上,因此不需要与集合中的其他元素进行比较。称这种对应关系 f 为哈希(hash)方法,按照这种思想建立的表为哈希表。
Java 使用哈希表类(Hashtable)来实现哈希表,以下是与哈希表相关的一些概念:
-
容量(Capacity):Hashtable 的容量不是固定的,随对象的加入其容量也可以自动增长。
-
关键字(Key):每个存储的对象都需要有一个关键字,key 可以是对象本身,也可以是对象的一部分(如某个属性)。要求在一个 Hashtable 中的所有关键字都是唯一的。
-
哈希码(Hash Code):若要将对象存储到 Hashtable 上,就需要将其关键字 key 映射到一个整型数据,成为 key 的哈希码。
-
项(Item):Hashtable 中的每一项都有两个域,分别是关键字域 key 和值域 value(存储的对象)。Key 和 value 都可以是任意的 Object 类型的对象,但不能为空。
-
装填因子(Load Factor):装填因子表示为哈希表的装满程度,其值等于元素数比上哈希表的长度。
链式哈希表的描述
链式哈希表从根本上说是由一组链表构成。每个链表都可以看做是一个“桶”,我们将所有的元素通过散列的方式放到具体的不同的桶中。插入元素时,首先将其键传入一个哈希函数(该过程称为哈希键),函数通过散列的方式告知元素属于哪个“桶”,然后在相应的链表头插入元素。查找或删除元素时,用同们的方式先找到元素的“桶”,然后遍历相应的链表,直到发现我们想要的元素。因为每个“桶”都是一个链表,所以链式哈希表并不限制包含元素的个数。然而,如果表变得太大,它的性能将会降低。
冲突问题:两个不同的键映射到同一个位置
链式哈希表的实现
链式哈希表的操作与属性有:初始化、添加元素、删除元素、查找key值、获取哈希表中key值的个数。
/**
* 描述: 实现链式哈希表结构
*
* 哈希函数:除留余数 data % bucket_size =
* 扩容的时候,按原来空间大小的2倍+1扩容就可以
*
*/
public class LinkHashTable<K extends Comparable<K>,V> {
// 哈希桶
private Entry<K,V>[] table;
// 装载因子 0.75
private double loadFactor;
// 记录已经占用的桶的数量
private int usedBucketSize;
/**
* 哈希表初始化
*/
public LinkHashTable(){
this.table = new Entry[3];
this.loadFactor = 0.75;
this.usedBucketSize = 0;
}
/**
* 给哈希表增加元素
* @param key
* @param value
*/
public void put(K key, V value){
double lf = usedBucketSize*1.0 / this.table.length;
if(lf > this.loadFactor){
expand();
}
int idx = key.hashCode() % this.table.length;
if(this.table[idx] == null){
// 桶为空
this.table[idx] = new Entry<>(key, value, null);
this.usedBucketSize++;
} else {
// 桶已经有元素, 防止key被重复插入
Entry<K,V> cur = this.table[idx];
while(cur != null){
if(cur.key.compareTo(key) == 0){
// key值已经存在
cur.value = value;
break;
}
cur = cur.next;
}
// cur == null表示上面的while循环没有找见key值相等的节点
if(cur == null){
this.table[idx] = new Entry<>(key, value, this.table[idx]);
}
}
}
/**
* 在哈希表中查询key是否存在,如果key存在,返回它对应的value值,
* 否则返回null
* @param key
* @return
*/
public V get(K key){
int idx = key.hashCode() % this.table.length;
if(this.table[idx] == null){
return null;
} else{
Entry<K,V> cur = this.table[idx];
while(cur != null){
if(cur.key.compareTo(key) == 0){
return cur.value;
}
cur = cur.next;
}
return null;
}
}
/**
* 删除哈希表中key值为参数指定的节点
* @param key
*/
public void remove(K key){
int idx = key.hashCode() % this.table.length;
if(this.table[idx] == null){
return;
} else {
/**
* 桶不为空,需要遍历桶中的链表节点
*/
Entry<K,V> pre = null;
Entry<K,V> cur = this.table[idx];
while(cur != null){
if(cur.key.compareTo(key) == 0){
if(pre == null){
// 删除的是桶中的第一个节点
this.table[idx] = cur.next;
} else {
// 删除的不是桶中的第一个节点
pre.next = cur.next;
}
// 桶的元素被删除完了
if(this.table[idx] == null){
this.usedBucketSize--;
}
return;
}
pre = cur;
cur = cur.next;
}
}
}
/**
* 哈希表的扩容函数
*/
private void expand(){
Entry<K,V>[] oldTable = this.table;
this.usedBucketSize = 0;
this.table = new Entry[2*oldTable.length];
for (int i = 0; i < oldTable.length; i++) {
Entry<K,V> cur = oldTable[i];
while(cur != null){
this.put(cur.key, cur.value);
cur = cur.next;
}
}
}
/**
* 链式哈希表中节点的类型
* @param <K,V>
*/
static class Entry<K extends Comparable<K>,V>{
K key; // student id
V value; // student
Entry<K,V> next;
public Entry(K key, V value, Entry<K, V> next) {
this.key = key;
this.value = value;
this.next = next;
}
}
public static void main(String[] args) {
LinkHashTable<Integer, String> ht
= new LinkHashTable<>();
/*ht.put(100, "张宇");
ht.put(103, "张璐");
ht.put(105, "冯超");
System.out.println(ht.get(104));*/
Random rd = new Random();
int[] arr = new int[1000000];
for (int i = 0; i < arr.length; i++) {
arr[i] = rd.nextInt(60000);
}
// key: 数字 值:数字出现的次数
LinkHashTable<Integer, Integer> ht1
= new LinkHashTable<>();
for (int i = 0; i < arr.length; i++) { // O(n)
Integer val = ht1.get(arr[i]); // O(1)
if(val == null){
ht1.put(arr[i], 1);
} else {
ht1.put(arr[i], val+1); // O(1)
}
}
}