【问题标题】:Open Addressing vs. Separate Chaining开放寻址与分离链接
【发布时间】:2011-05-02 19:49:57
【问题描述】:

当负载因子接近 1 时,哪种 hashmap 冲突处理方案更好,以确保最小的内存浪费?

我个人认为答案是使用线性探测的开放寻址,因为它不需要任何额外的存储空间以防发生冲突。这是正确的吗?

【问题讨论】:

  • 取决于插入 162 个项目时会发生多少次碰撞。不过,鉴于计数很少,我无法想象会有很大的差异(但如果你有很多碰撞,可能会出错)。
  • 这也取决于我假设的插入/查找操作的比率?

标签: hashtable hashmap hash-collision


【解决方案1】:

回答问题:当负载因子接近 1 以确保最小的内存浪费时,哪种 hashmap 冲突处理方案更好?

允许高填充的开放寻址/探测。因为正如您自己所说,碰撞不需要额外的空间(只是,好吧,可能是时间 - 当然这也是假设哈希函数并不完美)。

如果您没有在问题中指定“负载因子接近 1”或包含“成本”指标,那么情况将完全不同。

编码愉快。

【讨论】:

    【解决方案2】:

    已满的 hashmap 将降级为线性搜索,因此您需要将它们保持在 90% 以下。

    关于使用更少内存的开放寻址是正确的,链接将需要每个节点中的指针或偏移字段。

    我创建了一个 hasharray 数据结构,用于当我需要非常轻量级且不会有大量插入的哈希表时。为了保持低内存使用率,所有数据都嵌入在同一块内存中,开头是 HashArray 结构,然后是哈希和值的两个数组。 Hasharray 只能与查找键值一起使用。

    typedef uint16_t HashType;  /* this can be 32bits if needed. */
    typedef uint16_t HashSize;  /* this can be made 32bits if large hasharrays are needed. */
    struct HashArray {
      HashSize length;        /* hasharray length. */
      HashSize count;         /* number of hash/values pairs contained in the hasharray. */
      uint16_t value_size;    /* size of each value.  (maximum size of value 64Kbytes) */
      /* these last two fields are just for show, they are not defined in the HashArray struct. */
      uint16_t hashs[length]; /* array of hashs for each value, this helps with resolving bucket collision */
      uint8_t  values[length * value_size]; /* array holding all values. */
    };
    #define hasharray_get_hashs(array) (HashType *)(((uint8_t *)(array)) + sizeof(HashArray))
    #define hasharray_get_values(array) ((uint8_t *)(array)) + sizeof(HashArray) + \
                                           ((array)->length * sizeof(HashType))
    #define hasharray_get_value(array, idx) (hasharray_get_values(array) + ((idx) * (array)->value_size))
    

    宏 hasharray_get_hashs 和 hasharray_get_values 用于获取 'hashs' 和 'values' 数组。

    我用它来快速查找已经存储在数组中的复杂对象。对象具有用于查找的字符串“名称”字段。名称被散列并插入到带有对象索引的散列数组中。存储在 hasharray 中的值可以是索引/指针/整个对象(我只使用小的 16 位索引值)。

    如果您想将 hasharray 打包到几乎填满,那么您将需要使用完整的 32 位散列而不是上面定义的 16 位散列。当 hasharray 已满 90% 时,更大的 32 位散列将有助于保持快速搜索。

    上面定义的 hasharray 最多只能容纳 65535,这很好,因为我从不将它用于任何具有数百个值的东西。任何需要更多的东西都应该使用普通的哈希表。但如果内存真的是个问题,可以将 HashSize 类型更改为 32 位。此外,我使用 2 的幂长度来保持哈希查找速度快。有些人更喜欢使用素数桶长度,但这仅在散列函数分布不好时才需要。

    #define hasharray_empty_hash 0xFFFF /* hash value to mark empty slots. */
    void *hasharray_search(HashArray *array, HashType hash, uint32_t *next) {
      HashType *hashs = hasharray_get_hashs(array);
      uint32_t mask = array->length - 1;
      uint32_t start_idx;
      uint32_t idx;
    
      hash = (hash == hasharray_empty_hash) ? 0 : hash; /* need one hash value to mark empty slots. */
      start_hash_idx = (hash & mask);
      if(*next == 0) {
        idx = start_idx; /* new search. */
      } else {
        idx = *next & mask; /* continuing search to next slot. */
      }
    
      /* find hash in hash array. */
      do {
        /* check for hash match. */
        if(hashs[idx] == hash) goto found_hash;
        /* check for end of chain. */
        if(hashs[idx] == hasharray_empty_hash) break;
        idx++;
        idx &= mask;
      } while(idx != start_idx);
      /* maximum tries reached (i.e. did a linear search of whole array) or end of chain. */
      return NULL;
    
    found_hash:
      *next = idx + 1; /* where to continue search at, if this is not the right value. */
      return hasharray_get_values(array) + (idx * array->value_size);
    }
    

    会发生哈希冲突,因此调用 hasharray_search() 的代码需要将搜索键与存储在值对象中的键进行比较。如果它们不匹配,则再次调用 hasharray_search()。也可以存在非唯一键,因为可以继续搜索,直到返回“NULL”以查找与一个键匹配的所有值。搜索功能使用线性探测友好地缓存。

    typedef struct {
      char *name;   /* this is the lookup key. */
      char *type;
      /* other field info... */
    } Field;
    
    typedef struct {
      Field *list;          /* array of Field objects. */
      HashArray *lookup;    /* hasharray for fast lookup of Field objects by name.  The values stored in this hasharray are 16bit indices. */
      uint32_t field_count; /* number of Field objects in 'list'. */
    } Fields;
    
    extern Fields *fields_new(uint16_t count) {
      Fields *fields;
      fields = calloc(1, sizeof(Fields));
      fields->list = calloc(count, sizeof(Field));
      /* allocate hasharray to hold at most 'count' uint16_t values.
       * The hasharray will round 'count' up to the next power-of-2.
       * That power-of-2 length must be atleast (count+1), so that there will always be one empty slot.
       */
      fields->lookup = hasharray_new(count, sizeof(uint16_t));
      fields->field_count = count;
    }
    
    extern Field *fields_lookup_by_name(Fields *fields, const char *name) {
      HashType hash = str_to_hash(name);
      Field *field;
      uint32_t next = 0;
      uint16_t *rc;
      uint16_t idx;
    
      do {
        rc = hasharray_search(fields->lookup, hash, &next);
        if(rc == NULL) break; /* field not found. */
        /* found a possible match. */
        idx = *rc;
        assert(idx < fields->field_count);
        field = &(fields->list[idx]);
        /* compare lookup name with field's name. */
        if(strcmp(name, field->name) == 0) {
          /* found match. */
          return field;
        }
        /* field didn't match continue search to next field. */
      } while(1);
      return NULL;
    }
    

    如果 99% 已满且键不存在,最坏情况的搜索将降级为对整个数组的线性搜索。如果键是整数,那么线性搜索应该不会很糟糕,也只需要比较具有相同哈希值的键。我尝试保持哈希数组的大小,使它们只有大约 70-80% 满,如果值只有 16 位值,那么浪费在空槽上的空间并不多。使用这种设计,当使用 16 位散列和 16 位索引值时,每个空槽只浪费 4 个字节。对象数组(上例中的字段结构)没有空点。

    此外,我见过的大多数哈希表实现不存储计算的哈希值,并且需要完整的键比较来解决存储桶冲突。比较哈希值有很大帮助,因为只有一小部分哈希值用于查找存储桶。

    【讨论】:

    • 请说明您正在使用哪种碰撞处理方案。从您的回答中不是很明显。
    • 哈希数组使用开放寻址和线性探测。 hasharray_search() 函数不只比较键值,因此每次找到匹配的哈希值时都会返回该值,调用者必须进行键值比较。
    • “一个完整的哈希图将降级为线性搜索”这是不正确的。完整的哈希图并不意味着存在冲突。完整的哈希图可能意味着所有存储桶都由单个条目声明。
    • 如何通过一个条目来认领所有的桶?一个键只能有一个值。
    【解决方案3】:

    正如其他人所说,在线性探测中,当负载因子接近 1 时,时间复杂度接近线性搜索。 (当它充满时,它是无限的。)这里有一个内存效率的权衡。虽然分离链总是给我们理论上恒定的时间。

    通常,在线性探测下,建议将负载系数保持在 1/8 和 1/2 之间。当数组已满 1/2 时,我们将其调整为原始数组大小的两倍。 (参考:算法。罗伯特·塞奇威克。凯文·韦恩。)。删除时,我们也将数组大小调整为原始大小的 1/2。如果你真的有兴趣,从我上面提到的那本书开始对你有好处。 实际上,据说0.72是我们通常使用的经验值。

    【讨论】:

      猜你喜欢
      • 2013-03-29
      • 1970-01-01
      • 2016-05-29
      • 1970-01-01
      • 1970-01-01
      • 2011-02-03
      • 2018-09-17
      • 1970-01-01
      • 2012-03-24
      相关资源
      最近更新 更多