Linear Hashing
Linear Hashing is a dynamic hashing scheme invented by Witold Litwin in 1980 that allows hash tables to grow and shrink incrementally without the catastrophic latency spikes associated with traditional full-table rehashing. By maintaining a split pointer and utilizing a multi-phase hash function family, Linear Hashing guarantees amortized constant time O(1) performance for insertions, deletions, and lookups while preventing memory overflow and unbounded chain growth.
HOW-TO IMPLEMENT & CONFIGURE
- Modern C++ compiler supporting C++17 or newer
- Standard template library (STL) for basic vector and list allocations
- Basic understanding of modular arithmetic and bitwise hash functions
Step 1
Define the primary structures required for a Linear Hashing bucket array, including load factor tracking thresholds, the split pointer index, and the current hashing level.
#include <iostream>
#include <vector>
#include <list>
#include <cmath>
struct Entry {
int key;
int value;
};
class LinearHash {
private:
int level;
int split_pointer;
size_t capacity;
double load_factor_threshold;
size_t total_elements;
std::vector<std::list<Entry>> buckets;
size_t hash1(int key) const {
return key % (static_cast<size_t>(std::pow(2, level)) * capacity);
}
size_t hash2(int key) const {
return key % (static_cast<size_t>(std::pow(2, level + 1)) * capacity);
}
public:
LinearHash(size_t initial_capacity = 4, double threshold = 0.75)
: level(0), split_pointer(0), capacity(initial_capacity),
load_factor_threshold(threshold), total_elements(0) {
buckets.resize(capacity);
}
};
Step 2
Implement the core insertion routine alongside the incremental split mechanism triggered when the load factor exceeds the specified threshold.
void insert(int key, int value) {
size_t h = hash1(key);
if (h < static_cast<size_t>(split_pointer)) {
h = hash2(key);
}
// Check for existing key update
for (auto& entry : buckets[h]) {
if (entry.key == key) {
entry.value = value;
return;
}
}
buckets[h].push_back({key, value});
total_elements++;
if (get_load_factor() > load_factor_threshold) {
split();
}
}
double get_load_factor() const {
return static_cast<double>(total_elements) / (buckets.size() * 1.0);
}
void split() {
size_t old_index = split_pointer;
size_t new_index = capacity + split_pointer;
buckets.push_back(std::list<Entry>());
std::list<Entry>& old_bucket = buckets[old_index];
std::list<Entry> rehashed_items;
for (auto it = old_bucket.begin(); it != old_bucket.end(); ) {
size_t new_h = hash2(it->key);
if (new_h == new_index) {
rehashed_items.push_back(*it);
it = old_bucket.erase(it);
} else {
++it;
}
}
buckets[new_index] = std::move(rehashed_items);
split_pointer++;
if (split_pointer == static_cast<int>(capacity)) {
level++;
split_pointer = 0;
capacity *= 2;
}
}
Step 3
Write verification routines to ensure lookups succeed across split boundaries and capacity increases maintain complete data integrity.
bool search(int key, int& out_value) const {
size_t h = hash1(key);
if (h < static_cast<size_t>(split_pointer)) {
h = hash2(key);
}
for (const auto& entry : buckets[h]) {
if (entry.key == key) {
out_value = entry.value;
return true;
}
}
return false;
}
int main() {
LinearHash lh(2, 0.7);
for (int i = 0; i < 50; ++i) {
lh.insert(i, i * 10);
}
int val;
if (lh.search(42, val)) {
std::cout << "Found key 42 with value: " << val << std::endl;
}
return 0;
}
Step 4
Harden the implementation against skewed hash distributions and catastrophic chain growth by establishing upper bounds on bucket lengths.
// Mitigation for skewed keys: fallback overflow chain checks or secondary hashing
void validate_bucket_depth(size_t max_depth = 10) const {
for (size_t i = 0; i < buckets.size(); ++i) {
if (buckets[i].size() > max_depth) {
std::cerr << "Warning: Bucket " << i << " exceeds max chain depth.";
}
}
}
Mathematical Foundation and Invariants
Linear Hashing operates by maintaining a collection of buckets whose count doubles over a series of levels. Let L denote the current level (starting at 0) and N denote the initial capacity. The total number of primary address buckets is N * 2^L + s, where s is the split pointer index satisfying 0 <= s < N * 2^L. The hash functions utilized are h_L(key) = key mod (N * 2^L) and h_{L+1}(key) = key mod (N * 2^{L+1}). When an insertion triggers a split, the bucket referenced by s is partitioned using h_{L+1}, and s is incremented. Once s reaches N * 2^L, the level L is incremented, and s is reset to 0, effectively doubling the address space smoothly.
Amortized Analysis
While a single insertion that forces a split operation requires traversing elements in the split bucket and rehashing them, taking O(m) time where m is the bucket size, this cost is distributed across multiple insert operations. Using the potential function method, the amortized time complexity for an insertion or lookup is proven to be O(1). Space complexity remains O(n) proportional to the number of stored key-value pairs, maintaining high memory efficiency without requiring massive blocks of contiguous memory reallocation.
📚 STANDARDS & ACADEMIC RFC CITATIONS
- Litwin, Witold. 'Linear hashing: A new tool for files and tables.' Proceedings of the 6th International Conference on Very Large Data Bases (VLDB), 1980.
- Knisely, J. 'Data Structures, Algorithms, and Software Principles in C++.'
- Ramakrishnan, Raghu, and Johannes Gehrke. 'Database Management Systems.' McGraw-Hill.