CoreTechED

Loading

Explore hashing applications in C: password storage, file checksums, dictionaries, Bloom filters, load balancing, and symbol tables with code examples.

Applications of Hashing in C: Complete Guide

Applications of Hashing in C: A Comprehensive Tutorial

Hashing is a technique that converts data of any size into a fixed-size value, known as a hash, which can be used for quick data retrieval and comparison. With average O(1) insert, search, and delete, hashing powers some of the most critical systems in computing — from securing passwords to routing internet traffic.

This tutorial explores hashing’s major applications in C: data integrity and security, database and search optimization, network and system infrastructure, and specialized processing. Each application is illustrated with practical C code.


Conceptual Introduction & Fundamentals

What is Hashing?

Hashing maps arbitrary-sized input (a key) to a fixed-size value (a hash) via a hash function. The hash serves as an index into a hash table, enabling O(1) average lookups.

Why Applications Matter

  • Ubiquity: Hashing powers databases, caches, security, networks.
  • Performance: Replaces O(n) or O(log n) operations with O(1) average.
  • Versatility: Applies to strings, integers, files, images.
  • Real-world impact: Every secure login, CDN, and compiler uses hashing.

Categories of Applications

CategoryPrimary Use
Data Integrity & SecurityPassword storage, checksums, blockchain
Database & SearchIndexing, dictionaries, Rabin-Karp
Network & SystemsLoad balancing, Bloom filters, routing
SpecializedImage hashing, symbol tables, graphics

Syntax & Core Mechanics

Application 1: Data Integrity & Security

1.1 Password Storage

Store non-reversible hash instead of plaintext.

c

#include <stdio.h> #include <string.h> #include <stdint.h> /* FNV-1a hash (for educational demo only; use bcrypt/scrypt/argon2 in production) */ uint64_t fnv1a(const char *str) { uint64_t hash = 1469598103934665603ULL; while (*str) { hash ^= (unsigned char)*str++; hash *= 1099511628211ULL; } return hash; } int main(void) { const char *password = "MySecret123"; uint64_t stored = fnv1a(password); printf("Stored hash: 0x%016llx\n", (unsigned long long)stored); /* Verify */ const char *attempt = "MySecret123"; if (fnv1a(attempt) == stored) { printf("Login successful\n"); } else { printf("Login failed\n"); } return 0; }

Expected output:

text

Stored hash: 0x... Login successful

Production note: Use bcrypt, scrypt, or Argon2 with salt for real password storage — FNV is not cryptographically secure.

1.2 File Comparison (Checksum)

c

#include <stdio.h> #include <stdint.h> uint64_t fileHash(const char *path) { FILE *fp = fopen(path, "rb"); if (fp == NULL) return 0; uint64_t hash = 1469598103934665603ULL; int c; while ((c = fgetc(fp)) != EOF) { hash ^= (unsigned char)c; hash *= 1099511628211ULL; } fclose(fp); return hash; } int main(void) { /* Create test file first */ FILE *fp = fopen("test.txt", "w"); if (fp != NULL) { fprintf(fp, "Hello, World!\n"); fclose(fp); } uint64_t h1 = fileHash("test.txt"); printf("File hash: 0x%016llx\n", (unsigned long long)h1); return 0; }

Expected output:

text

File hash: 0x...

Use case: Detect corruption; any single-bit change alters the hash.

1.3 Rabin-Karp String Search

Hash windows of text for fast substring search.

c

#include <stdio.h> #include <string.h> #define BASE 256 #define MOD 1000000007 long long hashString(const char *str, int len) { long long h = 0; for (int i = 0; i < len; i++) { h = (h * BASE + (unsigned char)str[i]) % MOD; } return h; } int rabinKarp(const char *text, const char *pattern) { int n = (int)strlen(text); int m = (int)strlen(pattern); if (m > n) return -1; long long patHash = hashString(pattern, m); long long textHash = hashString(text, m); /* Precompute BASE^(m-1) % MOD */ long long h = 1; for (int i = 0; i < m - 1; i++) h = (h * BASE) % MOD; for (int i = 0; i <= n - m; i++) { if (patHash == textHash) { if (strncmp(text + i, pattern, m) == 0) return i; } if (i < n - m) { textHash = (textHash - (unsigned char)text[i] * h) % MOD; if (textHash < 0) textHash += MOD; textHash = (textHash * BASE + (unsigned char)text[i + m]) % MOD; } } return -1; } int main(void) { const char *text = "The quick brown fox jumps over the lazy dog"; const char *pattern = "brown"; int idx = rabinKarp(text, pattern); if (idx >= 0) printf("Found \"%s\" at index %d\n", pattern, idx); else printf("Not found\n"); return 0; }

Expected output:

text

Found "brown" at index 10

Application 2: Database & Search Optimization

2.1 Dictionary / Associative Array

c

#include <stdio.h> #include <stdlib.h> #include <string.h> #define TABLE_SIZE 101 #define KEY_LEN 32 typedef struct Entry { char key[KEY_LEN]; int value; struct Entry *next; } Entry; typedef struct { Entry *buckets[TABLE_SIZE]; } Dict; static unsigned int hashString(const char *s) { unsigned int h = 5381; while (*s) h = ((h << 5) + h) + (unsigned char)*s++; return h % TABLE_SIZE; } void dictPut(Dict *d, const char *key, int value) { unsigned int idx = hashString(key); Entry *cur = d->buckets[idx]; while (cur) { if (strcmp(cur->key, key) == 0) { cur->value = value; return; } cur = cur->next; } Entry *e = malloc(sizeof(Entry)); if (e == NULL) return; strncpy(e->key, key, KEY_LEN - 1); e->key[KEY_LEN - 1] = '\0'; e->value = value; e->next = d->buckets[idx]; d->buckets[idx] = e; } int dictGet(Dict *d, const char *key, int *out) { unsigned int idx = hashString(key); Entry *cur = d->buckets[idx]; while (cur) { if (strcmp(cur->key, key) == 0) { *out = cur->value; return 1; } cur = cur->next; } return 0; } void dictFree(Dict *d) { for (int i = 0; i < TABLE_SIZE; i++) { Entry *cur = d->buckets[i]; while (cur) { Entry *next = cur->next; free(cur); cur = next; } } } int main(void) { Dict d = {0}; dictPut(&d, "apple", 10); dictPut(&d, "banana", 20); dictPut(&d, "cherry", 30); int v; if (dictGet(&d, "banana", &v)) printf("banana → %d\n", v); if (dictGet(&d, "grape", &v)) printf("grape → %d\n", v); else printf("grape not found\n"); dictFree(&d); return 0; }

Expected output:

text

banana → 20 grape not found

2.2 Frequency Counting

c

#include <stdio.h> #include <stdlib.h> void countFrequency(const int arr[], int n, int maxValue) { int *freq = calloc(maxValue + 1, sizeof(int)); if (freq == NULL) return; for (int i = 0; i < n; i++) freq[arr[i]]++; for (int i = 0; i <= maxValue; i++) { if (freq[i] > 0) printf(" %d → %d\n", i, freq[i]); } free(freq); } int main(void) { int arr[] = {1, 3, 5, 3, 2, 1, 5, 3}; int n = sizeof(arr) / sizeof(arr[0]); printf("Frequencies:\n"); countFrequency(arr, n, 5); return 0; }

Expected output:

text

Frequencies: 1 → 2 2 → 1 3 → 3 5 → 2

Application 3: Network & System Infrastructure

3.1 Bloom Filter (Membership Test)

c

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #define BLOOM_SIZE 1024 /* bits */ #define NUM_HASHES 3 typedef struct { uint8_t bits[BLOOM_SIZE / 8]; } Bloom; static void bloomSet(Bloom *b, int bit) { b->bits[bit / 8] |= (uint8_t)(1u << (bit % 8)); } static int bloomGet(const Bloom *b, int bit) { return (b->bits[bit / 8] >> (bit % 8)) & 1u; } static unsigned int hash1(const char *s) { unsigned int h = 5381; while (*s) h = ((h << 5) + h) + (unsigned char)*s++; return h % BLOOM_SIZE; } static unsigned int hash2(const char *s) { unsigned int h = 0; while (*s) h = (h * 31) + (unsigned char)*s++; return h % BLOOM_SIZE; } static unsigned int hash3(const char *s) { unsigned int h = 7; while (*s) h = (h * 131) + (unsigned char)*s++; return h % BLOOM_SIZE; } void bloomAdd(Bloom *b, const char *key) { bloomSet(b, hash1(key)); bloomSet(b, hash2(key)); bloomSet(b, hash3(key)); } int bloomContains(const Bloom *b, const char *key) { return bloomGet(b, hash1(key)) && bloomGet(b, hash2(key)) && bloomGet(b, hash3(key)); } int main(void) { Bloom b = {{0}}; bloomAdd(&b, "apple"); bloomAdd(&b, "banana"); bloomAdd(&b, "cherry"); printf("apple: %s\n", bloomContains(&b, "apple") ? "maybe" : "no"); printf("banana: %s\n", bloomContains(&b, "banana") ? "maybe" : "no"); printf("grape: %s\n", bloomContains(&b, "grape") ? "maybe" : "no"); return 0; }

Expected output (typical):

text

apple: maybe banana: maybe grape: no

Key insight: Bloom filters never produce false negatives, only false positives.

3.2 Load Balancing (Consistent Hashing Skeleton)

c

#include <stdio.h> #include <string.h> #define NUM_SERVERS 4 const char *servers[NUM_SERVERS] = { "server-1", "server-2", "server-3", "server-4" }; unsigned int hashString(const char *s) { unsigned int h = 5381; while (*s) h = ((h << 5) + h) + (unsigned char)*s++; return h; } const char *routeRequest(const char *key) { unsigned int idx = hashString(key) % NUM_SERVERS; return servers[idx]; } int main(void) { const char *requests[] = {"user42", "session7", "cart9", "user7"}; int n = sizeof(requests) / sizeof(requests[0]); for (int i = 0; i < n; i++) { printf("%-10s → %s\n", requests[i], routeRequest(requests[i])); } return 0; }

Expected output (typical):

text

user42 → server-3 session7 → server-2 cart9 → server-1 user7 → server-4

Application 4: Specialized Processing

4.1 Symbol Table (Compiler Use Case)

Maps identifiers to metadata.

c

#include <stdio.h> #include <stdlib.h> #include <string.h> #define TABLE_SIZE 101 #define NAME_LEN 32 typedef enum { TYPE_INT, TYPE_FLOAT, TYPE_CHAR } VarType; typedef struct Symbol { char name[NAME_LEN]; VarType type; int scope; struct Symbol *next; } Symbol; typedef struct { Symbol *buckets[TABLE_SIZE]; } SymbolTable; static unsigned int hashName(const char *s) { unsigned int h = 5381; while (*s) h = ((h << 5) + h) + (unsigned char)*s++; return h % TABLE_SIZE; } void symAdd(SymbolTable *st, const char *name, VarType type, int scope) { unsigned int idx = hashName(name); Symbol *s = malloc(sizeof(Symbol)); if (s == NULL) return; strncpy(s->name, name, NAME_LEN - 1); s->name[NAME_LEN - 1] = '\0'; s->type = type; s->scope = scope; s->next = st->buckets[idx]; st->buckets[idx] = s; } Symbol *symFind(SymbolTable *st, const char *name) { unsigned int idx = hashName(name); Symbol *s = st->buckets[idx]; while (s) { if (strcmp(s->name, name) == 0) return s; s = s->next; } return NULL; } void symFree(SymbolTable *st) { for (int i = 0; i < TABLE_SIZE; i++) { Symbol *s = st->buckets[i]; while (s) { Symbol *next = s->next; free(s); s = next; } } } const char *typeName(VarType t) { switch (t) { case TYPE_INT: return "int"; case TYPE_FLOAT: return "float"; case TYPE_CHAR: return "char"; } return "unknown"; } int main(void) { SymbolTable st = {0}; symAdd(&st, "count", TYPE_INT, 0); symAdd(&st, "price", TYPE_FLOAT, 0); symAdd(&st, "initial", TYPE_CHAR, 1); Symbol *s = symFind(&st, "price"); if (s) printf("%s : %s (scope %d)\n", s->name, typeName(s->type), s->scope); symFree(&st); return 0; }

Expected output:

text

price : float (scope 0)

Full Practical Code Example with Explanation

This program combines multiple hashing applications in a single file-integrity and password manager demo.

c

/** * hashing_applications.c * Demonstrates hashing applications: password storage, * file checksums, frequency counting, and Bloom filters. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <errno.h> #define MAX_LINE_LEN 100 #define BLOOM_SIZE 1024 #define NUM_HASHES 3 #define FNV_OFFSET 1469598103934665603ULL #define FNV_PRIME 1099511628211ULL /* FNV-1a hash */ uint64_t fnv1a(const char *str) { uint64_t h = FNV_OFFSET; while (*str) { h ^= (unsigned char)*str++; h *= FNV_PRIME; } return h; } /* Password demo */ void demoPassword(void) { printf("\n=== Password Storage ===\n"); const char *password = "MySecret123"; uint64_t stored = fnv1a(password); printf("Stored hash: 0x%016llx\n", (unsigned long long)stored); const char *attempts[] = {"MySecret123", "wrongpass", "MySecret124"}; for (int i = 0; i < 3; i++) { uint64_t h = fnv1a(attempts[i]); printf(" Attempt \"%s\": %s\n", attempts[i], (h == stored) ? "SUCCESS" : "FAIL"); } } /* File checksum demo */ uint64_t fileHash(const char *path) { FILE *fp = fopen(path, "rb"); if (fp == NULL) return 0; uint64_t h = FNV_OFFSET; int c; while ((c = fgetc(fp)) != EOF) { h ^= (unsigned char)c; h *= FNV_PRIME; } fclose(fp); return h; } void demoFileHash(void) { printf("\n=== File Checksum ===\n"); /* Create test file */ FILE *fp = fopen("demo.txt", "w"); if (fp == NULL) { perror("fopen"); return; } fprintf(fp, "Hello, hashing!\n"); fclose(fp); uint64_t h = fileHash("demo.txt"); printf("Hash of demo.txt: 0x%016llx\n", (unsigned long long)h); /* Modify file */ fp = fopen("demo.txt", "a"); if (fp == NULL) return; fprintf(fp, "Extra line.\n"); fclose(fp); uint64_t h2 = fileHash("demo.txt"); printf("After modification: 0x%016llx\n", (unsigned long long)h2); printf("Files match: %s\n", (h == h2) ? "YES" : "NO"); } /* Frequency counting */ void demoFrequency(void) { printf("\n=== Frequency Counting ===\n"); int arr[] = {1, 3, 5, 3, 2, 1, 5, 3}; int n = sizeof(arr) / sizeof(arr[0]); int maxV = 5; int *freq = calloc(maxV + 1, sizeof(int)); if (freq == NULL) return; for (int i = 0; i < n; i++) freq[arr[i]]++; for (int i = 0; i <= maxV; i++) { if (freq[i]) printf(" %d → %d\n", i, freq[i]); } free(freq); } /* Bloom filter */ typedef struct { uint8_t bits[BLOOM_SIZE / 8]; } Bloom; static unsigned int h1(const char *s) { unsigned int h = 5381; while (*s) h = ((h << 5) + h) + (unsigned char)*s++; return h % BLOOM_SIZE; } static unsigned int h2(const char *s) { unsigned int h = 0; while (*s) h = (h * 31) + (unsigned char)*s++; return h % BLOOM_SIZE; } static unsigned int h3(const char *s) { unsigned int h = 7; while (*s) h = (h * 131) + (unsigned char)*s++; return h % BLOOM_SIZE; } static void bloomSet(Bloom *b, int bit) { b->bits[bit / 8] |= (uint8_t)(1u << (bit % 8)); } static int bloomGet(const Bloom *b, int bit) { return (b->bits[bit / 8] >> (bit % 8)) & 1u; } void bloomAdd(Bloom *b, const char *k) { bloomSet(b, h1(k)); bloomSet(b, h2(k)); bloomSet(b, h3(k)); } int bloomHas(const Bloom *b, const char *k) { return bloomGet(b, h1(k)) && bloomGet(b, h2(k)) && bloomGet(b, h3(k)); } void demoBloom(void) { printf("\n=== Bloom Filter ===\n"); Bloom b = {{0}}; const char *inserted[] = {"apple", "banana", "cherry"}; for (int i = 0; i < 3; i++) bloomAdd(&b, inserted[i]); const char *tests[] = {"apple", "banana", "cherry", "grape", "kiwi"}; for (int i = 0; i < 5; i++) { printf(" %-8s: %s\n", tests[i], bloomHas(&b, tests[i]) ? "maybe" : "no"); } } int main(void) { printf("=== Applications of Hashing ===\n"); demoPassword(); demoFileHash(); demoFrequency(); demoBloom(); printf("\nDone.\n"); return 0; }

Expected Output (sample run)

text

=== Applications of Hashing === === Password Storage === Stored hash: 0x... Attempt "MySecret123": SUCCESS Attempt "wrongpass": FAIL Attempt "MySecret124": FAIL === File Checksum === Hash of demo.txt: 0x... After modification: 0x... Files match: NO === Frequency Counting === 1 → 2 2 → 1 3 → 3 5 → 2 === Bloom Filter === apple : maybe banana : maybe cherry : maybe grape : no kiwi : no Done.

Common Pitfalls, Edge Cases & Memory Considerations

1. Using Weak Hashes for Security

FNV/MD5/SHA-1 are unsuitable for password storage.

Solution: Use bcrypt, scrypt, or Argon2 with unique salts.

2. Bloom Filter False Negatives

Bloom filters must use all hash functions consistently; missing bits cause false negatives.

Solution: Always set/check all hash outputs; never partial.

3. Hash Table Memory Leaks

Forgetting to free bucket lists or arrays causes leaks.

Solution: Walk each bucket and free nodes before freeing the table.

4. Collision Attacks

Adversarial inputs can trigger worst-case collisions in weak hashes.

Solution: Use randomized seeds (SipHash, murmur with seed).

5. Load Factor Overflow

Ignoring load factor degrades to O(n).

Solution: Rehash when load factor > 0.75.

6. Integer Overflow in Hash Functions

Large multiplications overflow 32-bit int.

Solution: Use uint64_t or unsigned int.

7. File Handle Leaks

Not closing files after hashing.

Solution: Always fclose in all paths.

8. Bloom Filter Sizing

Too small a filter yields many false positives.

Solution: Size based on expected n and desired false-positive rate.

9. Unsalted Passwords

Identical passwords produce identical hashes.

Solution: Always use unique per-user salts.

10. Memory Considerations

  • Hash tables: O(n) entries + bucket array.
  • Bloom filters: O(m) bits, independent of n.
  • Frequency arrays: O(max value) space.
  • Free all dynamic allocations.

Summary & Best Practices

Hashing is one of the most versatile techniques in computing. From securing passwords to routing traffic and deduplicating images, its O(1) average operations enable modern software at scale.

Key Takeaways

  • Hashing powers security, databases, networks, and specialized systems.
  • Password storage must use salted, adaptive hashes (bcrypt/Argon2).
  • Bloom filters trade a small error rate for massive memory savings.
  • Consistent hashing enables stable load balancing.
  • Rabin-Karp uses rolling hashes for fast substring search.
  • Symbol tables map identifiers to metadata during compilation.

Best Practices

  1. Never store plaintext passwords; use salted adaptive hashes.
  2. Use cryptographic hashes (SHA-256) for integrity, not FNV.
  3. Choose table sizes as primes for even distribution.
  4. Rehash when load factor exceeds 0.75.
  5. Free all hash table nodes on cleanup.
  6. Use uint64_t for hash arithmetic to avoid overflow.
  7. Size Bloom filters based on expected elements and target false-positive rate.
  8. Use randomized seeds to prevent collision attacks.
  9. Close file handles after reading.
  10. Profile for large datasets.

Hashing’s blend of speed, simplicity, and versatility makes it an essential tool for every systems programmer.

Learn hashing with chaining in C: simple chaining, chaining with rehashing, load factor, complexity analysis, and complete implementation.

Hashing with Chaining in C: Complete Guide

Hashing with Chaining Implementation in C: A Comprehensive Tutorial

Hashing with chaining is a collision resolution technique where each bucket in the hash table points to a linked list of records that hash to the same index. This approach elegantly handles collisions, supports efficient insert/delete, and forms the foundation of many hash map implementations in real-world systems.

This tutorial provides an in-depth exploration of hashing with chaining in C: fundamental concepts, simple chaining, chaining with rehashing, complexity analysis, practical implementation, common pitfalls, and best practices.


Conceptual Introduction & Fundamentals

What is Hashing with Chaining?

Chaining resolves collisions by allowing multiple entries to coexist at the same hash index. Each bucket is a linked list; when a collision occurs, the new entry is appended (or prepended) to the bucket’s list.

Key idea: Each cell of the hash table points to a linked list of records with the same hash value.

Why Chaining?

  • Simple to implement: Uses linked lists as buckets.
  • Handles high load factors: Performance degrades gracefully.
  • Easy deletion: No tombstones needed (unlike open addressing).
  • No clustering: Unlike linear probing, no primary/secondary clustering.
  • Dynamic growth: Buckets can grow arbitrarily.

Hash Function

For integer keys:

text

hashIndex = key % numberOfBuckets

Example: buckets = 7, keys = [15, 11, 27, 8]:

