Internal Array-Based Design

1

Internal Array-Based Design

A hash table is one of the most powerful and widely used data structures in computer science, capable of delivering near-constant-time performance for insertions, lookups, and deletions. Yet underneath all of this apparent magic lies a remarkably simple foundation: a plain, contiguous array. Understanding how a hash table is constructed on top of an internal array — and how a hash function bridges the gap between arbitrary keys and array indices — is essential for understanding why hash tables behave the way they do, both in their best and worst cases.

To appreciate the design, it helps to first recognize the core problem being solved. Suppose you need to store a collection of key-value pairs — usernames mapped to account records, words mapped to their frequencies, or product IDs mapped to prices. A naive solution might be to store everything in a list and search through it linearly, but that gives you O(n) lookup time. What if, instead, you could compute exactly where a value should live and jump straight to it? That is precisely what the internal array-based design of a hash table achieves.

Arrays as the Foundation of Hash Tables

At the heart of every hash table is a fixed-size (or dynamically resized) array, sometimes called a bucket array or slot array. When you create a hash table, one of the first things that happens internally is the allocation of this array, initialized with empty or null slots. Every value you later insert will ultimately be stored at a specific index position within this array.

The reason arrays form such an ideal foundation comes down to their most fundamental property: O(1) random access by index. Given an array and an integer index, the computer can calculate the exact memory address of that element in a single arithmetic operation — typically base_address + (index × element_size). There is no looping, no comparison, no traversal. You jump directly to the memory location and either read or write the value there. Hash tables inherit this performance characteristic entirely. When a hash table can be reduced to an array index lookup, the retrieval cost is effectively constant regardless of how many elements are stored.

Consider a simple conceptual example. Suppose we allocate an internal array of size 8:

Index:  [ 0 ] [ 1 ] [ 2 ] [ 3 ] [ 4 ] [ 5 ] [ 6 ] [ 7 ]
Value:  null  null  null  null  null  null  null  null

Every slot starts empty. As keys are inserted, their associated values will be placed at computed index positions. The entire efficiency of the hash table depends on how well we can determine those positions.

The size of the internal array is a meaningful design decision. A larger array reduces the chance that two keys end up competing for the same slot (a situation called a collision), but it also uses more memory, some of which may go to waste. A smaller array is more memory-efficient but increases collision probability. In practice, hash table implementations choose an initial size and then resize — typically doubling the array — when the ratio of stored elements to array slots (called the load factor) exceeds a threshold, commonly around 0.70 to 0.75.

The Role of Hash Functions

The hash function is the engine that makes the array-based design work. It accepts a key as its input — which might be a string, an integer, an object, or any other type — and produces an integer output. That integer is then used (usually after a further step) to identify which array slot the corresponding value belongs in.

A well-designed hash function has two critical properties. First, it must be deterministic: given the same key, it must always return the same integer, every single time, without exception. Second, it should produce uniform distribution: the integers it generates should spread as evenly as possible across the range of valid array indices, so that no single slot becomes overloaded while others stay empty.

To understand why uniform distribution matters, imagine a hash function so poorly designed that it maps every key to index 0. All values pile up at slot 0, and what was supposed to be a hash table degrades into a linked list. Every lookup now requires scanning all the stored items sequentially — exactly the O(n) performance that the hash table was designed to avoid. A good hash function prevents clustering, ensuring the array's capacity is used evenly.

Here is a simple illustrative hash function for strings, which sums the ASCII (or Unicode) code point values of each character:

def naive_string_hash(key):
    total = 0
    for character in key:
        total += ord(character)
    return total

For the key "cat": ord('c') + ord('a') + ord('t') = 99 + 97 + 116 = 312. For "act": the same three characters produce the same sum, 312. This reveals a weakness — anagrams hash identically. Real-world hash functions incorporate positional weighting to avoid this. A classic approach used in many languages multiplies a running total by a prime number at each step:

def polynomial_hash(key, prime=31):
    total = 0
    for i, character in enumerate(key):
        total += ord(character) * (prime ** i)
    return total

Now "cat" and "act" produce different hash values because each character's contribution is scaled by its position. Primes are preferred multipliers because they reduce mathematical patterns that can cause clustering when the modulo step is applied.

Mapping Keys to Indices

A hash function often produces very large integers — far larger than the size of the internal array. The standard solution is to apply the modulo operator. If the internal array has n slots, the final array index is computed as:

index = hash_function(key) % n

The modulo operation constrains any integer output to the range [0, n-1], which is always a valid index for an array of size n. For example, if the hash of "cat" is 312 and the array size is 8, the index is 312 % 8 = 0. If the array size were 10, the index would be 312 % 10 = 2.

This means the array size itself influences index distribution. If the array size shares common factors with many hash values, certain indices will be hit much more often than others. This is one reason hash table implementations frequently prefer prime numbers as array sizes — a prime size has fewer common factors with hash outputs, leading to more even distribution after the modulo step.

