Implementing Hash Tables in C - Andreinc
Implementing Hash Tables in C - Andreinc
NOTE(s): The article is in “draft” status. The content was originally written in 2017.
The intended audience for this article is undergrad students or seasoned developers who want to
refresh their knowledge on the subject.
The reader should already be familiar with C (pointers, pointer functions, macros, memory
management) and basic data structures knowledge (e.g., arrays, linked lists, and binary trees).
Table of contents
1 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
• Table of contents
• Code
• Introduction
• Hash Functions
◦ Division hashing
◦ Multiplicative hashing
◦ Hashing strings
▪ djb2
▪ sdbm
◦ More exploration
2 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
3 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
▪ A generic implementation
▪ The model
▪ The interface
▪ The model
▪ The interface
▪ Appending an element
4 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
◦ Open Addressing
▪ Tombstones
▪ A generic implementation
▪ The model
▪ The interface
▪ Modeling Tombstones
• References
Code
If you don’t want to read the article, and you just want to jump directly into the code for:
Introduction
Outside the domain of computers, the word hash means to chop/mix something.
In Computer Science, a hash table is a fundamental data structure that associates a set of keys
with a set of values. Each pair <key, value> is an entry in our hash table. Given a key, we can
5 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
get the value. Not only that, but we can add and remove <key, value> pairs whenever it is
needed.
In a way, a hash table share some similarities with the average “array”, so let’s look at the
following code:
If we were to run it, the output would be 200 . As we write arr[<index>] , we are peeping at the
value associated with the given <index> , and in our case, the value associated with 1 is 200 .
In this regard, a hash table can act very similar to an array, because it will allow us to map a
value to a given key. But there’s a catch, compared to an array, the key can be everything - we
are not limited to sorted numerical indexes.
Most modern computer programming languages have a hash table implementation in their
standard libraries. The names can be different, but the results are the same.
#include <iostream>
#include <string>
#include <unordered_map>
int main() {
6 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
import java.util.Map;
import java.util.HashMap;
colors.put("RED", "#FF0000");
colors.put("GREEN", "#00FF00");
colors.put("BLUE", "#0000FF");
System.out.println(colors.get("RED"));
}
}
// Output: #FF0000
Or in languages like python, support for hash tables is “built-in” in the language itself:
colors = {
"RED" : "#FF0000",
"GREEN" : "#00FF00",
"BLUE" :"#0000FF"
}
print(colors["RED"])
# Output: #FF0000
What is impressive about hash tables is they are very “efficient” in terms of (average) time
complexities. Given a key, we can return the value associated with it in(almost) constant time,
no matter how many pairs we have previously inserted. So they scale exceptionally well with
size. In a way, for a hash table “size doesn’t matter”, as long as we keep things under control:
Think of it for a moment. For a binary tree, searching for an element is O(logn) ; if the trees
grow, n grows, so searching for an element takes more time. But No!, not for hash tables. As
7 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
long as we know the key, we can have almost instant access to the stored value.
This remarkable data structure is internally powered by clever mathematical functions: called
hash functions. They are the “force” behind the fantastic properties of hash tables: the ability to
scale even if the number of pairs increases.
At this point, it wouldn’t be wise to jump directly to the implementation of a hash table, so we will
make a short mathematical detour into the wonderful world of hash functions. They are less
scary than you would typically expect, well, at least on the surface.
Hash Functions
In English, A hash function is a function that “chops” data of arbitrary size to data of fixed size.
From a mathematical perspective, A hash function is a function H : X → [0, M), that takes
an element in x ∈ X and associates to it a positive integer H(x) = m, where m ∈ [0, M).
X can be bounded or an un-bounded set of values, while M is always a positive finite number
0 < M < ∞.
8 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
In the above diagram Paris , Berlin , Warsaw , Bucharest , Athens are all capitals that ∈ X,
where X is a set containing all European Capitals, so n(X) = 48.
• H("Paris") returns 00 ;
• H("Berlin) returns 01 ;
• H("Warsaw) returns 04 ;
• H("Bucharest) returns 03 ;
• H("Athens") returns 04 ;
In total, there are around 48 countries in Europe, each with its capital. So the number of
elements in X is 48, while M = 16 so that the possible values are in the interval
[0, 1, 2, . . . 16).
Because 48 > 16, no matter how we write H , some European Capital Cities will share the same
number m ∈ [0, 1, . . . 16).
For our hypothetical example, we can see that happening for H("Warsaw) = H("Athens") = 4 .
In our example we can say that H has a hash collision for x1 = W arsaw and x2 = Athens,
because H(x1 ) = H(x2 ) = 4.
Hash Collisions are not game-breaking per se, as long as the n(X) > M they might happen.
9 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
But it’s important to understand that a good hash function creates fewer hash collisions than a
bad one.
Basically the worst hash function we can write is a function that returns a constant value, so that
H(x1 ) = H(x2 ) =. . . = H(xn ) = c, where n = n(X), and c ∈ [0, M). This is just another
way of saying that every element x ∈ X will collide with the others.
Another way we can shoot ourselves in the foot is to pick a hash function that is not
deterministic. When we call H(x) subsequently, it should render the same results without being
in any way affected by external factors.
Cryptographic hash functions are a special family of hash functions. For security
considerations, they exhibit an extra set of properties. The functions used in hash table
implementations are significantly less pretentious.
Another essential aspect when picking the right hash function is to pick something that it’s not
computationally intensive. Preferably it should be something close to O(1) . Hash tables are
using hash functions intensively, so we don’t want to complicate things too much.
Picking the proper hash function is a tedious job. It involves a lot of statistics, number theory, and
empiricism, but generally speaking, when we look for a hash function, we should take into
consideration the following requirements:
We can talk about “families” of hashing functions. On the one hand, you have cryptographic
hash functions that are computationally complex and have to be resistant to preimage attacks
(https://en.wikipedia.org/wiki/Preimage_attack) (that’s a topic for another day). Then you have simpler
hash functions that are suitable to be used to implement hash tables:
◦ Division hashing;
◦ Multiplicative hashing;
10 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
The advanced math behind hash functions eludes me. I am a simple engineer, no stranger to
math, but not an expert. There are PHD (https://en.wikipedia.org/wiki/Doctor_of_Philosophy) papers on the
subject, and for a select group of people, this is their actual job: finding better and faster ways of
hashing stuff. So, in the next two paragraphs, I will try to keep things as simple as possible by
avoiding making things more mathematical than needed.
Division hashing
The simplest hash function that we can write the mod (https://en.wikipedia.org/wiki/Modulo_operation) %
operation, and it’s called division hashing.
It works on positive numbers, so let’s suppose we can represent our initial input data
x1 , x2 , . . . xn ∈ X ⊂ N as a non-negative integers.
Then, the formula for our hash function is Hdivision (x) = x mod M , where
Hdivision : X → [0, M). In English this means that the hash of a given value x is the remainder
of x divided with M.
On the surface, hashf_division() checks all the requirements for a good hashing function.
And it’s a good hashing function as long as the input data (x1 , x2 , . . . , xn ) is guaranteed to be
perfectly random, without obvious numerical patterns.
• M=4 ;
Without writing any code, we would infer that all the input will be evenly distributed between 4
hash values: 0 , 1 , 2 and 3 . There are going to be collisions (as n(X) is 250000 bigger than
4 ), but theoretically, we can reduce them by increasing M further down the road.
11 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define M_VAL 4
#define X_VAL 1000000
// initiate rand
srand(time(0));
// buckets
int buckets[M_VAL];
12 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
• All input has been evenly distributed between the four values (buckets);
• The % operation is quite efficient (although it’s usually a number of times less efficient than
multiplication);
• Collisions are there, but they can be controlled by increasing the value M to accommodate
the input size.
Multiplication and division take longer time. Integer multiplication takes 11 clock cycles on
Pentium 4 processors, and 3 - 4 clock cycles on most other microprocessors. Integer
division takes 40 - 80 clock cycles, depending on the microprocessor. Integer division is
faster the smaller the integer size on AMD processors, but not on Intel processors.
Details about instruction latencies are listed in manual 4: “Instruction tables”. Tips about
how to speed up multiplications and divisions are given on page 146 and 147,
respectively. (source (https://www.agner.org/optimize/optimizing_cpp.pdf))
We call the resulting hashes buckets, and once we start implementing the actual hash
table, this will make more sense.
But what happens if our (input) data is not that random after all. What if the data follows a
particular obvious (or not so obvious) pattern? How is this pattern going to affect the distribution
of computed hashes?
To this:
Now, all our randomly generated numbers will be even. If that’s not an obvious pattern, I don’t
know what is.
Let’s see how our hashing function behaves in this scenario and how well the hashes are
distributed:
13 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
We see that values 1 and 3 are never used, which is unfortunate, but an expected
consequence of how the input is constructed. If all the input numbers are even, then their
remainder is either 0 or 2 .
So this means our function is not so good after all because it’s extremely sensitive to “input
patterns”?
The answer is not that simple, but what’s for sure is that changing M=4 to M=7 will render
different results.
The results look promising again. The hash values are again “evenly” distributed, but all kinds of
(subtle) problems can appear based on our choice of M .
Normally, Mshould be a prime number, and some prime numbers will work in practice better
than others, e.g.:
′
Hdivision (x) = x mod 127
′′
Hdivision (x) = x mod 511
′′′
Hdivision (x) = x mod 2311
14 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
Knuth
alternative solution, where Hdivision (x) = x(x + 3) mod M .
In practice, division hashing is not that commonly used. The reason is simple, even the results
are satisfactory (especially when is chosen wisely), division and modulo operations are more
M
“expensive” than addition or multiplication.
Multiplicative hashing
A common (and practical) approach for generating relatively uniform hash values is called
multiplicative hashing.
Similar to division hashing, the formula works for positive integers. So we assume that we have
a mechanism that converts our input space to positive numbers only.
M
Hmultip (x) = W
∗ (Ax mod W ), where Hmultip : X ∈ N → [0, M).
+
• A ∈ R∗ is a constant;
2m w m−w w Ax mod 2w
Hmultip (x) = w ∗ (Ax mod 2 ) = 2 ∗ (Ax mod 2 ) =
2 2w−m
By the magic of bitwise operations, Ax mod 2w is just a way of getting the w low order bits of
Ax. Think of it for a second, x % 2 returns the last bit of x, x % 4 returns the last 2 bits of x,
etc.
Dividing a number b by 2w−m actually means shifting right w-m bits. So our method actually
becomes:
15 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
I won’t get into all the mathematical details, but a good choice for A is A = ϕ ∗ 2w , where w is
the word machine word size (https://en.wikipedia.org/wiki/Word_(computer_architecture)), and ϕ (phi) is the
golden ratio (https://en.wikipedia.org/wiki/Golden_ratio).
ϕ, just like its more famous brother π (pi (https://en.wikipedia.org/wiki/Pi)), is an irrational number,
ϕ ∈ Q′ , that is a solution to the equation:
x2 − x − 1 = 0 => ϕ ≈ 0.6180339887498948482045868343656.
w A = 2w ∗ ϕ ≈
16 40,503
32 2,654,435,769
64 11,400,714,819,323,198,485
Having this said, we can now simplify the signature of our C function. We don’t need A , m , w
anymore as input params because they can be #defined as constants.
16 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
Hashing strings
Converting non-numerical data to positive integers ( uint32_t ) is quite simple. After all,
everything is a sequence of bits.
Unfortunately, the output for hashf_krlose will be “extremely” sensitive to input patterns. It’s
easy to apply a little Gematria (https://en.wikipedia.org/wiki/Gematria) ourselves to create input that will
repeatedly return identical hashes.
For example:
17 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
The hash values for "IJK" , "HJL" , "GJM" , "FJN" are all 222 .
The good news is that there’s a common type of hash function that works quite well with strings
( char* ).
djb2
If INIT=5381 and MULT=33 , the function is called Bernstein hash djb2, which dates 1991.
If you find better values, chances are your name will remain in the history books of
Computer Science.
18 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
If you look over the internet for djb2, you will find a different implementation that uses one clever,
simple trick. The code would be:
If we write a << x , we are shifting x bits of a to the left. By the magic of bitwise operations,
this is equivalent to multiplying a * 2^x .
So, our expression hash = ((hash << 5) + hash) + c is equivalent to hash = (hash * 2^5) +
hash + c , that is equivalent to hash = hash * (2^5 + 1) + c , that is equivalent hash = hash *
33 +c .
This is not just a fancy way of doing things. Historically speaking, most CPUs were performing
bitshifts faster than multiplication or division. They still do.
In modern times, modern compilers can perform all kinds of optimizations, including this one. So
it’s up to you to decide if making things harder to read is worth it. Again, some benchmarking is
recommended.
sdbm
19 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
If you search the internet for sdbm you won’t find a lot of details, except (http://www.cse.yorku.ca/~oz/
hash.html):
uint32_t ch_hash_fmix32(uint32_t h) {
h ^= h >> 16;
h *= 0x3243f6a9U;
h ^= h >> 16;
return h;
}
20 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
More exploration
What we’ve discussed so far about hash functions only scratches the surface of a vast subject.
There are hundreds of functions, some of them better than others. But, as Software Engineers,
we need to be pragmatic, know about them, ponder over their pros and cons and, in the end,
take things for granted. Most of the time, simple is better.
For high(er)-level programming languages (C++, C#, Java, python), the “hashing” problem is
already sorted out at “language” or “standard library” level, so we rarely have to write (or copy-
paste) a hash function by hand.
If you want to explore this topic more, I suggest you also take a look at the following articles:
• SipHash (https://github.com/skeeto/scratch/blob/master/siphash/siphash-embed.h#L8)
21 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
In a hash table, data is stored in an array (not surprisingly), where each pair <key, value> has
its own unique index (or bucket). Accessing the data becomes highly efficient as long as we
know the bucket.
Depending on how we plan to tackle potential hash collisions, there are various ways to
implement a hash table:
◦ To add a new entry (PUT) we compute the bucket ( hash(key) ), and then we append
the <key, value > to the corresponding linked list. If the key already exists, we just
update the value ;
◦ To identify an entry (GET), we compute the bucket, traverse the corresponding linked
list until we find the key .
◦ The buckets are being transformed into a red black tree if the number of elements
contained exceeds a certain threshold.
• Open Addressing
◦ There are no linked lists involved - there’s only one <key, value> entry per bucket;
◦ In case of collisions, we probe the array to find another suitable bucket for our entry,
and then we add the entry at this new-found empty location;
◦ Various algorithms for “probing” exists; the simplest one is called linear probing (https://
en.wikipedia.org/wiki/Linear_probing) - in case of collision, we just jump to the next available
bucket;
Each strategy has its PROs and CONs. For example, the creators of Java preferred to use
Separate Chaining in their HashMap implementation, while the creators of python went with Open
Addressing for their dict .
22 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
Separate Chaining is simpler to implement, and in case we have a high frequency hash
collisions, performance degradation is more graceful - not as exacerbated as for Open
Addressing.
Open Addressing is more complex to implement, but if we keep hash collisions at bay, it’s very
cache-friendly.
“Paris” “France”
“Berlin” “Germany”
“Warsaw” “Poland”
“Bucharest” “Romania”
“Athens” “Greece”
The first step is to reduce the keyspace to a numeric value to hash the keys further and
23 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
associate them with buckets. For this, we can use something like djb2 to hash the string to a
uint32_t value. We already know that djb works well with string inputs.
This step is not “part” of the hash table itself. It’s impossible for a hash table to understand all
data formats and reduce them to 32-bit unsigned integers( uint32_t ). In this regard, the data
needs to come with its mechanisms for this. So except the keys are uint32_t , additional hash
functions should be provided to the hash table (for the data, by the data).
After the keys are “projected” to the space, we must map them to buckets (indices in
uint32_t
our array). Another hash function will reduce the key-space further: from uint32_t to [0, M) ,
where M is the total number of buckets. For simplicity, we can use division hashing,
multiplicative hashing, or whatever hash function we think it’s good.
After the bucket is identified, we check if it’s empty ( NULL ). If it’s NULL , we create a new linked
list, and add the pair <key, value> to it. If it’s not empty, we append the entry to the existing
structure.
A generic implementation
Preferably, our hash table will be as generic as possible so that it can be re-used for various
key/values combinations. We want to write code once and re-use it later. Writing a new hash
table every time the key/value combination changes it’s not ideal.
The bad news, C is not exactly the most generic-friendly programming language there is.
The good news, C is flexible enough to make generic programming happen (with some effort) by:
• using #macros that generate specialized code after pre-processing (something similar to
C++ templates).
I’ve recently found an interesting generic data structures library called STC (https://
github.com/tylov/STC); looking at the code can be pretty inspirational.
I am the camp that prefers #macros over void* , but not when it comes to writing tutorials. With
#macros , everything becomes messy fast, and the code is hard to debug and maintain. So, for
this particular implementation, we will use void* for passing/retrieving generic data to/from the
hash table.
24 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
The model
We start by defining the struct (s) behind hash tables. The main structure ( ch_hash ) will
contain:
• Structures to keep tracking of data-specific operations (e.g., a function that checks if two
values are equal, etc.);
• uint32_t hash is not the actual bucket index, but it’s the hash of the data that is going
inside the hash table. For example, if our keys are strings, uint32_t hash is obtained by
applying a hashing function that works for strings (e.g.: djb2). This is only computed once,
and it’s (re)used whenever we need to ** rehash** our table;
• key and val of a void* type. This gives us enough flexibility to store almost anything in
the table.
• struct ch_node_s *next which is a reference to the next node in the list.
The only thing left to explain are the two “unknown” members: ch_hash->ch_key_ops and
25 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
ch_hash->ch_val_ops :
The two types, ch_key_ops and ch_val_ops are simple structures ( struct ) used to group
functions specific to the data inserted in the hash table. As we previously stated, it’s impossible
to think of all the possible combinations of keys/pairs and their types, so we let the user supply
us with the truth.
In case the above code is confusing, please refer to the following article: Function
pointers (https://en.wikipedia.org/wiki/Function_pointer); it will explain to you in great detail how we
can pass functions around through “pointers”.
It’s funny to think C had its first functional programing feature implemented decades
before Java…
Look, if you want to add a chr* as a key in our hash table, please tell us first how do
you: compute its hash, copy it, and free the memory. Our table will do the heavy work for
you, but first, we need to know this !?
As an example, the required functions for strings ( chr* ) can be implemented like this:
26 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
//Finalizer
static uint32_t ch_hash_fmix32(uint32_t h) {
h ^= h >> 16;
h *= 0x3243f6a9U;
h ^= h >> 16;
return h;
}
//djb2
uint32_t hash = (const uint32_t) 5381;
const char *str = (const char*) data;
char c;
while((c=*str++)) {
hash = ((hash << 5) + hash) + c;
}
return ch_hash_fmix32(hash);
}
27 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
As you can see, even if our methods are going to be used for strings ( chr* ) we are forced to
use void* instead and cast data manually.
Sadly, void* is not the T we know from C++ Templates or Java Generics. No compile-time
validations are performed. It’s up to us to use it accordingly.
And then, what it remains is two keep two instances around (one for keys and one for values):
Now let’s retake a look at the initial diagram and how are structures fit together with the visual
representation:
The interface
28 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
Our hash table ( ch_hash ) will support and publicly expose the following functions (interface):
// Free the memory associated with the hash (and all of its contents)
void ch_hash_free(ch_hash *hash);
As you might’ve noticed, there’s no function for deleting an entry. That’s intentionally left out as a
proposed exercise.
Before implementing the enumerated methods, it’s good to clarify a few things that are not
obvious from the interface itself.
Just for fun (best practices are fun!), our hash table will grow automatically if its size reaches a
certain threshold. So every time we insert a new element, we check if the threshold has been
reached and if it’s time to increase the capacity (and the number of available buckets). A
rehashing of everything will also be performed - old entries might go to new buckets.
In this regard let’s define the following constants (we can tweak their values later):
29 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
And then perform the following check each time we insert a new item:
// Grow if needed
if (hash->size > hash->capacity * CH_HASH_GROWTH) {
ch_hash_grow(hash);
// -> The function will perform a full rehashing to a new array of buckets
// of size [hash->capacity * CH_HASH_CAPACITY_MULT]
}
Another function that’s not exposed in our (public) interface is: ch_node*
ch_hash_get_node(ch_hash*, const void*) . This one is used to check if a node exists or not. In
case it exists, it retrieves the node. Otherwise, it returns NULL .
ch_hash_get_node works on a structure ( ch_node ) that is internal for our implementation, while
ch_hash_get will use ch_hash_get_node to retrieve the actual value.
ch_hash_free is a destructor-like function that free s all the memory associated with a
ch_hash . ch_hash_free goes even deeper and de-allocates memory for the internal buckets
and all the existing entries.
30 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
hash = malloc(sizeof(*hash));
if(NULL == hash) {
fprintf(stderr,"malloc() failed in file %s at line # %d", __FILE__,__LINE__);
exit(EXIT_FAILURE);
}
hash->size = 0;
hash->capacity = CH_HASH_CAPACITY_INIT;
hash->key_ops = k_ops;
hash->val_ops = v_ops;
return hash;
}
Note: using exit(EXIT_FAILURE); is not ideal, and it’s not a good practice when you are writing
libraries you want to share with the public. Basically, you are telling the program to stop, without
giving it any chance to do some cleaning first. Don’t do this if you plan to make your own hash
table library and share it with a larger audience.
31 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
ch_node *crt;
ch_node *next;
In terms of what’s happening in ch_hash_free , things are quite straightforward, except for those
two lines:
hash->key_ops.free(crt->key, hash->key_ops.arg);
hash->val_ops.free(crt->val, hash->val_ops.arg);
Because the hash table doesn’t know what type of data it holds in the entries ( void* is not very
explicit, isn’t it), it’s impossible to free the memory correctly.
So in this regard, we use the free functions referenced inside key_ops (for keys) and val_ops
(for values).
32 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
while(NULL!=crt) {
// Iterated through the linked list found at the bucket
// to determine if the element is present or not
if (crt->hash == h && hash->key_ops.eq(crt->key, key, hash->val_ops.arg)) {
result = crt;
break;
}
crt = crt->next;
}
33 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
// Grow if needed
if (hash->size > hash->capacity * CH_HASH_GROWTH) {
ch_hash_grow(hash);
}
}
}
ch_hash_grow is an internal method (not exposed in the public API) responsible for scaling up
34 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
the number of buckets ( hash->buckets ) based on the number of elements contained in the table
( hash->size ).
ch_hash_grow allocates memory for a new array ch_node **new_buckets , and then rehashes all
the elements from the old array ( hash->buckets ) by projecting them in the new buckets.
CH_HASH_GROWTH is the load factor - a constant that influences the growth condition: hash->size
> hash->capacity * CH_HASH_GROWTH .
If allocating a new array ( new_buckets ) fails, we will keep the old one - that’s a pragmatic
decision to make - so, instead of crashing the program, we accept a potential drop in
performance.
35 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
ch_node **new_buckets;
ch_node *crt;
size_t new_capacity;
size_t new_idx;
// Rehash
// For each bucket
for(int i = 0; i < hash->capacity; i++) {
// For each linked list
crt = hash->buckets[i];
while(NULL!=crt) {
// Finding the new bucket
new_idx = crt->hash % new_capacity;
ch_node *cur = crt;
crt = crt->next;
cur->next = new_buckets[new_idx];
new_buckets[new_idx] = cur;
}
}
hash->capacity = new_capacity;
36 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
ch_node *crt;
printf("Hash Buckets:\n");
for(int i = 0; i < hash->capacity; i++) {
crt = hash->buckets[i];
printf("\tbucket[%d]:\n", i);
while(NULL!=crt) {
printf("\t\thash=%" PRIu32 ", key=", crt->hash);
print_key(crt->key);
printf(", value=");
print_val(crt->val);
printf("\n");
crt=crt->next;
}
}
}
37 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
• chained_hash.c (https://github.com/nomemory/chained-hash-table-c/blob/main/chained_hash.c)
• chained_hash.h (https://github.com/nomemory/chained-hash-table-c/blob/main/chained_hash.h)
Using the hash table is quite trivial. Let’s take for example the following:
38 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
return 0;
Greece
Romania
Hash Capacity: 32
Hash Size: 5
Hash Buckets:
bucket[0]:
bucket[1]:
hash=2838988225, key=Berlin, value=Germany
bucket[2]:
bucket[3]:
bucket[4]:
hash=232639524, key=Paris, value=France
bucket[5]:
bucket[6]:
hash=2999025862, key=Bucharest, value=Romana
bucket[7]:
bucket[8]:
hash=2817274824, key=Athens, value=Greece
// ...
// and so on
39 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
The previous implementation is rather naive, so don’t judge it too harshly. I would be a little
concerned if you will use it in practice.
The elephant in the room when it comes to Linked Lists is how unfriendly they are to caching. A
linked list is suitable for inserting items (depending on the scenario). Still, they are not well
equipped for current hardware architectures, especially when it comes to iteration and reading
elements.
CPUs are usually looking to cache two things: the recently accessed memory, and then it tries to
predict which memory will be used next. For linked lists, the data is not contiguous; nodes are
somewhat scattered (also depends on the malloc() implementation), so calling node->next
might be subject to cache misses.
So what if we plan to use a “self-expanding” array instead of a linked list. When I say self-
expanding array, I am thinking something akin to C++’s std::vector or Java’s ArrayList .
The number of cache misses will decrease, but we will need to grow the array; realloc is
expensive.
This “optimization” will improve the read times, but it will increase (significantly) the writes.
40 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
Each of our buckets will be a vector, with the following internal structure:
The capacity is the actual memory allocated for the ch_vect vector internal ch_vect->array .
This may, or may be not fully used.
The size is the actual number of elements in the ch_vect . It’s the exact length we use for
iteration ( i<vect->size ).
The value of VECT_GROWTH_MULTI is ideally a number in the [1, 2] interval. For simplicity we’ve
picked 2 , but there’s a risk our array will grow faster than it’s needed; doubling its size every
time might be an exaggeration.
The interface
When it comes to the interface, for simplicity, we will limit ourselves in implementing only the
functions we are going to use:
41 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
ch_vect_new is the constructor-like function. It accepts only a parameter which is the initial
ch_vect->capacity .
ch_vect_free is the destructor-like function. It only de-allocates the memory of the structure
itself, but not for the data.
ch_vect_get is used to access an item from the internal array ( ch_vect->array ) at the specified
index idx .
ch_vect_set is used to set the value of an item from the internal array ( ch_vect->array ) at the
specified index.
ch_vect_append appends an element at the end of the ch_vect->array . In case there’s not
enough allocated space for it, it allocates a new array.
It’s important to understand there’s not a good idea to interact with ch_vect->array directly. If
you do this, ch_vect->size won’t be updated, so growth is not guaranteed to work.
42 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
ch_vect* ch_vect_new_default() {
return ch_vect_new(VECT_INIT_CAPACITY);
}
It’s a bad idea to call exit(EXIT_FAILURE) if you are building a library. You need to give
the program a chance to recover from an error. So don’t do it in practice.
43 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
Appending an element
Appending a new element to the ch_vect should take into consideration the following:
• It actual size of the vector is equal or becomes bigger than the capacity we need to:
◦ Compute a new_capacity ;
◦ Copy all elements from the old array ch_vect->array to the new array new_array ;
The equivalent code for what has been described above is the following:
44 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
SIZE_MAX is a C99 macro that describes the maximum possible value for a size_t type.
We will simply create another version, and to make a distinction between the two we will name it:
ch_hashv (with v from v ector at the end):
45 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
// VERSUS
As you can see, only the type of the buckets changes: ch_node **buckets vs. ch_vect
**buckets .
For the actual methods, we are going to keep 90% of the code from the previous implementation.
But, instead of using a while loop for iterating through the linked list, a for loop will be used
for iterating through the dynamically-expanding array ( ch_vect ).
In case you are curious about the changes and the implementation of ch_hashv , you can take a
look at the code directly on github (https://github.com/nomemory/chained-hash-table-c):
• chained_hashv.c (https://github.com/nomemory/chained-hash-table-c/blob/main/chained_hashv.c)
• chained_hash.h (https://github.com/nomemory/chained-hash-table-c/blob/main/chained_hashv.h)
• ch_vect.c (https://github.com/nomemory/chained-hash-table-c/blob/main/vect.c)
• ch_vect.h (https://github.com/nomemory/chained-hash-table-c/blob/main/vect.h)
46 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
it reduces the number of cache misses introduced by our use of linked lists.
But from a theoretical perspective searching in a bucket is still O(N) , where N is the number of
elements in the bucket. So, how can we improve the reads further ( ch_hash_get() )?
• If this number increases, after a certain threshold, we can “morph” our bucket into a red-
black tree (https://en.wikipedia.org/wiki/Red%E2%80%93black_tree).
Without getting into many details, red-black trees are binary search trees are self-balancing
themselves during the insertion and deletion of new elements. This means that searching
performance doesn’t degrade, and it will remain in logarithmic bounds.
This is the route the creators of Java took when HashMap was implemented.
Open Addressing
47 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
Compared to Separate Chaining, a hash table implemented with Open Adressing stores only
one element per bucket. We handle collisions by inserting the colliding pairs to unused slots in
the bucket array.
Finding unused locations in the bucket array is achieved through a probing algorithm that
determines what the is second (or the nth) most natural index if the previous ones are occupied.
The most accessible probing algorithm is called linear probing. In case of collision, we iterate
over each bucket (starting with the first bucket computed), until we find an empty slot to make the
insertion.
To avoid probing for new slots in excess, we need to keep the load factor of the hash table at a
satisfactory level, and the hash function should have remarkable diffusion properties. If the load
factor (defined as size/capacity ) reaches a certain threshold, we increase the buckets and
perform full-rehashing.
If we fail to increase the number of buckets, clusters of adjacent elements will form. **
Clustering** diminishes the performance of both read and insert operations because we will need
to iterate more over more buckets to obtain the correct value.
For example, python’s dict implementation uses a more advanced variant of Open Adressing.
Compared to the Separate Chaining approach, cache misses are reduced because we operate
on a single contiguous block of memory (the array of buckets). And, as long as we use a decent
hash function, and the load factor is low, it can lead to superior performance, especially for read
operations.
48 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
We want to insert <key, value> pairs with the keys ( char* ) from the set: [ "A" , "B" , "C" ,
etc.]:
• First, we insert "A" . The corresponding bucket for "A" (after we compute the hash) is 0 ;
• Then we insert "B" . The corresponding bucket for "B" is 0 . But 0 is occupied by "A" .
So, by applying linear probing we go to the next index, which 1 , and it’s free. So "B" is
now inserted at bucket 1 ;
• Then we insert "C" . The corresponding bucket for "C" is 0 . But 0 is occupied by "A" ,
the next bucket, 1 is occupied by "B" , so we end-up inserting "C" to 2.
Let’s look further; what is happening for "D" , "E" , "F" , "G" , "H" is the beginning of a not so
lovely cluster of elements: areas in the array of buckets where intertwined elements are grouped
together.
Having intertwined groups leads to decreased performance. Think of it, for reading the value
associated with "G" we have to iterate over an area that is naturally associated with bucket
number 7 (eg., "F" ).
• The load factor (size/capacity) is close to 1 , and we need to allocate more empty buckets
to increase the spacing between elements;
• The hash function doesn’t have good diffusion properties, and it’s biased towards specific
buckets.
Reducing Clustering can also be achieved by changing linear probing to another algorithm to
identify the next good bucket (e.g., quadratic probing).
Tombstones
Compared to Separate Chaining, deleting elements in a hash table that uses Open
49 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
After a delete operation occurs, we cannot simply empty the bucket (associate NULL with it). If
we do that, we might break a sequence of entries and unjustly isolate some. The separated
entries will become unreachable for the following read operations. They become reachable only if
the previous sequence is somehow restored through a lucky insertion.
One solution would be to rehash everything after each delete. But think of it for a moment; this
means to allocate a new array of buckets, calculate the new index (bucket) for each element, and
then free the memory associated with the old and broken array. It’s a costly and unacceptable
endeavor, especially when there are better alternatives.
One alternative for avoiding full rehashing is to introduce the notion of tombstones. Each time a
delete operation occurs, instead of emptying the bucket, we put a sentinel value, metaphorically
called a tombstone.
If we want to perform a read operation or another delete , the tombstone will act just like an
actual entry; probing will ignore it and continue further. This way, the rest of the entries are not
isolated from the others.
If we want to perform a put (insert) operation, the tombstone will act as a very inviting empty
slot.
50 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
A high density of tombstones will increase the load factor, just like the actual entries do, so an
intelligent hash table implementation needs to consider this.
There are ways of avoiding tombstones altogether, and if you are interested to read about this,
check this article (https://attractivechaos.wordpress.com/2019/12/28/deletion-from-hash-tables-without-tombstones/).
But, for simplicity and academic purposes, our hash table will use tombstones.
A generic implementation
If you want to jump directly into the code, without reading the explanation,s you can clone the
following repo (https://github.com/nomemory/open-adressing-hash-table-c):
Similar to the implementations for Separate Chaining, we will use the void* pointer to achieve
some sort of genericity.
The model
For the model layer, we will keep almost the same structures as before.
51 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
oa_key_ops_s and oa_val_ops_s are collections of functions that are related to the data kept
inside the keys and values.
The oa_pair_s is the structure corresponding to the actual <key, value> entry.
52 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
• size - the number of elements (gets incremented each time we add a new element, and
decremented when a removal is performed);
probing_fct is a pointer function that allows our users to choose the algorithm for probing.
For example, for linear probing, we can come with something like this:
oa_hash_lp_idx is a simple function that increments the index ( idx ). If we reach the end of the
bucket array we simply start again.
We can also write a function for quadratic probing later and pass it to the hash table. It’s good
to offer some flexibility, after all.
• OA_HASH_INIT_CAPACITY - The initial capacity (nuber of buckets for the hash table).
The interface
53 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
// Pair related
oa_pair *oa_pair_new(uint32_t hash, const void *key, const void *val);
// String operations
uint32_t oa_string_hash(const void *data, void *arg);
void* oa_string_cp(const void *data, void *arg);
bool oa_string_eq(const void *data1, const void *data2, void *arg);
void oa_string_free(void *data, void *arg);
void oa_string_print(const void *data);
• oa_hash_free is a destructor-like function that frees the memory associated with oa_hash
and its elements.
54 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
oa_hash* oa_hash_new(
oa_key_ops key_ops,
oa_val_ops val_ops,
void (*probing_fct)(struct oa_hash_s *htable, size_t *from_idx))
{
oa_hash *htable;
htable = malloc(sizeof(*htable));
if (NULL==htable) {
fprintf(stderr,"malloc() failed in file %s at line # %d", __FILE__,__LINE__);
exit(EXIT_FAILURE);
}
htable->size = 0;
htable->capacity = OA_HASH_INIT_CAPACITY;
htable->val_ops = val_ops;
htable->key_ops = key_ops;
htable->probing_fct = probing_fct;
return htable;
}
In practice, it’s not a good idea to free the memory associated with the pairs, but for simplicity our
implementation frees the memory:
55 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
Given the fact we are going to use linear probing for our implementation we can overload the
allocator to use the previously defined algorithm for linear probing:
Modeling Tombstones
We will need a way to model tombstones for our hash table. In this regard we will call a
tombstone a non- NULL oa_pair element with oa_pair->hash=0 , oa_pair->key=NULL and
oa_pair->val=NULL .
And another function that puts a tombstone at given bucket index every time we delete an
element:
56 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
In this regard, we will define a function oa_hash_should_grow that will check after each addition if
we need to grow the hash table.
We will continue by writing the growth method oa_hash_grow . This method will:
• perform a complete rehash of all the elements (skipping any potential tombstone).
57 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
old_capacity = htable->capacity;
old_buckets = htable->buckets;
if (NULL == htable->buckets) {
fprintf(stderr,"malloc() failed in file %s at line # %d", __FILE__,__LINE__);
exit(EXIT_FAILURE);
}
// Perform a complete rehash of all the elements (skipping any potential tombstone).
for(size_t i = 0; i < old_capacity; i++) {
crt_pair = old_buckets[i];
if (NULL!=crt_pair && !oa_hash_is_tombstone(htable, i)) {
oa_hash_put(htable, crt_pair->key, crt_pair->val);
htable->key_ops.free(crt_pair->key, htable->key_ops.arg);
htable->val_ops.free(crt_pair->val, htable->val_ops.arg);
free(crt_pair);
}
}
58 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
static size_t oa_hash_getidx(oa_hash *htable, size_t idx, uint32_t hash_val, const void
*key, enum oa_ret_ops op) {
do {
if (op==PUT && oa_hash_is_tombstone(htable, idx)) {
break;
}
if (htable->buckets[idx]->hash == hash_val &&
htable->key_ops.eq(key, htable->buckets[idx]->key, htable->key_ops.arg)) {
break;
}
htable->probing_fct(htable, &idx);
} while(NULL!=htable->buckets[idx]);
return idx;
}
• idx - this is the starting index from where we will start probing (using htable-
>probing_fct , the pointer function supplied at construction-time);
• op - the operation. For example, if we PUT an element, tombstones won’t be ignored, but
if we use this function tp GET or DELETE elements, tombstones will act just as normal
elements.
idx is the natural choice for each entry, but in the case of hash collisions, we need to find
another free index. Here’s where the oa_hash_getidx functions come into play. It returns the
most natural choice starting from idx .
59 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
60 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
61 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
if (oa_hash_should_grow(htable)) {
oa_hash_grow(htable);
}
if (NULL==htable->buckets[idx]) {
// Key doesn't exist & we add it anew
htable->buckets[idx] = oa_pair_new(
hash_val,
htable->key_ops.cp(key, htable->key_ops.arg),
htable->val_ops.cp(val, htable->val_ops.arg)
);
} else {
// // Probing for the next good index
idx = oa_hash_getidx(htable, idx, hash_val, key, PUT);
if (NULL==htable->buckets[idx]) {
htable->buckets[idx] = oa_pair_new(
hash_val,
htable->key_ops.cp(key, htable->key_ops.arg),
htable->val_ops.cp(val, htable->val_ops.arg)
);
} else {
// Update the existing value
// Free the old values
htable->val_ops.free(htable->buckets[idx]->val, htable->val_ops.arg);
htable->key_ops.free(htable->buckets[idx]->key, htable->key_ops.arg);
// Update the new values
htable->buckets[idx]->val = htable->val_ops.cp(val, htable->val_ops.arg);
htable->buckets[idx]->key = htable->val_ops.cp(key, htable->key_ops.arg);
htable->buckets[idx]->hash = hash_val;
}
}
htable->size++;
}
62 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
63 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
64 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
• If the bucket is not empty we check if the element should be removed or not;
• If it’s not the good element, it means we need to probe to find it.
if (NULL==htable->buckets[idx]) {
return;
}
htable->val_ops.free(htable->buckets[idx]->val, htable->val_ops.arg);
htable->key_ops.free(htable->buckets[idx]->key, htable->key_ops.arg);
oa_hash_put_tombstone(htable, idx);
}
65 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
66 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
67 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
68 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
if (NULL==htable->buckets[idx]) {
return NULL;
}
return (NULL==htable->buckets[idx]) ?
NULL : htable->buckets[idx]->val;
}
return 0;
}
If you want to read more about Open Addressing techniques, checkout my new blog post. The
code is in Java, but hey, if you can read C, Java should be easier to grasp.
References
69 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
70 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
71 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
72 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
73 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
• CSci 335 Software Design and Analysis - Chapter 5 Hashing and Hash Tables, Steward
Weiss (http://www.compsci.hunter.cuny.edu/~sweiss/course_materials/csci335/lecture_notes/chapter05.pdf)
• CSE 241 Algorithms and Data Structures - Chosing hash functions (https://
classes.engineering.wustl.edu/cse241/handouts/hash-functions.pdf)
• Why did the designers of Java preferred chaining over open addressing (https://
stackoverflow.com/questions/12019434/why-did-the-language-designers-of-java-preferred-chaining-over-open-
addressing-f)
• Traits (https://en.wikipedia.org/wiki/Trait_(computer_programming))
COMMENTS
74 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
75 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
76 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
77 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
78 of 79 7/3/24, 12:07
Implementing Hash Tables in C | andreinc https://www.andreinc.net/2021/10/02/implementing-has...
79 of 79 7/3/24, 12:07