数据结构与算法之哈希表
哈希表
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。
如上图,我们的哈希表底层是通过数组加链表(其他数据结构也行)的形式实现的,在数组的每个元素中存储的是一个链表,因为数组是有限的,每个链表中有很多元素,所以我们需要先找到对应数组的下标,然后在存储到该链表中,找下标我们使用的是hash函数;这里我们就要提到hash冲突,所谓hash冲突就是因为我们的数组有限,添加元素个数多于数组长度时,我们通过取余找该元素对应的数组下标,该下标下已经存在元素,这就是为什么在数组中使用链表存储元素的原因。
解决hash冲突的方法
链地址法:
开放地址法(Open Addressing):线性探测法,遇到hash冲突+1;平方探测,遇到哈希冲突+1,+4,+9,+16。。。二次哈希法 +hash2(key);
再哈希法Rehashing 重新计算hash值;
Colasced Hashing 融合了链地址法和开放地址法。
这种存储方式我们要避免两种极端情况的出现,一种时数组特别长,但每个数组中链表元素很少(胖短),另一种则是数组特别短,但数组中链表的元素特别多(高瘦);那如何解决这个问题呢:一个解决办法是模一个素数,例如7,我们可以得到1-9的数字比较均匀。
假设共有N个元素,数组长度为M,M<N,hash冲突的次数为N-M次,就相当于每个链表中有N/M个元素,其时间复杂度为O(logN/M);
Hashtable底层实现
我们的代码是基于数组+二分搜索树实现的。
Hashtable数组初始容量设置
package tree;
import ifce.List;
import ifce.Map;
import ifce.Set;
import lianbiao.LinkedList;
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
};
Hashtable属性定义及其初始化
//哈希表中每个桶的上限和下限,超过上限需要扩容,小于下限需要缩容,避免出现极端高瘦和矮胖情况。
//主要原因是每个桶中最多有N/M个元素。
private static final int upperTol = 10;
private static final int lowerTol = 2;
//素数表中的角标,用来标记当前哈希表的长度;
private static int capacityIndex = 0;
private static int M;//桶的个数
private int size;//哈希表中的元素个数。
//定义桶;
private AVLTreeMap<K,V>[] hashTable;
public HashTable(){
M = capacity[capacityIndex];
hashTable = new AVLTreeMap[M];
for(int i=0;i<M;i++){
hashTable[i] = new AVLTreeMap<>();
}
}
hash方法计算元素的下标
让该key的hashcode值&0x7fffffff 再对数组容量取余得到该键值对对应的下标
public int hash(K key){
//获取key的哈希值,然后纠正负数,再对M取余,得到要去桶的角标;
return key.hashCode() & 0x7fffffff % M;
}
添加元素实现
通过hash算法算到下标,然后再判断该key是否存在,存在则是修改,不存在则是插入该键值对,元素个数要加1,并且要判断是否需要扩容。条件就是当元素个数超过了每个数组中平衡树元素总和,并且capacityIndex不能超出我们所给的素数组。
@Override
public void put(K key, V value) {
int index = hash(key);
AVLTreeMap<K,V> map = hashTable[index];
//存在则是修改;
if(map.contains(key)){
map.put(key,value);
}else{
map.put(key,value);
size++;
//判断是否扩容;
if(size > upperTol * M && capacityIndex+1<capacity.length){
capacityIndex++;
resize(capacity[capacityIndex]);
}
}
}
删除元素实现
删除元素也是先找到下标,然后判断该键值对是否已经存在,存在就删除,元素数减一,之后判断是否需要缩容。最终将删除的元素返回。条件就是当元素个数超过了每个数组中平衡树元素总和,并且capacityIndex不能超出我们所给的素数组。
@Override
public V remove(K key) {
int index = hash(key);
AVLTreeMap<K,V> map = hashTable[index];
V ret = null;
if(map.contains(key)){
ret = remove(key);
size--;
//考虑缩容;
if(size<lowerTol * M && capacityIndex - 1 >= 0){
resize(capacity[capacityIndex]);
}
}
return ret;
}
扩容、缩容实现
扩容其实就是再新建一个容量增加的平衡树,然后将原来平衡树中的元素赋值到新的树当中。将新树重新赋给原树即可。
private void resize(int newM) {
AVLTreeMap<K,V>[] newHashTable = new AVLTreeMap[newM];
for(int i=0;i<newHashTable.length;i++){
newHashTable[i] = new AVLTreeMap<>();
}
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;
}
判断是否包含某个元素
@Override
public boolean contains(K key) {
int index = hash(key);
AVLTreeMap<K,V> map = hashTable[index];
return map.contains(key);
}
通过key获取对应的值
@Override
public V get(K key) {
int index = hash(key);
AVLTreeMap<K,V> map = hashTable[index];
return map.get(key);
}
通过键值对修改值
@Override
public void set(K key, V value) {
int index = hash(key);
AVLTreeMap<K,V> map = hashTable[index];
map.set(key,value);
}
返回元素个数及其判空操作
@Override
public int size() {
return size;
}
@Override
public boolean isEmpty() {
return size == 0;
}
所有key元素遍历
@Override
public Set<K> keySet() {
TreeSet<K> set = new TreeSet<>();
for(int i=0;i<hashTable.length;i++){
AVLTreeMap<K,V> map = hashTable[i];
for(K key : map.keySet()){
set.add(key);
}
}
return set;
}
所有值的遍历
@Override
public List<V> values() {
LinkedList<V> list = new LinkedList<>();
for(int i=0;i<hashTable.length;i++){
AVLTreeMap<K,V> map = hashTable[i];
for(V value : map.values()){
list.add(value);
}
}
return list;
}
entry键值对的遍历
@Override
public Set<Entry<K, V>> entrySet() {
TreeSet<Entry<K,V>> set = new TreeSet<>();
for(int i=0;i<hashTable.length;i++){
AVLTreeMap<K,V> map = hashTable[i];
for(Entry entry:map.entrySet()){
set.add(entry);
}
}
return set;
}