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.
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.
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.
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:
Compute the hash value and reduce it to a bucket index.
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.
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.
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:
Select the bucket.
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.
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.
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}