16.4. Open hashing

One collision-resolution strategy is separate chaining, also called open hashing. In separate chaining, the hash table is an array of buckets. Each bucket refers to a collection that can hold every key mapped to that bucket. A linked list is a traditional choice for the collection, which is where the term "chaining" originates, but other containers are possible.

The first item in a bucket is not itself a collision. A collision occurs when a second or later key maps to the same bucket index. The bucket's collection stores all of those keys together.

Example set

The following diagram uses seven buckets. The bucket assignments are shown directly so that the two chains are easy to follow; a real table would compute each assignment with its hash function.

Fruit set with separate chains

Example map

When the ADT is a map, the process is similar. The value hashed is the map key, since the key uniquely identifies a map entry. Each bucket provides access to one or more key-value pairs.

Fruit inventory map with separate chains

The linked lists allow the contents of each bucket to grow as needed, while the array provides the first lookup step. A fixed bucket count is used here to keep the implementation focused; production hash tables also monitor load factor and rehash when appropriate.

A set provides a simple demonstration of a hashed data structure. Recall that set stores unique items. The implementation below is similar to a set, but intentionally leaves out iterators over all buckets, rehashing, allocator support, and many other standard-library features.

hash_set

The template parameters describe the key type, the fixed number of buckets, the hash function, and the equality predicate. Equivalent keys must compare equal and must produce the same hash value.

Simplified interface
template <class Key,
          std::size_t N,
          class Hash = std::hash<Key>,
          class KeyEqual = std::equal_to<Key>>
class hash_set {
  using iterator = typename Container::iterator;
  using const_iterator = typename Container::const_iterator;

  std::pair<iterator, bool> insert(const Key& value);
  iterator find(const Key& value);
  const_iterator find(const Key& value) const;
  std::size_t count(const Key& value) const;
  std::size_t erase(const Key& value);
};

find

Finding a value using separate chaining has two steps:

  1. Compute the hash value and reduce it to a bucket index.

  2. Search the selected bucket, comparing keys with the equality predicate.

The following diagram uses value % 10 as its illustrative hash for integer keys. The chain in bucket 4 contains two values, so finding 54 requires checking more than one entry.

Integer hash table with a collision chain

To find 54, the table computes 54 % 10 == 4 and selects bucket 4. It compares 54 with 34 first, then with 54. The equality predicate confirms the match, and find returns an iterator to the stored value. If the value is absent, it returns the bucket's end() iterator.

Pseudocode
bucket <- buckets[hash(value) % bucket_count]
for each item in bucket:
    if equal(item, value):
        return iterator to item
return bucket.end()

A hash function that always returns 42 still satisfies the basic hash contract: equivalent keys produce the same hash value, and collisions are allowed. It is nevertheless a poor choice. Every key would be placed in bucket 42 % bucket_count, so a lookup would linearly scan one growing chain and lose the expected constant-time benefit.

insert

Inserting into a set follows the same two steps as finding:

  1. Select the bucket.

  2. Search for an equivalent key.

If the key is already present, the set remains unchanged. Otherwise, the new key is appended to the bucket. Returning an iterator and a Boolean follows the contract used by standard associative containers: the Boolean is true only when a new key was inserted.

Pseudocode
bucket <- buckets[hash(value) % bucket_count]
position <- find equivalent value in bucket
if position != bucket.end():
    return {position, false}
append value to bucket
return {iterator to new value, true}

erase

Erasing is also a bucket search followed by an equality comparison. If the key is found, remove it and return 1. Otherwise return 0.

Pseudocode
bucket <- buckets[hash(value) % bucket_count]
position <- find equivalent value in bucket
if position == bucket.end():
    return 0
erase position from bucket
return 1

Run it

