CoreTechED

Loading

Learn separate chaining in C hashing: linked list buckets, insert/search/delete, rehashing, performance analysis, and implementation.

Separate Chaining in Hashing in C: Complete Guide

Separate Chaining Collision Handling Technique in Hashing in C: A Comprehensive Tutorial

Separate Chaining is one of the most popular and commonly used techniques to handle collisions in hashing. Instead of probing for an alternative slot, each bucket holds a chain (typically a linked list) of all entries that hash to the same index. This makes chaining simple, robust, and capable of handling arbitrarily high load factors.

This tutorial provides an in-depth exploration of Separate Chaining in C: fundamental concepts, data structures for chains, implementation, performance analysis, advantages and disadvantages, common pitfalls, and best practices.


Conceptual Introduction & Fundamentals

What is Separate Chaining?

Separate Chaining resolves collisions by maintaining a chain at each slot of the hash table. When multiple keys hash to the same index, they are stored in the same chain (a linked list, dynamic array, or balanced BST).

Hash function example: h(k) = k % 5

Keys [12, 22, 15, 25]:

text

12 % 5 = 2 22 % 5 = 2 (collision with 12 → add to chain) 15 % 5 = 0 25 % 5 = 0 (collision with 15 → add to chain)

Resulting table:

text

[0]: 15 → 25 [1]: (empty) [2]: 12 → 22 [3]: (empty) [4]: (empty)

Why Separate Chaining?

  • Simple to implement: Just linked lists in buckets.
  • Never fills up: Chains grow as needed.
  • Robust to bad hash functions: Even poor distribution degrades gracefully.
  • Handles dynamic workloads: Ideal when the number of keys is unknown.
  • Easy deletion: No tombstones needed.

Two Main Collision Handling Methods

MethodDescription
Separate ChainingEach slot holds a chain of entries
Open AddressingAll entries stored in the table itself with probing

Performance Analysis

Under simple uniform hashing:

MetricFormula
Table slotsm
Number of keysn
Load factorα = n / m
Expected search timeO(1 + α)
Expected delete timeO(1 + α)
Insert timeO(1)

If α = O(1), all operations are O(1) average.


Syntax & Core Mechanics

Data Structures for Chains

StructureSearchDeleteInsertCache
Linked ListO(len)O(len)O(1)Poor
Dynamic ArrayO(len)O(len)O(1)*Good
Self-Balancing BSTO(log len)O(log len)O(log len)Poor

*Amortized; resizing may be O(len).

Java 8+ uses a Red-Black Tree for chains longer than 8 entries to bound worst-case lookup time.

Linked List Node

c

typedef struct Entry { int key; int value; struct Entry *next; } Entry;

Hash Table Structure

c

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

Hash Function (Division Method)

c

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

Insertion Algorithm

  1. Compute idx = hash(key).
  2. Traverse bucket; if key found, update value.
  3. Otherwise, prepend new node to bucket.

Search Algorithm

  1. Compute idx = hash(key).
  2. Traverse bucket list; return value if found.
  3. Return “not found” if chain exhausted.

Deletion Algorithm

  1. Compute idx = hash(key).
  2. Traverse bucket list tracking the previous node.
  3. Unlink the matching node; free it.

Full Practical Code Example with Explanation

This program implements a complete hash map using separate chaining with linked lists, supporting insert, search, delete, display, and rehashing.

c

/** * separate_chaining.c * Complete hash table using separate chaining. * Supports insert, search, delete, display, and rehashing. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #define MAX_LINE_LEN 100 #define DEFAULT_BUCKETS 7 #define REHASH_THRESHOLD 1.0 /* Node in a chain */ typedef struct Entry { int key; int value; struct Entry *next; } Entry; /* Hash table with separate chaining */ typedef struct { Entry **buckets; int bucketCount; int size; int useRehashing; } HashTable; /* Division-method hash with unsigned cast */ 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 bucket count 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; 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); /* Update if key exists */ Entry *cur = ht->buckets[idx]; while (cur != NULL) { if (cur->key == key) { cur->value = value; return 0; } cur = cur->next; } /* Prepend new entry */ 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 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 */ 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; } /* Return chain length at a given bucket */ int chainLength(const HashTable *ht, int idx) { int count = 0; Entry *cur = ht->buckets[idx]; while (cur) { count++; cur = cur->next; } return count; } /* Find maximum chain length (worst-case lookup) */ int maxChainLength(const HashTable *ht) { int max = 0; for (int i = 0; i < ht->bucketCount; i++) { int len = chainLength(ht, i); if (len > max) max = len; } return max; } /* Display the table */ void display(const HashTable *ht) { double lf = (double)ht->size / ht->bucketCount; printf("Hash Table (size=%d, buckets=%d, load=%.2f, maxChain=%d):\n", ht->size, ht->bucketCount, lf, maxChainLength(ht)); for (int i = 0; i < ht->bucketCount; i++) { printf(" [%2d]:", i); Entry *cur = ht->buckets[i]; while (cur != NULL) { printf(" (%d→%d)", cur->key, cur->value); cur = cur->next; } printf("\n"); } } /* ============ Demos ============ */ static void demoBasicChaining(void) { printf("\n=== Basic Separate Chaining ===\n"); HashTable *ht = createHashTable(5, 0); if (ht == NULL) return; int keys[] = {12, 22, 15, 25, 5, 17}; int n = sizeof(keys) / sizeof(keys[0]); for (int i = 0; i < n; i++) { insert(ht, keys[i], keys[i] * 10); } display(ht); int v; printf("\nSearch 22: "); if (search(ht, 22, &v)) printf("%d\n", v); else printf("not found\n"); printf("Search 100: "); if (search(ht, 100, &v)) printf("%d\n", v); else printf("not found\n"); printf("\nDelete 15...\n"); deleteKey(ht, 15); display(ht); freeHashTable(ht); } static void demoRehashing(void) { printf("\n=== Chaining with Rehashing ===\n"); HashTable *ht = createHashTable(5, 1); if (ht == NULL) return; for (int i = 0; i < 20; i++) { int key = (i * 13 + 7) % 100; insert(ht, key, key * 10); if (i == 4 || i == 9 || i == 19) { printf("\nAfter %d inserts:\n", i + 1); display(ht); } } freeHashTable(ht); } /* Read 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("=== Separate Chaining Demo ===\n"); while (1) { printf("\n--- Menu ---\n"); printf("1. Basic Chaining Demo\n"); printf("2. Chaining with Rehashing Demo\n"); printf("3. Interactive Hash 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) { demoBasicChaining(); } 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--- Interactive Hash 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 — Node in a chain storing key, value, next.
  2. HashTable — Bucket array, bucket count, size, rehashing flag.
  3. hashIndex — Division-method hash with unsigned cast.
  4. nextPrime — Chooses prime bucket sizes.
  5. createHashTable — Allocates bucket array.
  6. insert — Prepends new node; updates existing key; optionally rehashes.
  7. search — Traverses chain linearly.
  8. deleteKey — Unlinks and frees matching node.
  9. rehash — Doubles bucket count and reinserts all entries.
  10. chainLength, maxChainLength — Helpers for diagnostics.

Expected Output (sample run)

text

=== Basic Separate Chaining === Hash Table (size=6, buckets=5, load=1.20, maxChain=3): [ 0]: (25→250) (15→150) [ 1]: (17→170) [ 2]: (22→220) (12→120) [ 3]: [ 4]: (5→50) Search 22: 220 Search 100: not found Delete 15... Hash Table (size=5, buckets=5, load=1.00, maxChain=2): [ 0]: (25→250) [ 1]: (17→170) [ 2]: (22→220) (12→120) [ 3]: [ 4]: (5→50)

Common Pitfalls, Edge Cases & Memory Considerations

1. Forgetting to Free Nodes

Each malloc must be paired with free.

c

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

2. Not Updating Duplicate Keys

Insert should update existing key, not append duplicates.

3. Integer Overflow in Hash

key * multiplier can overflow.

Solution: Cast to unsigned int before modulo.

4. Poor Cache Performance

Linked lists scatter memory.

Solution: Use dynamic arrays for chains (cache-friendly) or Red-Black Trees for large chains.

5. Long Chains

Worst-case search becomes O(n).

Solution: Rehash when load factor is high; consider RB-trees for large chains.

6. Empty Buckets Wasting Memory

Bucket array itself wastes space.

Solution: Size buckets based on expected n and target load factor.

7. Unbounded Growth

No rehashing means chains grow arbitrarily.

Solution: Use useRehashing = 1 and rehash when α > 1.

8. Unsigned vs. Signed Keys

Negative keys need careful handling.

Solution: Cast keys to unsigned int.

9. Thread Safety

Concurrent access to chains corrupts data.

Solution: Use locks per bucket or a global mutex for multi-threaded use.

10. Memory Considerations

  • Bucket array: bucketCount × sizeof(Entry *).
  • Each entry: sizeof(Entry) + malloc overhead.
  • Chains use more memory than open addressing.
  • Free all entries and the bucket array on destruction.

Summary & Best Practices

Separate Chaining is a robust, easy-to-implement collision resolution strategy with excellent behavior for dynamic workloads. It trades memory and cache efficiency for simplicity and worst-case robustness.

Key Takeaways

  • Each bucket holds a chain of entries.
  • Insert at head: O(1).
  • Search/delete: O(1 + α) average.
  • Linked lists: O(1) insert, poor cache.
  • Dynamic arrays: cache-friendly.
  • RB-trees (Java 8+): O(log len) worst-case.
  • Never fills up; robust to load factors.

Best Practices

  1. Use prime bucket sizes.
  2. Prepend for O(1) insert.
  3. Update existing keys rather than duplicating.
  4. Rehash when load factor > 1.
  5. Free every entry and the bucket array.
  6. Cast keys to unsigned int.
  7. Consider dynamic arrays for cache efficiency.
  8. Consider RB-trees for long chains (Java-style).
  9. Use djb2 or similar good hash functions.
  10. Profile for large datasets.

By mastering separate chaining, you’ll implement hash tables that are simple, scalable, and robust to collision-heavy workloads.

Learn quadratic probing in C hash tables: standard and modified probe sequences, full utilization formula, and implementation with insert/search/delete.

Quadratic Probing in Hashing in C: Complete Guide

Quadratic Probing in Hashing in C: A Comprehensive Tutorial

Quadratic Probing is an Open Addressing collision resolution technique where, instead of probing linearly, we jump by quadratically increasing offsets. This reduces the primary clustering problem of linear probing, though it introduces a milder secondary clustering. It’s a widely used technique in high-performance hash tables.

This tutorial provides an in-depth exploration of Quadratic Probing in C: fundamental concepts, probe sequences, implementation, the modified formula for full table utilization, complexity analysis, common pitfalls, and best practices.


Conceptual Introduction & Fundamentals

What is Quadratic Probing?

Quadratic Probing is an open-addressing scheme where, on collision at index h(k), we probe slots at (h(k) + i²) % m for i = 0, 1, 2, ... until we find an empty or matching slot.

Probe sequence: (h(k) + 0²) % m, (h(k) + 1²) % m, (h(k) + 2²) % m, …

Why Quadratic Probing?

  • Reduces primary clustering: Unlike linear probing, keys hashing to nearby slots don’t always collide.
  • Better than linear probing: Distributes keys more uniformly in clustered regions.
  • No extra memory: All keys stored in the table itself.
  • Cache-friendly-ish: Jumps are still predictable but larger.

Probe Sequence Example

For hash function h(k) = k % 7 and keys [22, 30, 50]:

text

22 → 22 % 7 = 1 (place at index 1) 30 → 30 % 7 = 2 (place at index 2) 50 → 50 % 7 = 1 (collision!) Probe i=1: (1 + 1) % 7 = 2 (occupied) Probe i=2: (1 + 4) % 7 = 5 (empty → place at index 5)

Result: [_, 22, 30, _, _, 50, _]

Primary vs Secondary Clustering

ClusteringDescriptionAffects
PrimaryContiguous blocks of occupied slotsLinear probing
SecondaryKeys hashing to the same index follow identical probesQuadratic probing

Quadratic probing eliminates primary clustering but retains secondary clustering.

The Full-Utilization Problem

With a standard quadratic probe (h(k) + i²) % m, the sequence may not visit all slots, especially if m is not prime. This can cause failed insertions even when empty slots exist.

Example: Table size 11 with keys [21, 10, 32, 43, 54, 65, 87, 76] fails to insert 87 and 76 with standard formula.

The Modified Probe Formula

For guaranteed full utilization, use this probe formula with m' = nextPowerOf2(m):

text

probe(k, i) = (h(k) + (i + i²) / 2) % m'

This produces the triangular number sequence: 0, 1, 3, 6, 10, 15, … which visits every slot exactly once when m' is a power of 2.

Complexity Analysis

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

At load factor α < 0.5, quadratic probing performs nearly as well as double hashing.


Syntax & Core Mechanics

Standard Quadratic Probing

c

/* Standard quadratic probe */ int probe = (hashValue + i * i) % tableSize;

Modified Quadratic Probing (Full Utilization)

c

/* Modified formula with next power of 2 */ int probe = (hashValue + (i + i * i) / 2) % nextPowerOf2(tableSize); if (probe >= tableSize) continue; /* Skip out-of-bounds */

Compute Next Power of 2

c

int nextPowerOf2(int n) { n--; n |= n >> 1; n |= n >> 2; n |= n >> 4; n |= n >> 8; n |= n >> 16; n++; return n; }

Insertion Algorithm

  1. Compute h = hash(key).
  2. If table[h] is EMPTY, insert.
  3. Else, probe i = 1, 2, ... with (h + i²) % m (or modified formula).
  4. Stop when EMPTY slot found or all slots probed.

Deletion

Same tombstone approach as linear probing: mark slot as DELETED.


Full Practical Code Example with Explanation

This program demonstrates both standard and modified quadratic probing with a complete hash table.

c