KeyIndex
151
114
276
81 (collision with 15)

Load Factor

text

α = n / m
  • n = total elements
  • m = number of buckets

Guidelines:

  • Simple chaining: α can be > 1; typically keep α < 1 for good performance.
  • Chaining with rehashing: rehash when α > 0.5.

Complexity Analysis

OperationTime Complexity
SearchO(1 + α)
InsertO(1 + α)
DeleteO(1 + α)

Where α = n / m (load factor). Under uniform distribution, expected chain length is O(α).

Two Variants

VariantDescription
Simple ChainingFixed number of buckets; no resizing
Chaining with RehashingDoubles bucket count when load factor exceeds threshold

Syntax & Core Mechanics

Basic Chaining Structure

c

typedef struct Entry { int key; int value; struct Entry *next; } Entry; typedef struct { Entry **buckets; int bucketCount; int size; } HashTable;

Hash Function

c

static unsigned int hashIndex(int key, int bucketCount) { return ((unsigned int)key) % (unsigned int)bucketCount; }

Insert Operation

  1. Compute index = hashIndex(key).
  2. Traverse bucket list; if key exists, update value.
  3. Otherwise, prepend new node to bucket.

Delete Operation

  1. Compute index = hashIndex(key).
  2. Traverse bucket; find node with matching key.
  3. Unlink node; free memory.

Search Operation

  1. Compute index = hashIndex(key).
  2. Traverse bucket; return value if found.

Rehashing Operation

When loadFactor > threshold:

  1. Double bucket count (or choose next prime).
  2. Create new bucket array.
  3. Reinsert all entries using the new hash function.
  4. Free old bucket array.

Full Practical Code Example with Explanation

This program demonstrates both simple chaining and chaining with rehashing in a unified, complete implementation.

c

/** * hashing_chaining.c * Demonstrates hashing with chaining: simple and with rehashing. * Supports insert, search, delete, display, and dynamic resizing. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #define MAX_LINE_LEN 100 #define DEFAULT_BUCKETS 7 #define REHASH_THRESHOLD 0.75 /* Entry in a bucket's linked list */ typedef struct Entry { int key; int value; struct Entry *next; } Entry; /* Hash table with chaining */ typedef struct { Entry **buckets; int bucketCount; int size; int useRehashing; } HashTable; /* Hash function: division method */ static unsigned int hashIndex(int key, int bucketCount) { return ((unsigned int)key) % (unsigned int)bucketCount; } /* Find the next prime >= n */ static int nextPrime(int n) { if (n <= 2) return 2; if (n % 2 == 0) n++; while (1) { int isPrime = 1; for (int i = 3; i * i <= n; i += 2) { if (n % i == 0) { isPrime = 0; break; } } if (isPrime) return n; n += 2; } } /* Create a hash table */ HashTable *createHashTable(int bucketCount, int useRehashing) { HashTable *ht = malloc(sizeof(HashTable)); if (ht == NULL) return NULL; ht->buckets = calloc(bucketCount, sizeof(Entry *)); if (ht->buckets == NULL) { free(ht); return NULL; } ht->bucketCount = bucketCount; ht->size = 0; ht->useRehashing = useRehashing; return ht; } /* Free all entries and the table */ void freeHashTable(HashTable *ht) { if (ht == NULL) return; for (int i = 0; i < ht->bucketCount; i++) { Entry *cur = ht->buckets[i]; while (cur != NULL) { Entry *next = cur->next; free(cur); cur = next; } } free(ht->buckets); free(ht); } /* Rehash: grow buckets and reinsert all entries */ static int rehash(HashTable *ht) { int newBucketCount = nextPrime(ht->bucketCount * 2); Entry **newBuckets = calloc(newBucketCount, sizeof(Entry *)); if (newBuckets == NULL) return -1; /* Reinsert all entries */ for (int i = 0; i < ht->bucketCount; i++) { Entry *cur = ht->buckets[i]; while (cur != NULL) { Entry *next = cur->next; unsigned int idx = hashIndex(cur->key, newBucketCount); cur->next = newBuckets[idx]; newBuckets[idx] = cur; cur = next; } } free(ht->buckets); ht->buckets = newBuckets; ht->bucketCount = newBucketCount; return 0; } /* Insert or update a key-value pair */ int insert(HashTable *ht, int key, int value) { if (ht->useRehashing) { double lf = (double)(ht->size + 1) / ht->bucketCount; if (lf > REHASH_THRESHOLD) { if (rehash(ht) != 0) return -1; } } unsigned int idx = hashIndex(key, ht->bucketCount); Entry *cur = ht->buckets[idx]; while (cur != NULL) { if (cur->key == key) { cur->value = value; /* Update */ return 0; } cur = cur->next; } Entry *newEntry = malloc(sizeof(Entry)); if (newEntry == NULL) return -1; newEntry->key = key; newEntry->value = value; newEntry->next = ht->buckets[idx]; ht->buckets[idx] = newEntry; ht->size++; return 0; } /* Search for a key; returns 1 and sets *outValue if found */ int search(const HashTable *ht, int key, int *outValue) { unsigned int idx = hashIndex(key, ht->bucketCount); Entry *cur = ht->buckets[idx]; while (cur != NULL) { if (cur->key == key) { *outValue = cur->value; return 1; } cur = cur->next; } return 0; } /* Delete a key; returns 0 on success, -1 if not found */ int deleteKey(HashTable *ht, int key) { unsigned int idx = hashIndex(key, ht->bucketCount); Entry *cur = ht->buckets[idx]; Entry *prev = NULL; while (cur != NULL) { if (cur->key == key) { if (prev == NULL) ht->buckets[idx] = cur->next; else prev->next = cur->next; free(cur); ht->size--; return 0; } prev = cur; cur = cur->next; } return -1; } /* Display the table */ void display(const HashTable *ht) { double lf = (ht->bucketCount > 0) ? (double)ht->size / ht->bucketCount : 0.0; printf("Hash Table (size=%d, buckets=%d, load=%.2f):\n", ht->size, ht->bucketCount, lf); for (int i = 0; i < ht->bucketCount; i++) { printf(" [%d]:", i); Entry *cur = ht->buckets[i]; while (cur != NULL) { printf(" (%d→%d)", cur->key, cur->value); cur = cur->next; } printf("\n"); } } /* Demo: simple chaining */ static void demoSimpleChaining(void) { printf("\n=== Simple Chaining ===\n"); HashTable *ht = createHashTable(7, 0); if (ht == NULL) return; int keys[] = {15, 11, 27, 8, 12, 22, 29, 33}; int n = sizeof(keys) / sizeof(keys[0]); for (int i = 0; i < n; i++) { insert(ht, keys[i], keys[i] * 10); } display(ht); int value; printf("\nSearch 27: "); if (search(ht, 27, &value)) printf("found → %d\n", value); else printf("not found\n"); printf("Search 100: "); if (search(ht, 100, &value)) printf("found → %d\n", value); else printf("not found\n"); printf("\nDelete 11...\n"); deleteKey(ht, 11); display(ht); freeHashTable(ht); } /* Demo: chaining with rehashing */ static void demoRehashing(void) { printf("\n=== Chaining with Rehashing ===\n"); HashTable *ht = createHashTable(5, 1); if (ht == NULL) return; int keys[] = {15, 11, 27, 8, 12, 22, 29, 33, 42, 55, 61, 70}; int n = sizeof(keys) / sizeof(keys[0]); for (int i = 0; i < n; i++) { printf("Inserting %d (buckets=%d, size=%d)...\n", keys[i], ht->bucketCount, ht->size); insert(ht, keys[i], keys[i] * 10); } printf("\nFinal table:\n"); display(ht); freeHashTable(ht); } /* Read an integer safely */ static int readInteger(const char *prompt, int *outValue) { char buffer[MAX_LINE_LEN]; char *endPtr; long val; printf("%s", prompt); fflush(stdout); if (fgets(buffer, sizeof(buffer), stdin) == NULL) return -1; errno = 0; val = strtol(buffer, &endPtr, 10); if (endPtr == buffer || (*endPtr != '\n' && *endPtr != '\0')) return -1; if (errno == ERANGE || val < -2147483647 - 1 || val > 2147483647) return -1; *outValue = (int)val; return 0; } int main(void) { int choice; char buffer[MAX_LINE_LEN]; printf("=== Hashing with Chaining Demo ===\n"); while (1) { printf("\n--- Menu ---\n"); printf("1. Simple Chaining Demo\n"); printf("2. Chaining with Rehashing Demo\n"); printf("3. Interactive Chaining Table\n"); printf("4. Exit\n"); printf("Enter choice: "); fflush(stdout); if (fgets(buffer, sizeof(buffer), stdin) == NULL) { fprintf(stderr, "Error reading choice.\n"); return EXIT_FAILURE; } choice = atoi(buffer); if (choice == 1) { demoSimpleChaining(); } else if (choice == 2) { demoRehashing(); } else if (choice == 3) { HashTable *ht = createHashTable(DEFAULT_BUCKETS, 1); if (ht == NULL) continue; int opt; while (1) { printf("\n--- Chaining Table ---\n"); printf("1. Insert\n2. Search\n3. Delete\n4. Display\n5. Back\n"); printf("Enter choice: "); fflush(stdout); if (fgets(buffer, sizeof(buffer), stdin) == NULL) break; opt = atoi(buffer); if (opt == 1) { int k, v; if (readInteger("Key: ", &k) == 0 && readInteger("Value: ", &v) == 0) { insert(ht, k, v); printf("Inserted (%d → %d)\n", k, v); } } else if (opt == 2) { int k, v; if (readInteger("Key: ", &k) == 0) { if (search(ht, k, &v)) printf("Found: %d → %d\n", k, v); else printf("Key %d not found\n", k); } } else if (opt == 3) { int k; if (readInteger("Key: ", &k) == 0) { if (deleteKey(ht, k) == 0) printf("Deleted %d\n", k); else printf("Key %d not found\n", k); } } else if (opt == 4) { display(ht); } else if (opt == 5) { break; } } freeHashTable(ht); } else if (choice == 4) { printf("Exiting.\n"); return EXIT_SUCCESS; } else { printf("Invalid choice.\n"); } } return EXIT_SUCCESS; }

