【问题标题】:Expand hash table without rehash?无需重新哈希即可扩展哈希表?
【发布时间】:2016-01-02 11:43:20
【问题描述】:

我正在寻找不需要rehash 进行扩展和收缩的hash table 数据结构? Rehash 是一项消耗 CPU 的工作。我想知道是否有可能以根本不需要重新散列的方式设计散列表数据结构?你以前听说过这样的数据结构吗?

【问题讨论】:

  • 想想哈希表在内部是如何工作的,以及为什么它们是这样设计的。您希望无重新哈希哈希表如何工作?
  • 用对象存储对象的哈希值,并使用多级哈希表。
  • 如果您担心将数据复制到新的 HT 导致的暂停(对于实时系统可能会出现问题),您可以逐步进行复制,即保留两个旧的随着时间的推移,新的 HT 和复制元素。在此期间,如果查找失败,您将不得不使用旧的 HT。如果你想以小幅度增加大小并避免在这些过程中复制所有数据,你应该研究一致的散列,这是 DHT 经常使用的东西。

标签: c data-structures hashtable


【解决方案1】:

扩展和收缩不需要重新散列? Rehash 是一项消耗 CPU 的工作。我想知道是否有可能以根本不需要重新散列的方式设计散列表数据结构?你以前听说过这样的数据结构吗?

这取决于你所说的“rehash”:

  • 如果您只是意味着在调整大小期间表级重新散列不应该将散列函数重新应用到每个键,那么对于大多数库来说这很容易:例如将密钥及其原始(预模表大小)实际哈希值包装在一起,如struct X { size_t hash_; Key key_ };,为哈希表库提供返回hash_ 的哈希函数,但提供比较key_s 的比较函数(根据key_比较的复杂程度,你或许可以使用hash_进行优化,例如lhs.hash_ == rhs.hash_ && lhs.key_ == rhs.key_)。

    • 如果密钥的散列特别耗时(例如,较长密钥的加密强度),这将很有帮助。对于非常简单的散列(例如 ints 的直通),它会减慢您的速度并浪费内存。
  • 1234563和正常的性能配置文件。下面讨论。

仅举一个例子,您可以利用更典型的哈希表实现(我们称之为 H),方法是让您的自定义哈希表 (C) 有一个 H** p - 直到初始大小限制 - 将有 p[0] H 的唯一实例,并简单地将操作/结果通过。如果表超出此范围,则保持 p[0] 引用现有 H,同时创建第二个 H 哈希表以供 p[1] 跟踪。然后事情开始变得冒险:

  • 要在 C 中搜索或擦除,您的实现需要先搜索 p[1],然后再搜索 p[0] 并报告任何匹配项

  • 要在 C 中插入新值,您的实现必须确认它不在 p[0] 中,然后插入到 p[1]

    • 对于每个insert(甚至可能用于其他操作),它可以选择将任何匹配的——或任意的p[0]条目——迁移到p[1],这样p[0]就会逐渐清空;你可以很容易地保证p[0]p[1] 被填满之前是空的(因此需要更大的表)。当p[0] 为空时,您可能需要p[0] = p[1]; p[1] = NULL; 来保持简单的思维模型,即在哪里 - 有很多选择。

一些现有的哈希表实现在迭代元素时非常有效(例如 GNU C++ std::unordered_set),因为所有值都有一个单链表,而哈希表实际上只是指针的集合(用 C++ 的说法, 迭代器) 到链表中。这可能意味着,如果您的唯一/较大哈希表的利用率低于某个阈值(例如 10% 负载因子),您知道可以非常有效地将剩余元素迁移到较小的表中。

一些哈希表使用这些技巧来避免重新哈希期间突然出现大量开销,而是将痛苦更均匀地分散到许多后续操作中,避免可能出现的令人讨厌的延迟峰值。

一些实现选项仅对开放的封闭的散列实现有意义,或者仅在键和/或值很小或很大并且取决于表是否嵌入时才有用他们或指向他们。了解它的最好方法是编写代码......

