哈希表的实现和HashMap的原理
哈希冲突最常用的解决办法有开放定址法和链地址
1、开放定址法
就是当产生冲突时,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
2、链地址法
上面所说的开发定址法的原理是遇到冲突的时候查找顺着原来哈希地址查找下一个空闲地址然后插入,但是也有一个问题就是如果空间不足,那他无法
处理冲突也无法插入数据,
因此需要装填因子(插入数据/空间)<=1。
那有没有一种方法可以解决这种问题呢?链地址法可以,链地址法的原理时如果遇到冲突,他就会在原地址新建一个空间,然后以链表结点的形式插入
到该空间。我感觉业界上用的最多的就是链地址法。下面从百度上截取来一张图片,可以很清晰明了反应下面的结构。比如说我有一堆数据
{1,12,26,337,353...},而我的哈希算法是H(key)=key mod 16,第一个数据1的哈希值f(1)=1,插入到1结点的后面,第二个数据12的哈希值f(12)=12,插
入到12结点,第三个数据26的哈希值f(26)=10,插入到10结点后面,第4个数据337,计算得到哈希值是1,遇到冲突,但是依然只需要找到该1结点的最
后链结点插入即可,同理353。
3、如何降低哈希表内的冲突概率
1、在计算出哈希值 H 之后,我们通常会用哈希值去对哈希表的长度length取模,使元素比较均匀的分布在哈希表内。
2、还有一种效率更高的方法,就是用哈希值去和哈希表的初始长度length-1进行 与 运算,H & (length-1)就是元素在哈希表内的地址
但是这种方法对哈希表的长度有要求,只有哈希表的初始长度是 2^n才可以,为什么?
因为2^n-1的二进制表示每一位都是1,跟哈希值进行与运算的时候能产生更多不同的结果,从而降低冲突的可能
以哈希表长度为4=2^2举个例子:
看下图,左边两组是数组长度为16(2的4次方),右边两组是数组长度为15。两组的hashcode均为8和9,但是很明显,当它们和1110“与”的时候,产生了相同的结果,
也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到同一个链表上,那么查询的时候就需要遍历这个链表,得到8或者9,这样就降低了查询的效率。
同时,我们也可以发现,当数组长度为15的时候,hashcode的值会与14(1110)进行“与”,那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,
空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!
所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,
这样查询效率也就较高了。
说到这里,我们再回头看一下hashmap中默认的数组大小是多少,查看源代码可以得知是16,为什么是16,而不是15,也不是20呢,看到上面annegu的解释之后我们就清楚了吧,显然是因为16是2的整数次幂的原因,在小数据量的情况下16比15和20更能减少key之间的碰撞,而加快查询的效率。
所以,在存储大容量数据的时候,最好预先指定hashmap的size为2的整数次幂次方。就算不指定的话,也会以大于且最接近指定值大小的2次幂来初始化的
C语言实现代码
#include<iostream> #include<stdlib.h> #include<string.h> #define HashSize 10 //哈希表结点个数 using namespace std; struct node//定义一个节点 { char *key; char *val; node* next; }; struct HashTable//定义一个哈希表 { node* head; }Table[HashSize]; //初始化哈希表 void HashTable_Init()//哈希表初始化 { for(size_t i=0;i<HashSize;i++) Table[i].head=NULL; } //哈希函数(开放寻址) size_t Hash(char* key) { size_t ans=0; for(;*key;key++) ans=ans*33+*key; return ans%HashSize; } //查找键是否已经存在 node* find(char* key) { node* New; size_t index; index=Hash(key); New=Table[index].head; while(New!=NULL) { if(!strcmp(key,New->key)) return New; New=New->next; } return NULL; } //插入结点建立映射关系 void insert(char* key,char* val) { node* New=find(key); //不存在就插入节点 if(New==NULL) { size_t index=Hash(key); New=(node*)malloc(sizeof(node)); New->key=key; //链表的头插法,Table[index]是头指针 New->next=Table[index].head; //头指针前移 Table[index].head=New; } //如果键已经被映射过,直接改变对应的值即可 New->val=val; } void Print() { for(size_t i=0;i<HashSize;i++) { cout<<"index="<<i<<" "; if(Table[i].head==NULL) cout<<"[][]"<<endl; else { node* now=Table[i].head; while(now!=NULL) { cout<<"["<<now->key<<"]"<<"["<<now->val<<"]"<<" "; now=now->next; } cout<<endl; } } } int main() { HashTable_Init(); char key[5][100], val[5][100]; for (size_t i = 0; i < 5; i++) { cin >> key[i]; cin >> val[i]; insert(key[i], val[i]); } Print(); return 0; }
HashMap的原理
Map的底层都是通过哈希表进行实现的,那先来看看什么是哈希表。
JDK1.8之前,哈希表底层采用数组+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。
JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。如下图
HashMap的扩容机制
当hashmap中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对hashmap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,
所以这是一个通用的操作,很多人对它的性能表示过怀疑,不过想想我们的“均摊”原理,就释然了,而在hashmap数组扩容之后,最消耗性能的点就出现了:
原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。
那么hashmap什么时候进行扩容呢?当hashmap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,
也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,
而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000),
但是理论上来讲new HashMap(1024)更合适,不过上面annegu已经说过,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,
因为0.75*1000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。
以上转载自https://www.cnblogs.com/williamjie/p/9358291.html