哈希表是非常重要的一种数据结构,例如HashMap集合底层也是基于哈希表来实现的;关于哈希表的知识点也是经常在面试中被问到,通过这几天对于哈希表的学习,包括看了哈希表的源码,以及手动编写了一个简单的哈希表,加深了对哈希表的理解,并在此进行总结!
1. 什么是哈希表
Hash表也称散列表,也有直接译作哈希表,Hash表是一种根据关键字值(key - value)而直接进行访问的数据结构。它基于数组,通过把关键字映射到数组的某个下标来加快查找速度,但是又和链表、树等数据结构不同,在这些数据结构中查找某个关键字,通常要遍历整个数据结构,也就是O(N)的时间级,但是对于哈希表来说,只是O(1)的时间级。
哈希表也可以当做一种缓存产品来使用,我们知道,频繁的访问数据库会造成非常大的系统开销,因而出现了很多的缓存产品,例如redis;我们也可以将频繁访问的数据存放在哈希表中,这样每次获取哈希表的值就不用从数据库获取,减少系统开销。
2. 哈希函数
为什么需要哈希函数?哈希函数就是关键字转换为数组的下标,这个转换的函数称为哈希函数(也称散列函数),转换的过程称为哈希化。
哈希函数就是把一个大范围的数字哈希(转化)成一个小范围的数字,这个小范围的数对应着数组的下标。使用哈希函数向数组插入数据后,这个数组就是哈希表。
下面使用简单取模法作为哈希函数:
public int hashFun(int id){
return id%size; //size表示哈希表的容量,也就是数组的大小
}
3. 哈希冲突
多个key映射到相同的数组下标,即发生了哈希冲突;常见解决冲突的方法有:开放地址法、链地址法、桶
4. 开放地址法
若数据项不能直接存放在由哈希函数所计算出来的数组下标时,就要寻找其他的位置。分别有三种方法:线性探测、二次探测以及再哈希法。
4.1 线性探测
线性探测,指的就是线性的查找空白单元,例如我们要插入的key对应哈希表数组的下标是3,并且这个位置3已经被其它数据占用了,那么会查看下一个位置4是否被占用,若被占用,继续往下递增查找,直到找到一个空白的位置。
4.2 二次探测
二次探测的思想是探测相距较远的单元,而不是和原始位置相邻的单元,二次探测可以防止聚集的产生;但是二次探测法也会导致二次聚集的产生。
线性探测中,如果哈希函数计算的原始下标是x, 线性探测就是x+1, x+2, x+3, 以此类推;而在二次探测中,探测的过程是x+1, x+4, x+9, x+16,以此类推,到原始位置的距离是步数的平方。
4.3 再哈希法
再哈希法是为了消除聚集和二次聚集提出来的;因为线性探测和二次探测产生的探测序列步长总是固定的,容易产生聚集,而再哈希法是指出现冲突后,把关键字用不同的哈希函数再做一遍哈希化,用这个结果作为步长。对于指定的关键字,步长在整个探测中是不变的,不过不同的关键字使用不同的步长。
5. 链地址法
链地址法的实现原理就是使用数组加链表,在哈希表每个单元中设置链表,当出现冲突后,不需要在原始的数组中寻找空位,而是将其他同样映射到这个位置的数据项加到链表中。
6. 桶
类似于链地址法,它是在每个数据项中使用子数组,而不是链表。这样的数组称为桶。
这个方法显然不如链表有效,因为桶的容量不好选择,如果容量太小,可能会溢出,如果太大,又造成性能浪费,而链表是动态分配的,不存在此问题。所以一般不使用桶。
7. 聚集和装填因子
7.1 聚集
当哈希表变得比较满时,我们每插入一个新的数据,都要频繁的探测插入位置,因为可能很多位置都被前面插入的数据所占用了,这称为聚集。数组填的越满,聚集越可能发生。
7.2 装填因子
已填入哈希表的数据项和哈希表容量的比率叫做装填因子
通过查看HashMap的源码可以看到,HashMap默认的装填因子就是0.75。
// 默认的加载因子 (扩容因子)
static final float DEFAULT_LOAD_FACTOR = 0.75f;
装填因子也叫加载因子、扩容因子或负载因子,用来判断什么时候进行扩容的,假如加载因子是0.5,HashMap的初始化容量是16,那么当HashMap中有16*0.5=8个元素时,HashMap就会进行扩容。
那加载因子为什么是 0.75 而不是 0.5 或者 1.0 呢?
这其实是出于容量和性能之间平衡的结果:
- 当加载因子设置比较大的时候,扩容的门槛就被提高了,扩容发生的频率比较低,占用的空间会比较小,但此时发生Hash冲突的几率就会提升,因此需要更复杂的数据结构来存储元素,这样对元素的操作时间就会增加,运行效率也会因此降低;
- 而当加载因子值比较小的时候,扩容的门槛会比较低,因此会占用更多的空间,此时元素的存储就比较稀疏,发生哈希冲突的可能性就比较小,因此操作性能会比较高。
所以综合了以上情况就取了一个 0.5 到 1.0 的平均数 0.75 作为加载因子。
8. 关于哈希表的扩容
回顾数组的基本知识,我们知道,数组的大小是固定的,无法进行扩展,所以哈希表的扩容只能另外创建一个更大的数组,然后把旧数组中的数据插到新的数组中。
但是需要注意的是:哈希表是根据数组大小计算给定数据的位置的,所以这些数据项不能再放在新数组中和老数组相同的位置上。因此不能直接拷贝,需要按顺序遍历老数组,并使用insert方法向新数组中插入每个数据项。
示例:哈希表扩容的源码
public void insert(DataItem item){
if(isFull()){
//扩展哈希表
System.out.println("哈希表已满,重新哈希化...");
extendHashTable();
}
int key = item.getKey();
int hashVal = hashFunction(key);
while(hashArray[hashVal] != null && hashArray[hashVal].getKey() != -1){
++hashVal;
hashVal %= arraySize;
}
hashArray<
哈希表详解

最低0.47元/天 解锁文章
346

被折叠的 条评论
为什么被折叠?