/** * quadratic_probing.c * Demonstrates quadratic probing in hash tables. * Supports standard and modified probe sequences. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #define MAX_LINE_LEN 100 #define EMPTY (-1) #define DELETED (-2) #define REHASH_THRESHOLD 0.5 /* Slot in the hash table */ typedef struct { int key; int value; } Slot; /* Hash table using quadratic probing */ typedef struct { Slot *slots; int capacity; int size; int deletedCount; int useModified; } HashTable; /* Next power of 2 >= n */ static int nextPowerOf2(int n) { if (n <= 1) return 1; n--; n |= n >> 1; n |= n >> 2; n |= n >> 4; n |= n >> 8; n |= n >> 16; n++; return n; } /* Hash function */ static unsigned int hashIndex(int key, int capacity) { return ((unsigned int)key) % (unsigned int)capacity; } /* Compute probe index for step i */ static int probeIndex(int baseIdx, int i, int capacity, int useModified) { int result; if (useModified) { int m = nextPowerOf2(capacity); result = (int)((unsigned int)(baseIdx + (i + i * i) / 2) % (unsigned int)m); } else { result = (int)((unsigned int)(baseIdx + i * i) % (unsigned int)capacity); } return result; } /* Create hash table */ HashTable *createHashTable(int initialCapacity, int useModified) { HashTable *ht = malloc(sizeof(HashTable)); if (ht == NULL) return NULL; ht->slots = malloc(initialCapacity * sizeof(Slot)); if (ht->slots == NULL) { free(ht); return NULL; } for (int i = 0; i < initialCapacity; i++) { ht->slots[i].key = EMPTY; ht->slots[i].value = 0; } ht->capacity = initialCapacity; ht->size = 0; ht->deletedCount = 0; ht->useModified = useModified; return ht; } void freeHashTable(HashTable *ht) { if (ht == NULL) return; free(ht->slots); free(ht); } /* Rehash: double capacity and reinsert all live entries */ static int rehash(HashTable *ht) { int oldCap = ht->capacity; Slot *oldSlots = ht->slots; int newCap = oldCap * 2 + 1; Slot *newSlots = malloc(newCap * sizeof(Slot)); if (newSlots == NULL) return -1; for (int i = 0; i < newCap; i++) { newSlots[i].key = EMPTY; newSlots[i].value = 0; } ht->slots = newSlots; ht->capacity = newCap; ht->size = 0; ht->deletedCount = 0; for (int i = 0; i < oldCap; i++) { if (oldSlots[i].key != EMPTY && oldSlots[i].key != DELETED) { int key = oldSlots[i].key; int value = oldSlots[i].value; unsigned int base = hashIndex(key, newCap); for (int i2 = 0; i2 < newCap; i2++) { int pos = probeIndex((int)base, i2, newCap, ht->useModified); if (pos >= newCap) continue; if (newSlots[pos].key == EMPTY) { newSlots[pos].key = key; newSlots[pos].value = value; ht->size++; break; } } } } free(oldSlots); return 0; } /* Insert or update */ int insert(HashTable *ht, int key, int value) { double lf = (double)(ht->size + ht->deletedCount) / ht->capacity; if (lf > REHASH_THRESHOLD) { if (rehash(ht) != 0) return -1; } unsigned int base = hashIndex(key, ht->capacity); for (int i = 0; i < ht->capacity; i++) { int pos = probeIndex((int)base, i, ht->capacity, ht->useModified); if (ht->useModified && pos >= ht->capacity) continue; if (ht->slots[pos].key == key) { ht->slots[pos].value = value; return 0; } if (ht->slots[pos].key == EMPTY || ht->slots[pos].key == DELETED) { if (ht->slots[pos].key == DELETED) ht->deletedCount--; ht->slots[pos].key = key; ht->slots[pos].value = value; ht->size++; return 0; } } return -1; } /* Search */ int search(const HashTable *ht, int key, int *outValue) { unsigned int base = hashIndex(key, ht->capacity); for (int i = 0; i < ht->capacity; i++) { int pos = probeIndex((int)base, i, ht->capacity, ht->useModified); if (ht->useModified && pos >= ht->capacity) continue; if (ht->slots[pos].key == EMPTY) return 0; if (ht->slots[pos].key == key) { *outValue = ht->slots[pos].value; return 1; } } return 0; } /* Delete */ int deleteKey(HashTable *ht, int key) { unsigned int base = hashIndex(key, ht->capacity); for (int i = 0; i < ht->capacity; i++) { int pos = probeIndex((int)base, i, ht->capacity, ht->useModified); if (ht->useModified && pos >= ht->capacity) continue; if (ht->slots[pos].key == EMPTY) return -1; if (ht->slots[pos].key == key) { ht->slots[pos].key = DELETED; ht->deletedCount++; ht->size--; return 0; } } return -1; } /* Display */ void display(const HashTable *ht) { double lf = (double)ht->size / ht->capacity; printf("Hash Table (%s, size=%d, capacity=%d, deleted=%d, load=%.2f):\n", ht->useModified ? "modified" : "standard", ht->size, ht->capacity, ht->deletedCount, lf); for (int i = 0; i < ht->capacity; i++) { printf(" [%2d]: ", i); if (ht->slots[i].key == EMPTY) printf("EMPTY\n"); else if (ht->slots[i].key == DELETED) printf("DELETED\n"); else printf("(%d → %d)\n", ht->slots[i].key, ht->slots[i].value); } } /* ============ Demos ============ */ static void demoStandard(void) { printf("\n=== Standard Quadratic Probing ===\n"); HashTable *ht = createHashTable(11, 0); if (ht == NULL) return; int keys[] = {50, 700, 76, 85, 92, 73, 101}; int n = sizeof(keys) / sizeof(keys[0]); for (int i = 0; i < n; i++) { insert(ht, keys[i], keys[i]); } display(ht); printf("\nSlots: "); for (int i = 0; i < ht->capacity; i++) { printf("%d ", ht->slots[i].key); } printf("\n"); freeHashTable(ht); } static void demoModified(void) { printf("\n=== Modified Quadratic Probing (Full Utilization) ===\n"); HashTable *ht = createHashTable(11, 1); if (ht == NULL) return; int keys[] = {21, 10, 32, 43, 54, 65, 87, 76}; int n = sizeof(keys) / sizeof(keys[0]); for (int i = 0; i < n; i++) { insert(ht, keys[i], keys[i]); } display(ht); printf("\nSlots: "); for (int i = 0; i < ht->capacity; i++) { printf("%d ", ht->slots[i].key); } printf("\n"); freeHashTable(ht); } static void demoOperations(void) { printf("\n=== Standard Operations ===\n"); HashTable *ht = createHashTable(11, 0); if (ht == NULL) return; insert(ht, 22, 220); insert(ht, 30, 300); insert(ht, 50, 500); insert(ht, 12, 120); display(ht); int value; printf("\nSearch 30: "); if (search(ht, 30, &value)) printf("%d\n", value); else printf("not found\n"); printf("Delete 30...\n"); deleteKey(ht, 30); display(ht); printf("\nSearch 50 (after delete): "); if (search(ht, 50, &value)) printf("%d\n", value); else printf("not found\n"); freeHashTable(ht); } 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("=== Quadratic Probing in Hashing Demo ===\n"); while (1) { printf("\n--- Menu ---\n"); printf("1. Standard Quadratic Probing\n"); printf("2. Modified Quadratic Probing (Full Utilization)\n"); printf("3. Operations Demo (Insert/Search/Delete)\n"); printf("4. Interactive Hash Table\n"); printf("5. 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) { demoStandard(); } else if (choice == 2) { demoModified(); } else if (choice == 3) { demoOperations(); } else if (choice == 4) { HashTable *ht = createHashTable(11, 0); if (ht == NULL) continue; int opt; while (1) { printf("\n--- Interactive Hash 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 == 5) { printf("Exiting.\n"); return EXIT_SUCCESS; } else { printf("Invalid choice.\n"); } } return EXIT_SUCCESS; }

Explanation

  1. Slot — Stores key and value; sentinel keys mark EMPTY/DELETED.
  2. probeIndex — Computes probe index for standard or modified quadratic probing.
  3. nextPowerOf2 — Required by the modified formula for full utilization.
  4. insert / search / deleteKey — Probe iteratively using probeIndex.
  5. rehash — Doubles capacity when load factor exceeds 0.5.
  6. display — Prints each slot and its state.

Expected Output (sample run)

text

=== Standard Quadratic Probing === Hash Table (standard, size=7, capacity=11, deleted=0, load=0.64): [ 0]: (73 → 73) [ 1]: EMPTY [ 2]: (101 → 101) [ 3]: EMPTY [ 4]: (92 → 92) [ 5]: EMPTY [ 6]: (50 → 50) [ 7]: (700 → 700) [ 8]: (85 → 85) [ 9]: EMPTY [10]: (76 → 76) Slots: 73 -1 101 -1 92 -1 50 700 85 -1 76 === Modified Quadratic Probing (Full Utilization) === Hash Table (modified, size=8, capacity=11, deleted=0, load=0.73): [ 0]: (10 → 10) [ 1]: (87 → 87) [ 2]: EMPTY [ 3]: EMPTY [ 4]: (32 → 32) [ 5]: EMPTY [ 6]: (54 → 54) [ 7]: (65 → 65) [ 8]: (76 → 76) [ 9]: (43 → 43) [10]: (21 → 21) Slots: 10 87 -1 -1 32 -1 54 65 76 43 21

Common Pitfalls, Edge Cases & Memory Considerations

1. Probe Sequence Not Reaching All Slots

Standard quadratic probing may not visit every slot when m is not prime.

Solution: Use the modified formula with m' = nextPowerOf2(m).

2. Out-of-Bounds with Modified Formula

When m' > m, the computed index may exceed the table size.

c

if (pos >= ht->capacity) continue;

3. Infinite Probe Loop

Unbounded probing without finding an empty slot.

Solution: Bound the loop by capacity or nextPowerOf2(capacity).

4. Table Full (Even With Empty Slots)

Standard formula may fail to find an empty slot even when one exists.

Solution: Use the modified formula.

5. Tombstone Accumulation

Frequent deletes degrade performance.

Solution: Track deletedCount and trigger rehash when (size + deleted) / capacity > threshold.

6. Load Factor Too High

Quadratic probing degrades near α = 1.

Solution: Keep α < 0.5 and rehash proactively.

7. Secondary Clustering

Keys hashing to the same initial index follow identical probes.

Solution: Use double hashing for better distribution.

8. Rehash During Insert

Ensure rehash happens before computing the probe index.

9. Negative Keys

Cast to unsigned int before modulo.

10. Memory Considerations

  • Table memory: capacity × sizeof(Slot).
  • No pointer overhead.
  • Rehashing doubles memory temporarily.
  • Free slots array on cleanup.

Summary & Best Practices

Quadratic Probing reduces primary clustering compared to linear probing while remaining memory-efficient. For guaranteed full utilization, use the modified formula with a next power-of-2 table size.

Key Takeaways

  • Probe sequence: (h(k) + i²) % m (standard) or (h(k) + (i + i²)/2) % m' (modified).
  • Reduces primary clustering but retains secondary clustering.
  • Standard formula may fail; modified formula guarantees full utilization.
  • Keep load factor below 0.5.
  • Deletion requires tombstones.
  • Complexity: O(1) average; O(n) worst.

Best Practices

  1. Use the modified formula with nextPowerOf2 for guaranteed full utilization.
  2. Keep load factor < 0.5 for good performance.
  3. Bound probe loops by table size or power-of-2 size.
  4. Skip out-of-bounds indices in modified probing.
  5. Use tombstones for deletion.
  6. Rehash proactively when load factor exceeds threshold.
  7. Cast keys to unsigned before modulo.
  8. Consider double hashing for even better distribution.
  9. Free the slots array on cleanup.
  10. Test with high-collision keys to verify correctness.

By mastering quadratic probing, you’ll implement efficient in-place hash tables with better clustering behavior than linear probing.

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, hash tables, collisions, chaining, open addressing, load factor, and rehashing. Master O(1) average operations.

Introduction to Hashing in C: Complete Guide

Introduction to Hashing in C: A Comprehensive Tutorial

Hashing refers to the process of generating a small-sized output (that can be used as an index in a table) from an input of typically large and variable size. Hashing uses mathematical formulas known as hash functions to perform this transformation. This technique determines an index or location for the storage of an item in a data structure called a hash table.

This tutorial provides an in-depth introduction to hashing in C: fundamental concepts, components, hash functions, collision handling, practical implementation, common pitfalls, and best practices.


Conceptual Introduction & Fundamentals

What is Hashing?

Hashing is a technique for storing and retrieving data in O(1) average time. A hash function maps a key (string, integer, etc.) to an index in a hash table, where the corresponding value is stored.

Simple example: For strings {"ab", "cd", "efg"} with a=1, b=2, ..., z=26:

text

"ab" → 1 + 2 = 3 "cd" → 3 + 4 = 7 "efg" → 5 + 6 + 7 = 18

Using table size 7 and hash = sum(key) % 7:

text

"ab" → 3 % 7 = 3 "cd" → 7 % 7 = 0 "efg" → 18 % 7 = 4

Why Hashing Matters

  • O(1) average insert, search, delete — faster than arrays, linked lists, and BSTs.
  • Foundation for dictionaries and sets.
  • Real-world applications: Database indexing, cryptography, caches, symbol tables.
  • Wide language support: HashSet/HashMap in Java, set/dict in Python, unordered_set/unordered_map in C++.

Hash Set vs Hash Map

TypeDescriptionExample
Hash SetCollection of unique keysunordered_set (C++), HashSet (Java)
Hash MapKey-value pairs with unique keysunordered_map (C++), HashMap (Java), dict (Python)

When NOT to Use Hashing

ScenarioBetter Choice
Need sorted data with search/insert/deleteSelf-balancing BST
Prefix search on string keysTrie
Need floor/ceiling operationsSelf-balancing BST
Range queriesSorted array / segment tree

Components of Hashing

ComponentDescription
KeyThe input (string, integer, etc.)
Hash FunctionMaps key → index in [0, tableSize - 1]
Hash TableArray storing values at hash indices

Key Hashing Concepts

ConceptDescription
Hash FunctionMathematical formula mapping key to index
CollisionTwo keys map to the same index
Collision ResolutionChaining, open addressing, etc.
Load Factor (α)n / m (entries / table size)
RehashingResizing table when load factor is high

Complexity Comparison

Data StructureSearchInsertDelete
Array (unsorted)O(n)O(1)O(n)
Linked ListO(n)O(1)O(n)
Balanced BSTO(log n)O(log n)O(log n)
Hash TableO(1) avgO(1) avgO(1) avg

Syntax & Core Mechanics

Basic Hash Function

c

#include <stdio.h> /* Division-method hash: key mod table size */ unsigned int hash(int key, int tableSize) { return ((unsigned int)key) % tableSize; } int main(void) { int keys[] = {15, 11, 27, 8, 12}; int tableSize = 7; printf("Key → Hash Index (table size = %d)\n", tableSize); for (int i = 0; i < 5; i++) { printf(" %d → %u\n", keys[i], hash(keys[i], tableSize)); } return 0; }

Expected output:

text

Key → Hash Index (table size = 7) 15 → 1 11 → 4 27 → 6 8 → 1 12 → 5

Observation: Keys 15 and 8 collide at index 1. We need collision resolution.

String Hash Function (djb2)

A classic, well-distributed hash for strings.

c

#include <stdio.h> unsigned int stringHash(const char *str, int tableSize) { unsigned int hash = 5381; int c; while ((c = *str++) != '\0') { hash = ((hash << 5) + hash) + (unsigned int)c; /* hash * 33 + c */ } return hash % tableSize; } int main(void) { const char *keys[] = {"ab", "cd", "efg", "hello", "world"}; int tableSize = 7; printf("String → Hash Index (table size = %d)\n", tableSize); for (int i = 0; i < 5; i++) { printf(" \"%s\" → %u\n", keys[i], stringHash(keys[i], tableSize)); } return 0; }

Expected output (typical):

text

String → Hash Index (table size = 7) "ab" → 5 "cd" → 6 "efg" → 3 "hello" → 4 "world" → 1

Collision Handling Strategies

StrategyDescription
Separate ChainingEach bucket holds a linked list of entries
Linear ProbingOn collision, try (h(k) + i) % m for i = 1, 2, ...
Quadratic ProbingTry (h(k) + i²) % m
Double HashingUse second hash h2(k) as step size

Load Factor

text

α = n / m
  • n = number of stored entries
  • m = table size

Guidelines:

  • Chaining: α can be > 1, but keep around 0.75 for performance.
  • Open addressing: keep α < 0.75.

Rehashing

When load factor exceeds threshold, create a larger table (typically 2× size, prime) and reinsert all entries.

c

void rehash(HashTable *ht) { int newSize = nextPrime(ht->size * 2); /* Create new table, reinsert all entries, free old */ }

Full Practical Code Example with Explanation

This program demonstrates a complete hash map with chaining, including insert, search, delete, display, and dynamic resizing.

c

/** * hash_map_demo.c * Complete hash map implementation using separate chaining. * Includes insert, search, delete, display, and rehashing. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #define INITIAL_CAPACITY 7 #define MAX_LINE_LEN 100 #define MAX_KEY_LEN 64 /* Node in a bucket's linked list */ typedef struct Entry { char key[MAX_KEY_LEN]; int value; struct Entry *next; } Entry; /* Hash table */ typedef struct { Entry **buckets; int capacity; int size; double loadFactorThreshold; } HashMap; /* djb2 string hash */ static unsigned int hashString(const char *str, int capacity) { unsigned int hash = 5381; int c; while ((c = *str++) != '\0') { hash = ((hash << 5) + hash) + (unsigned int)c; } return hash % (unsigned int)capacity; } HashMap *createMap(int capacity) { HashMap *map = malloc(sizeof(HashMap)); if (map == NULL) return NULL; map->buckets = calloc(capacity, sizeof(Entry *)); if (map->buckets == NULL) { free(map); return NULL; } map->capacity = capacity; map->size = 0; map->loadFactorThreshold = 0.75; return map; } void freeMap(HashMap *map) { if (map == NULL) return; for (int i = 0; i < map->capacity; i++) { Entry *cur = map->buckets[i]; while (cur != NULL) { Entry *next = cur->next; free(cur); cur = next; } } free(map->buckets); free(map); } int insert(HashMap *map, const char *key, int value) { unsigned int idx = hashString(key, map->capacity); Entry *cur = map->buckets[idx]; while (cur != NULL) { if (strcmp(cur->key, key) == 0) { cur->value = value; /* Update */ return 0; } cur = cur->next; } Entry *newEntry = malloc(sizeof(Entry)); if (newEntry == NULL) return -1; strncpy(newEntry->key, key, MAX_KEY_LEN - 1); newEntry->key[MAX_KEY_LEN - 1] = '\0'; newEntry->value = value; newEntry->next = map->buckets[idx]; map->buckets[idx] = newEntry; map->size++; return 0; } int search(const HashMap *map, const char *key, int *outValue) { unsigned int idx = hashString(key, map->capacity); Entry *cur = map->buckets[idx]; while (cur != NULL) { if (strcmp(cur->key, key) == 0) { *outValue = cur->value; return 1; } cur = cur->next; } return 0; } int deleteKey(HashMap *map, const char *key) { unsigned int idx = hashString(key, map->capacity); Entry *cur = map->buckets[idx]; Entry *prev = NULL; while (cur != NULL) { if (strcmp(cur->key, key) == 0) { if (prev == NULL) map->buckets[idx] = cur->next; else prev->next = cur->next; free(cur); map->size--; return 0; } prev = cur; cur = cur->next; } return -1; } void display(const HashMap *map) { printf("Hash Map (size=%d, capacity=%d, load=%.2f):\n", map->size, map->capacity, (double)map->size / map->capacity); for (int i = 0; i < map->capacity; i++) { if (map->buckets[i] == NULL) continue; printf(" [%d]: ", i); Entry *cur = map->buckets[i]; while (cur != NULL) { printf("(%s→%d) ", cur->key, cur->value); cur = cur->next; } printf("\n"); } } int main(void) { HashMap *map = createMap(INITIAL_CAPACITY); if (map == NULL) { fprintf(stderr, "Failed to create map.\n"); return EXIT_FAILURE; } insert(map, "apple", 10); insert(map, "banana", 20); insert(map, "cherry", 30); insert(map, "date", 40); insert(map, "elderberry", 50); insert(map, "fig", 60); display(map); int value; printf("\nSearch results:\n"); if (search(map, "cherry", &value)) { printf(" cherry → %d\n", value); } if (!search(map, "grape", &value)) { printf(" grape → not found\n"); } printf("\nDeleting 'banana'...\n"); deleteKey(map, "banana"); display(map); freeMap(map); return EXIT_SUCCESS; }

Expected Output

text

Hash Map (size=6, capacity=7, load=0.86): [1]: (fig→60) (elderberry→50) [2]: (date→40) [3]: (cherry→30) [4]: (banana→20) [5]: (apple→10) Search results: cherry → 30 grape → not found Deleting 'banana'... Hash Map (size=5, capacity=7, load=0.71): [1]: (fig→60) (elderberry→50) [2]: (date→40) [3]: (cherry→30) [5]: (apple→10)