【讨论】:

    【解决方案2】:

    这取决于你想避免什么。重新散列意味着重新计算散列值。您可以通过将哈希值存储在哈希结构中来避免这种情况。将条目重新分配到重新分配的哈希表中可能会更便宜(通常是单个模或掩码操作),并且对于简单的哈希表实现来说几乎是不可避免的。

    【讨论】:

      【解决方案3】:

      假设您确实需要这个.. 这是可能的。在这里,我将给出一个简单的示例,您可以在此基础上进行构建。

      // Basic types we deal with
      typedef uint32_t key_t;
      typedef void *   value_t;
      typedef struct 
      {
          key_t key;
          value_t value;
      } hash_table_entry_t;
      
      typedef struct
      {
          uint32_t initialSize;
          uint32_t size; // current max entries
          uint32_t count; // current filled entries
          hash_table_entry_t *entries;
      } hash_table_t;
      
      // Hash function depends on the size of the table
      key_t hash(value_t value, uint32_t size)
      {
          // Simple hash function that just does modulo hash table size;
          return *(key_t*)&value % size;
      }
      
      void init(hash_table_t *pTable, uint32_t initialSize)
      {
          pTable->initialSize = initialSize;
          pTable->size = initialSize;
          pTable->count = 0;
          pTable->entries = malloc(pTable->size * sizeof(*pTable->entries));
          /// @todo handle null return;
          // Set to ~0 to signal invalid keys.
          memset(pTable->entries, ~0, pTable->size * sizeof(*pTable->entries));
      }
      
      void insert(hash_table_t *pTable, value_t val)
      {
          key_t key = hash(val, pTable->size);
          for (key_t i = key; i != (key-1); i=(i+1)%pTable->size)
          {
              if (pTable->entries[i].key == ~0)
              {
                  pTable->entries[i].key = key;
                  pTable->entries[i].value = val;
                  pTable->count++;
                  break;
              }
          }
      
          // Expand when 50% full
          if (pTable->count > pTable->size/2)
          {
              pTable->size *= 2;
              pTable->entries = realloc(pTable->entries, pTable->size * sizeof(*pTable->entries));
              /// @todo handle null return;
              memset(pTable->entries + pTable->size/2, ~0, pTable->size * sizeof(*pTable->entries));
          }
      }
      
      _Bool contains(hash_table_t *pTable, value_t val)
      {
          // Try current size first
          uint32_t sizeToTry = pTable->size;
          do
          {
              key_t key = hash(val, sizeToTry);
              for (key_t i = key; i != (key-1); i=(i+1)%pTable->size)
              {
                  if (pTable->entries[i].key == ~0)
                      break;
                  if (pTable->entries[i].key == key && pTable->entries[i].value == val)
                      return true;
              }
      
              // Try all previous sizes we had. Only report failure if found for none.
              sizeToTry /= 2;
          } while (sizeToTry != pTable->initialSize);
          return false;
      }
      

      这个想法是哈希函数取决于表的大小。当您更改表的大小时,您不会重新散列当前条目。您使用新的哈希函数添加新的。在读取条目时,您会尝试在此表上使用过的所有哈希函数。

      这样,get()/contains() 和类似的操作会随着您扩展表的次数越多而花费更长的时间,但您不会出现重新散列的巨大峰值。我可以想象一些系统会要求这样做。

      【讨论】:

      • 所以如果我理解正确的话,它将类似于一个普通的哈希表,除了它必须通过 log(n) 可能的哈希函数来找到元素?因此,在平均情况下查找是 O(log(n)) 而不是 O(1)?
      猜你喜欢
      • 2021-02-13
      • 2015-11-27
      • 1970-01-01
      • 2017-05-11
      • 1970-01-01
      • 2012-12-25
      • 2012-04-15
      • 2023-03-31
      • 1970-01-01
      相关资源
      最近更新 更多