一篇HashMap解决hash冲突的方法的文章 及 一些思考
2018-01-23 18:10
441 查看
被问到关于hash函数的问题一时间发现忘得差不多了,回顾了一下,觉得难点无非在于hash()的算法设计以及冲突的解决。看到这篇文章算讲得很简单了。也大概了解了JAVA对HashMap这个结构遇到冲突的解决思路。
文:
在Java编程语言中,最基本的结构就是两种,一种是数组,一种是模拟指针(引用),所有的数据结构都可以用这两个基本结构构造,HashMap也一样。当程序试图将多个 key-value 放入 HashMap 中时,以如下代码片段为例:
HashMap<String,Object> m=new HashMap<String,Object>();
m.put("a", "rrr1");
m.put("b", "tt9");
m.put("c", "tt8");
m.put("d", "g7");
m.put("e", "d6");
m.put("f", "d4");
m.put("g", "d4");
m.put("h", "d3");
m.put("i", "d2");
m.put("j", "d1");
m.put("k", "1");
m.put("o", "2");
m.put("p", "3");
m.put("q", "4");
m.put("r", "5");
m.put("s", "6");
m.put("t", "7");
m.put("u", "8");
m.put("v", "9");
HashMap 采用一种所谓的“Hash 算法”来决定每个元素的存储位置。当程序执行 map.put(String,Obect)方法 时,系统将调用String的 hashCode() 方法得到其 hashCode 值——每个 Java 对象都有 hashCode() 方法,都可通过该方法获得它的 hashCode 值。得到这个对象的 hashCode 值之后,系统会根据该
hashCode 值来决定该元素的存储位置。源码如下:
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//判断当前确定的索引位置是否存在相同hashcode和相同key的元素,如果存在相同的hashcode和相同的key的元素,那么新值覆盖原来的旧值,并返回旧值。
//如果存在相同的hashcode,那么他们确定的索引位置就相同,这时判断他们的key是否相同,如果不相同,这时就是产生了hash冲突。
//Hash冲突后,那么HashMap的单个bucket里存储的不是一个 Entry,而是一个 Entry 链。
//系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),
//那系统必须循环到最后才能找到该元素。
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
上面程序中用到了一个重要的内部接口:Map.Entry,每个 Map.Entry 其实就是一个 key-value 对。从上面程序中可以看出:当系统决定存储 HashMap 中的 key-value 对时,完全没有考虑 Entry 中的 value,仅仅只是根据 key 来计算并决定每个 Entry 的存储位置。这也说明了前面的结论:我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置之后,value
随之保存在那里即可.HashMap程序经过我改造,我故意的构造出了hash冲突现象,因为HashMap的初始大小16,但是我在hashmap里面放了超过16个元素,并且我屏蔽了它的resize()方法。不让它去扩容。这时HashMap的底层数组Entry[] table结构如下:
Hashmap里面的bucket出现了单链表的形式,散列表要解决的一个问题就是散列值的冲突问题,通常是两种方法:链表法和开放地址法。链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位;开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。java.util.HashMap采用的链表法的方式,链表是单向链表。形成单链表的核心代码如下:
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
bsp;
上面方法的代码很简单,但其中包含了一个设计:系统总是将新添加的 Entry 对象放入 table 数组的 bucketIndex 索引处——如果 bucketIndex 索引处已经有了一个 Entry 对象,那新添加的 Entry 对象指向原有的 Entry 对象(产生一个 Entry 链),如果 bucketIndex 索引处没有 Entry 对象,也就是上面程序代码的 e 变量是 null,也就是新放入的 Entry 对象指向 null,也就是没有产生
Entry 链。
HashMap里面没有出现hash冲突时,没有形成单链表时,hashmap查找元素很快,get()方法能够直接定位到元素,但是出现单链表后,单个bucket 里存储的不是一个 Entry,而是一个 Entry 链,系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),那系统必须循环到最后才能找到该元素。
当创建 HashMap 时,有一个默认的负载因子(load factor),其默认值为 0.75,这是时间和空间成本上一种折衷:增大负载因子可以减少 Hash 表(就是那个 Entry 数组)所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的的操作(HashMap 的 get() 与 put() 方法都要用到查询);减小负载因子会提高数据查询的性能,但会增加 Hash 表所占用的内存空间。
以上转自http://xiaolu123456.iteye.com/blog/1485349
思考:编写一个简单的实例
搜索的时间,通过hash()找到hashlist[i]是O(1)的,时间开销主要在搜索链表。如果在链表头还好,在表尾的话就要遍历整个链表。
那能不能通过额外的空间开销,给Node添加一个times属性记录访问的历史次数,维护时,一定时间间隔就对链表进行排序,将访问频率高的Node放置链表头部,预计未来访问频率仍然较高?
又突然想到了缓存过期策略中的LRU(Least Recently Used)算法,对应的是不是也能有一种“最近刚好使用算法”?在findNode()方法中添加一步,每当一个Node被查询,意味着它活跃度可能较高,未来被查询的可能性也会比较高,就将这个Node移动到链表最前端。因为链表中节点的移动消耗非常低,但可能为下一次或n次查询节省进行一次完全遍历的时间。
未曾从事过海量数据处理方面的工作,瞎想想又不用负责- -。
文:
在Java编程语言中,最基本的结构就是两种,一种是数组,一种是模拟指针(引用),所有的数据结构都可以用这两个基本结构构造,HashMap也一样。当程序试图将多个 key-value 放入 HashMap 中时,以如下代码片段为例:
HashMap<String,Object> m=new HashMap<String,Object>();
m.put("a", "rrr1");
m.put("b", "tt9");
m.put("c", "tt8");
m.put("d", "g7");
m.put("e", "d6");
m.put("f", "d4");
m.put("g", "d4");
m.put("h", "d3");
m.put("i", "d2");
m.put("j", "d1");
m.put("k", "1");
m.put("o", "2");
m.put("p", "3");
m.put("q", "4");
m.put("r", "5");
m.put("s", "6");
m.put("t", "7");
m.put("u", "8");
m.put("v", "9");
HashMap 采用一种所谓的“Hash 算法”来决定每个元素的存储位置。当程序执行 map.put(String,Obect)方法 时,系统将调用String的 hashCode() 方法得到其 hashCode 值——每个 Java 对象都有 hashCode() 方法,都可通过该方法获得它的 hashCode 值。得到这个对象的 hashCode 值之后,系统会根据该
hashCode 值来决定该元素的存储位置。源码如下:
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//判断当前确定的索引位置是否存在相同hashcode和相同key的元素,如果存在相同的hashcode和相同的key的元素,那么新值覆盖原来的旧值,并返回旧值。
//如果存在相同的hashcode,那么他们确定的索引位置就相同,这时判断他们的key是否相同,如果不相同,这时就是产生了hash冲突。
//Hash冲突后,那么HashMap的单个bucket里存储的不是一个 Entry,而是一个 Entry 链。
//系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),
//那系统必须循环到最后才能找到该元素。
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
上面程序中用到了一个重要的内部接口:Map.Entry,每个 Map.Entry 其实就是一个 key-value 对。从上面程序中可以看出:当系统决定存储 HashMap 中的 key-value 对时,完全没有考虑 Entry 中的 value,仅仅只是根据 key 来计算并决定每个 Entry 的存储位置。这也说明了前面的结论:我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置之后,value
随之保存在那里即可.HashMap程序经过我改造,我故意的构造出了hash冲突现象,因为HashMap的初始大小16,但是我在hashmap里面放了超过16个元素,并且我屏蔽了它的resize()方法。不让它去扩容。这时HashMap的底层数组Entry[] table结构如下:
Hashmap里面的bucket出现了单链表的形式,散列表要解决的一个问题就是散列值的冲突问题,通常是两种方法:链表法和开放地址法。链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位;开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。java.util.HashMap采用的链表法的方式,链表是单向链表。形成单链表的核心代码如下:
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
bsp;
上面方法的代码很简单,但其中包含了一个设计:系统总是将新添加的 Entry 对象放入 table 数组的 bucketIndex 索引处——如果 bucketIndex 索引处已经有了一个 Entry 对象,那新添加的 Entry 对象指向原有的 Entry 对象(产生一个 Entry 链),如果 bucketIndex 索引处没有 Entry 对象,也就是上面程序代码的 e 变量是 null,也就是新放入的 Entry 对象指向 null,也就是没有产生
Entry 链。
HashMap里面没有出现hash冲突时,没有形成单链表时,hashmap查找元素很快,get()方法能够直接定位到元素,但是出现单链表后,单个bucket 里存储的不是一个 Entry,而是一个 Entry 链,系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),那系统必须循环到最后才能找到该元素。
当创建 HashMap 时,有一个默认的负载因子(load factor),其默认值为 0.75,这是时间和空间成本上一种折衷:增大负载因子可以减少 Hash 表(就是那个 Entry 数组)所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的的操作(HashMap 的 get() 与 put() 方法都要用到查询);减小负载因子会提高数据查询的性能,但会增加 Hash 表所占用的内存空间。
以上转自http://xiaolu123456.iteye.com/blog/1485349
思考:编写一个简单的实例
#include<iostream> #define LISTSIZE 3 //hashlist的大小 #define TESTSIZE 8 //测试时添加的<key,value>个数,大于LISTSIZE就一定会发生冲突 using namespace std; struct Node{ int key; int value; Node * next=NULL; //int times=0; }; class Hashtest{ public: Hashtest(); //~Hashtest(); int hash(int key); void addNode(Node & p); Node * findNode(int key); private: Node * hashlist[LISTSIZE]; }; Hashtest::Hashtest(){ for(int i=0;i<LISTSIZE;i++){ hashlist[i]=NULL; } } //hashlist初始化为NULL /* Hashtest::~Hashtest(){ for(int i=0;i<LISTSIZE;i++) { Node *head=hashlist[i]; while(head!=NULL){ Node *temp = head; head = head->next; delete temp; } } }*/ //析构,因为本次测试main中的Node图方便使用数组创建的储存于stack而不是new Node()储存于heap,所以不用。 int Hashtest::hash(int key){ int pos=key%LISTSIZE; return pos; } //一个简单的hash函数 void Hashtest::addNode(Node & p){ int pos=this->hash(p.key); Node *head = hashlist[pos]; if(head==NULL){ hashlist[pos]=&p; return ; } while(head->next!=NULL){ head=head->next; } head->next=&p; } //添加一组数据p(key,value) ,通过key及hash()找到p应该存放的链表的头指针; Node * Hashtest::findNode(int itemkey){ int pos=this->hash(itemkey); Node *head=hashlist[pos]; while(head->key!=itemkey&&head!=NULL){ head=head->next; } return head; } //搜索一组数据p,先定位其在hashlist中的位置,再定位在链表中的位置。存储结构其实就是一个半静态半动态的二维数组。 int main(){ Hashtest ht; Node nodelist[TESTSIZE]; for(int i=0;i<TESTSIZE;i++){ nodelist[i].key=i; nodelist[i].value=i; ht.addNode(nodelist[i]); } for(int i=0;i<TESTSIZE;i++){ cout<<ht.findNode(i)->value<<endl; } return 0; }照预期输出0至(TESTSIZE-1)。逻辑是OK,不过至于效率问题暂时只能仅限于理论无法测试了。
搜索的时间,通过hash()找到hashlist[i]是O(1)的,时间开销主要在搜索链表。如果在链表头还好,在表尾的话就要遍历整个链表。
那能不能通过额外的空间开销,给Node添加一个times属性记录访问的历史次数,维护时,一定时间间隔就对链表进行排序,将访问频率高的Node放置链表头部,预计未来访问频率仍然较高?
又突然想到了缓存过期策略中的LRU(Least Recently Used)算法,对应的是不是也能有一种“最近刚好使用算法”?在findNode()方法中添加一步,每当一个Node被查询,意味着它活跃度可能较高,未来被查询的可能性也会比较高,就将这个Node移动到链表最前端。因为链表中节点的移动消耗非常低,但可能为下一次或n次查询节省进行一次完全遍历的时间。
未曾从事过海量数据处理方面的工作,瞎想想又不用负责- -。
相关文章推荐
- HashMap的实现原理及hash冲突解决方法
- HashMap解决hash冲突的方法
- HashMap解决hash冲突的方法
- HashMap解决hash冲突的方法
- HashMap解决hash冲突的方法
- HashMap解决Hash冲突的方法
- java基础--HashMap解决hash冲突的方法
- HashMap解决hash冲突的方法
- 链表法HashMap解决hash冲突的方法
- HashMap解决hash冲突的方法
- HashMap解决hash冲突的方法
- hash冲突的解决方法以及hashMap的底层实现
- HashMap解决hash冲突的方法
- HashMap解决hash冲突的方法
- HashMap解决hash冲突的方法
- HashMap解决hash冲突的方法
- HashMap的put、get方法分析与Hash冲突的分析、解决
- HashMap解决hash冲突的方法
- HashMap解决hash冲突的方法