This complete example uses an identity hash for positive integers so that its bucket assignments are predictable. Values such as 34 and 45 deliberately collide in bucket 1 when there are 11 buckets.

  1#include <algorithm>
  2#include <array>
  3#include <cstddef>
  4#include <functional>
  5#include <iostream>
  6#include <iterator>
  7#include <list>
  8#include <ostream>
  9#include <utility>
 10
 11struct identity_hash {
 12  std::size_t operator()(int value) const noexcept {
 13    return static_cast<std::size_t>(value);
 14  }
 15};
 16
 17template <class Key,
 18          std::size_t N,
 19          class Hash = std::hash<Key>,
 20          class KeyEqual = std::equal_to<Key>>
 21class hash_set {
 22  static_assert(N > 0, "hash_set needs at least one bucket");
 23
 24  using container_type = std::list<Key>;
 25
 26public:
 27  using value_type = Key;
 28  using key_type = Key;
 29  using size_type = std::size_t;
 30  using iterator = typename container_type::iterator;
 31  using const_iterator = typename container_type::const_iterator;
 32  using insert_result = std::pair<iterator, bool>;
 33
 34  insert_result insert(const Key& value) {
 35    auto& bucket = find_bucket(value);
 36    const auto position = find_in_bucket(bucket, value);
 37    if (position != bucket.end()) {
 38      return {position, false};
 39    }
 40
 41    bucket.push_back(value);
 42    ++size_;
 43    return {std::prev(bucket.end()), true};
 44  }
 45
 46  iterator find(const Key& value) {
 47    return find_in_bucket(find_bucket(value), value);
 48  }
 49
 50  const_iterator find(const Key& value) const {
 51    return find_in_bucket(find_bucket(value), value);
 52  }
 53
 54  size_type count(const Key& value) const {
 55    const auto& bucket = find_bucket(value);
 56    return find_in_bucket(bucket, value) == bucket.end() ? 0 : 1;
 57  }
 58
 59  size_type erase(const Key& value) {
 60    auto& bucket = find_bucket(value);
 61    const auto position = find_in_bucket(bucket, value);
 62    if (position == bucket.end()) {
 63      return 0;
 64    }
 65
 66    bucket.erase(position);
 67    --size_;
 68    return 1;
 69  }
 70
 71  size_type size() const noexcept {
 72    return size_;
 73  }
 74
 75  bool empty() const noexcept {
 76    return size_ == 0;
 77  }
 78
 79  friend std::ostream& operator<<(std::ostream& os,
 80                                  const hash_set& set) {
 81    os << '[';
 82    for (size_type bucket_index = 0;
 83         bucket_index < N; ++bucket_index) {
 84      for (const auto& value : set.buckets_[bucket_index]) {
 85        os << bucket_index << ':' << value << ' ';
 86      }
 87    }
 88    return os << ']';
 89  }
 90
 91private:
 92  container_type& find_bucket(const Key& value) {
 93    return buckets_[hasher_(value) % N];
 94  }
 95
 96  const container_type& find_bucket(const Key& value) const {
 97    return buckets_[hasher_(value) % N];
 98  }
 99
100  iterator find_in_bucket(container_type& bucket,
101                          const Key& value) {
102    return std::find_if(bucket.begin(), bucket.end(),
103                        [this, &value](const Key& item) {
104                          return equal_(item, value);
105                        });
106  }
107
108  const_iterator find_in_bucket(const container_type& bucket,
109                                const Key& value) const {
110    return std::find_if(bucket.begin(), bucket.end(),
111                        [this, &value](const Key& item) {
112                          return equal_(item, value);
113                        });
114  }
115
116  std::array<container_type, N> buckets_;
117  Hash hasher_;
118  KeyEqual equal_;
119  size_type size_ = 0;
120};
121
122int main() {
123  hash_set<int, 11, identity_hash> values;
124
125  const auto first = values.insert(34);
126  const auto duplicate = values.insert(34);
127  values.insert(45);
128  values.insert(21);
129
130  std::cout << std::boolalpha
131            << "first insertion: " << first.second << '\n'
132            << "duplicate insertion: " << duplicate.second << '\n'
133            << "count(45): " << values.count(45) << '\n'
134            << "values: " << values << '\n';
135
136  values.erase(34);
137  std::cout << "after erase, count(34): " << values.count(34)
138            << '\n';
139}