Different key types require different strategies for producing the initial integer hash value:

  • Integers: The key itself can often be used directly as the hash value, sometimes after mixing bits to avoid trivial patterns. For example, hash(42) = 42, then 42 % array_size.
  • Strings: Each character is converted to its numeric code point, then combined using positional polynomial arithmetic (as shown above) to produce a single integer.
  • Floating-point numbers: The raw bit representation is typically reinterpreted as an integer, then hashed.
  • Composite objects / tuples: Each field or element is hashed individually, and the results are combined — often using XOR operations, polynomial combination, or the approach Python uses with hash(tuple).
  • Custom objects: The programmer must define a custom hash function based on the object's meaningful fields, and critically, any two objects that compare as equal must produce the same hash value.

The quality of this key-to-index mapping is the single biggest factor in hash table performance. A mapping that clusters keys into a few slots causes many collisions and degrades performance. An even mapping keeps collisions rare and maintains near-O(1) behavior across the board.

Storage and Retrieval Efficiency

The insertion and retrieval processes in an array-based hash table are elegant in their symmetry:

During insertion, the process is:

  • Accept a key-value pair.
  • Call the hash function on the key to produce an integer.
  • Apply modulo with the array size to get a valid index.
  • Store the value at that index in the internal array.

During retrieval, the process is:

  • Accept a key.
  • Call the same hash function on the key to produce the same integer.
  • Apply modulo with the array size to get the same index.
  • Read and return the value at that index in the internal array.

The crucial insight is that no searching is required. The hash function directly computes where the value was stored. This is called direct addressing, and it is what separates hash tables from linear search structures. Compare the two approaches:

Operation Linear Search (Array/List) Hash Table
Insert O(n) to check for duplicates O(1) average
Lookup O(n) — must scan from start O(1) average
Delete O(n) — must find element first O(1) average

For lookup-heavy workloads — counting word frequencies in a document, checking membership in a large set, caching computed results — this difference is transformative. A linear search through a million elements takes up to a million comparisons. A hash table lookup takes a fixed, tiny number of operations regardless of table size.

Here is a minimal Python-style pseudocode illustration of the complete insertion and retrieval cycle:

class SimpleHashTable:
    def __init__(self, size=8):
        self.size = size
        self.array = [None] * self.size   # internal array

    def _hash(self, key):
        total = 0
        for i, ch in enumerate(str(key)):
            total += ord(ch) * (31 ** i)
        return total % self.size           # always a valid index

    def insert(self, key, value):
        index = self._hash(key)
        self.array[index] = value          # direct write

    def retrieve(self, key):
        index = self._hash(key)
        return self.array[index]           # direct read

Calling insert("name", "Alice") computes an index and writes "Alice" there. A subsequent call to retrieve("name") computes the identical index and returns "Alice" instantly, without scanning any other slots.

Determinism and Consistency Requirements

The entire design described above collapses completely if the hash function is not deterministic. Determinism means that for any given key, the hash function returns the same integer every time it is called, regardless of when it is called, how many times, or in what order relative to other operations.

Why is this so critical? Because the index used during retrieval must match the index used during insertion. If the hash function could return different values for the same key on different calls, you would store a value at index 3 during insertion, but then compute index 7 during retrieval and find nothing — even though the value is safely sitting at index 3. The hash table would appear to lose data that it has actually stored.

A hash function must never incorporate any of the following sources of variability:

  • Random numbers: Calling random() inside a hash function would produce a different integer each time, destroying determinism entirely.
  • Timestamps or system clocks: The time at which the hash is computed is irrelevant to the key's identity and must not influence the result.
  • Memory addresses: Some languages allow using an object's memory address as its hash. This is fragile — if a garbage collector moves the object in memory, its address changes and its hash changes, making stored entries unretrievable. Python, for example, explicitly forbids this for mutable objects, which is why list objects are not hashable.
  • External mutable state: Global counters, database reads, or any other value that changes over the lifetime of the program must not influence a hash function.

A related consistency requirement is that if two keys are considered equal, they must produce the same hash. This is sometimes called the hash-equality contract. If key_a == key_b is true, then hash(key_a) == hash(key_b) must also be true. Violating this contract means two keys that compare as equal would map to different array slots, making it impossible to reliably retrieve values using logically equivalent keys.

Interestingly, the reverse does not need to hold: two keys with the same hash value do not need to be equal. When two different keys produce the same index, it is a collision, and hash tables have well-defined strategies (chaining and open addressing) to handle it. But two equal keys must never produce different indices — that is an unrecoverable logical error in the hash function's design.

The complete picture of the internal array-based design can be summarized as a pipeline: a key enters the system, the hash function converts it into an integer, the modulo operation constrains that integer to a valid array index, and the underlying array provides O(1) access to that slot. The determinism of the hash function ensures this pipeline produces the same result every time the same key is used, making the entire structure reliable, predictable, and efficient.

NotesThe simplified hash table pseudocode intentionally omits collision handling to keep the focus on the core array-and-hash-function design. Instructors may wish to note that a production implementation would need to handle the case where two keys map to the same index.