您的位置:首页 > 其它

一篇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

思考:编写一个简单的实例

#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次查询节省进行一次完全遍历的时间。

未曾从事过海量数据处理方面的工作,瞎想想又不用负责- -。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: