1. 哈希表:散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。 给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。
因此哈希表最关键的两个问题就是,如何设计哈希函数,如何解决哈希冲突。
2. 哈希函数:" 键 "通过哈希函数得到的" 索引 "分布越均匀越好,对于些特殊领域,有特殊领域的哈希函数设计方式,甚至有专门的论文。我们只关注一般的哈希函数的设计。我们对通过hashCode计算出的值进行取正后再对一个素数取余( 可以将hash表容量定义成素数,对hash表容量取余 )来计算hash值。
3. 哈希冲突:不同的数据计算出的hash值相同即为哈希冲突。
4. 哈希冲突的解决方案:
1>. 开放地址法( Open Addressing )
线性探测法,遇到哈希冲突 +1。
平方探测法,遇到哈希冲突+1 +4 +9 +16。
二次哈希法 +hash2(key)。
2>. 再哈希法( Rehashing )
重新计算hash值。
3>. Colasced Hashing
融合了链地址法和开放地址法。
5. 哈希表实现的接口:
public interface Map<K, V> {
//向map中添加键值对
public void put(K key, V value);
//删除map中指定key的键值对
public V remove(K key);
//判断二分搜索树中是否包含指定key
public boolean contains(K key);
//通过key从map中获取key对应的值
public V get(K key);
//修改指定key对应的值
public void set(K key, V value);
//获取map中有效元素的个数
public int size();
//判断map是否为空
public boolean isEmpty();
//获取map中的键的集合 因为map中键key是唯一的 所以用集合set进行存储
public Set<K> keySet();
//获取map中的值的列表 因为map中键value不是唯一的 所以用列表list进行存储
public List<V> values();
//获取map中键值对的集合 因为map中键值对(key:value)是唯一的 所以用集合set进行存储
public Set<Entry<K, V>> entrySet();
//定义一个获取键值对Entry<K, V>的接口
public interface Entry<K, V> extends Comparable<Entry<K, V>>{
//获取键值对的键
public K getKey();
//获取键值对的值
public V getValue();
}
}
6. 哈希表数据结构的实现:我们底层通过AVL平衡树映射来实现哈希
//定义hash表 底层通过AVL平衡树映射来实现
//AVL平衡树是最早的一种二分搜索树之一 因此AVL平衡树实现的映射中的key应具有比较性
//因此hash表中的映射的key也要具有可比性
public class HashTable<K extends Comparable<K>, V> implements Map<K, V>{
//预制数组容量 是一个容量逐渐递增的数组
private static final int[] capacity = {
53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593,
49157, 98317, 196613, 393241, 768433, 1572869, 3145739, 6291469,
12582917, 25165843, 50331653, 100663319, 201326611, 402653189, 805306457, 1610612741
};
//每个桶的最大存储数据数量
private static final int upperTol = 10;
//每个桶的最小存储数据数量
private static final int lowerTol = 2;
//预制容量数组的索引
private int capacityIndex = 0;
//表示hash表中桶的数量 即hash表的容量
private int M;
//表示hash表中键值对的个数
private int size;
//存放数据的数组即为hash表 AVLTreeMap类型的数组
//用来存储AVLTreeMap 即一个AVLTreeMap代表一个桶
private AVLTreeMap<K, V>[] hashTable;
//构造方法 初始化hash表容量 hash表 hash表中键值对的个数
public HashTable() {
M = capacity[capacityIndex];
hashTable = new AVLTreeMap[M];
for(int i = 0; i < M; i++) {
hashTable[i] = new AVLTreeMap<K, V>();
}
size = 0;
}
//计算key的hash值 即指定键在hash表中桶的角标
private int hash(K key) {
//为防止计算的结果超过整型范围 并且 计算的结果在数组中下标越界
//则 对hashCode计算出的hash值 先与0x7fffffff进行位运算 再对M取余
return key.hashCode() & 0x7ffffff % M;
}
//向hash表中添加指定键值对 如果key已经存在 则修改原来的值
@Override
public void put(K key, V value) {
//先获取指定key所在的AVLTreeMap
int index = hash(key);
AVLTreeMap<K, V> map = hashTable[index];
//判断key是否存在
if(map.contains(key)) {
map.put(key, value); //存在 则修改原先key的值
}else { //不存在 则将键值对添加到treeMap中
map.put(key, value);
size++;
//判断是否需要扩容 如果size超过总体的hash表的最大容量 即需要扩容 且容量数组的索引加一在容量数组中
//即将容量数组的索引后移到容量数组中容量数组的索引的下一个位置
if(size > upperTol * M && capacityIndex + 1 < capacity.length) {
capacityIndex++;
//对数组进行扩容 新长度为容量数组的指定索引位置的值
resize(capacity[capacityIndex]);
}
}
}
//删除hash表中添加指定键值对 并返回要删除key的值
@Override
public V remove(K key) {
//先获取指定key所在的AVLTreeMap
int index = hash(key);
AVLTreeMap<K, V> map = hashTable[index];
V ret = null;
//判断key是否存在
if(map.contains(key)) {
//存在 则删除key
ret = map.remove(key);
size--;
//判断是否需要缩容 如果size小于总体的hash表的最小容量 即需要缩容 且容量数组的索引减一在容量数组中
//即将容量数组的索引前移到容量数组中容量数组的索引的上一个位置
if(size < lowerTol * M && capacityIndex - 1 >= 0) {
capacityIndex--;
//对数组进行缩容 新长度为容量数组的指定索引位置的值
resize(capacity[capacityIndex]);
}
}
return ret;
}
//对数组进行缩容或者扩容
private void resize(int newM) {
//创建新数组和数组中的每一个桶 长度为newM
AVLTreeMap<K, V>[] newHashTable = new AVLTreeMap[newM];
for(int i = 0; i < newHashTable.length; i++) {
newHashTable[i] = new AVLTreeMap<K, V>();
}
//遍历原数组 将原数组的键值对重新添加到新数组中
//添加的位置要重新通过hash方法计算 但是hash方法中用的是原数组容量计算的
//因此在计算前要将hash中的原数组容量改为新数组容量
M = newM;
for(int i = 0; i < hashTable.length; i++) {
AVLTreeMap<K, V> map = hashTable[i];
for(K key : map.keySet()) {
newHashTable[hash(key)].put(key, map.get(key));
}
}
hashTable = newHashTable;
}
//判断hash表中是否包含指定key
//先获取指定key所在的AVLTreeMap 再判断指定key是否在其所在的AVLTreeMap中即可
@Override
public boolean contains(K key) {
int index = hash(key);
AVLTreeMap<K, V> map = hashTable[index];
return map.contains(key);
}
//获取hash表中指定key的值
//先获取指定key所在的AVLTreeMap 再获取指定key在其所在的AVLTreeMap中的值即可
@Override
public V get(K key) {
int index = hash(key);
AVLTreeMap<K, V> map = hashTable[index];
return map.get(key);
}
//修改hash表中指定key的值
//先获取指定key所在的AVLTreeMap 如果指定的key在map中 就将其值进行修改
@Override
public void set(K key, V value) {
int index = hash(key);
AVLTreeMap<K, V> map = hashTable[index];
map.set(key, value);
}
//获取hash表中键值对的个数
@Override
public int size() {
return size;
}
//判断hash表是否为空
@Override
public boolean isEmpty() {
return size == 0;
}
//获取hash表中键的集合 并返回
/*
* 先创建一个存放key的容器
* 遍历数组 并获取对应位置的桶(AVLTreeMap)
* 再遍历桶的键的集合 将每个桶的键依次添加到新容器中 并返回新容器
*/
@Override
public Set<K> keySet() {
TreeSet<K> set = new TreeSet<K>();
for(int i = 0; i < hashTable.length; i++) {
AVLTreeMap<K, V> map = hashTable[i];
for(K key : map.keySet()) {
set.add(key);
}
}
return set;
}
//获取hash表中值的列表 并返回
/*
* 先创建一个存放value的容器
* 遍历数组 并获取对应位置的桶(AVLTreeMap)
* 再遍历桶的键值集合 将每个桶的值依次添加到新容器中 并返回新容器
*/
@Override
public List<V> values() {
LinkedList<V> list = new LinkedList<V>();
for(int i = 0; i < hashTable.length; i++) {
AVLTreeMap<K, V> map = hashTable[i];
for(V value : map.values()) {
list.add(value);
}
}
return list;
}
//获取hash表中键值对的集合 并返回
/*
* 先创建一个存放value的容器
* 遍历数组 并获取对应位置的桶(AVLTreeMap)
* 再遍历桶的键值对的集合 将每个桶的键值对依次添加到新容器中 并返回新容器
*/
@Override
public Set<Entry<K, V>> entrySet() {
TreeSet<Entry<K, V>> set = new TreeSet<Entry<K,V>>();
for(int i = 0; i < hashTable.length; i++) {
AVLTreeMap<K, V> map = hashTable[i];
for(Entry entry : map.entrySet()) {
set.add(entry);
}
}
return set;
}
}