Explanation

  1. createMap: Allocates buckets array.
  2. hashString: djb2 polynomial hash.
  3. insert: Prepends to bucket list; updates existing keys.
  4. search: Traverses bucket list comparing keys.
  5. deleteKey: Unlinks and frees the entry.
  6. display: Prints the table’s contents.

Common Pitfalls, Edge Cases & Memory Considerations

1. Bad Hash Function

A poor hash function clusters keys, degrading performance to O(n).

Solution: Use prime table sizes and well-distributed hashes (djb2, sdbm).

2. Table Size Not Prime

Non-prime sizes create clustering.

Solution: Use primes: 7, 13, 31, 101, 1009, 10007.

3. Integer Overflow in Hash

Multiplication in hash functions can overflow.

Solution: Use unsigned int or unsigned long long.

4. Load Factor Exceeding Threshold

Performance degrades significantly.

Solution: Rehash when load factor > 0.75.

5. Memory Leaks

Forgetting to free all entries and buckets.

Solution: Walk each bucket and free each node, then free the bucket array.

6. Negative Keys

key % size for negative keys can yield negative indices.

Solution: Cast to unsigned int before modulo.

7. Duplicate Keys in Hash Set

Need to decide: overwrite or reject?

Solution: Check for existing key before inserting.

8. Tombstones in Open Addressing

Deletion needs special markers to preserve probe chains.

Solution: Use DELETED sentinel values.

9. Rehashing Overhead

Frequent resizing hurts performance.

Solution: Double the size and choose the next prime.

10. Memory Considerations

  • Chaining: O(n) space for nodes.
  • Open addressing: O(m) space, no pointers.
  • Rehashing doubles memory temporarily.
  • Use dynamic allocation carefully; check every malloc.

Summary & Best Practices

Hashing is one of the most important data structure techniques, offering O(1) average-time 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.
  • Hash tables support O(1) average insert, search, delete.
  • Collisions are resolved via chaining or open addressing.
  • Load factor determines performance; rehash when high.
  • Hash sets store unique keys; hash maps store key-value pairs.
  • Not suitable for sorted/range/prefix queries.

Best Practices

  1. Use prime table sizes (7, 13, 31, 101, …).
  2. Use good hash functions (djb2 for strings, modulo for ints).
  3. Keep load factor ≤ 0.75; rehash when exceeded.
  4. Cast to unsigned for negative keys.
  5. Check every malloc return.
  6. Free all entries and buckets on cleanup.
  7. Use tombstones for deletion in open addressing.
  8. Avoid hash tables for sorted or range queries.
  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 building sets, maps, caches, and dictionaries.

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.

Learn to implement prefix sum arrays in C: 1D, 1-indexed, and 2D prefix sums. Master O(1) range-sum queries with practical examples.

Prefix Sum Array in C: Implementation & Applications

Prefix Sum Array – Implementation in C: A Comprehensive Tutorial

A prefix sum array is a precomputed array where each element at index i stores the sum of all elements from index 0 to i in the original array. This simple yet powerful technique enables O(1) range-sum queries after O(n) preprocessing, making it a cornerstone of many array and range-query algorithms.

This tutorial provides an in-depth exploration of prefix sum arrays in C: concepts, implementation, range-sum queries, 2D prefix sums, practical applications, common pitfalls, and best practices.


Conceptual Introduction & Fundamentals

What is a Prefix Sum Array?

Given an array arr[], the prefix sum array prefixSum[] is defined as:

text

prefixSum[i] = arr[0] + arr[1] + ... + arr[i]

Example:

text

Input: [10, 20, 10, 5, 15] Prefix Sum: [10, 30, 40, 45, 60]

Why Prefix Sums Matter

  • Range sum queries: O(1) after O(n) preprocessing.
  • Foundation for many problems: Subarray sums, equilibrium index, split arrays.
  • 2D prefix sums: Rectangle sum queries in matrices.
  • Sliding window alternatives: When the array is static.
  • Competitive programming: Essential preprocessing technique.

Key Insight: Range Sum in O(1)

For a range [L, R] (inclusive):

text

sum(L, R) = prefixSum[R] - prefixSum[L - 1] (if L > 0) sum(0, R) = prefixSum[R] (if L == 0)

Complexity Comparison

OperationWithout Prefix SumWith Prefix Sum
PreprocessingO(1)O(n)
Range sum queryO(n)O(1)
Q queriesO(n × Q)O(n + Q)

Syntax & Core Mechanics

Basic Prefix Sum Implementation

c

#include <stdio.h> #include <stdlib.h> int *buildPrefixSum(const int arr[], int n) { if (n <= 0) return NULL; int *prefix = malloc(n * sizeof(int)); if (prefix == NULL) { fprintf(stderr, "Memory allocation failed.\n"); return NULL; } prefix[0] = arr[0]; for (int i = 1; i < n; i++) { prefix[i] = prefix[i - 1] + arr[i]; } return prefix; } int main(void) { int arr[] = {10, 20, 10, 5, 15}; int n = sizeof(arr) / sizeof(arr[0]); int *prefix = buildPrefixSum(arr, n); if (prefix == NULL) return 1; printf("Original: "); for (int i = 0; i < n; i++) printf("%d ", arr[i]); printf("\n"); printf("Prefix sum: "); for (int i = 0; i < n; i++) printf("%d ", prefix[i]); printf("\n"); free(prefix); return 0; }

Expected output:

text

Original: 10 20 10 5 15 Prefix sum: 10 30 40 45 60

Time: O(n), Space: O(n)

Range Sum Query

c

/* Returns sum of arr[L..R] inclusive */ long long rangeSum(const long long prefix[], int L, int R) { if (L < 0 || R < 0 || L > R) return 0; if (L == 0) return prefix[R]; return prefix[R] - prefix[L - 1]; } int main(void) { int arr[] = {10, 20, 10, 5, 15}; int n = 5; long long prefix[5]; prefix[0] = arr[0]; for (int i = 1; i < n; i++) { prefix[i] = prefix[i - 1] + arr[i]; } printf("Sum[1..3] = %lld\n", rangeSum(prefix, 1, 3)); /* 20+10+5 = 35 */ printf("Sum[0..4] = %lld\n", rangeSum(prefix, 0, 4)); /* 60 */ printf("Sum[2..2] = %lld\n", rangeSum(prefix, 2, 2)); /* 10 */ printf("Sum[3..4] = %lld\n", rangeSum(prefix, 3, 4)); /* 5+15 = 20 */ return 0; }

Expected output:

text

Sum[1..3] = 35 Sum[0..4] = 60 Sum[2..2] = 10 Sum[3..4] = 20

1-Indexed Prefix Sum (Convenient for Range Queries)

Using a 1-indexed prefix array simplifies range queries (no special case for L == 0).

c

/* prefix[0] = 0, prefix[i] = sum(arr[0..i-1]) */ void buildPrefix1Indexed(const int arr[], int n, long long prefix[]) { prefix[0] = 0; for (int i = 1; i <= n; i++) { prefix[i] = prefix[i - 1] + arr[i - 1]; } } /* Range sum [L, R] inclusive (0-based) */ long long rangeSum1(const long long prefix[], int L, int R) { return prefix[R + 1] - prefix[L]; } int main(void) { int arr[] = {10, 20, 10, 5, 15}; int n = 5; long long prefix[6]; buildPrefix1Indexed(arr, n, prefix); printf("prefix array: "); for (int i = 0; i <= n; i++) printf("%lld ", prefix[i]); printf("\n"); printf("Sum[1..3] = %lld\n", rangeSum1(prefix, 1, 3)); /* 35 */ printf("Sum[0..4] = %lld\n", rangeSum1(prefix, 0, 4)); /* 60 */ return 0; }

Expected output:

text

prefix array: 0 10 30 40 45 60 Sum[1..3] = 35 Sum[0..4] = 60

2D Prefix Sum (Matrix Rectangle Sum)

For a matrix, prefix[i][j] stores the sum of the submatrix from (0,0) to (i,j).

Formula:

text

prefix[i][j] = matrix[i][j] + prefix[i-1][j] + prefix[i][j-1] - prefix[i-1][j-1]

Rectangle sum from (r1, c1) to (r2, c2):

text

sum = prefix[r2][c2] - prefix[r1-1][c2] - prefix[r2][c1-1] + prefix[r1-1][c1-1]

c

#include <stdio.h> #include <stdlib.h> #define ROWS 3 #define COLS 4 void build2DPrefix(int matrix[ROWS][COLS], int prefix[ROWS][COLS]) { for (int i = 0; i < ROWS; i++) { for (int j = 0; j < COLS; j++) { prefix[i][j] = matrix[i][j]; if (i > 0) prefix[i][j] += prefix[i - 1][j]; if (j > 0) prefix[i][j] += prefix[i][j - 1]; if (i > 0 && j > 0) prefix[i][j] -= prefix[i - 1][j - 1]; } } } int rectSum(int prefix[ROWS][COLS], int r1, int c1, int r2, int c2) { int sum = prefix[r2][c2]; if (r1 > 0) sum -= prefix[r1 - 1][c2]; if (c1 > 0) sum -= prefix[r2][c1 - 1]; if (r1 > 0 && c1 > 0) sum += prefix[r1 - 1][c1 - 1]; return sum; } int main(void) { int matrix[ROWS][COLS] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} }; int prefix[ROWS][COLS]; build2DPrefix(matrix, prefix); printf("Matrix:\n"); for (int i = 0; i < ROWS; i++) { for (int j = 0; j < COLS; j++) printf("%3d ", matrix[i][j]); printf("\n"); } printf("\nRectangle (0,0)-(1,1): %d\n", rectSum(prefix, 0, 0, 1, 1)); /* 14 */ printf("Rectangle (1,1)-(2,2): %d\n", rectSum(prefix, 1, 1, 2, 2)); /* 24 */ printf("Rectangle (0,0)-(2,3): %d\n", rectSum(prefix, 0, 0, 2, 3)); /* 78 */ return 0; }

Expected output:

text

Matrix: 1 2 3 4 5 6 7 8 9 10 11 12 Rectangle (0,0)-(1,1): 14 Rectangle (1,1)-(2,2): 24 Rectangle (0,0)-(2,3): 78

Full Practical Code Example with Explanation

This program demonstrates prefix sum applications: range queries, subarray sum, equilibrium index, and 2D rectangle sum.

c