Explanation of the Code

  1. Entry — A node in a bucket’s linked list storing key, value, and next pointer.
  2. HashTable — Contains buckets array, bucket count, size, and a rehashing flag.
  3. hashIndex — Division-method hash with unsigned cast for negatives.
  4. nextPrime — Finds the next prime ≥ n for bucket sizing.
  5. createHashTable — Allocates bucket array; supports optional rehashing.
  6. insert — Updates existing key or prepends new entry; triggers rehash if needed.
  7. search — Traverses the bucket to find the key.
  8. deleteKey — Unlinks and frees the entry.
  9. rehash — Doubles bucket count, reinserts all entries.
  10. display — Prints each bucket’s chain and current load factor.

Expected Output (sample run)

text

=== Simple Chaining === Hash Table (size=8, buckets=7, load=1.14): [0]: (22→220) (29→290) [1]: (15→150) (8→80) (33→330) [2]: [3]: [4]: (11→110) [5]: (12→120) [6]: (27→270) Search 27: found → 270 Search 100: not found Delete 11... Hash Table (size=7, buckets=7, load=1.00): [0]: (22→220) (29→290) [1]: (15→150) (8→80) (33→330) [2]: [3]: [4]: [5]: (12→120) [6]: (27→270) === Chaining with Rehashing === Inserting 15 (buckets=5, size=0)... Inserting 11 (buckets=5, size=1)... Inserting 27 (buckets=5, size=2)... Inserting 8 (buckets=5, size=3)... Inserting 12 (buckets=5, size=4)... Inserting 22 (buckets=11, size=5)... Inserting 29 (buckets=11, size=6)... Inserting 33 (buckets=11, size=7)... ... Final table: Hash Table (size=12, buckets=23, load=0.52): ...

Common Pitfalls, Edge Cases & Memory Considerations

1. Forgetting to Free Entries

Each malloc for an Entry requires a corresponding free.

c

free(cur); /* Required for every node */

2. Integer Overflow in Hash

key * multiplier can overflow.

c

return ((unsigned int)key) % bucketCount; /* Safe cast */

3. Negative Keys

key % m for negative keys yields negative results.

Solution: Cast to unsigned int first.

4. Rehashing Infinite Loop

If load factor remains > threshold after rehash, rehash repeatedly.

c

if (lf > REHASH_THRESHOLD) { rehash(ht); } /* Only once per insert */

5. Deleting from Middle of Bucket

Must correctly update the previous node’s next pointer.

c

if (prev == NULL) buckets[idx] = cur->next; else prev->next = cur->next;

6. Table Size Not Prime

Non-prime sizes create clustering.

Solution: Use nextPrime to choose prime sizes.

7. Duplicate Keys

Insert should update existing key, not add duplicate.

8. Empty Table Display

Ensure display handles empty buckets gracefully.

9. Rehashing Correctness

Rehash must preserve all entries and use new bucket count.

10. Memory Considerations

  • Each entry: sizeof(Entry) + list overhead.
  • Bucket array: bucketCount × sizeof(Entry *).
  • Load factor controls chain length.
  • Use prime table sizes for best distribution.

Summary & Best Practices

Hashing with chaining is a robust collision resolution strategy that scales gracefully with load. Its simplicity and dynamic behavior make it a popular choice for real-world hash maps.

Key Takeaways

  • Chaining resolves collisions via linked lists in buckets.
  • Hash function: key % bucketCount (with unsigned cast).
  • Complexity: O(1 + α) for search/insert/delete.
  • Rehashing maintains good performance as the table grows.
  • Prime bucket sizes improve distribution.
  • Free all entries to avoid memory leaks.

Best Practices

  1. Use prime bucket sizes for better distribution.
  2. Cast keys to unsigned before modulo.
  3. Rehash when load factor > 0.5–0.75.
  4. Prepend new entries to bucket lists for O(1) insert.
  5. Update existing keys rather than adding duplicates.
  6. Free every entry and bucket array on cleanup.
  7. Check malloc/calloc return values.
  8. Test with collisions to verify correctness.
  9. Choose good hash functions (djb2 for strings).
  10. Profile for large datasets.

By mastering hashing with chaining, you’ll implement efficient, scalable hash maps that form the backbone of dictionaries, caches, and symbol tables.

