一,什么是散列表
散列表,又叫哈希表(hash table),是能够通过给定的关键字的值直接访问到具体对应的值的一个数据结构。也就是说,把关键字映射到一个表中的位置来直接访问记录,以加快访问速度。
通常,通过一个关键字key访问一个映射表来得到对应记录value的地址,而这个映射表,也叫散列函数或者哈希函数,存放记录的数组叫做散列表。
其中,通过不同的key,可能访问到同一个地址,这种现象叫做碰撞,而通过某个key一定会得到唯一的value地址。
几种哈希函数:
1,直接寻址法
去关键字或关键字的某个线性函数值作为散列地址。
2,数字分析法
3,平方取中法
4,取随机数法
5,保留取余法
二,对散列表函数产生冲突的解决方法
1,开发地址法(也叫开放寻址法)
实际上就是当需要存储值时,对key哈希之后,发现这个地址应有值了,这时对计算出来的地址进行一次探测再哈希,比如往后移动一个地址,如果没人占用,就用这个地址。如果超过最大长度,则可以对总长度取余,这里移动的地址是产生冲突时的增列序量。
2,再哈希法
在产生冲突之后,使用关键字的其他部分继续计算地址,如果还有冲突,则继续使用其他部分再计算地址。这种方式的缺点是时间增加了。
3,链地址法
链地址法其实就是对key通过哈希之后落在同一个地址上的值,做一个链表。其实在很多高级语言的实现当中,也就是使用这种方式处理冲突的。
4,建立一个公共溢出区
这种方式是建立一个公共溢出区,当地址存在冲突时,把新的地址放在公共溢出区里。
解决hash碰撞的其他博客:
https://blog.csdn.net/zeb_perfect/article/details/52574915
三,散列表的特点
散列表有两种用法:
set(集合):key的值与value的值一样。
map:key和value所对应的内容不一样。
散列表的特点:
1,访问速度快。
通过散列函数,可以将指定的key都映射到一个地址上,访问key对应的value时,不要一个个地进行查找,可以直接跳到哪个地址。
2,需要额外的空间
首先,散列表实际上是存不满的,如果是一个散列表刚好能够装满,那么肯定是个巧合。而且当散列表中元素的使用率越来越高时,性能会下降,所以一般会选择扩容来解决这个问题。
另外,如果有冲突的话,则也是需要额外的空间去存储的,比如链地址法,不但需要额外的空间,甚至需要使用其他数据结构。
这个特点叫做空间换时间。
3,无序
4,可能会产生碰撞
没有完美的散列函数,无论如何总会产生冲突,不同的高级语言的实现中,对冲突的解决方案不一定一样。
四,散列表的适用场景
1,缓存
2,快速查找
五,性能
散列表的访问,如果没有碰撞,可以认为对元素的访问时O(1)的时间复杂度。
如果采用链表方式解决哈希碰撞(有一些语言使用开发寻址方式解决,Java使用链表解决),由于可能会产生碰撞,而碰撞之后的访问需要遍历链表,所以时间复杂度将变为O(L),其中L为链表的长度。
当散列表的元素大部分被使用了,这时再向散列表中添加元素,就很容易产生碰撞了,甚至散列表分配的地址越在后面使用,越容易被占用,这时就需要扩容了。
散列表的Java实现
package me.irfen.algorithm.ch11;
import java.util.Arrays;
public class HashMap<K, V> {
/**
* 默认散列表初始化长度(16)
*/
private static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
/**
* 默认扩容因子
*/
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 散列表数组
*/
private Entry<K, V>[] table;
/**
* 扩容因子
*/
final float loadFactor;
private int size = 0; // 散列表元素个数
private int use = 0; // 散列表使用数组元素数
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY);
}
public HashMap(int initCapacity) {
this(initCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initCapacity, float loadFactor) {
this.table = new Entry[initCapacity];
this.loadFactor = loadFactor;
}
/**
* 添加/修改
*
* @param key
* @param value
*/
public void put(K key, V value) {
int index = hash(key);
Entry<K, V> e = table[index];
if (e == null) {
// 不存在值,向链表添加,有可能需要扩容
table[index] = new Entry<K, V>(key, value, null);
size++;
use++;
// 不存在值,说明是个未用过的地址,需要判断是否需要扩容
// 其实这里可以使用size判断,可以保持某一个数组元素下的链表不要太长
if (use >= table.length * loadFactor) {
resize();
}
} else {
// 本身存在值,修改已有的值
for (; e != null; e = e.next) {
Object k = e.key;
if (k == key || k.equals(key)) {
e.value = value;
return;
}
}
// 不存在相同的值,直接往链表添加元素
Entry<K, V> temp = table[index];
Entry<K, V> newEntry = new Entry<K, V>(key, value, temp);
table[index] = newEntry;
size++;
}
}
/**
* 删除
*
* @param key
*/
public void remove(K key) {
int index = hash(key);
Entry<K, V> e = table[index];
Entry<K, V> pre = null;
for (; e != null; pre = e, e = e.next) {
K k = e.key;
if (k == key || k.equals(key)) {
if (pre == null) {
// 说明删除的是第一个元素
table[index] = null;
} else {
pre.next = e.next;
size--;
}
return;
}
}
}
/**
* 获取
*
* @param key
* @return
*/
public V get(K key) {
int index = hash(key);
Entry<K, V> e = table[index];
for (; e != null; e = e.next) {
Object k = e.key;
if (k == key || k.equals(key)) {
return e.value;
}
}
// 没有找到返回null
return null;
}
/**
* 想想这里为什么不直接get之后判断是否为空呢
* @param key
* @return
*/
public boolean containsKey(K key) {
int index = hash(key);
Entry<K, V> e = table[index];
for (; e != null; e = e.next) {
Object k = e.key;
if (k == key || k.equals(key)) {
return true;
}
}
return false;
}
/**
* 清空散列表
*/
public void clear() {
Arrays.fill(table, null);
size = 0;
use = 0;
}
/**
* 获取散列表中元素个数
*
* @return
*/
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
/**
* 根据key,通过哈希函数获取位于散列表数组中的哪个位置
*
* @param key
* @return
*/
private int hash(K key) {
int hashCode = Math.abs(key.hashCode());
hashCode %= table.length;
return hashCode;
}
/**
* 扩容
*/
private void resize() {
int newLength = table.length * 2;
Entry<K, V>[] oldTable = table;
table = new Entry[newLength];
use = 0;
for (int i = 0; i < oldTable.length; i++) {
Entry<K, V> e = oldTable[i];
while (null != e) {
// 重新计算哈希值,放入新的地址中
int index = hash(e.key);
if (table[index] == null) {
use++;
table[index] = new Entry<K, V>(e.key, e.value, null);
} else {
Entry<K, V> temp = table[index];
Entry<K, V> newEntry = new Entry<K, V>(e.key, e.value, temp);
table[index] = newEntry;
}
e = e.next;
}
}
}
static class Entry<K, V> {
K key;
V value;
Entry<K, V> next;
Entry(K key, V value, Entry<K, V> next) {
this.key = key;
this.value = value;
this.next = next;
}
}
}
六,参考资料
《轻松学算法》