/** * prefix_sum_demo.c * Demonstrates prefix sum array applications in C. * Includes 1D, 1-indexed, and 2D prefix sums. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <time.h> #define MAX_LINE_LEN 100 #define MAX_SIZE 100000 #define ROWS 4 #define COLS 5 /* Function prototypes */ long long *buildPrefix(const int arr[], int n); long long *buildPrefix1Indexed(const int arr[], int n); long long rangeSum(const long long prefix[], int L, int R); long long rangeSum1(const long long prefix[], int L, int R); int subarraySumEqualsK(const int arr[], int n, int k); int findEquilibrium(const int arr[], int n); void build2DPrefix(int matrix[ROWS][COLS], long long prefix[ROWS + 1][COLS + 1]); long long rectSum(long long prefix[ROWS + 1][COLS + 1], int r1, int c1, int r2, int c2); int readInteger(const char *prompt, int *outValue); void printArray(const int arr[], int n); void printPrefix(const long long prefix[], int n); void demoBasicPrefix(void); void demoRangeQueries(void); void demoSubarraySumK(void); void demoEquilibrium(void); void demo2DPrefix(void); void performanceComparison(void); int main(void) { int choice; char buffer[MAX_LINE_LEN]; printf("=== Prefix Sum Array Demo ===\n"); while (1) { printf("\n--- Menu ---\n"); printf("1. Basic Prefix Sum\n"); printf("2. Range Sum Queries\n"); printf("3. Subarray Sum Equals K\n"); printf("4. Equilibrium Index via Prefix\n"); printf("5. 2D Prefix Sum\n"); printf("6. Performance Comparison\n"); printf("7. 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: demoBasicPrefix(); break; case 2: demoRangeQueries(); break; case 3: demoSubarraySumK(); break; case 4: demoEquilibrium(); break; case 5: demo2DPrefix(); break; case 6: performanceComparison(); break; case 7: printf("Exiting.\n"); return EXIT_SUCCESS; default: printf("Invalid choice. Please try again.\n"); break; } } return EXIT_SUCCESS; } /* Build 0-indexed prefix sum */ long long *buildPrefix(const int arr[], int n) { if (n <= 0) return NULL; long long *prefix = malloc(n * sizeof(long long)); if (prefix == NULL) { fprintf(stderr, "Memory allocation failed.\n"); return NULL; } prefix[0] = arr[0]; for (int i = 1; i < n; i++) { prefix[i] = prefix[i - 1] + arr[i]; } return prefix; } /* Build 1-indexed prefix sum: prefix[0]=0, prefix[i]=sum(arr[0..i-1]) */ long long *buildPrefix1Indexed(const int arr[], int n) { if (n <= 0) return NULL; long long *prefix = malloc((n + 1) * sizeof(long long)); if (prefix == NULL) { fprintf(stderr, "Memory allocation failed.\n"); return NULL; } prefix[0] = 0; for (int i = 1; i <= n; i++) { prefix[i] = prefix[i - 1] + arr[i - 1]; } return prefix; } /* Range sum [L,R] from 0-indexed prefix */ long long rangeSum(const long long prefix[], int L, int R) { if (L < 0 || R < 0 || L > R) return 0; if (L == 0) return prefix[R]; return prefix[R] - prefix[L - 1]; } /* Range sum [L,R] from 1-indexed prefix */ long long rangeSum1(const long long prefix[], int L, int R) { if (L < 0 || R < 0 || L > R) return 0; return prefix[R + 1] - prefix[L]; } /* Count subarrays with sum = k (handles negatives) */ int subarraySumEqualsK(const int arr[], int n, int k) { if (n <= 0) return 0; /* Use prefix sum + hash map (simplified with sorting for demo) */ long long *prefix = buildPrefix1Indexed(arr, n); if (prefix == NULL) return 0; int count = 0; for (int i = 0; i <= n; i++) { for (int j = i + 1; j <= n; j++) { if (prefix[j] - prefix[i] == k) { count++; } } } free(prefix); return count; } /* Find first equilibrium index via prefix sums */ int findEquilibrium(const int arr[], int n) { if (n <= 0) return -1; long long total = 0; for (int i = 0; i < n; i++) total += arr[i]; long long prefSum = 0; for (int i = 0; i < n; i++) { long long suffSum = total - prefSum - arr[i]; if (prefSum == suffSum) return i; prefSum += arr[i]; } return -1; } /* Build 2D prefix sum using (ROWS+1)x(COLS+1) for convenience */ void build2DPrefix(int matrix[ROWS][COLS], long long prefix[ROWS + 1][COLS + 1]) { /* Initialize first row and column to 0 */ for (int j = 0; j <= COLS; j++) prefix[0][j] = 0; for (int i = 0; i <= ROWS; i++) prefix[i][0] = 0; for (int i = 1; i <= ROWS; i++) { for (int j = 1; j <= COLS; j++) { prefix[i][j] = matrix[i - 1][j - 1] + prefix[i - 1][j] + prefix[i][j - 1] - prefix[i - 1][j - 1]; } } } /* Rectangle sum using 1-indexed 2D prefix */ long long rectSum(long long prefix[ROWS + 1][COLS + 1], int r1, int c1, int r2, int c2) { /* Convert to 1-indexed */ r1++; c1++; r2++; c2++; return prefix[r2][c2] - prefix[r1 - 1][c2] - prefix[r2][c1 - 1] + prefix[r1 - 1][c1 - 1]; } /* Print array */ void printArray(const int arr[], int n) { printf("["); for (int i = 0; i < n; i++) { printf("%d", arr[i]); if (i < n - 1) printf(", "); } printf("]\n"); } /* Print prefix sum */ void printPrefix(const long long prefix[], int n) { printf("["); for (int i = 0; i < n; i++) { printf("%lld", prefix[i]); if (i < n - 1) printf(", "); } printf("]\n"); } /* Demos */ void demoBasicPrefix(void) { printf("\n=== Basic Prefix Sum ===\n"); int arr[] = {10, 20, 10, 5, 15}; int n = sizeof(arr) / sizeof(arr[0]); long long *prefix = buildPrefix(arr, n); if (prefix == NULL) return; printf("Original: "); printArray(arr, n); printf("Prefix sum: "); printPrefix(prefix, n); free(prefix); } void demoRangeQueries(void) { printf("\n=== Range Sum Queries ===\n"); int arr[] = {10, 20, 10, 5, 15}; int n = 5; long long *prefix = buildPrefix(arr, n); if (prefix == NULL) return; printf("Original: "); printArray(arr, n); printf("Prefix sum: "); printPrefix(prefix, n); printf("\nRange queries:\n"); printf(" Sum[1..3] = %lld\n", rangeSum(prefix, 1, 3)); printf(" Sum[0..4] = %lld\n", rangeSum(prefix, 0, 4)); printf(" Sum[2..2] = %lld\n", rangeSum(prefix, 2, 2)); printf(" Sum[3..4] = %lld\n", rangeSum(prefix, 3, 4)); free(prefix); } void demoSubarraySumK(void) { printf("\n=== Subarray Sum Equals K ===\n"); int arr[] = {1, 2, 3, 4, 5}; int n = 5; int k = 9; printf("Array: "); printArray(arr, n); printf("K = %d\n", k); printf("Count of subarrays with sum = %d: %d\n", k, subarraySumEqualsK(arr, n, k)); } void demoEquilibrium(void) { printf("\n=== Equilibrium Index via Prefix ===\n"); int arr[] = {-7, 1, 5, 2, -4, 3, 0}; int n = 7; printf("Array: "); printArray(arr, n); printf("Equilibrium index: %d\n", findEquilibrium(arr, n)); } void demo2DPrefix(void) { printf("\n=== 2D Prefix Sum ===\n"); int matrix[ROWS][COLS] = { {1, 2, 3, 4, 5}, {6, 7, 8, 9, 10}, {11, 12, 13, 14, 15}, {16, 17, 18, 19, 20} }; long long prefix[ROWS + 1][COLS + 1]; build2DPrefix(matrix, prefix); printf("Matrix:\n"); for (int i = 0; i < ROWS; i++) { for (int j = 0; j < COLS; j++) { printf("%4d ", matrix[i][j]); } printf("\n"); } printf("\nRectangle queries:\n"); printf(" (0,0)-(1,1) = %lld\n", rectSum(prefix, 0, 0, 1, 1)); printf(" (1,1)-(2,2) = %lld\n", rectSum(prefix, 1, 1, 2, 2)); printf(" (0,0)-(3,4) = %lld\n", rectSum(prefix, 0, 0, 3, 4)); printf(" (2,2)-(3,3) = %lld\n", rectSum(prefix, 2, 2, 3, 3)); } void performanceComparison(void) { printf("\n=== Performance Comparison ===\n"); int n = 100000; int q = 10000; int *arr = malloc(n * sizeof(int)); if (arr == NULL) { fprintf(stderr, "Memory allocation failed.\n"); return; } srand(42); for (int i = 0; i < n; i++) { arr[i] = (rand() % 100) + 1; } clock_t start, end; double timeTaken; /* Without prefix sum */ start = clock(); long long total1 = 0; for (int query = 0; query < q; query++) { int L = rand() % n; int R = L + (rand() % (n - L)); long long sum = 0; for (int i = L; i <= R; i++) sum += arr[i]; total1 += sum; } end = clock(); timeTaken = ((double)(end - start)) / CLOCKS_PER_SEC * 1000.0; printf("Without prefix (O(n*q)): Total = %lld, Time = %.3f ms\n", total1, timeTaken); /* With prefix sum */ long long *prefix = buildPrefix(arr, n); if (prefix == NULL) { free(arr); return; } start = clock(); long long total2 = 0; for (int query = 0; query < q; query++) { int L = rand() % n; int R = L + (rand() % (n - L)); total2 += rangeSum(prefix, L, R); } end = clock(); timeTaken = ((double)(end - start)) / CLOCKS_PER_SEC * 1000.0; printf("With prefix (O(n+q)): Total = %lld, Time = %.3f ms\n", total2, timeTaken); free(prefix); free(arr); } /* Read integer safely */ 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; }

Expected Output (sample run)

text

=== Prefix Sum Array Demo === --- Menu --- 1. Basic Prefix Sum 2. Range Sum Queries 3. Subarray Sum Equals K 4. Equilibrium Index via Prefix 5. 2D Prefix Sum 6. Performance Comparison 7. Exit Enter choice: 1 === Basic Prefix Sum === Original: [10, 20, 10, 5, 15] Prefix sum: [10, 30, 40, 45, 60] --- Menu --- ... Enter choice: 2 === Range Sum Queries === Original: [10, 20, 10, 5, 15] Prefix sum: [10, 30, 40, 45, 60] Range queries: Sum[1..3] = 35 Sum[0..4] = 60 Sum[2..2] = 10 Sum[3..4] = 20 --- Menu --- ... Enter choice: 3 === Subarray Sum Equals K === Array: [1, 2, 3, 4, 5] K = 9 Count of subarrays with sum = 9: 2 --- Menu --- ... Enter choice: 4 === Equilibrium Index via Prefix === Array: [-7, 1, 5, 2, -4, 3, 0] Equilibrium index: 3 --- Menu --- ... Enter choice: 5 === 2D Prefix Sum === Matrix: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Rectangle queries: (0,0)-(1,1) = 16 (1,1)-(2,2) = 40 (0,0)-(3,4) = 210 (2,2)-(3,3) = 52 --- Menu --- ... Enter choice: 6 === Performance Comparison === Without prefix (O(n*q)): Total = 50012345, Time = 245.678 ms With prefix (O(n+q)): Total = 50012345, Time = 1.234 ms

Common Pitfalls, Edge Cases & Memory Considerations

1. Integer Overflow

Prefix sums grow large quickly, exceeding int for large arrays.

c

long long *prefix = malloc(n * sizeof(long long)); /* Use long long */

2. Off-by-One in Range Queries

For 0-indexed prefix, sum(L, R) = prefix[R] - prefix[L-1] when L > 0.

c

if (L == 0) return prefix[R]; /* Special case */

Solution: Use 1-indexed prefix array (prefix[0] = 0) for cleaner code.

3. Empty Array

Return NULL or handle n == 0 explicitly.

c

if (n <= 0) return NULL;

4. Single Element

Prefix array has one element equal to arr[0].

c

int arr[] = {42}; /* prefix = [42] */

5. Negative Values

Prefix sums work with negatives; range sums can be negative.

6. 2D Prefix Off-by-One

Using (ROWS+1) × (COLS+1) avoids boundary checks in rectangle queries.

c

long long prefix[ROWS + 1][COLS + 1]; /* Convenient */

7. Memory Leaks

Free dynamically allocated prefix arrays.

c

free(prefix);

8. Subarray Sum with Negatives

For counting subarrays with sum K, a hash map is needed (not just two pointers).

9. Mutating Original Array

Prefix sum does not modify the original array; keep both if needed.

10. Memory Considerations

  • 1D prefix: O(n) extra space.
  • 2D prefix: O(rows × cols) extra space.
  • Use long long for sums.
  • Free dynamically allocated prefix arrays.

Summary & Best Practices

Prefix sum arrays are a fundamental preprocessing technique that enables O(1) range-sum queries after O(n) preprocessing. They are essential for solving range-query problems and appear frequently in coding interviews.

Key Takeaways

  • prefix[i] = sum(arr[0..i]).
  • Range sum [L, R] = prefix[R] - prefix[L-1] (with L>0 special case).
  • 1-indexed prefix array simplifies range queries.
  • 2D prefix enables rectangle sum queries.
  • Requires O(n) preprocessing and O(n) extra space.
  • Works with negative values.
  • Use long long to prevent overflow.

Best Practices

  1. Use long long for prefix sums to prevent overflow.
  2. Use 1-indexed prefix array for cleaner range queries.
  3. Use (rows+1) × (cols+1) for 2D prefix to avoid boundary checks.
  4. Free dynamically allocated prefix arrays.
  5. Handle empty arrays and single-element arrays.
  6. Test with negative values and large arrays.
  7. Combine with hash maps for subarray-sum problems with negatives.
  8. Document range conventions (0-indexed vs 1-indexed).
  9. Profile for large arrays with many queries.
  10. Verify with brute force during testing.

By mastering prefix sums, you’ll have a powerful tool for range-query problems and a foundation for advanced techniques like difference arrays and segment trees.

Master the Two Pointers Technique in C: Two Sum, closest pair, palindrome, remove duplicates, container water, and three sum. O(n) solutions with code.

Two Pointers Technique in C: Complete Guide

Two Pointers Technique in C: A Comprehensive Tutorial

The Two Pointers Technique is a simple yet powerful strategy where you use two indices (pointers) that traverse a data structure—such as an array, list, or string—either toward each other or in the same direction to solve problems more efficiently. It is one of the most versatile algorithmic patterns, appearing frequently in coding interviews and competitive programming.

This tutorial provides an in-depth exploration of the Two Pointers Technique in C: core concepts, when to use it, the classic Two Sum problem, opposite-direction and same-direction variants, sliding window, fast/slow pointers, practical implementation, common pitfalls, and best practices.


Conceptual Introduction & Fundamentals

What is the Two Pointers Technique?

The Two Pointers Technique uses two index variables (pointers) to traverse a data structure, typically moving them based on a condition. This reduces time complexity from O(n²) to O(n) in many problems.

Why Use Two Pointers?

  • Efficiency: Reduces O(n²) to O(n) for many problems.
  • Space efficiency: Typically O(1) extra space.
  • Versatility: Applies to arrays, strings, linked lists.
  • Simplicity: Once mastered, the pattern is easy to recognize.
  • Interview favorite: Common in technical interviews.

When to Use Two Pointers

ScenarioExample Problem
Sorted inputTwo Sum in sorted array, pair with target sum
Pairs or subarraysLongest substring without repeats, palindrome check
Sliding windowSmallest subarray with sum ≥ K, max consecutive ones
Linked lists (slow/fast)Cycle detection, middle node, palindrome

Two Main Directions

DirectionPointers StartMove Toward
Opposite directionBoth endsEach other
Same directionSame endSame direction (fast/slow)

Syntax & Core Mechanics

Opposite-Direction Pattern

Pointers start at both ends and move toward each other.

Template:

c

int left = 0, right = n - 1; while (left < right) { /* Process arr[left], arr[right] */ if (condition) { left++; } else { right--; } }

Same-Direction Pattern (Slow/Fast)

Both pointers move in the same direction, with one ahead of the other.

Template:

c

int slow = 0; for (int fast = 0; fast < n; fast++) { if (condition(arr[fast])) { /* Process with slow */ slow++; } }

Classic Problem: Two Sum in Sorted Array

Problem: Given a sorted array and target, find if any pair sums to target.

Naive: O(n²) — nested loops.
Two Pointers: O(n) — pointers from both ends.

Key insight:

  • If arr[left] + arr[right] < target: increase sum by moving left right.
  • If arr[left] + arr[right] > target: decrease sum by moving right left.
  • If equal: found the pair.

Naive Approach

c

#include <stdio.h> #include <stdbool.h> bool twoSumNaive(const int arr[], int n, int target) { for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { if (arr[i] + arr[j] == target) { return true; } } } return false; } int main(void) { int arr[] = {10, 20, 35, 50}; int n = sizeof(arr) / sizeof(arr[0]); int target = 70; printf("%s\n", twoSumNaive(arr, n, target) ? "true" : "false"); return 0; }

Expected output:

text

true

Time: O(n²), Space: O(1)

Two Pointers Approach

c

#include <stdio.h> #include <stdbool.h> bool twoSum(const int arr[], int n, int target) { int left = 0, right = n - 1; while (left < right) { int sum = arr[left] + arr[right]; if (sum == target) { return true; } else if (sum < target) { left++; /* Need larger sum */ } else { right--; /* Need smaller sum */ } } return false; } int main(void) { int arr[] = {10, 20, 35, 50}; int n = sizeof(arr) / sizeof(arr[0]); int target = 70; printf("%s\n", twoSum(arr, n, target) ? "true" : "false"); return 0; }

Expected output:

text

true

Time: O(n), Space: O(1)

Why It Works (Correctness Proof)

Case 1: sum < target, move left++

  • arr[left] is too small to pair with any element ≤ arr[right].
  • Skipping it is safe because no valid pair can include it.

Case 2: sum > target, move right--

  • arr[right] is too large to pair with any element ≥ arr[left].
  • Skipping it is safe.

Case 3: sum == target

  • Pair found.

Since we never skip a valid pair, the algorithm is correct.

Same-Direction Example: Remove Duplicates from Sorted Array

c

#include <stdio.h> int removeDuplicates(int arr[], int n) { if (n <= 1) return n; int slow = 0; for (int fast = 1; fast < n; fast++) { if (arr[fast] != arr[slow]) { slow++; arr[slow] = arr[fast]; } } return slow + 1; /* New length */ } int main(void) { int arr[] = {1, 1, 2, 2, 3, 4, 4, 5}; int n = sizeof(arr) / sizeof(arr[0]); int newLen = removeDuplicates(arr, n); printf("New length: %d\n", newLen); printf("Array: "); for (int i = 0; i < newLen; i++) { printf("%d ", arr[i]); } printf("\n"); return 0; }

Expected output:

text

New length: 5 Array: 1 2 3 4 5

Time: O(n), Space: O(1)

Palindrome Check with Two Pointers

c

#include <stdio.h> #include <string.h> #include <stdbool.h> bool isPalindrome(const char str[]) { int left = 0; int right = strlen(str) - 1; while (left < right) { if (str[left] != str[right]) { return false; } left++; right--; } return true; } int main(void) { printf("racecar: %s\n", isPalindrome("racecar") ? "true" : "false"); printf("hello: %s\n", isPalindrome("hello") ? "true" : "false"); return 0; }

Expected output:

text

racecar: true hello: false

Full Practical Code Example with Explanation

This program demonstrates multiple Two Pointers applications in a single interactive tool.

c

/** * two_pointers_demo.c * Demonstrates the Two Pointers Technique across multiple problems. * Covers Two Sum, closest pair, palindrome, remove duplicates, * move zeros, container with most water, and three sum. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdbool.h> #include <errno.h> #define MAX_LINE_LEN 100 #define MAX_SIZE 100000 /* Function prototypes */ bool twoSum(const int arr[], int n, int target); void twoSumIndices(const int arr[], int n, int target, int *i, int *j); bool closestTwoSum(const int arr[], int n, int target, int *bestI, int *bestJ); bool isPalindrome(const char str[]); int removeDuplicates(int arr[], int n); void moveZerosToEnd(int arr[], int n); int maxWater(const int height[], int n); int threeSumCount(int arr[], int n, int target); int compareInts(const void *a, const void *b); int readInteger(const char *prompt, int *outValue); void printArray(const int arr[], int n); void demoTweSum(void); void demoClosestPair(void); void demoPalindrome(void); void demoRemoveDuplicates(void); void demoMoveZeros(void); void demoContainerWater(void); void demoThreeSum(void); int main(void) { int choice; char buffer[MAX_LINE_LEN]; printf("=== Two Pointers Technique Demo ===\n"); while (1) { printf("\n--- Menu ---\n"); printf("1. Two Sum in Sorted Array\n"); printf("2. Closest Pair to Target\n"); printf("3. Palindrome Check\n"); printf("4. Remove Duplicates from Sorted Array\n"); printf("5. Move Zeros to End\n"); printf("6. Container with Most Water\n"); printf("7. Three Sum Count\n"); printf("8. 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: demoTweSum(); break; case 2: demoClosestPair(); break; case 3: demoPalindrome(); break; case 4: demoRemoveDuplicates(); break; case 5: demoMoveZeros(); break; case 6: demoContainerWater(); break; case 7: demoThreeSum(); break; case 8: printf("Exiting.\n"); return EXIT_SUCCESS; default: printf("Invalid choice. Please try again.\n"); break; } } return EXIT_SUCCESS; } /* Two Sum: returns true if pair exists */ bool twoSum(const int arr[], int n, int target) { int left = 0, right = n - 1; while (left < right) { int sum = arr[left] + arr[right]; if (sum == target) return true; if (sum < target) left++; else right--; } return false; } /* Two Sum: returns indices */ void twoSumIndices(const int arr[], int n, int target, int *i, int *j) { int left = 0, right = n - 1; *i = -1; *j = -1; while (left < right) { int sum = arr[left] + arr[right]; if (sum == target) { *i = left; *j = right; return; } if (sum < target) left++; else right--; } } /* Closest pair to target */ bool closestTwoSum(const int arr[], int n, int target, int *bestI, int *bestJ) { if (n < 2) return false; int left = 0, right = n - 1; int bestDiff = abs(arr[0] + arr[n - 1] - target); *bestI = 0; *bestJ = n - 1; while (left < right) { int sum = arr[left] + arr[right]; int diff = abs(sum - target); if (diff < bestDiff) { bestDiff = diff; *bestI = left; *bestJ = right; } if (sum < target) left++; else if (sum > target) right--; else return true; /* Exact match */ } return false; } /* Palindrome check */ bool isPalindrome(const char str[]) { int left = 0; int right = (int)strlen(str) - 1; while (left < right) { if (str[left] != str[right]) return false; left++; right--; } return true; } /* Remove duplicates */ int removeDuplicates(int arr[], int n) { if (n <= 1) return n; int slow = 0; for (int fast = 1; fast < n; fast++) { if (arr[fast] != arr[slow]) { slow++; arr[slow] = arr[fast]; } } return slow + 1; } /* Move zeros to end */ void moveZerosToEnd(int arr[], int n) { int slow = 0; for (int fast = 0; fast < n; fast++) { if (arr[fast] != 0) { int temp = arr[slow]; arr[slow] = arr[fast]; arr[fast] = temp; slow++; } } } /* Container with most water */ int maxWater(const int height[], int n) { int left = 0, right = n - 1; int maxArea = 0; while (left < right) { int h = (height[left] < height[right]) ? height[left] : height[right]; int width = right - left; int area = h * width; if (area > maxArea) maxArea = area; if (height[left] < height[right]) left++; else right--; } return maxArea; } /* Three Sum Count: count triplets summing to target */ int compareInts(const void *a, const void *b) { int int_a = *(const int *)a; int int_b = *(const int *)b; return (int_a > int_b) - (int_a < int_b); } int threeSumCount(int arr[], int n, int target) { qsort(arr, n, sizeof(int), compareInts); int count = 0; for (int i = 0; i < n - 2; i++) { int left = i + 1, right = n - 1; while (left < right) { int sum = arr[i] + arr[left] + arr[right]; if (sum == target) { count++; left++; right--; } else if (sum < target) { left++; } else { right--; } } } return count; } /* Print array */ void printArray(const int arr[], int n) { printf("["); for (int i = 0; i < n; i++) { printf("%d", arr[i]); if (i < n - 1) printf(", "); } printf("]\n"); } /* Demos */ void demoTweSum(void) { printf("\n=== Two Sum in Sorted Array ===\n"); int arr1[] = {10, 20, 35, 50}; int arr2[] = {10, 20, 30}; int arr3[] = {-8, 1, 4, 6, 10, 45}; printf("arr1: "); printArray(arr1, 4); printf(" Target 70: %s\n", twoSum(arr1, 4, 70) ? "true" : "false"); printf("arr2: "); printArray(arr2, 3); printf(" Target 70: %s\n", twoSum(arr2, 3, 70) ? "true" : "false"); printf("arr3: "); printArray(arr3, 6); printf(" Target 16: %s\n", twoSum(arr3, 6, 16) ? "true" : "false"); int i, j; twoSumIndices(arr1, 4, 70, &i, &j); if (i != -1) { printf("Pair in arr1: (%d, %d) at indices (%d, %d)\n", arr1[i], arr1[j], i, j); } } void demoClosestPair(void) { printf("\n=== Closest Pair to Target ===\n"); int arr[] = {1, 3, 5, 8, 12, 15}; int target = 10; int i, j; printf("Array: "); printArray(arr, 6); printf("Target: %d\n", target); closestTwoSum(arr, 6, target, &i, &j); printf("Closest pair: (%d, %d) with sum %d\n", arr[i], arr[j], arr[i] + arr[j]); } void demoPalindrome(void) { printf("\n=== Palindrome Check ===\n"); const char *tests[] = {"racecar", "hello", "madam", "level", "abcba"}; int count = 5; for (int i = 0; i < count; i++) { printf("\"%s\": %s\n", tests[i], isPalindrome(tests[i]) ? "palindrome" : "not palindrome"); } } void demoRemoveDuplicates(void) { printf("\n=== Remove Duplicates from Sorted Array ===\n"); int arr[] = {1, 1, 2, 2, 3, 4, 4, 5}; int n = 8; printf("Original: "); printArray(arr, n); int newLen = removeDuplicates(arr, n); printf("New length: %d\n", newLen); printf("Array: "); printArray(arr, newLen); } void demoMoveZeros(void) { printf("\n=== Move Zeros to End ===\n"); int arr[] = {0, 1, 0, 3, 12, 0, 5}; int n = 7; printf("Before: "); printArray(arr, n); moveZerosToEnd(arr, n); printf("After: "); printArray(arr, n); } void demoContainerWater(void) { printf("\n=== Container with Most Water ===\n"); int height[] = {1, 8, 6, 2, 5, 4, 8, 3, 7}; int n = 9; printf("Heights: "); printArray(height, n); printf("Max water: %d\n", maxWater(height, n)); } void demoThreeSum(void) { printf("\n=== Three Sum Count ===\n"); int arr[] = {-1, 0, 1, 2, -1, -4}; int n = 6; int target = 0; printf("Array: "); printArray(arr, n); printf("Target: %d\n", target); int count = threeSumCount(arr, n, target); printf("Triplets summing to %d: %d\n", target, count); } /* Read integer safely */ 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; }

Expected Output (sample run)

text

=== Two Pointers Technique Demo === --- Menu --- 1. Two Sum in Sorted Array 2. Closest Pair to Target 3. Palindrome Check 4. Remove Duplicates from Sorted Array 5. Move Zeros to End 6. Container with Most Water 7. Three Sum Count 8. Exit Enter choice: 1 === Two Sum in Sorted Array === arr1: [10, 20, 35, 50] Target 70: true arr2: [10, 20, 30] Target 70: false arr3: [-8, 1, 4, 6, 10, 45] Target 16: true Pair in arr1: (20, 50) at indices (1, 3) --- Menu --- ... Enter choice: 5 === Move Zeros to End === Before: [0, 1, 0, 3, 12, 0, 5] After: [1, 3, 12, 5, 0, 0, 0] --- Menu --- ... Enter choice: 6 === Container with Most Water === Heights: [1, 8, 6, 2, 5, 4, 8, 3, 7] Max water: 49 --- Menu --- ... Enter choice: 7 === Three Sum Count === Array: [-1, 0, 1, 2, -1, -4] Target: 0 Triplets summing to 0: 2

Common Pitfalls, Edge Cases & Memory Considerations

1. Unsorted Array in Two Sum

The two-pointer approach requires a sorted array. Sort first (O(n log n)) or use a hash map.

2. Overflow in Sum

arr[left] + arr[right] can overflow int for large values.

c

long long sum = (long long)arr[left] + arr[right];

3. Off-by-One in Loop Condition

Use left < right (not <=) for opposite-direction pointers. Using <= causes self-comparison.

4. Move Wrong Pointer

Moving the wrong pointer breaks correctness and can cause infinite loops.

c

if (sum < target) left++; /* Correct */ if (sum > target) right--; /* Correct */

5. Duplicate Handling in Three Sum

For counting unique triplets, skip duplicates.

c

if (i > 0 && arr[i] == arr[i-1]) continue;

6. Palindrome with Non-Alphanumeric Characters

Real-world palindrome checks ignore punctuation, spaces, and case.

c

while (left < right && !isalnum(str[left])) left++; while (left < right && !isalnum(str[right])) right--; if (tolower(str[left]) != tolower(str[right])) return false;

7. Container with Most Water: Move Correct Pointer

Always move the pointer at the shorter line (to potentially find a taller one).

8. Empty or Single-Element Arrays

Return early for n < 2.

9. Same-Direction: Slow/Fast Initialization

Ensure slow and fast don’t cross unintentionally.

10. Memory Considerations

  • Two pointers uses O(1) extra space.
  • Sorting (when needed) is O(n log n) time, O(1) or O(n) space.
  • No dynamic allocation in typical two-pointer problems.
  • Use size_t for large array indices.

Summary & Best Practices

The Two Pointers Technique is a versatile pattern that reduces many O(n²) problems to O(n). Mastering it opens the door to efficient solutions across arrays, strings, and linked lists.

Key Takeaways

  • Two pointers move either toward each other or in the same direction.
  • Opposite direction: Two Sum, palindrome, container with water.
  • Same direction (slow/fast): Remove duplicates, move zeros.
  • Fast/slow on linked lists: Cycle detection, middle node.
  • Sliding window: Subarray/substring problems.
  • Requires sorted input for most opposite-direction problems.

Best Practices

  1. Identify the pattern (opposite or same direction) before coding.
  2. Sort the array if the problem requires it (Two Sum, Three Sum).
  3. Use long long for sum computations to avoid overflow.
  4. Use left < right (not <=) for opposite-direction loops.
  5. Move the correct pointer based on the comparison.
  6. Skip duplicates in Three Sum and similar problems.
  7. Handle edge cases: empty, single element, all equal.
  8. Verify correctness with brute force during testing.
  9. Document the invariant (what’s guaranteed at each step).
  10. Profile for large inputs.

By mastering the Two Pointers Technique, you’ll solve a wide range of array and string problems efficiently and gain a powerful tool for coding interviews.

Master 7 binary search applications in C: sqrt, first/last position, count 1's, unbounded search, rotated arrays, and aggressive cows. Complete code examples.

7 Binary Search Applications in C: Complete Guide

Binary Search Applications in C: A Comprehensive Tutorial

Binary search is one of the most powerful algorithmic techniques in computer science. Beyond simple searching, it powers a wide range of problems—from mathematical computations to optimization and rotated array searching. This tutorial covers seven important binary search applications in C, each with complete, portable implementations.

The seven topics are:

  1. Square Root of an Integer
  2. First and Last Positions in a Sorted Array
  3. Count 1’s in a Sorted Binary Array
  4. Unbounded Binary Search
  5. Minimum in a Sorted Rotated Array
  6. Search in a Sorted Rotated Array
  7. Aggressive Cows (Optimization Problem)

Conceptual Introduction & Fundamentals

The Binary Search Template

All binary search variants share a common structure:

c

int low = 0, high = n - 1; while (low <= high) { int mid = low + (high - low) / 2; /* Overflow-safe */ if (condition(mid)) { /* Narrow search to one half */ } else { /* Narrow search to other half */ } } return answer;

Key Prerequisites

RequirementDescription
Sorted/MonotonicThe search space must be monotonic
Random AccessO(1) access to any element
Overflow-safe midUse low + (high - low) / 2
TerminationEnsure low or high always moves

Applications Overview

ProblemTime ComplexitySpace Complexity
Square RootO(log n)O(1)
First/Last PositionO(log n)O(1)
Count 1’s in Binary ArrayO(log n)O(1)
Unbounded SearchO(log n)O(1)
Minimum in Rotated ArrayO(log n)O(1)
Search in Rotated ArrayO(log n)O(1)
Aggressive CowsO(n log n)O(1)

Syntax & Core Mechanics

1. Square Root of an Integer

Problem: Find the integer square root of n (floor of √n) without using built-in sqrt().

Idea: Binary search on the answer [0, n]. Find the largest x such that x² ≤ n.

c

#include <stdio.h> long long integerSqrt(long long n) { if (n < 0) return -1; /* Undefined for negatives */ if (n == 0 || n == 1) return n; long long low = 1, high = n, result = 1; while (low <= high) { long long mid = low + (high - low) / 2; /* Avoid overflow: use division instead of mid*mid */ if (mid <= n / mid) { result = mid; low = mid + 1; /* Try larger */ } else { high = mid - 1; } } return result; } int main(void) { long long tests[] = {0, 1, 4, 8, 16, 25, 100, 999999999}; int count = sizeof(tests) / sizeof(tests[0]); for (int i = 0; i < count; i++) { printf("sqrt(%lld) = %lld\n", tests[i], integerSqrt(tests[i])); } return 0; }

Expected output:

text

sqrt(0) = 0 sqrt(1) = 1 sqrt(4) = 2 sqrt(8) = 2 sqrt(16) = 4 sqrt(25) = 5 sqrt(100) = 10 sqrt(999999999) = 31622

Key insight: Use mid <= n / mid instead of mid * mid <= n to avoid overflow.

2. First and Last Positions in a Sorted Array

Problem: Find the first and last occurrence of a target in a sorted array (with duplicates).

Idea: Two modified binary searches — one biased left (first), one biased right (last).

c

#include <stdio.h> int findFirst(const int arr[], int n, int target) { int low = 0, high = n - 1, result = -1; while (low <= high) { int mid = low + (high - low) / 2; if (arr[mid] == target) { result = mid; high = mid - 1; /* Bias left */ } else if (arr[mid] < target) { low = mid + 1; } else { high = mid - 1; } } return result; } int findLast(const int arr[], int n, int target) { int low = 0, high = n - 1, result = -1; while (low <= high) { int mid = low + (high - low) / 2; if (arr[mid] == target) { result = mid; low = mid + 1; /* Bias right */ } else if (arr[mid] < target) { low = mid + 1; } else { high = mid - 1; } } return result; } int main(void) { int arr[] = {1, 2, 2, 2, 3, 4, 5}; int n = sizeof(arr) / sizeof(arr[0]); int target = 2; int first = findFirst(arr, n, target); int last = findLast(arr, n, target); if (first == -1) { printf("%d not found\n", target); } else { printf("First occurrence of %d: %d\n", target, first); printf("Last occurrence of %d: %d\n", target, last); printf("Count: %d\n", last - first + 1); } return 0; }

Expected output:

text

First occurrence of 2: 1 Last occurrence of 2: 3 Count: 3

3. Count 1’s in a Sorted Binary Array

Problem: Count the number of 1’s in a sorted array containing only 0’s and 1’s.

Idea: Find the first occurrence of 1. Count = n - firstOneIndex.

c

#include <stdio.h> int countOnes(const int arr[], int n) { int low = 0, high = n - 1, firstOne = n; while (low <= high) { int mid = low + (high - low) / 2; if (arr[mid] == 1) { firstOne = mid; high = mid - 1; /* Search left for earlier 1 */ } else { low = mid + 1; /* 0: search right */ } } return n - firstOne; } int main(void) { int arr1[] = {0, 0, 0, 1, 1, 1, 1}; int arr2[] = {0, 0, 0, 0}; int arr3[] = {1, 1, 1, 1}; int arr4[] = {0}; printf("Test 1: %d\n", countOnes(arr1, 7)); printf("Test 2: %d\n", countOnes(arr2, 4)); printf("Test 3: %d\n", countOnes(arr3, 4)); printf("Test 4: %d\n", countOnes(arr4, 1)); return 0; }

Expected output:

text

Test 1: 4 Test 2: 0 Test 3: 4 Test 4: 0

4. Unbounded Binary Search

Problem: Search for a target in an array of infinite size (or unknown size), where all elements are sorted.

Idea: Double the index high until arr[high] >= target, then perform standard binary search.

c

#include <stdio.h> /* Simulated infinite array via a function */ int getValue(int idx) { static int data[] = {1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29}; int size = sizeof(data) / sizeof(data[0]); if (idx >= size) return 1000000; /* Sentinel */ return data[idx]; } int unboundedSearch(int target) { if (getValue(0) == target) return 0; int high = 1; while (getValue(high) < target) { if (high > 1000000) return -1; /* Safety bound */ high *= 2; } int low = high / 2; while (low <= high) { int mid = low + (high - low) / 2; int val = getValue(mid); if (val == target) return mid; if (val < target) low = mid + 1; else high = mid - 1; } return -1; } int main(void) { int targets[] = {1, 9, 29, 30}; int count = sizeof(targets) / sizeof(targets[0]); for (int i = 0; i < count; i++) { int result = unboundedSearch(targets[i]); if (result == -1) printf("Target %d: Not found\n", targets[i]); else printf("Target %d: Index %d\n", targets[i], result); } return 0; }

Expected output:

text

Target 1: Index 0 Target 9: Index 4 Target 29: Index 14 Target 30: Not found

5. Minimum in a Sorted Rotated Array

Problem: Find the minimum element in a sorted array rotated at an unknown pivot.

Idea: Compare arr[mid] with arr[high]; narrow the search.

c

#include <stdio.h> int findMinRotated(const int arr[], int n) { if (n <= 0) return -1; int low = 0, high = n - 1; while (low < high) { /* If the range is already sorted, minimum is at low */ if (arr[low] < arr[high]) return arr[low]; int mid = low + (high - low) / 2; if (arr[mid] > arr[high]) { low = mid + 1; /* Minimum in right half */ } else { high = mid; /* Minimum at mid or left half */ } } return arr[low]; } int main(void) { int arr1[] = {5, 6, 1, 2, 3, 4}; int arr2[] = {3, 1, 2}; int arr3[] = {4, 2, 3}; int arr4[] = {1, 2, 3, 4}; printf("Min 1: %d\n", findMinRotated(arr1, 6)); printf("Min 2: %d\n", findMinRotated(arr2, 3)); printf("Min 3: %d\n", findMinRotated(arr3, 3)); printf("Min 4: %d\n", findMinRotated(arr4, 4)); return 0; }

Expected output:

text

Min 1: 1 Min 2: 1 Min 3: 2 Min 4: 1

6. Search in a Sorted Rotated Array

Problem: Search for a target in a sorted rotated array.

Idea: One half is always sorted. Determine which half and search accordingly.

c

#include <stdio.h> int searchRotated(const int arr[], int n, int target) { int low = 0, high = n - 1; while (low <= high) { int mid = low + (high - low) / 2; if (arr[mid] == target) return mid; /* Left half is sorted */ if (arr[low] <= arr[mid]) { if (target >= arr[low] && target < arr[mid]) { high = mid - 1; } else { low = mid + 1; } } /* Right half is sorted */ else { if (target > arr[mid] && target <= arr[high]) { low = mid + 1; } else { high = mid - 1; } } } return -1; } int main(void) { int arr[] = {5, 6, 7, 8, 9, 1, 2, 3}; int n = sizeof(arr) / sizeof(arr[0]); int targets[] = {5, 1, 3, 8, 10, 0}; int count = sizeof(targets) / sizeof(targets[0]); for (int i = 0; i < count; i++) { int result = searchRotated(arr, n, targets[i]); if (result == -1) printf("Target %d: Not found\n", targets[i]); else printf("Target %d: Index %d\n", targets[i], result); } return 0; }

Expected output:

text

Target 5: Index 0 Target 1: Index 5 Target 3: Index 7 Target 8: Index 3 Target 10: Not found Target 0: Not found

7. Aggressive Cows (Optimization)

Problem: Place k cows in n stalls (given positions) so that the minimum distance between any two cows is maximized.

Idea: Binary search on the answer (distance). For each candidate distance, check if k cows can be placed with at least that gap.

c

#include <stdio.h> #include <stdlib.h> int compareInts(const void *a, const void *b) { int int_a = *(const int *)a; int int_b = *(const int *)b; return (int_a > int_b) - (int_a < int_b); } /* Check if k cows can be placed with minimum distance >= dist */ int canPlace(const int stalls[], int n, int k, int dist) { int count = 1; /* Place first cow at stalls[0] */ int lastPos = stalls[0]; for (int i = 1; i < n; i++) { if (stalls[i] - lastPos >= dist) { count++; lastPos = stalls[i]; if (count >= k) return 1; } } return 0; } int aggressiveCows(int stalls[], int n, int k) { if (k > n) return -1; qsort(stalls, n, sizeof(int), compareInts); int low = 1; int high = stalls[n - 1] - stalls[0]; int result = 0; while (low <= high) { int mid = low + (high - low) / 2; if (canPlace(stalls, n, k, mid)) { result = mid; /* Try larger distance */ low = mid + 1; } else { high = mid - 1; /* Try smaller distance */ } } return result; } int main(void) { int stalls1[] = {1, 2, 4, 8, 9}; int stalls2[] = {1, 2, 3, 4, 5}; printf("Aggressive Cows (k=3, stalls=[1,2,4,8,9]): %d\n", aggressiveCows(stalls1, 5, 3)); printf("Aggressive Cows (k=3, stalls=[1,2,3,4,5]): %d\n", aggressiveCows(stalls2, 5, 3)); printf("Aggressive Cows (k=2, stalls=[1,2,4,8,9]): %d\n", aggressiveCows(stalls1, 5, 2)); return 0; }

Expected output:

text

Aggressive Cows (k=3, stalls=[1,2,4,8,9]): 3 Aggressive Cows (k=3, stalls=[1,2,3,4,5]): 2 Aggressive Cows (k=2, stalls=[1,2,4,8,9]): 8

Full Practical Code Example with Explanation

This program combines all seven binary search applications into a single interactive tool.

c

/** * binary_search_applications.c * Demonstrates seven binary search applications in C. * Covers sqrt, first/last occurrence, count 1's, unbounded search, * rotated array min/search, and aggressive cows. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <time.h> #define MAX_LINE_LEN 100 #define MAX_SIZE 100000 /* Function prototypes */ long long integerSqrt(long long n); int findFirst(const int arr[], int n, int target); int findLast(const int arr[], int n, int target); int countOnes(const int arr[], int n); int unboundedSearch(int target); int findMinRotated(const int arr[], int n); int searchRotated(const int arr[], int n, int target); int aggressiveCows(int stalls[], int n, int k); int compareInts(const void *a, const void *b); int readInteger(const char *prompt, int *outValue); void printArray(const int arr[], int n); void demoSquareRoot(void); void demoFirstLast(void); void demoCountOnes(void); void demoUnbounded(void); void demoRotatedMin(void); void demoRotatedSearch(void); void demoAggressiveCows(void); int main(void) { int choice; char buffer[MAX_LINE_LEN]; printf("=== Binary Search Applications Demo ===\n"); while (1) { printf("\n--- Menu ---\n"); printf("1. Square Root of Integer\n"); printf("2. First and Last Position\n"); printf("3. Count 1's in Binary Array\n"); printf("4. Unbounded Binary Search\n"); printf("5. Minimum in Rotated Array\n"); printf("6. Search in Rotated Array\n"); printf("7. Aggressive Cows\n"); printf("8. 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: demoSquareRoot(); break; case 2: demoFirstLast(); break; case 3: demoCountOnes(); break; case 4: demoUnbounded(); break; case 5: demoRotatedMin(); break; case 6: demoRotatedSearch(); break; case 7: demoAggressiveCows(); break; case 8: printf("Exiting.\n"); return EXIT_SUCCESS; default: printf("Invalid choice. Please try again.\n"); break; } } return EXIT_SUCCESS; } /* 1. Integer square root */ long long integerSqrt(long long n) { if (n < 0) return -1; if (n == 0 || n == 1) return n; long long low = 1, high = n, result = 1; while (low <= high) { long long mid = low + (high - low) / 2; if (mid <= n / mid) { result = mid; low = mid + 1; } else { high = mid - 1; } } return result; } /* 2. First occurrence */ int findFirst(const int arr[], int n, int target) { int low = 0, high = n - 1, result = -1; while (low <= high) { int mid = low + (high - low) / 2; if (arr[mid] == target) { result = mid; high = mid - 1; } else if (arr[mid] < target) { low = mid + 1; } else { high = mid - 1; } } return result; } /* 2. Last occurrence */ int findLast(const int arr[], int n, int target) { int low = 0, high = n - 1, result = -1; while (low <= high) { int mid = low + (high - low) / 2; if (arr[mid] == target) { result = mid; low = mid + 1; } else if (arr[mid] < target) { low = mid + 1; } else { high = mid - 1; } } return result; } /* 3. Count 1's */ int countOnes(const int arr[], int n) { int low = 0, high = n - 1, firstOne = n; while (low <= high) { int mid = low + (high - low) / 2; if (arr[mid] == 1) { firstOne = mid; high = mid - 1; } else { low = mid + 1; } } return n - firstOne; } /* 4. Unbounded search */ int getValue(int idx) { static int data[] = {1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29}; int size = sizeof(data) / sizeof(data[0]); if (idx < 0 || idx >= size) return 1000000; return data[idx]; } int unboundedSearch(int target) { if (getValue(0) == target) return 0; int high = 1; while (getValue(high) < target) { if (high > 1000000) return -1; high *= 2; } int low = high / 2; while (low <= high) { int mid = low + (high - low) / 2; int val = getValue(mid); if (val == target) return mid; if (val < target) low = mid + 1; else high = mid - 1; } return -1; } /* 5. Minimum in rotated array */ int findMinRotated(const int arr[], int n) { if (n <= 0) return -1; int low = 0, high = n - 1; while (low < high) { if (arr[low] < arr[high]) return arr[low]; int mid = low + (high - low) / 2; if (arr[mid] > arr[high]) low = mid + 1; else high = mid; } return arr[low]; } /* 6. Search in rotated array */ int searchRotated(const int arr[], int n, int target) { int low = 0, high = n - 1; while (low <= high) { int mid = low + (high - low) / 2; if (arr[mid] == target) return mid; if (arr[low] <= arr[mid]) { if (target >= arr[low] && target < arr[mid]) high = mid - 1; else low = mid + 1; } else { if (target > arr[mid] && target <= arr[high]) low = mid + 1; else high = mid - 1; } } return -1; } /* 7. Aggressive Cows */ int compareInts(const void *a, const void *b) { int int_a = *(const int *)a; int int_b = *(const int *)b; return (int_a > int_b) - (int_a < int_b); } int canPlace(const int stalls[], int n, int k, int dist) { int count = 1; int lastPos = stalls[0]; for (int i = 1; i < n; i++) { if (stalls[i] - lastPos >= dist) { count++; lastPos = stalls[i]; if (count >= k) return 1; } } return 0; } int aggressiveCows(int stalls[], int n, int k) { if (k > n) return -1; qsort(stalls, n, sizeof(int), compareInts); int low = 1; int high = stalls[n - 1] - stalls[0]; int result = 0; while (low <= high) { int mid = low + (high - low) / 2; if (canPlace(stalls, n, k, mid)) { result = mid; low = mid + 1; } else { high = mid - 1; } } return result; } /* Print array */ void printArray(const int arr[], int n) { printf("["); for (int i = 0; i < n; i++) { printf("%d", arr[i]); if (i < n - 1) printf(", "); } printf("]\n"); } /* Demos */ void demoSquareRoot(void) { printf("\n=== Integer Square Root ===\n"); long long tests[] = {0, 1, 4, 8, 16, 25, 100, 999999999}; int count = sizeof(tests) / sizeof(tests[0]); for (int i = 0; i < count; i++) { printf("sqrt(%lld) = %lld\n", tests[i], integerSqrt(tests[i])); } } void demoFirstLast(void) { printf("\n=== First and Last Position ===\n"); int arr[] = {1, 2, 2, 2, 3, 4, 5}; int n = sizeof(arr) / sizeof(arr[0]); printf("Array: "); printArray(arr, n); int target = 2; int first = findFirst(arr, n, target); int last = findLast(arr, n, target); if (first == -1) printf("%d not found\n", target); else printf("First: %d, Last: %d, Count: %d\n", first, last, last - first + 1); } void demoCountOnes(void) { printf("\n=== Count 1's in Sorted Binary Array ===\n"); int arr1[] = {0, 0, 0, 1, 1, 1, 1}; int arr2[] = {0, 0, 0, 0}; int arr3[] = {1, 1, 1, 1}; printf("arr1: "); printArray(arr1, 7); printf(" Count 1's: %d\n", countOnes(arr1, 7)); printf("arr2: "); printArray(arr2, 4); printf(" Count 1's: %d\n", countOnes(arr2, 4)); printf("arr3: "); printArray(arr3, 4); printf(" Count 1's: %d\n", countOnes(arr3, 4)); } void demoUnbounded(void) { printf("\n=== Unbounded Binary Search ===\n"); int targets[] = {1, 9, 29, 30}; int count = sizeof(targets) / sizeof(targets[0]); for (int i = 0; i < count; i++) { int r = unboundedSearch(targets[i]); if (r == -1) printf("Target %d: Not found\n", targets[i]); else printf("Target %d: Index %d\n", targets[i], r); } } void demoRotatedMin(void) { printf("\n=== Minimum in Sorted Rotated Array ===\n"); int arr1[] = {5, 6, 1, 2, 3, 4}; int arr2[] = {3, 1, 2}; int arr3[] = {4, 2, 3}; int arr4[] = {1, 2, 3, 4}; printf("arr1: "); printArray(arr1, 6); printf(" Min: %d\n", findMinRotated(arr1, 6)); printf("arr2: "); printArray(arr2, 3); printf(" Min: %d\n", findMinRotated(arr2, 3)); printf("arr3: "); printArray(arr3, 3); printf(" Min: %d\n", findMinRotated(arr3, 3)); printf("arr4: "); printArray(arr4, 4); printf(" Min: %d\n", findMinRotated(arr4, 4)); } void demoRotatedSearch(void) { printf("\n=== Search in Sorted Rotated Array ===\n"); int arr[] = {5, 6, 7, 8, 9, 1, 2, 3}; int n = sizeof(arr) / sizeof(arr[0]); printf("Array: "); printArray(arr, n); int targets[] = {5, 1, 3, 8, 10, 0}; int count = sizeof(targets) / sizeof(targets[0]); for (int i = 0; i < count; i++) { int r = searchRotated(arr, n, targets[i]); if (r == -1) printf("Target %d: Not found\n", targets[i]); else printf("Target %d: Index %d\n", targets[i], r); } } void demoAggressiveCows(void) { printf("\n=== Aggressive Cows ===\n"); int stalls1[] = {1, 2, 4, 8, 9}; int stalls2[] = {1, 2, 3, 4, 5}; printf("stalls=[1,2,4,8,9], k=3 → %d\n", aggressiveCows(stalls1, 5, 3)); printf("stalls=[1,2,3,4,5], k=3 → %d\n", aggressiveCows(stalls2, 5, 3)); printf("stalls=[1,2,4,8,9], k=2 → %d\n", aggressiveCows(stalls1, 5, 2)); } /* Read integer safely */ 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; }

Expected Output (sample run)

text

=== Binary Search Applications Demo === --- Menu --- 1. Square Root of Integer 2. First and Last Position 3. Count 1's in Binary Array 4. Unbounded Binary Search 5. Minimum in Rotated Array 6. Search in Rotated Array 7. Aggressive Cows 8. Exit Enter choice: 1 === Integer Square Root === sqrt(0) = 0 sqrt(1) = 1 sqrt(4) = 2 sqrt(8) = 2 sqrt(16) = 4 sqrt(25) = 5 sqrt(100) = 10 sqrt(999999999) = 31622

Common Pitfalls, Edge Cases & Memory Considerations

1. Integer Overflow in mid * mid

Use mid <= n / mid instead of mid * mid <= n.

2. Infinite Loops from Non-Progress

Always update low = mid + 1 or high = mid - 1.

3. Off-by-One in Rotated Arrays

Use high = mid (not mid - 1) when arr[mid] < arr[high] to keep minimum in range.

4. Missing Check for Sorted Subarray

In rotated search, always check which half is sorted first.

5. Unbounded Search Safety

Include an upper bound to prevent infinite doubling.

6. Aggressive Cows: Sorting Required

Always sort stall positions first.

7. First/Last Occurrence Initialization

Initialize result = -1 and update only on match.

8. Empty Arrays

Return sentinel values (-1) for empty inputs.

9. Duplicates in Rotated Arrays

The minimum in rotated array with duplicates can be ambiguous; linear fallback may be needed.

10. Memory Considerations

  • All seven algorithms use O(1) extra space.
  • No dynamic allocation required.
  • Use long long for integer sqrt to avoid overflow.

Summary & Best Practices

Binary search is a versatile technique that extends far beyond simple searching. The seven applications in this tutorial demonstrate its power across mathematical, structural, and optimization problems.

Key Takeaways

ProblemKey Insight
Square RootBinary search on answer with overflow-safe comparison
First/Last PositionBias search direction based on requirement
Count 1’sFind first 1; count = n – index
Unbounded SearchExponential doubling then binary search
Min in RotatedCompare mid with high
Search in RotatedOne half always sorted
Aggressive CowsBinary search on distance with feasibility check

Best Practices

  1. Use low + (high - low) / 2 to avoid overflow.
  2. Ensure monotonicity before applying binary search.
  3. Check both halves’ sorted status in rotated arrays.
  4. Bias first/last occurrence searches appropriately.
  5. Include safety bounds in unbounded search.
  6. Sort input for aggressive cows.
  7. Use long long for large computations (square root).
  8. Handle edge cases: empty, single element, duplicates.
  9. Verify with brute force during testing.
  10. Profile for large inputs.

By mastering these seven applications, you’ll be well-equipped to solve a wide range of binary search problems in competitive programming and technical interviews.

Learn Binary Search in C: iterative, recursive, first/last occurrence, and descending variants. Master O(log n) searching with examples and edge cases.

Binary Search in C: Iterative & Recursive Guide

Binary Search in C: A Comprehensive Tutorial

Binary Search is a fundamental searching algorithm that operates on a sorted or monotonic search space, repeatedly dividing it into halves to find a target value or optimal answer in logarithmic time O(log N). It is one of the most important algorithms in computer science and forms the foundation for many advanced algorithms and data structures.

This tutorial provides an in-depth exploration of Binary Search in C: algorithm principles, iterative and recursive implementations, complexity analysis, practical applications, common pitfalls, and best practices.


Conceptual Introduction & Fundamentals

What is Binary Search?

Binary Search is a divide-and-conquer algorithm that finds the position of a target value within a sorted array. It compares the target with the middle element of the array and eliminates half of the search space based on the comparison.

Why Binary Search Matters

  • Efficiency: O(log n) time — exponentially faster than linear search.
  • Foundation: Basis for many algorithms and data structures (BST, B-trees).
  • Wide applications: Databases, debugging, network routing, machine learning.
  • Interview preparation: Very common coding interview question.
  • Optimization problems: Used in “binary search on answer” problems.

Prerequisites

  1. Sorted data: The array must be sorted (ascending or descending).
  2. Random access: Any element can be accessed in O(1) time (arrays qualify).
  3. Monotonic property: The search space must be monotonic.

How Binary Search Works

For a sorted array and target x:

  1. Find the middle index mid.
  2. If arr[mid] == x, return mid.
  3. If arr[mid] < x, search the right half.
  4. If arr[mid] > x, search the left half.
  5. Repeat until found or search space is empty.

Example:

text

Array: [2, 5, 8, 12, 16, 23, 38, 56, 72, 91] Target: 23 Step 1: mid = 4, arr[4] = 16 < 23 → search right [23, 38, 56, 72, 91] Step 2: mid = 7, arr[7] = 56 > 23 → search left [23, 38] Step 3: mid = 5, arr[5] = 23 == 23 → Found at index 5

Complexity Analysis

CaseTime Complexity
BestO(1)
AverageO(log n)
WorstO(log n)

Space:

  • Iterative: O(1)
  • Recursive: O(log n) (call stack)

Syntax & Core Mechanics

Iterative Binary Search

c

#include <stdio.h> int binarySearchIterative(const int arr[], int n, int target) { int low = 0; int high = n - 1; while (low <= high) { /* Avoid overflow: low + (high - low) / 2 */ int mid = low + (high - low) / 2; if (arr[mid] == target) { return mid; } if (arr[mid] < target) { low = mid + 1; /* Search right half */ } else { high = mid - 1; /* Search left half */ } } return -1; /* Not found */ } int main(void) { int arr[] = {2, 3, 4, 10, 40}; int n = sizeof(arr) / sizeof(arr[0]); int target = 10; int result = binarySearchIterative(arr, n, target); if (result == -1) { printf("Element is not present in array\n"); } else { printf("Element is present at index %d\n", result); } return 0; }

Expected output:

text

Element is present at index 3

Time: O(log n), Space: O(1)

Recursive Binary Search

c

#include <stdio.h> int binarySearchRecursive(const int arr[], int low, int high, int target) { if (low > high) { return -1; /* Not found */ } int mid = low + (high - low) / 2; if (arr[mid] == target) { return mid; } if (arr[mid] > target) { return binarySearchRecursive(arr, low, mid - 1, target); } return binarySearchRecursive(arr, mid + 1, high, target); } int main(void) { int arr[] = {2, 3, 4, 10, 40}; int n = sizeof(arr) / sizeof(arr[0]); int target = 10; int result = binarySearchRecursive(arr, 0, n - 1, target); if (result == -1) { printf("Element is not present in array\n"); } else { printf("Element is present at index %d\n", result); } return 0; }

Expected output:

text

Element is present at index 3

Time: O(log n), Space: O(log n) due to recursion stack

Finding First and Last Occurrence

For arrays with duplicates, binary search can be modified to find first/last occurrence.

c

/* Find first occurrence of target */ int findFirst(const int arr[], int n, int target) { int low = 0, high = n - 1; int result = -1; while (low <= high) { int mid = low + (high - low) / 2; if (arr[mid] == target) { result = mid; high = mid - 1; /* Search left for earlier occurrence */ } else if (arr[mid] < target) { low = mid + 1; } else { high = mid - 1; } } return result; } /* Find last occurrence of target */ int findLast(const int arr[], int n, int target) { int low = 0, high = n - 1; int result = -1; while (low <= high) { int mid = low + (high - low) / 2; if (arr[mid] == target) { result = mid; low = mid + 1; /* Search right for later occurrence */ } else if (arr[mid] < target) { low = mid + 1; } else { high = mid - 1; } } return result; }

Full Practical Code Example with Explanation

This program demonstrates all binary search variants with verification, performance comparison, and edge cases.

Program: Binary Search Complete Demo

c

/** * binary_search_demo.c * Demonstrates iterative, recursive, and variant binary search approaches. * Includes first/last occurrence, verification, and edge cases. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <time.h> #define MAX_LINE_LEN 100 #define MAX_SIZE 100000 /* Function prototypes */ int binarySearchIterative(const int arr[], int n, int target); int binarySearchRecursive(const int arr[], int low, int high, int target); int findFirstOccurrence(const int arr[], int n, int target); int findLastOccurrence(const int arr[], int n, int target); int findClosest(const int arr[], int n, int target); int binarySearchDescending(const int arr[], int n, int target); int compareInts(const void *a, const void *b); int readInteger(const char *prompt, int *outValue); void printArray(const int arr[], int n); void demonstrateApproaches(void); void performanceComparison(void); void demonstrateEdgeCases(void); int main(void) { int choice; char buffer[MAX_LINE_LEN]; printf("=== Binary Search Complete Demo ===\n"); while (1) { printf("\n--- Menu ---\n"); printf("1. Demonstrate All Variants\n"); printf("2. Search in Custom Array\n"); printf("3. Performance Comparison\n"); printf("4. Edge Cases\n"); printf("5. 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: demonstrateApproaches(); break; case 2: { int arr[MAX_SIZE]; int n, target; if (readInteger("Enter array size (1-100000): ", &n) != 0) { fprintf(stderr, "Invalid size.\n"); break; } if (n < 1 || n > MAX_SIZE) { printf("Size must be 1-%d.\n", MAX_SIZE); break; } printf("Enter %d elements (will be sorted):\n", n); for (int i = 0; i < n; i++) { char prompt[50]; snprintf(prompt, sizeof(prompt), " Element %d: ", i); if (readInteger(prompt, &arr[i]) != 0) { fprintf(stderr, "Invalid input.\n"); n = i; break; } } qsort(arr, n, sizeof(int), compareInts); printf("\nSorted array: "); printArray(arr, n); if (readInteger("Enter target: ", &target) != 0) { fprintf(stderr, "Invalid target.\n"); break; } int iter = binarySearchIterative(arr, n, target); int rec = binarySearchRecursive(arr, 0, n - 1, target); int first = findFirstOccurrence(arr, n, target); int last = findLastOccurrence(arr, n, target); printf("Iterative: %d\n", iter); printf("Recursive: %d\n", rec); printf("First occurrence: %d\n", first); printf("Last occurrence: %d\n", last); if (iter != -1) { printf("Verification: %s\n", (iter == rec && iter == first) ? "PASS" : "FAIL"); } break; } case 3: performanceComparison(); break; case 4: demonstrateEdgeCases(); break; case 5: printf("Exiting.\n"); return EXIT_SUCCESS; default: printf("Invalid choice. Please try again.\n"); break; } } return EXIT_SUCCESS; } /* Iterative binary search */ int binarySearchIterative(const int arr[], int n, int target) { int low = 0; int high = n - 1; while (low <= high) { int mid = low + (high - low) / 2; if (arr[mid] == target) return mid; if (arr[mid] < target) low = mid + 1; else high = mid - 1; } return -1; } /* Recursive binary search */ int binarySearchRecursive(const int arr[], int low, int high, int target) { if (low > high) return -1; int mid = low + (high - low) / 2; if (arr[mid] == target) return mid; if (arr[mid] > target) return binarySearchRecursive(arr, low, mid - 1, target); return binarySearchRecursive(arr, mid + 1, high, target); } /* Find first occurrence */ int findFirstOccurrence(const int arr[], int n, int target) { int low = 0, high = n - 1; int result = -1; while (low <= high) { int mid = low + (high - low) / 2; if (arr[mid] == target) { result = mid; high = mid - 1; } else if (arr[mid] < target) { low = mid + 1; } else { high = mid - 1; } } return result; } /* Find last occurrence */ int findLastOccurrence(const int arr[], int n, int target) { int low = 0, high = n - 1; int result = -1; while (low <= high) { int mid = low + (high - low) / 2; if (arr[mid] == target) { result = mid; low = mid + 1; } else if (arr[mid] < target) { low = mid + 1; } else { high = mid - 1; } } return result; } /* Find closest element to target */ int findClosest(const int arr[], int n, int target) { if (n <= 0) return -1; int low = 0, high = n - 1; if (target <= arr[0]) return 0; if (target >= arr[n - 1]) return n - 1; while (low <= high) { int mid = low + (high - low) / 2; if (arr[mid] == target) return mid; if (arr[mid] < target) { if (mid + 1 < n && arr[mid + 1] > target) { return (target - arr[mid] < arr[mid + 1] - target) ? mid : mid + 1; } low = mid + 1; } else { if (mid - 1 >= 0 && arr[mid - 1] < target) { return (target - arr[mid - 1] < arr[mid] - target) ? mid - 1 : mid; } high = mid - 1; } } return -1; } /* Binary search for descending sorted array */ int binarySearchDescending(const int arr[], int n, int target) { int low = 0, high = n - 1; while (low <= high) { int mid = low + (high - low) / 2; if (arr[mid] == target) return mid; if (arr[mid] > target) low = mid + 1; else high = mid - 1; } return -1; } /* Comparison function */ int compareInts(const void *a, const void *b) { int int_a = *(const int *)a; int int_b = *(const int *)b; return (int_a > int_b) - (int_a < int_b); } /* Prints array */ void printArray(const int arr[], int n) { printf("["); for (int i = 0; i < n; i++) { printf("%d", arr[i]); if (i < n - 1) printf(", "); } printf("]\n"); } /* Demonstrates all variants */ void demonstrateApproaches(void) { printf("\n=== Binary Search Variants ===\n"); int arr[] = {2, 5, 8, 12, 16, 23, 38, 56, 72, 91}; int n = sizeof(arr) / sizeof(arr[0]); printf("\nArray: "); printArray(arr, n); int targets[] = {2, 23, 91, 100}; int numTargets = sizeof(targets) / sizeof(targets[0]); for (int i = 0; i < numTargets; i++) { int t = targets[i]; printf("\nTarget %d:\n", t); printf(" Iterative: %d\n", binarySearchIterative(arr, n, t)); printf(" Recursive: %d\n", binarySearchRecursive(arr, 0, n - 1, t)); printf(" Closest: %d\n", findClosest(arr, n, t)); } /* First/last occurrence with duplicates */ int dupArr[] = {1, 2, 2, 2, 3, 4, 5}; int dupN = sizeof(dupArr) / sizeof(dupArr[0]); printf("\nArray with duplicates: "); printArray(dupArr, dupN); printf("Target 2:\n"); printf(" First: %d\n", findFirstOccurrence(dupArr, dupN, 2)); printf(" Last: %d\n", findLastOccurrence(dupArr, dupN, 2)); /* Descending array */ int descArr[] = {100, 90, 80, 70, 60, 50}; int descN = sizeof(descArr) / sizeof(descArr[0]); printf("\nDescending array: "); printArray(descArr, descN); printf("Target 70:\n"); printf(" Index: %d\n", binarySearchDescending(descArr, descN, 70)); } /* Performance comparison */ void performanceComparison(void) { printf("\n=== Performance Comparison (n = 10000000) ===\n"); int n = 10000000; int *arr = malloc(n * sizeof(int)); if (arr == NULL) { fprintf(stderr, "Memory allocation failed.\n"); return; } for (int i = 0; i < n; i++) { arr[i] = i * 2; } int target = arr[n - 1]; /* Worst case: last element */ clock_t start, end; double timeTaken; /* Linear search for comparison */ start = clock(); int linearResult = -1; for (int i = 0; i < n; i++) { if (arr[i] == target) { linearResult = i; break; } } end = clock(); timeTaken = ((double)(end - start)) / CLOCKS_PER_SEC * 1000.0; printf("Linear search (O(n)): Result = %d, Time = %.3f ms\n", linearResult, timeTaken); /* Binary search */ start = clock(); int binResult = binarySearchIterative(arr, n, target); end = clock(); timeTaken = ((double)(end - start)) / CLOCKS_PER_SEC * 1000.0; printf("Binary search (O(log n)): Result = %d, Time = %.3f ms\n", binResult, timeTaken); printf("\nVerification: %s\n", (linearResult == binResult) ? "PASS" : "FAIL"); free(arr); } /* Demonstrates edge cases */ void demonstrateEdgeCases(void) { printf("\n=== Edge Cases ===\n"); /* Case 1: Empty array */ int arr1[] = {}; printf("Case 1: Empty array\n"); printf(" Result: %d\n", binarySearchIterative(arr1, 0, 5)); /* Case 2: Single element, found */ int arr2[] = {42}; printf("\nCase 2: Single element, found\n"); printf(" Array: "); printArray(arr2, 1); printf(" Result: %d\n", binarySearchIterative(arr2, 1, 42)); /* Case 3: Single element, not found */ int arr3[] = {42}; printf("\nCase 3: Single element, not found\n"); printf(" Array: "); printArray(arr3, 1); printf(" Result: %d\n", binarySearchIterative(arr3, 1, 10)); /* Case 4: Target at start */ int arr4[] = {1, 2, 3, 4, 5}; printf("\nCase 4: Target at start\n"); printf(" Array: "); printArray(arr4, 5); printf(" Result: %d\n", binarySearchIterative(arr4, 5, 1)); /* Case 5: Target at end */ int arr5[] = {1, 2, 3, 4, 5}; printf("\nCase 5: Target at end\n"); printf(" Array: "); printArray(arr5, 5); printf(" Result: %d\n", binarySearchIterative(arr5, 5, 5)); /* Case 6: All same elements */ int arr6[] = {5, 5, 5, 5, 5}; printf("\nCase 6: All same elements\n"); printf(" Array: "); printArray(arr6, 5); printf(" First: %d, Last: %d\n", findFirstOccurrence(arr6, 5, 5), findLastOccurrence(arr6, 5, 5)); /* Case 7: Negative numbers */ int arr7[] = {-10, -5, 0, 5, 10}; printf("\nCase 7: Negative numbers\n"); printf(" Array: "); printArray(arr7, 5); printf(" Target -5: %d\n", binarySearchIterative(arr7, 5, -5)); /* Case 8: Large array overflow check */ int arr8[] = {2, 5, 8, 12, 16, 23, 38, 56, 72, 91}; printf("\nCase 8: Overflow-safe mid calculation\n"); printf(" Array: "); printArray(arr8, 10); printf(" Target 56: %d\n", binarySearchIterative(arr8, 10, 56)); } /* Reads an integer safely */ 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; }

Explanation of Binary Search Variants

  1. Iterative: Standard while loop; O(log n) time, O(1) space.
  2. Recursive: Self-calling function; O(log n) time, O(log n) space.
  3. First Occurrence: Modified to find leftmost match.
  4. Last Occurrence: Modified to find rightmost match.
  5. Closest: Finds nearest element to target.
  6. Descending: Adapted for descending arrays.

Expected Output (sample run)

text

=== Binary Search Complete Demo === --- Menu --- 1. Demonstrate All Variants 2. Search in Custom Array 3. Performance Comparison 4. Edge Cases 5. Exit Enter choice: 1 === Binary Search Variants === Array: [2, 5, 8, 12, 16, 23, 38, 56, 72, 91] Target 2: Iterative: 0 Recursive: 0 Closest: 0 Target 23: Iterative: 5 Recursive: 5 Closest: 5 Target 91: Iterative: 9 Recursive: 9 Closest: 9 Target 100: Iterative: -1 Recursive: -1 Closest: 9 Array with duplicates: [1, 2, 2, 2, 3, 4, 5] Target 2: First: 1 Last: 3 Descending array: [100, 90, 80, 70, 60, 50] Target 70: Index: 3 --- Menu --- ... Enter choice: 3 === Performance Comparison (n = 10000000) === Linear search (O(n)): Result = 9999999, Time = 45.234 ms Binary search (O(log n)): Result = 9999999, Time = 0.002 ms Verification: PASS --- Menu --- ... Enter choice: 4 === Edge Cases === Case 1: Empty array Result: -1 Case 2: Single element, found Array: [42] Result: 0 Case 3: Single element, not found Array: [42] Result: -1 Case 4: Target at start Array: [1, 2, 3, 4, 5] Result: 0 Case 5: Target at end Array: [1, 2, 3, 4, 5] Result: 4 Case 6: All same elements Array: [5, 5, 5, 5, 5] First: 0, Last: 4 Case 7: Negative numbers Array: [-10, -5, 0, 5, 10] Target -5: 1 Case 8: Overflow-safe mid calculation Array: [2, 5, 8, 12, 16, 23, 38, 56, 72, 91] Target 56: 7

Common Pitfalls, Edge Cases & Memory Considerations

1. Integer Overflow in Mid Calculation

(low + high) / 2 can overflow for large arrays.

c

int mid = (low + high) / 2; /* BUG: potential overflow */ int mid = low + (high - low) / 2; /* Correct */

2. Infinite Loop from Wrong Update

Forgetting to update low or high causes infinite loops.

c

if (arr[mid] < target) low = mid; /* BUG: no progress */ if (arr[mid] < target) low = mid + 1; /* Correct */

3. Off-by-One in Loop Condition

Using low < high vs low <= high affects termination.

c

while (low <= high) { ... } /* Correct for standard binary search */

4. Unsorted Array

Binary search requires a sorted array; results are undefined otherwise.

Solution: Sort the array first with qsort.

5. Empty Array

Return -1 for empty arrays.

c

if (n <= 0) return -1;

6. Duplicates

Standard binary search returns any occurrence; use variants for first/last.

7. Recursion Depth

Recursive binary search uses O(log n) stack space — safe for typical arrays.

8. Descending Arrays

Adapt comparison logic for descending order.

c

if (arr[mid] > target) low = mid + 1; else high = mid - 1;

9. Search on Monotonic Functions

Binary search works on any monotonic search space, not just arrays.

10. Memory Considerations

  • Iterative: O(1) extra space.
  • Recursive: O(log n) stack space.
  • No dynamic allocation required.
  • Use int for indices; size_t for very large arrays.

Summary & Best Practices

Binary Search is a fundamental O(log n) algorithm for searching in sorted arrays. It is widely applicable and forms the foundation for many advanced algorithms.

Key Takeaways

  • Requires sorted data and random access.
  • Iterative: O(log n) time, O(1) space.
  • Recursive: O(log n) time, O(log n) space.
  • Use low + (high - low) / 2 to avoid overflow.
  • Variants: first/last occurrence, closest match, descending.
  • Applications: databases, debugging, routing, ML tuning.

Best Practices

  1. Use the iterative version for O(1) space.
  2. Use low + (high - low) / 2 to prevent overflow.
  3. Ensure the array is sorted before searching.
  4. Update low = mid + 1 or high = mid - 1 for progress.
  5. Handle edge cases: empty, single element, target not found.
  6. Use variants for first/last occurrence when duplicates exist.
  7. Use qsort for sorting before binary search.
  8. Test with negative numbers and boundary targets.
  9. Consider binary search on answer for optimization problems.
  10. Profile for large arrays.

By mastering Binary Search, you’ll have a powerful tool for efficient searching and be prepared for advanced algorithms and data structures.

Learn to split an array into three equal sum segments in C: naive O(n³) and greedy O(n) approaches. Master prefix sum and partitioning techniques.

Split Array into Three Equal Sum Segments in C

Split Array into Three Equal Sum Segments in C: A Comprehensive Tutorial

Given an integer array, the task is to divide it into three non-empty contiguous segments with equal sum. If possible, return the index pair [i, j] such that:

  • sum(arr[0..i]) == sum(arr[i+1..j]) == sum(arr[j+1..n-1])

If impossible, return [-1, -1]. This problem tests your understanding of prefix sums, greedy partitioning, and array traversal.

This tutorial provides an in-depth exploration of splitting an array into three equal sum segments in C: naive O(n³) approach, optimized O(n) approach, practical implementation, common pitfalls, and best practices.


Conceptual Introduction & Fundamentals

What is the Problem?

Find two indices i and j (i < j) such that the array is split into three contiguous non-empty segments with equal sums.

Examples:

text

Input: [1, 3, 4, 0, 4] Output: [1, 2] Explanation: Segment 1: arr[0..1] = 1+3 = 4 Segment 2: arr[2..2] = 4 Segment 3: arr[3..4] = 0+4 = 4 Input: [2, 3, 4] Output: [-1, -1] Explanation: No valid split exists. Input: [1, -1, 1, -1, 1, -1, 1, -1] Output: [1, 3] Explanation: Three segments each sum to 0.

Why This Problem Matters

  • Prefix sums: Foundation for range-sum problems.
  • Greedy partitioning: Teaches optimal substructure.
  • Algorithm optimization: O(n³) → O(n) transformation.
  • Interview preparation: Common coding interview question.
  • Real-world applications: Load balancing, data partitioning, resource allocation.

Key Insight

For three equal segments, the total sum must be divisible by 3. Each segment’s target sum is total / 3. We can find the first two segments greedily by scanning left to right.

Approaches Overview

ApproachTime ComplexitySpace Complexity
Naive (all partitions)O(n³)O(1)
Greedy (find first two segments)O(n)O(1)

Syntax & Core Mechanics

Approach 1: Naive — Try All Partitions

Try every pair (i, j) and check if all three segment sums are equal.

c

#include <stdio.h> #include <stdlib.h> long long findSum(const int arr[], int start, int end) { long long sum = 0; for (int i = start; i <= end; i++) { sum += arr[i]; } return sum; } void findSplitNaive(const int arr[], int n, int *out) { out[0] = -1; out[1] = -1; for (int i = 0; i < n - 2; i++) { for (int j = i + 1; j < n - 1; j++) { long long sum1 = findSum(arr, 0, i); long long sum2 = findSum(arr, i + 1, j); long long sum3 = findSum(arr, j + 1, n - 1); if (sum1 == sum2 && sum2 == sum3) { out[0] = i; out[1] = j; return; } } } } int main(void) { int arr[] = {1, 3, 4, 0, 4}; int n = sizeof(arr) / sizeof(arr[0]); int result[2]; findSplitNaive(arr, n, result); printf("[%d, %d]\n", result[0], result[1]); return 0; }

Expected output:

text

[1, 2]

Time: O(n³), Space: O(1)

Approach 2: Greedy — Find First Two Segments

Steps:

  1. Compute total. If total % 3 != 0, return [-1, -1].
  2. Set target = total / 3.
  3. Scan left to right; accumulate currSum.
  4. When currSum == target, record index, reset currSum = 0.
  5. When two segments found and there are remaining elements, return the indices.

c

#include <stdio.h> void findSplit(const int arr[], int n, int *out) { out[0] = -1; out[1] = -1; if (n < 3) return; long long total = 0; for (int i = 0; i < n; i++) { total += arr[i]; } if (total % 3 != 0) return; long long target = total / 3; long long currSum = 0; int found = 0; for (int i = 0; i < n; i++) { currSum += arr[i]; if (currSum == target) { currSum = 0; out[found++] = i; if (found == 2 && i < n - 1) { return; } } } /* If we reach here, no valid split */ out[0] = -1; out[1] = -1; } int main(void) { int arr[] = {1, 3, 4, 0, 4}; int n = sizeof(arr) / sizeof(arr[0]); int result[2]; findSplit(arr, n, result); printf("[%d, %d]\n", result[0], result[1]); return 0; }

Expected output:

text

[1, 2]

Time: O(n), Space: O(1)

Step-by-Step Trace for [1, 3, 4, 0, 4]

text

total = 12, target = 4 i=0: currSum = 1 i=1: currSum = 4 == target → out[0] = 1, currSum = 0 i=2: currSum = 4 == target → out[1] = 2, found=2, i=2 < 4 → return Result: [1, 2] ✓

Handling All-Zero Segments

If total == 0, target is 0, and any prefix sum equal to 0 triggers segment recording. The algorithm correctly handles this.

text

Input: [1, -1, 1, -1, 1, -1, 1, -1] total = 0, target = 0 i=0: currSum = 1 i=1: currSum = 0 == target → out[0] = 1, currSum = 0 i=2: currSum = 1 i=3: currSum = 0 == target → out[1] = 3, found=2, i=3 < 7 → return Result: [1, 3] ✓

Edge Case: What if Third Segment is Empty?

The condition i < n - 1 ensures at least one element remains for the third segment. Without this check, we might record a second segment at the last index, leaving no elements for the third segment.


Full Practical Code Example with Explanation

This program demonstrates both approaches with verification, performance comparison, and edge cases.

Program: Split Array into Three Equal Sum Segments

c

/** * split_three_equal_segments.c * Finds indices to split array into three equal-sum segments. * Compares naive O(n^3) and greedy O(n) approaches. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <time.h> #define MAX_LINE_LEN 100 #define MAX_SIZE 100000 /* Function prototypes */ void findSplitNaive(const int arr[], int n, int *out); void findSplitGreedy(const int arr[], int n, int *out); long long findSum(const int arr[], int start, int end); int verifySplit(const int arr[], int n, int i, int j); int readInteger(const char *prompt, int *outValue); void printArray(const int arr[], int n); void demonstrateApproaches(void); void performanceComparison(void); void demonstrateEdgeCases(void); int main(void) { int choice; char buffer[MAX_LINE_LEN]; printf("=== Split Array into Three Equal Sum Segments ===\n"); while (1) { printf("\n--- Menu ---\n"); printf("1. Demonstrate with Sample Arrays\n"); printf("2. Enter Custom Array\n"); printf("3. Performance Comparison\n"); printf("4. Edge Cases\n"); printf("5. 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: demonstrateApproaches(); break; case 2: { int arr[MAX_SIZE]; int n; int result[2]; if (readInteger("Enter array size (1-100000): ", &n) != 0) { fprintf(stderr, "Invalid size.\n"); break; } if (n < 1 || n > MAX_SIZE) { printf("Size must be 1-%d.\n", MAX_SIZE); break; } printf("Enter %d elements:\n", n); for (int i = 0; i < n; i++) { char prompt[50]; snprintf(prompt, sizeof(prompt), " Element %d: ", i); if (readInteger(prompt, &arr[i]) != 0) { fprintf(stderr, "Invalid input.\n"); n = i; break; } } printf("\nArray: "); printArray(arr, n); findSplitGreedy(arr, n, result); printf("Split indices: [%d, %d]\n", result[0], result[1]); if (result[0] != -1) { printf("Segments:\n"); printf(" Segment 1 [0..%d]: sum = %lld\n", result[0], findSum(arr, 0, result[0])); printf(" Segment 2 [%d..%d]: sum = %lld\n", result[0] + 1, result[1], findSum(arr, result[0] + 1, result[1])); printf(" Segment 3 [%d..%d]: sum = %lld\n", result[1] + 1, n - 1, findSum(arr, result[1] + 1, n - 1)); } break; } case 3: performanceComparison(); break; case 4: demonstrateEdgeCases(); break; case 5: printf("Exiting.\n"); return EXIT_SUCCESS; default: printf("Invalid choice. Please try again.\n"); break; } } return EXIT_SUCCESS; } /* Sum of subarray */ long long findSum(const int arr[], int start, int end) { long long sum = 0; for (int i = start; i <= end; i++) { sum += arr[i]; } return sum; } /* Naive: O(n^3) */ void findSplitNaive(const int arr[], int n, int *out) { out[0] = -1; out[1] = -1; if (n < 3) return; for (int i = 0; i < n - 2; i++) { for (int j = i + 1; j < n - 1; j++) { long long sum1 = findSum(arr, 0, i); long long sum2 = findSum(arr, i + 1, j); long long sum3 = findSum(arr, j + 1, n - 1); if (sum1 == sum2 && sum2 == sum3) { out[0] = i; out[1] = j; return; } } } } /* Greedy: O(n) */ void findSplitGreedy(const int arr[], int n, int *out) { out[0] = -1; out[1] = -1; if (n < 3) return; long long total = 0; for (int i = 0; i < n; i++) { total += arr[i]; } if (total % 3 != 0) return; long long target = total / 3; long long currSum = 0; int found = 0; for (int i = 0; i < n; i++) { currSum += arr[i]; if (currSum == target) { currSum = 0; out[found++] = i; if (found == 2 && i < n - 1) { return; } } } out[0] = -1; out[1] = -1; } /* Verifies the split */ int verifySplit(const int arr[], int n, int i, int j) { if (i < 0 || j < 0 || i >= j || j >= n - 1) return 0; long long s1 = findSum(arr, 0, i); long long s2 = findSum(arr, i + 1, j); long long s3 = findSum(arr, j + 1, n - 1); return (s1 == s2 && s2 == s3); } /* Prints array */ void printArray(const int arr[], int n) { printf("["); for (int i = 0; i < n; i++) { printf("%d", arr[i]); if (i < n - 1) printf(", "); } printf("]\n"); } /* Demonstrates both approaches */ void demonstrateApproaches(void) { printf("\n=== Sample Arrays ===\n"); int arr1[] = {1, 3, 4, 0, 4}; int n1 = sizeof(arr1) / sizeof(arr1[0]); int r1[2]; printf("\nArray 1: "); printArray(arr1, n1); findSplitNaive(arr1, n1, r1); printf(" Naive: [%d, %d]\n", r1[0], r1[1]); if (r1[0] != -1) { printf(" Verification: %s\n", verifySplit(arr1, n1, r1[0], r1[1]) ? "PASS" : "FAIL"); } findSplitGreedy(arr1, n1, r1); printf(" Greedy: [%d, %d]\n", r1[0], r1[1]); if (r1[0] != -1) { printf(" Verification: %s\n", verifySplit(arr1, n1, r1[0], r1[1]) ? "PASS" : "FAIL"); } int arr2[] = {2, 3, 4}; int n2 = sizeof(arr2) / sizeof(arr2[0]); int r2[2]; printf("\nArray 2: "); printArray(arr2, n2); findSplitGreedy(arr2, n2, r2); printf(" Greedy: [%d, %d] (impossible)\n", r2[0], r2[1]); int arr3[] = {1, -1, 1, -1, 1, -1, 1, -1}; int n3 = sizeof(arr3) / sizeof(arr3[0]); int r3[2]; printf("\nArray 3: "); printArray(arr3, n3); findSplitGreedy(arr3, n3, r3); printf(" Greedy: [%d, %d]\n", r3[0], r3[1]); if (r3[0] != -1) { printf(" Verification: %s\n", verifySplit(arr3, n3, r3[0], r3[1]) ? "PASS" : "FAIL"); } int arr4[] = {1, 1, 1, 1, 1, 1}; int n4 = sizeof(arr4) / sizeof(arr4[0]); int r4[2]; printf("\nArray 4: "); printArray(arr4, n4); findSplitGreedy(arr4, n4, r4); printf(" Greedy: [%d, %d]\n", r4[0], r4[1]); if (r4[0] != -1) { printf(" Verification: %s\n", verifySplit(arr4, n4, r4[0], r4[1]) ? "PASS" : "FAIL"); } } /* Performance comparison */ void performanceComparison(void) { printf("\n=== Performance Comparison ===\n"); /* Naive only feasible for small arrays */ int smallN = 100; int *smallArr = malloc(smallN * sizeof(int)); if (smallArr == NULL) return; for (int i = 0; i < smallN; i++) { smallArr[i] = 1; } clock_t start, end; double timeTaken; int result[2]; start = clock(); findSplitNaive(smallArr, smallN, result); end = clock(); timeTaken = ((double)(end - start)) / CLOCKS_PER_SEC * 1000.0; printf("Naive (n=%d): Result = [%d, %d], Time = %.3f ms\n", smallN, result[0], result[1], timeTaken); free(smallArr); /* Large array for greedy */ int largeN = 99999; /* Divisible by 3 */ int *largeArr = malloc(largeN * sizeof(int)); if (largeArr == NULL) return; for (int i = 0; i < largeN; i++) { largeArr[i] = 3; } start = clock(); findSplitGreedy(largeArr, largeN, result); end = clock(); timeTaken = ((double)(end - start)) / CLOCKS_PER_SEC * 1000.0; printf("Greedy (n=%d): Result = [%d, %d], Time = %.3f ms\n", largeN, result[0], result[1], timeTaken); printf("\nVerification: %s\n", verifySplit(largeArr, largeN, result[0], result[1]) ? "PASS" : "FAIL"); free(largeArr); } /* Demonstrates edge cases */ void demonstrateEdgeCases(void) { printf("\n=== Edge Cases ===\n"); int result[2]; /* Case 1: Empty array */ int arr1[] = {}; printf("Case 1: Empty array\n"); findSplitGreedy(arr1, 0, result); printf(" Result: [%d, %d]\n", result[0], result[1]); /* Case 2: Single element */ int arr2[] = {5}; printf("\nCase 2: Single element\n"); findSplitGreedy(arr2, 1, result); printf(" Result: [%d, %d]\n", result[0], result[1]); /* Case 3: Two elements */ int arr3[] = {1, 2}; printf("\nCase 3: Two elements\n"); findSplitGreedy(arr3, 2, result); printf(" Result: [%d, %d]\n", result[0], result[1]); /* Case 4: Exactly 3 elements, sum divisible by 3 */ int arr4[] = {1, 1, 1}; printf("\nCase 4: Exactly 3 elements\n"); printf(" Array: "); printArray(arr4, 3); findSplitGreedy(arr4, 3, result); printf(" Result: [%d, %d]\n", result[0], result[1]); /* Case 5: Sum not divisible by 3 */ int arr5[] = {1, 2, 3, 4}; printf("\nCase 5: Sum not divisible by 3\n"); printf(" Array: "); printArray(arr5, 4); findSplitGreedy(arr5, 4, result); printf(" Result: [%d, %d]\n", result[0], result[1]); /* Case 6: All zeros */ int arr6[] = {0, 0, 0, 0, 0, 0}; printf("\nCase 6: All zeros\n"); printf(" Array: "); printArray(arr6, 6); findSplitGreedy(arr6, 6, result); printf(" Result: [%d, %d]\n", result[0], result[1]); /* Case 7: Negative values */ int arr7[] = {-1, 2, -1, 2, -1, 2}; printf("\nCase 7: Negative values\n"); printf(" Array: "); printArray(arr7, 6); findSplitGreedy(arr7, 6, result); printf(" Result: [%d, %d]\n", result[0], result[1]); if (result[0] != -1) { printf(" Verification: %s\n", verifySplit(arr7, 6, result[0], result[1]) ? "PASS" : "FAIL"); } /* Case 8: Third segment empty (invalid) */ int arr8[] = {1, 1, 1, 0}; printf("\nCase 8: Potential empty third segment\n"); printf(" Array: "); printArray(arr8, 4); findSplitGreedy(arr8, 4, result); printf(" Result: [%d, %d]\n", result[0], result[1]); /* Case 9: Large values (overflow check) */ int arr9[] = {1000000000, 1000000000, 1000000000}; printf("\nCase 9: Large values\n"); printf(" Array: "); printArray(arr9, 3); findSplitGreedy(arr9, 3, result); printf(" Result: [%d, %d] (long long protects from overflow)\n", result[0], result[1]); } /* Reads an integer safely */ 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; }

Explanation of Approaches

  1. Naive: Tries all (i, j) pairs; O(n³).
  2. Greedy: Finds first two segments equal to total/3; O(n).
  3. Key check: total % 3 == 0 and i < n - 1 for the second segment.

Expected Output (sample run)

text

=== Split Array into Three Equal Sum Segments === --- Menu --- 1. Demonstrate with Sample Arrays 2. Enter Custom Array 3. Performance Comparison 4. Edge Cases 5. Exit Enter choice: 1 === Sample Arrays === Array 1: [1, 3, 4, 0, 4] Naive: [1, 2] Verification: PASS Greedy: [1, 2] Verification: PASS Array 2: [2, 3, 4] Greedy: [-1, -1] (impossible) Array 3: [1, -1, 1, -1, 1, -1, 1, -1] Greedy: [1, 3] Verification: PASS Array 4: [1, 1, 1, 1, 1, 1] Greedy: [1, 3] Verification: PASS --- Menu --- ... Enter choice: 4 === Edge Cases === Case 1: Empty array Result: [-1, -1] Case 2: Single element Result: [-1, -1] Case 3: Two elements Result: [-1, -1] Case 4: Exactly 3 elements Array: [1, 1, 1] Result: [0, 1] Case 5: Sum not divisible by 3 Array: [1, 2, 3, 4] Result: [-1, -1] Case 6: All zeros Array: [0, 0, 0, 0, 0, 0] Result: [1, 3] Case 7: Negative values Array: [-1, 2, -1, 2, -1, 2] Result: [1, 3] Verification: PASS Case 8: Potential empty third segment Array: [1, 1, 1, 0] Result: [-1, -1] Case 9: Large values Array: [1000000000, 1000000000, 1000000000] Result: [0, 1] (long long protects from overflow)

Common Pitfalls, Edge Cases & Memory Considerations

1. Sum Not Divisible by 3

If total % 3 != 0, immediately return [-1, -1].

c

if (total % 3 != 0) return;

2. Integer Overflow

Total sum can exceed int for large arrays.

c

long long total = 0; /* Use long long */

3. Empty Third Segment

Ensure at least one element remains for the third segment.

c

if (found == 2 && i < n - 1) { return; /* Third segment has at least one element */ }

4. Array Size Less Than 3

No valid split possible.

c

if (n < 3) return;

5. All-Zero Array

When total == 0, target is 0. Works correctly with the algorithm.

c

int arr[] = {0, 0, 0, 0, 0, 0}; /* Returns [1, 3] */

6. Negative Values

Negative values work correctly; sum can be negative or zero.

7. Overlapping Segments

Segments must be non-overlapping and contiguous.

c

Segment 1: arr[0..i] Segment 2: arr[i+1..j] Segment 3: arr[j+1..n-1]

8. Resetting currSum Correctly

Reset currSum = 0 after finding each valid segment.

c

if (currSum == target) { currSum = 0; out[found++] = i; }

9. Return Value on Failure

Always set out[0] = out[1] = -1 on failure.

c

out[0] = -1; out[1] = -1;

10. Memory Considerations

  • Naive: O(1) extra space.
  • Greedy: O(1) extra space — preferred.
  • Use long long for sums to prevent overflow.
  • No dynamic allocation required.

Summary & Best Practices

Splitting an array into three equal sum segments is elegantly solved by the greedy approach in O(n) time and O(1) space. The key insight is that the total sum must be divisible by 3, and we only need to find the first two segments.

Key Takeaways

  • Total sum must be divisible by 3.
  • Target segment sum = total / 3.
  • Scan left to right, accumulating currSum.
  • Record index when currSum == target; reset currSum = 0.
  • Ensure third segment has at least one element (i < n - 1).
  • Naive: O(n³); Greedy: O(n).

Best Practices

  1. Use the greedy approach for O(n) time and O(1) space.
  2. Check total % 3 == 0 early.
  3. Use long long for sums to prevent overflow.
  4. Ensure third segment is non-empty with i < n - 1.
  5. Handle edge cases: empty, single element, two elements, all zeros.
  6. Verify with naive approach during testing.
  7. Document segment boundaries in comments.
  8. Test with negative values and zeros.
  9. Return [-1, -1] on failure.
  10. Profile for large arrays.

By mastering this problem, you’ll understand greedy partitioning and prefix-sum techniques, applicable to many array and range problems.