Learn hashing in C: hash functions, separate chaining, open addressing, insert/search/delete, and collision handling. Master O(1) average operations.

Hashing in Data Structure: Complete Guide in C

Hashing in Data Structure: A Comprehensive Tutorial in C

Hashing is a technique used in data structures that efficiently stores and retrieves data in a way that allows for quick access. It involves mapping data to a specific index in a hash table using a hash function, enabling fast retrieval based on keys. With hashing, we can achieve search, insert, and delete operations in O(1) average time.

This tutorial provides an in-depth exploration of hashing in C: fundamental concepts, hash functions, collision handling (separate chaining and open addressing), practical implementations, common pitfalls, and best practices.


Conceptual Introduction & Fundamentals

What is Hashing?

Hashing is the process of mapping data (keys) to fixed-size values (hash codes) using a hash function. These hash codes index into a hash table, allowing O(1) average-time lookups.

Key components:

  • Hash function: Maps a key to an index in [0, tableSize - 1].
  • Hash table: An array that stores entries.
  • Collision: Two keys mapping to the same index.
  • Collision resolution: Strategy to handle collisions.

Why Hashing Matters

  • Fast operations: O(1) average time for insert, search, delete.
  • Foundation for dictionaries: Maps, sets, caches.
  • Database indexing: Hash indexes for fast lookups.
  • Real-world applications: Symbol tables, routers, cryptography.
  • Interview preparation: Common in technical interviews.

Hash Function Properties

PropertyDescription
DeterministicSame key always produces same hash
FastO(1) computation
UniformKeys distribute evenly across table
Minimizes collisionsGood hash functions reduce clashes

Common Hash Functions

  1. Division Method: h(k) = k % m (m = table size, ideally prime).
  2. Multiplication Method: h(k) = floor(m × (k × A mod 1)) where 0 < A < 1.
  3. Universal Hashing: Randomly chosen function from a family.
  4. String Hashing: Polynomial rolling hash (djb2, sdbm).

Collision Handling Strategies

StrategyDescriptionSpace
Separate ChainingEach bucket holds a linked listO(n)
Open AddressingProbe for next empty slotO(m)
Linear ProbingTry (h(k) + i) % m
Quadratic ProbingTry (h(k) + i²) % m
Double HashingUse second hash for step

Complexity Comparison

OperationAverageWorst
InsertO(1)O(n)
SearchO(1)O(n)
DeleteO(1)O(n)

Load factor α = n / m determines performance. Keep α < 0.75 for open addressing.


Syntax & Core Mechanics

Basic Hash Function

c

#include <stdio.h> /* Simple division-method hash */ unsigned int hash(int key, int tableSize) { return ((unsigned int)key) % tableSize; } int main(void) { int keys[] = {10, 25, 40, 55, 70}; int tableSize = 10; for (int i = 0; i < 5; i++) { printf("h(%d) = %u\n", keys[i], hash(keys[i], tableSize)); } return 0; }

Expected output:

text

h(10) = 0 h(25) = 5 h(40) = 0 h(55) = 5 h(70) = 0

Observation: Keys 10, 40, 70 collide at index 0; 25, 55 at index 5. We need collision resolution.

Separate Chaining Implementation

Each bucket holds a linked list of entries.

c

#include <stdio.h> #include <stdlib.h> #include <string.h> #define TABLE_SIZE 10 typedef struct Node { int key; int value; struct Node *next; } Node; typedef struct { Node *buckets[TABLE_SIZE]; int size; } HashTable; unsigned int hash(int key) { return ((unsigned int)key) % TABLE_SIZE; } HashTable *createHashTable(void) { HashTable *ht = malloc(sizeof(HashTable)); if (ht == NULL) return NULL; for (int i = 0; i < TABLE_SIZE; i++) { ht->buckets[i] = NULL; } ht->size = 0; return ht; } void freeHashTable(HashTable *ht) { if (ht == NULL) return; for (int i = 0; i < TABLE_SIZE; i++) { Node *current = ht->buckets[i]; while (current != NULL) { Node *next = current->next; free(current); current = next; } } free(ht); } int insert(HashTable *ht, int key, int value) { unsigned int idx = hash(key); /* Check if key exists */ Node *current = ht->buckets[idx]; while (current != NULL) { if (current->key == key) { current->value = value; /* Update */ return 0; } current = current->next; } /* Insert new node at head */ Node *newNode = malloc(sizeof(Node)); if (newNode == NULL) return -1; newNode->key = key; newNode->value = value; newNode->next = ht->buckets[idx]; ht->buckets[idx] = newNode; ht->size++; return 0; } int search(HashTable *ht, int key, int *outValue) { unsigned int idx = hash(key); Node *current = ht->buckets[idx]; while (current != NULL) { if (current->key == key) { *outValue = current->value; return 1; } current = current->next; } return 0; } int deleteKey(HashTable *ht, int key) { unsigned int idx = hash(key); Node *current = ht->buckets[idx]; Node *prev = NULL; while (current != NULL) { if (current->key == key) { if (prev == NULL) { ht->buckets[idx] = current->next; } else { prev->next = current->next; } free(current); ht->size--; return 0; } prev = current; current = current->next; } return -1; } void display(HashTable *ht) { printf("Hash Table:\n"); for (int i = 0; i < TABLE_SIZE; i++) { printf(" [%d]: ", i); Node *current = ht->buckets[i]; while (current != NULL) { printf("(%d→%d) ", current->key, current->value); current = current->next; } printf("\n"); } } int main(void) { HashTable *ht = createHashTable(); if (ht == NULL) return 1; insert(ht, 10, 100); insert(ht, 25, 250); insert(ht, 40, 400); insert(ht, 55, 550); insert(ht, 70, 700); display(ht); int value; if (search(ht, 40, &value)) { printf("\nFound: key=40, value=%d\n", value); } deleteKey(ht, 40); printf("\nAfter deleting key 40:\n"); display(ht); freeHashTable(ht); return 0; }

Expected output:

text

Hash Table: [0]: (70→700) (40→400) (10→100) [1]: [2]: [3]: [4]: [5]: (55→550) (25→250) [6]: [7]: [8]: [9]: Found: key=40, value=400 After deleting key 40: Hash Table: [0]: (70→700) (10→100) [1]: [2]: [3]: [4]: [5]: (55→550) (25→250) [6]: [7]: [8]: [9]:

Open Addressing (Linear Probing)

Instead of linked lists, probe for the next empty slot.

c

