`
whitesock
  • 浏览: 484459 次
  • 性别: Icon_minigender_1
  • 来自: 大连
社区版块
存档分类
最新评论

Open Addressing

    博客分类:
  • SE
阅读更多

1 Overview

      Open addressing和Chaining是两种不同的解决hash冲突的策略。当多个不同的key被映射到相同的slot时,chaining方式采用链表保存所有的value。而Open addressing则尝试在该slot的邻近位置查找,直到找到对应的value或者空闲的slot, 这个过程被称作probing。常见的probing策略有Linear probing,Quadratic probing和Double hashing。

 

2 Chaining

2.1 Chaining in java.util.HashMap

      在分析open addressing策略之前,首先简单介绍一下大多数的Java 核心集合类采用的chaining策略,以便比较。 java.util.HashMap有一个Entry[]类型的成员变量table,其每个非null元素都是一个单向链表的表头。

  • put():如果存在hash冲突,那么对应的table元素的链表长度会大于1。
  • get():需要对链表进行遍历,在遍历的过程中不仅要判断key的hash值是否相等,还要判断key是否相等或者equals。
  • 对于put()和get()操作,如果其参数key为null,那么HashMap会有些特别的优化。

      Chaining策略的主要缺点是需要通过Entry保存key,value以及指向链表下个节点的引用(Map.Entry就有四个成员变量),这意味着更多的内存使用(尤其是当key,value本身使用的内存很小时,额外使用的内存所占的比例就显得比较大)。此外链表对CPU的高速缓存不太友好。

 

3 Open Addressing

3.1 Probing

3.1.1 Linear probing

      两次查找位置的间隔为一固定值,即每次查找时在原slot位置的基础上增加一个固定值(通常为1),例如:P = (P + 1) mod SLOT_LENGTH。其最大的优点在于计算速度快,另外对CPU高速缓存更友好。其缺点也非常明显:

      假设key1,key2,key3的hash code都相同并且key1被映射到slot(p),那么在计算key2的映射位置时需要查找slot(p), slot(p+1),计算key3的映射位置时需要查找slot(p), slot(p+1),slot(p+2)。也就是说对于导致hash冲突的所有key,在probing过程中会重复查找以前已经查找过的位置,这种现象被称为clustering。

 
3.1.2 Quadratic probing

      两次查找位置的间隔线性增长,例如P(i) = (P + c1*i + c2*i*i) mod SLOT_LENGTH,其中c1和c2为常量且c2不为0(如果为0,那么降级为Linear probing)。 Quadratic probing的各方面性能介于Linear probing和Double hashing之间。


3.1.3 Double hashing
       两次查找位置的间隔为一固定值,但是该值通过另外一个hash算法生成,例如P = (P + INCREMENT(key)) mod SLOT_LENGTH,其中INCREMENT即另外一个hash算法。以下是个简单的例子:

      H(key) = key mod 10
      INCREMENT(key) = 1 + (key mod 7)
      P(15): H(15) = 5;
      P(35): H(35) = 5, 与P(15)冲突,因此需要进行probe,位置是 (5 + INCREMENT(35)) mod 10 = 6
      P(25): H(25) = 5, 与P(15)冲突,因此需要进行probe,位置是 (5 + INCREMENT(25)) mod 10 = 0

      P(75): H(75) = 5, 与P(15)冲突,因此需要进行probe,位置是 (5 + INCREMENT(75)) mod 10 = 1

      从以上例子可以看出,跟Linear probing相比,减少了重复查找的次数。

 

3.2 Load Factor

      基于open addressing的哈希表的性能对其load factor属性值非常敏感。如果该值超过0.7 (Trove maps/sets的默认load factor是0.5),那么性能会下降的非常明显。由于hash冲突导致的probing次数跟(loadFactor) / (1 - loadFactor)成正比。当loadFactor为1时,如果哈希表中的空闲slot非常少,那么可能会导致probing的次数非常大。

 

3.3 Open addressing in gnu.trove.THashMap

      GNU Trove (http://trove4j.sourceforge.net/) 是一个Java 集合类库。在某些场景下,Trove集合类库提供了更好的性能,而且内存使用更少。以下是Trove中跟open addressing相关的几个特性:

  • Trove maps/sets没有使用chaining解决hash冲突,而是使用了open addressing。
  • 跟chaining相比,open addressing对hash算法的要求更高。通过TObjectHashingStrategy 接口, Trove支持定制hash算法(例如不希望使用String或者数组的默认hash算法)。
  • Trove提供的maps/sets的capaicity属性一定是质数,这有助于减少hash冲突。
  • 跟java.util.HashSet不同,Trove sets没有使用maps,因此不需要额外分配value的引用。

      跟java.util.HashMap相比,gnu.trove.THashMap没有Entry[] table之类的成员变量,而是分别通过Object[] _set,V[] _values直接保存key和value。在逻辑上,Object[] _set中的每个元素都有三种状态:

  • FREE:该slot目前空闲;
  • REMOVED:该slot之前被使用过,但是目前数据已被移除;
  • OCCUPIED:该slot目前被使用中;

      这三种状态的迁移过程如下:

  • 在构造或者resize时,所有_set元素都会赋值为FREE(FREE状态);
  • 向THashMap中put某个key/value对时,_set中对应的元素会被赋值为put()方法的参数key(OCCUPIED状态);
  • 从THashMap中以key进行remove的时,_set中对应的元素会被赋值为REMOVED(注意:不是赋值为FREE);

      以下是关于状态迁移的简单例子(:= 的含义是赋值, H(key) = key mod 11):

  • put(7, value):  _set[7] := 7;
  • put(9, value):  _set[9] := 9;
  • put(18, value):  由于_set[7]处于OCCUPIED状态,导致hash冲突;假设第一次probe计算得出9,由于_set[9]处于OCCUPIED状态,仍然hash冲突; 假设再次probe计算得到1, 由于_set[1]处于FREE状态,所以_set[1] := 18;
  • get(18): _set[7]处于OCCUPIED状态并且与18不等;假设第一次probe计算得出9,_set[9]的值与18不等;假设再次probe计算得到1, 由于_set[1] 的值等于18,所以返回对应value;
  • remove(9): _set[9] := REMOVED;
  • get(18): _set[7]的值与18不等;假设第一次probe计算得出9,由于_set[9]状态为REMOVED,需要再次probe;假设再次probe计算得到1, 由于_set[1] 的值等于18,所以返回对应value;
  • put(9, value):  _set[9] := 9;

      以下是与get()方法相关的代码片段:

public V get(Object key) {
    int index = index((K) key);
    return index < 0 ? null : _values[index];
}

protected int index(T obj) {
    final TObjectHashingStrategy<T> hashing_strategy = _hashingStrategy;

    final Object[] set = _set;
    final int length = set.length;
    final int hash = hashing_strategy.computeHashCode(obj) & 0x7fffffff;
    int index = hash % length;
    Object cur = set[index];

    if ( cur == FREE ) return -1;

    // NOTE: here it has to be REMOVED or FULL (some user-given value)
    if ( cur == REMOVED || ! hashing_strategy.equals((T) cur, obj)) {
        // see Knuth, p. 529
        final int probe = 1 + (hash % (length - 2));

        do {
            index -= probe;
            if (index < 0) {
                index += length;
            }
            cur = set[index];
        } while (cur != FREE
             && (cur == REMOVED || ! _hashingStrategy.equals((T) cur, obj)));
    }

    return cur == FREE ? -1 : index;
}

      从以上代码可以看出get()方法的流程如下, 根据key的hash值找到对应的set元素,判断是否存在hash冲突。

      如果不存在hash冲突,那么该set元素的可能状态如下:

  • FREE:意味着THashMap中不存在该key;
  • OCCUPIED,并且该元素的值等于get()方法的参数key:意味着THashMap中存在该key;
  • 非以上两种情况:意味着存在hash冲突,需要进行probe,直到找到状态为以上两种状态的set元素;

      以下是与put()方法相关的代码片段:

public V put(K key, V value) {
    int index = insertionIndex(key);
    return doPut(key, value, index);
}

private V doPut(K key, V value, int index) {
    V previous = null;
    Object oldKey;
    boolean isNewMapping = true;
    if (index < 0) {
        index = -index -1;
        previous = _values[index];
        isNewMapping = false;
    }
    oldKey = _set[index];
    _set[index] = key;
    _values[index] = value;
    if (isNewMapping) {
        postInsertHook(oldKey == FREE);
    }

    return previous;
}

protected int insertionIndex(T obj) {
    final TObjectHashingStrategy<T> hashing_strategy = _hashingStrategy;

    final Object[] set = _set;
    final int length = set.length;
    final int hash = hashing_strategy.computeHashCode(obj) & 0x7fffffff;
    int index = hash % length;
    Object cur = set[index];

    if (cur == FREE) {
        return index;       // empty, all done
    } else if (cur != REMOVED && hashing_strategy.equals((T) cur, obj)) {
        return -index -1;   // already stored
    } else {                // already FULL or REMOVED, must probe
        // compute the double hash
        final int probe = 1 + (hash % (length - 2));

        // if the slot we landed on is FULL (but not removed), probe
        // until we find an empty slot, a REMOVED slot, or an element
        // equal to the one we are trying to insert.
        // finding an empty slot means that the value is not present
        // and that we should use that slot as the insertion point;
        // finding a REMOVED slot means that we need to keep searching,
        // however we want to remember the offset of that REMOVED slot
        // so we can reuse it in case a "new" insertion (i.e. not an update)
        // is possible.
        // finding a matching value means that we've found that our desired
        // key is already in the table
        if (cur != REMOVED) {
            // starting at the natural offset, probe until we find an
            // offset that isn't full.
            do {
                index -= probe;
                if (index < 0) {
                    index += length;
                }
                cur = set[index];
            } while (cur != FREE
                     && cur != REMOVED
                     && ! hashing_strategy.equals((T) cur, obj));
        }

        // if the index we found was removed: continue probing until we
        // locate a free location or an element which equal()s the
        // one we have.
        if (cur == REMOVED) {
            int firstRemoved = index;
            while (cur != FREE
                   && (cur == REMOVED || ! hashing_strategy.equals((T) cur, obj))) {
                index -= probe;
                if (index < 0) {
                    index += length;
                }
                cur = set[index];
            }
            // NOTE: cur cannot == REMOVED in this block
            return (cur != FREE) ? -index -1 : firstRemoved;
        }
        // if it's full, the key is already stored
        // NOTE: cur cannot equal REMOVE here (would have retuned already (see above)
        return (cur != FREE) ? -index -1 : index;
    }
}

       从以上代码可以看出,THashMap使用Double hashing。用来计算增量的hash算法是final int probe = 1 + (hash % (length - 2));  如果insertionIndex()方法的返回值为正值,那么该值就是可用的slot位置;如果为负值,那么说明该key之前已经保存过,(-index-1)就是之前的slot位置。

 

      put()方法的流程如下, 根据key的hash值找到对应的set元素,判断是否存在hash冲突。

      如果不存在hash冲突,那么该set元素的可能状态如下:

  • FREE:意味着可以在该位置插入;
  • OCCUPIED,并且该元素的值等于put()方法的参数key:意味着该位置之前已经插入过相同key的数据,本次put操作需要对已有值进行替换;
  • 非以上两种情况:意味着存在hash冲突,需要进行probe;在probe的过程中不能轻易重用状态为REMOVED的set元素:如果在整个probe过程中没有发现与put()方法的参数key相等的set元素,那么才可以重用probe过程中遇到的第一个状态为REMOVED的set元素。
7
0
分享到:
评论
1 楼 lzg406 2010-10-18  
唤回大学学的一些知识,LZ强啊,佩服

相关推荐

    数据结构C语言实现散列:创建、解决冲突、查找元素[文].pdf

    首先,实验采用了两种冲突解决策略:Open Addressing 和 Separate Chaining。Open Addressing 是当发生冲突时,寻找下一个空槽位来存放元素的方法。在这个实验中,特别提到了平方探测(quadratic probing)作为冲突...

    数据结构实验C语言实现散列表-构造-解决冲突-查找.pdf

    首先,实验采用Open Addressing和Separate Chaining两种方法来处理冲突。Open Addressing是指当发生冲突时,在散列表中找到下一个可用位置的方法,如平方探测或线性探测。平方探测是冲突解决的一种策略,当一个位置...

    布隆过滤器-BloomFilter

    5. **优化策略**:为了降低误判率,可以使用Open Addressing、Resizing等策略。Open Addressing是在位数组已满时,重新计算哈希值寻找未被使用的位;Resizing是动态调整位数组的大小以适应元素数量的变化。 在实际...

    搜索引擎技术之数据结构

    - **Coalesced hashing**:结合Open addressing和Chaining的优点,通过将链表融入Hashtable的数组中,节省空间。例如,在一个包含特定顺序的字符串序列中,Coalesced hashing会利用线性探测找到空位并插入数据,同时...

    算法训练营第二天1

    在 Hash Table 部分,课程还讲解了如何使用链地址法(Chaining)和开放寻址法(Open Addressing)来解决哈希冲突问题。接着,课程介绍了 Trie 数据结构的基本概念,包括前缀树、后缀树、Trie 的应用场景等。 在 ...

    数据结构chpPPT学习教案.pptx

    当冲突发生时,需要有策略来处理,比如开放寻址法(Open Addressing)和链地址法(Chaining)。开放寻址法是在冲突时寻找下一个空的散列地址,而链地址法则是在每个散列地址上维护一个链表,所有映射到该地址的...

    荣政第七章算法题代码 (1)1

    在给定的信息中,我们可以看到两个不同的哈希表实现,一个是开放寻址法(Open Addressing)的哈希表,另一个是链地址法(Chaining)的哈希表。这两种方法都是解决哈希冲突问题的有效策略。 首先,我们来看开放寻址...

    java 数据结构3

    解决冲突的方法有很多,常见的有开放寻址法(Open Addressing)和链地址法(Chaining)。开放寻址法是在哈希表内寻找下一个可用的槽位来存储冲突的元素,而链地址法则是在每个槽位维护一个链表,所有映射到该槽位的...

    CU Sketch算法实现

    - **冲突处理**:设计有效的冲突处理策略,比如使用Open Addressing或Double Hashing等方法。 - **并行处理**:如果数据量非常大,可以考虑利用多线程或分布式计算框架来加速处理速度。 - **错误容忍**:考虑到近似...

    HashTable的java实现

    接下来,我们讨论开放地址寻址(Open Addressing)。这种方法处理冲突的方式是,当哈希函数找到的位置已被占用时,不是创建一个新的链表节点,而是寻找下一个可用的数组位置。开放地址寻址有多种策略,如线性探测...

    数据结构与算法-作者Aho

    - **哈希表**:介绍了一种重要的数据结构——哈希表(Hash Table),并探讨了使用链地址法(Chaining)和开放寻址法(Open Addressing)解决哈希冲突的方法。 #### 结语 通过对《数据结构与算法》这本书的深入学习...

    部分算法的C实现_算法

    - **哈希表**:通过哈希函数实现快速查找,如Chained Hashing和Open Addressing。 5. **实践应用**: - **文件系统**:许多文件系统的操作,如文件查找、目录遍历,都依赖于高效的算法实现。 - **网络编程**:...

    Go-Go的hashmap使用加密随机种子散列提示开放寻址和罗宾汉哈希

    接下来是**开放寻址**(Open Addressing),这是一种解决哈希冲突的方法。不同于链地址法(每个桶内维护一个链表来处理冲突),开放寻址在哈希表中直接寻找下一个未占用的位置。当冲突发生时,Go的HashMap会使用线性...

    数据结构英文教学课件:chapter10 Hashing.ppt

    解决冲突的方法有多种,如开放寻址法(Open Addressing)和链地址法(Chaining)。开放寻址法是当发生冲突时,寻找下一个空槽;链地址法是在每个数组位置维护一个链表,所有映射到该位置的键都链接在这个链表上。这...

    hash_src.zip_c hashtable_hash_hashtable

    解决冲突的常见方法有开放寻址法(Open Addressing)和链地址法(Chaining)。在C语言中,链地址法通常使用指针链接冲突的键值对,而开放寻址法则需要在数组内找到下一个可用位置。 3. **内存管理**:C语言不提供...

    C++哈希表实现.zip

    常见的方法有开放寻址法(Open Addressing)和链地址法(Chaining)。链地址法是为每个数组元素存储一个链表,相同的哈希值的键会被放入同一个链表中。 3. **插入操作**:在哈希表中插入元素时,首先计算键的哈希值...

    yxy版c++教程 Hash 浅谈哈希算法(csdn)————程序.pdf

    2. **开放地址法(Open Addressing)**:当冲突发生时,不是在链表中寻找下一个位置,而是直接在哈希表中寻找下一个可用的位置。常见的开放地址法有线性探测再散列、二次探测再散列和双哈希探测等。这种方法避免了...

    数据结构Advanced-Data-Structures

    Open addressing 476 Lazy deletion 479 Linear probing 479 Quadratic probing 480 Double hashing 484 Cuckoo hashing 486 Coalesced hashing 491 Perfect hash function 494 Universal hashing 496 Linear ...

    Hash-table-CPP.zip_Table_hash

    常见的解决冲突方法有开放寻址法(Open Addressing)和链地址法(Chaining)。C++中,通常使用链地址法,即在每个桶内维护一个链表,存放所有映射到该位置的键值对。 4. **插入操作**:当向哈希表插入一个键值对时...

    cpp-C仅头文件高性能并行Hashmap

    一种可能的解决方案是使用开放寻址(open addressing)或链地址法(chaining),并在冲突时采用线程安全的探测顺序或链表链接。 在`greg7mdp-parallel-hashmap-2c8470f`这个开源项目中,作者可能采用了上述的一种或...

Global site tag (gtag.js) - Google Analytics