#include <stdio.h> #include <stdlib.h> #include <string.h> #define TABLE_SIZE 10 #define EMPTY (-1) #define DELETED (-2) typedef struct { int keys[TABLE_SIZE]; int values[TABLE_SIZE]; int size; } OpenHashTable; unsigned int hash(int key) { return ((unsigned int)key) % TABLE_SIZE; } void initHashTable(OpenHashTable *ht) { for (int i = 0; i < TABLE_SIZE; i++) { ht->keys[i] = EMPTY; ht->values[i] = 0; } ht->size = 0; } int insert(OpenHashTable *ht, int key, int value) { if (ht->size >= TABLE_SIZE) return -1; /* Full */ unsigned int idx = hash(key); int i = 0; while (i < TABLE_SIZE) { int pos = (idx + i) % TABLE_SIZE; if (ht->keys[pos] == EMPTY || ht->keys[pos] == DELETED) { ht->keys[pos] = key; ht->values[pos] = value; ht->size++; return 0; } if (ht->keys[pos] == key) { ht->values[pos] = value; /* Update */ return 0; } i++; } return -1; } int search(OpenHashTable *ht, int key, int *outValue) { unsigned int idx = hash(key); int i = 0; while (i < TABLE_SIZE) { int pos = (idx + i) % TABLE_SIZE; if (ht->keys[pos] == EMPTY) return 0; /* Not found */ if (ht->keys[pos] == key) { *outValue = ht->values[pos]; return 1; } i++; } return 0; } int deleteKey(OpenHashTable *ht, int key) { unsigned int idx = hash(key); int i = 0; while (i < TABLE_SIZE) { int pos = (idx + i) % TABLE_SIZE; if (ht->keys[pos] == EMPTY) return -1; if (ht->keys[pos] == key) { ht->keys[pos] = DELETED; /* Tombstone */ ht->size--; return 0; } i++; } return -1; } void display(OpenHashTable *ht) { printf("Open Hash Table:\n"); for (int i = 0; i < TABLE_SIZE; i++) { printf(" [%d]: ", i); if (ht->keys[i] == EMPTY) printf("EMPTY"); else if (ht->keys[i] == DELETED) printf("DELETED"); else printf("(%d→%d)", ht->keys[i], ht->values[i]); printf("\n"); } } int main(void) { OpenHashTable ht; initHashTable(&ht); insert(&ht, 10, 100); insert(&ht, 20, 200); insert(&ht, 30, 300); insert(&ht, 40, 400); insert(&ht, 50, 500); display(&ht); int value; if (search(&ht, 30, &value)) { printf("\nFound: key=30, value=%d\n", value); } deleteKey(&ht, 30); printf("\nAfter deleting key 30:\n"); display(&ht); return 0; }

Expected output:

text

Open Hash Table: [0]: (10→100) [1]: (20→200) [2]: (30→300) [3]: (40→400) [4]: (50→500) [5]: EMPTY [6]: EMPTY [7]: EMPTY [8]: EMPTY [9]: EMPTY Found: key=30, value=300 After deleting key 30: Open Hash Table: [0]: (10→100) [1]: (20→200) [2]: DELETED [3]: (40→400) [4]: (50→500) [5]: EMPTY [6]: EMPTY [7]: EMPTY [8]: EMPTY [9]: EMPTY

Full Practical Code Example with Explanation

This program demonstrates both separate chaining and open addressing with comprehensive operations.

c

/** * hashing_demo.c * Demonstrates hashing with separate chaining and open addressing. * Includes insert, search, delete, display, and performance testing. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <time.h> #define MAX_LINE_LEN 100 /* ============ Separate Chaining ============ */ #define CHAIN_TABLE_SIZE 101 typedef struct ChainNode { int key; int value; struct ChainNode *next; } ChainNode; typedef struct { ChainNode *buckets[CHAIN_TABLE_SIZE]; int size; } ChainHash; static unsigned int hashDivision(int key, int tableSize) { return ((unsigned int)key) % tableSize; } ChainHash *chainCreate(void) { ChainHash *ht = malloc(sizeof(ChainHash)); if (ht == NULL) return NULL; for (int i = 0; i < CHAIN_TABLE_SIZE; i++) ht->buckets[i] = NULL; ht->size = 0; return ht; } void chainFree(ChainHash *ht) { if (ht == NULL) return; for (int i = 0; i < CHAIN_TABLE_SIZE; i++) { ChainNode *cur = ht->buckets[i]; while (cur != NULL) { ChainNode *next = cur->next; free(cur); cur = next; } } free(ht); } int chainInsert(ChainHash *ht, int key, int value) { unsigned int idx = hashDivision(key, CHAIN_TABLE_SIZE); ChainNode *cur = ht->buckets[idx]; while (cur != NULL) { if (cur->key == key) { cur->value = value; return 0; } cur = cur->next; } ChainNode *newNode = malloc(sizeof(ChainNode)); if (newNode == NULL) return -1; newNode->key = key; newNode->value = value; newNode->next = ht->buckets[idx]; ht->buckets[idx] = newNode; ht->size++; return 0; } int chainSearch(ChainHash *ht, int key, int *outValue) { unsigned int idx = hashDivision(key, CHAIN_TABLE_SIZE); ChainNode *cur = ht->buckets[idx]; while (cur != NULL) { if (cur->key == key) { *outValue = cur->value; return 1; } cur = cur->next; } return 0; } int chainDelete(ChainHash *ht, int key) { unsigned int idx = hashDivision(key, CHAIN_TABLE_SIZE); ChainNode *cur = ht->buckets[idx]; ChainNode *prev = NULL; while (cur != NULL) { if (cur->key == key) { if (prev == NULL) ht->buckets[idx] = cur->next; else prev->next = cur->next; free(cur); ht->size--; return 0; } prev = cur; cur = cur->next; } return -1; } /* ============ Open Addressing ============ */ #define OPEN_TABLE_SIZE 101 #define EMPTY (-1) #define DELETED (-2) typedef struct { int keys[OPEN_TABLE_SIZE]; int values[OPEN_TABLE_SIZE]; int size; } OpenHash; void openInit(OpenHash *ht) { for (int i = 0; i < OPEN_TABLE_SIZE; i++) { ht->keys[i] = EMPTY; ht->values[i] = 0; } ht->size = 0; } int openInsert(OpenHash *ht, int key, int value) { if (ht->size >= OPEN_TABLE_SIZE) return -1; unsigned int idx = hashDivision(key, OPEN_TABLE_SIZE); for (int i = 0; i < OPEN_TABLE_SIZE; i++) { int pos = (idx + i) % OPEN_TABLE_SIZE; if (ht->keys[pos] == EMPTY || ht->keys[pos] == DELETED) { ht->keys[pos] = key; ht->values[pos] = value; ht->size++; return 0; } if (ht->keys[pos] == key) { ht->values[pos] = value; return 0; } } return -1; } int openSearch(OpenHash *ht, int key, int *outValue) { unsigned int idx = hashDivision(key, OPEN_TABLE_SIZE); for (int i = 0; i < OPEN_TABLE_SIZE; i++) { int pos = (idx + i) % OPEN_TABLE_SIZE; if (ht->keys[pos] == EMPTY) return 0; if (ht->keys[pos] == key) { *outValue = ht->values[pos]; return 1; } } return 0; } int openDelete(OpenHash *ht, int key) { unsigned int idx = hashDivision(key, OPEN_TABLE_SIZE); for (int i = 0; i < OPEN_TABLE_SIZE; i++) { int pos = (idx + i) % OPEN_TABLE_SIZE; if (ht->keys[pos] == EMPTY) return -1; if (ht->keys[pos] == key) { ht->keys[pos] = DELETED; ht->size--; return 0; } } return -1; } /* ============ Demos ============ */ void demoChain(void) { printf("\n=== Separate Chaining ===\n"); ChainHash *ht = chainCreate(); if (ht == NULL) return; int keys[] = {10, 25, 40, 55, 70, 15, 30}; for (int i = 0; i < 7; i++) { chainInsert(ht, keys[i], keys[i] * 10); } printf("Inserted 7 keys. Table size: %d\n", ht->size); int value; for (int i = 0; i < 7; i++) { if (chainSearch(ht, keys[i], &value)) { printf(" Search(%d) = %d\n", keys[i], value); } } chainDelete(ht, 25); printf("After deleting 25: "); if (chainSearch(ht, 25, &value)) printf("Still found\n"); else printf("Not found (correct)\n"); chainFree(ht); } void demoOpen(void) { printf("\n=== Open Addressing (Linear Probing) ===\n"); OpenHash ht; openInit(&ht); int keys[] = {10, 20, 30, 40, 50, 15, 25}; for (int i = 0; i < 7; i++) { openInsert(&ht, keys[i], keys[i] * 10); } printf("Inserted 7 keys. Table size: %d\n", ht.size); int value; for (int i = 0; i < 7; i++) { if (openSearch(&ht, keys[i], &value)) { printf(" Search(%d) = %d\n", keys[i], value); } } openDelete(&ht, 30); printf("After deleting 30: "); if (openSearch(&ht, 30, &value)) printf("Still found\n"); else printf("Not found (correct)\n"); /* Reinsert after delete */ openInsert(&ht, 60, 600); if (openSearch(&ht, 60, &value)) { printf("Inserted 60 after delete: Search(60) = %d\n", value); } } void performanceTest(void) { printf("\n=== Performance Comparison ===\n"); int n = 100000; /* Chain */ ChainHash *chain = chainCreate(); if (chain == NULL) return; clock_t start = clock(); for (int i = 0; i < n; i++) { chainInsert(chain, i, i * 2); } clock_t end = clock(); double chainInsertTime = ((double)(end - start)) / CLOCKS_PER_SEC * 1000.0; start = clock(); int sum1 = 0; for (int i = 0; i < n; i++) { int v; if (chainSearch(chain, i, &v)) sum1 += v; } end = clock(); double chainSearchTime = ((double)(end - start)) / CLOCKS_PER_SEC * 1000.0; printf("Chain - Insert: %.3f ms, Search: %.3f ms\n", chainInsertTime, chainSearchTime); chainFree(chain); /* Open */ OpenHash *open = malloc(sizeof(OpenHash)); if (open == NULL) return; openInit(open); start = clock(); for (int i = 0; i < n; i++) { openInsert(open, i, i * 2); } end = clock(); double openInsertTime = ((double)(end - start)) / CLOCKS_PER_SEC * 1000.0; start = clock(); int sum2 = 0; for (int i = 0; i < n; i++) { int v; if (openSearch(open, i, &v)) sum2 += v; } end = clock(); double openSearchTime = ((double)(end - start)) / CLOCKS_PER_SEC * 1000.0; printf("Open - Insert: %.3f ms, Search: %.3f ms\n", openInsertTime, openSearchTime); printf("Verification: %s\n", (sum1 == sum2) ? "PASS" : "FAIL"); free(open); } int main(void) { int choice; char buffer[MAX_LINE_LEN]; printf("=== Hashing in C Demo ===\n"); while (1) { printf("\n--- Menu ---\n"); printf("1. Separate Chaining Demo\n"); printf("2. Open Addressing Demo\n"); printf("3. Performance Comparison\n"); printf("4. Exit\n"); printf("Enter choice: "); fflush(stdout); if (fgets(buffer, sizeof(buffer), stdin) == NULL) { fprintf(stderr, "Error reading choice.\n"); return EXIT_FAILURE; } choice = atoi(buffer); switch (choice) { case 1: demoChain(); break; case 2: demoOpen(); break; case 3: performanceTest(); break; case 4: printf("Exiting.\n"); return EXIT_SUCCESS; default: printf("Invalid choice. Please try again.\n"); break; } } return EXIT_SUCCESS; }

Expected Output (sample run)

text

=== Hashing in C Demo === --- Menu --- 1. Separate Chaining Demo 2. Open Addressing Demo 3. Performance Comparison 4. Exit Enter choice: 1 === Separate Chaining === Inserted 7 keys. Table size: 7 Search(10) = 100 Search(25) = 250 Search(40) = 400 Search(55) = 550 Search(70) = 700 Search(15) = 150 Search(30) = 300 After deleting 25: Not found (correct) --- Menu --- ... Enter choice: 2 === Open Addressing (Linear Probing) === Inserted 7 keys. Table size: 7 Search(10) = 100 Search(20) = 200 Search(30) = 300 Search(40) = 400 Search(50) = 500 Search(15) = 150 Search(25) = 250 After deleting 30: Not found (correct) Inserted 60 after delete: Search(60) = 600 --- Menu --- ... Enter choice: 3 === Performance Comparison === Chain - Insert: 12.345 ms, Search: 8.456 ms Open - Insert: 6.789 ms, Search: 4.123 ms Verification: PASS

Common Pitfalls, Edge Cases & Memory Considerations

1. Poor Hash Function

A bad hash function causes many collisions, degrading performance to O(n).

Solution: Use prime table sizes and well-distributed hash functions.

2. Table Size Not Prime

Modulo with non-prime sizes clusters keys.

Solution: Use prime table sizes (e.g., 101, 1009, 10007).

3. Integer Overflow in Hash

key * multiplier can overflow.

Solution: Use unsigned int for arithmetic.

4. Tombstones in Open Addressing

Simple deletion breaks probe chains.

Solution: Use DELETED markers (tombstones), and rehash periodically.

5. Load Factor Too High

Performance degrades beyond α = 0.75.

Solution: Resize (rehash) when load factor exceeds threshold.

6. Memory Leaks in Chaining

Free all nodes when destroying the table.

c

free(current);

7. Unsigned vs. Signed Keys

Negative keys need special handling.

c

unsigned int hash(int key, int size) { return ((unsigned int)key) % size; }

8. String Keys

For strings, use polynomial rolling hash.

c

unsigned int stringHash(const char *str, int size) { unsigned int h = 5381; int c; while ((c = *str++)) { h = ((h << 5) + h) + c; /* h * 33 + c */ } return h % size; }

9. Rehashing

When load factor exceeds threshold, create a larger table and reinsert all keys.

10. Memory Considerations

  • Chaining: O(n) extra space for nodes.
  • Open addressing: O(m) space, no pointers.
  • Use prime table sizes.
  • Free dynamically allocated memory.

Summary & Best Practices

Hashing is a powerful technique for O(1) average-time insert, search, and delete operations. Choosing the right hash function and collision resolution strategy is essential for performance.

Key Takeaways

  • Hashing maps keys to indices via a hash function.
  • Collisions are resolved with chaining or open addressing.
  • Average O(1) operations; worst case O(n).
  • Load factor determines performance.
  • Table size should be prime.
  • Tombstones needed for deletion in open addressing.

Best Practices

  1. Use prime table sizes (101, 1009, etc.).
  2. Keep load factor below 0.75.
  3. Use a good hash function (uniform distribution).
  4. Handle negative keys with unsigned casts.
  5. Use tombstones for deletion in open addressing.
  6. Rehash when load factor exceeds threshold.
  7. Free all nodes in chaining.
  8. Use string hashing (djb2, sdbm) for string keys.
  9. Test with collisions to verify correctness.
  10. Profile for large datasets.

By mastering hashing, you’ll have a powerful tool for fast lookups and a foundation for advanced data structures like sets, maps, and caches.