CoreTechED

Loading

Master C compound data types with 19 practice problems on arrays, pointers, strings, structs, and unions. Full C99 code with edge cases.

C Compound Data Types Practice Problems

C Compound Data Types Practice Problems: A Comprehensive Tutorial

Compound data types in C—arrays, pointers, strings, structures, and unions—are built on top of the primitive types and enable programmers to model real-world entities. Fluency with them is non-negotiable for systems programming, embedded work, and technical interviews.

This tutorial walks through 19 graded practice problems organized into Easy and Medium tiers. Each problem is a complete, portable C99/C11 program with inline comments and precise expected output.


Conceptual Introduction & Fundamentals

What are Compound Data Types?

Compound data types are constructed from primitive types to model more complex data:

TypeDescription
ArrayContiguous collection of same-type elements
PointerVariable storing a memory address
StringNull-terminated array of char
StructGrouping of different-typed fields
UnionOverlapping storage for multiple types

Why Practice Them?

  • Real-world modeling: Records, buffers, packets, matrices.
  • Foundation for data structures: Linked lists, trees, hash tables.
  • Interview staples: Almost every C interview problem uses one or more.
  • Bug magnet: Pointer arithmetic, string termination, struct padding are common sources of subtle bugs.

The Concepts You Must Know

  • Array decay: Arrays pass to functions as pointers.
  • Pointer arithmetic: p + 1 advances by sizeof(*p).
  • Null termination: C strings end with '\0'.
  • Struct layout: Fields may be padded for alignment.
  • Union semantics: Only the last-written member is valid.

Syntax & Core Mechanics

Array Basics

c

int arr[5] = {1, 2, 3, 4, 5}; size_t n = sizeof(arr) / sizeof(arr[0]); /* number of elements */

Pointer Basics

c

int x = 10; int *p = &x; *p = 20; /* modify x */ printf("%p\n", (void*)p); /* print address */

String Basics

c

char name[32]; if (fgets(name, sizeof(name), stdin) == NULL) { /* error */ } name[strcspn(name, "\n")] = '\0'; /* strip newline */

Struct Basics

c

struct Point { int x, y; }; struct Point p = {1, 2}; struct Point *pp = &p; printf("%d\n", pp->x);

Union Basics

c

union Value { int i; float f; };

Full Practical Code Example with Explanation

Below are all 19 problems, grouped by difficulty.


Easy Problems

Problem 1: Size of an Array

c

#include <stdio.h> int main(void) { int arr[] = {1, 2, 3, 4, 5}; size_t total = sizeof(arr); size_t elem = sizeof(arr[0]); size_t count = total / elem; printf("Size: %zu bytes, Elements: %zu\n", total, count); return 0; }

Expected output:

text

Size: 20 bytes, Elements: 5

Problem 2: Print Array

c

#include <stdio.h> void printArray(const int arr[], int n) { for (int i = 0; i < n; i++) printf("%d ", arr[i]); printf("\n"); } int main(void) { int arr[] = {10, 20, 30, 40, 50}; printArray(arr, 5); return 0; }

Expected output:

text

10 20 30 40 50

Problem 3: Print Array in Reverse

c

#include <stdio.h> void printReverse(const int arr[], int n) { for (int i = n - 1; i >= 0; i--) printf("%d ", arr[i]); printf("\n"); } int main(void) { int arr[] = {10, 20, 30, 40, 50}; printReverse(arr, 5); return 0; }

Expected output:

text

50 40 30 20 10

Problem 4: Array Insert at End

c

#include <stdio.h> void insertAtEnd(int arr[], int *n, int capacity, int value) { if (*n >= capacity) return; /* caller must check */ arr[(*n)++] = value; } int main(void) { int arr[10] = {1, 2, 3}; int n = 3; insertAtEnd(arr, &n, 10, 4); insertAtEnd(arr, &n, 10, 5); for (int i = 0; i < n; i++) printf("%d ", arr[i]); printf("\n"); return 0; }

Expected output:

text

1 2 3 4 5

Problem 5: Count Smaller Than X

c

#include <stdio.h> int countSmaller(const int arr[], int n, int x) { int count = 0; for (int i = 0; i < n; i++) if (arr[i] < x) count++; return count; } int main(void) { int arr[] = {10, 5, 20, 15, 25}; printf("%d\n", countSmaller(arr, 5, 20)); /* 2 */ return 0; }

Expected output:

text

2

Problem 6: Sum of All Array Elements

c

#include <stdio.h> long long arraySum(const int arr[], int n) { long long sum = 0; for (int i = 0; i < n; i++) sum += arr[i]; return sum; } int main(void) { int arr[] = {1, 2, 3, 4, 5}; printf("%lld\n", arraySum(arr, 5)); return 0; }

Expected output:

text

15

Problem 7: Largest Element in Array

c

#include <stdio.h> int findLargest(const int arr[], int n) { if (n <= 0) return 0; int max = arr[0]; for (int i = 1; i < n; i++) if (arr[i] > max) max = arr[i]; return max; } int main(void) { int arr[] = {3, 7, 1, 9, 4}; printf("%d\n", findLargest(arr, 5)); return 0; }

Expected output:

text

9

Medium Problems

Problem 8: Increment Variable Using Pointer

c

#include <stdio.h> void increment(int *p) { if (p != NULL) (*p)++; } int main(void) { int x = 42; increment(&x); printf("%d\n", x); return 0; }

Expected output:

text

43

Problem 9: Print Address of Variable

c

#include <stdio.h> int main(void) { int x = 10; int *p = &x; printf("Address of x: %p\n", (void*)&x); printf("Value of p: %p\n", (void*)p); printf("Value of x via pointer: %d\n", *p); return 0; }

Expected output (addresses vary):

text

Address of x: 0x7ffc1234 Value of p: 0x7ffc1234 Value of x via pointer: 10

Problem 10: Array Insert at Index

c

#include <stdio.h> int insertAtIndex(int arr[], int n, int capacity, int idx, int value) { if (idx < 0 || idx > n || n >= capacity) return -1; for (int i = n; i > idx; i--) arr[i] = arr[i - 1]; arr[idx] = value; return n + 1; } int main(void) { int arr[10] = {1, 2, 3, 5}; int n = 4; n = insertAtIndex(arr, n, 10, 3, 4); for (int i = 0; i < n; i++) printf("%d ", arr[i]); printf("\n"); return 0; }

Expected output:

text

1 2 3 4 5

Problem 11: First Repeating Element

c

#include <stdio.h> #include <stdlib.h> #define TABLE_SIZE 1024 typedef struct { int *keys; int *seen; int capacity; } IntSet; static unsigned int hashFunc(int key, int cap) { return ((unsigned int)key) % (unsigned int)cap; } IntSet *setCreate(int cap) { IntSet *s = malloc(sizeof(IntSet)); if (s == NULL) return NULL; s->keys = malloc((size_t)cap * sizeof(int)); s->seen = calloc((size_t)cap, sizeof(int)); if (s->keys == NULL || s->seen == NULL) { free(s->keys); free(s->seen); free(s); return NULL; } s->capacity = cap; return s; } void setFree(IntSet *s) { if (s) { free(s->keys); free(s->seen); free(s); } } int setInsert(IntSet *s, int key) { unsigned int idx = hashFunc(key, s->capacity); int start = (int)idx; while (s->seen[idx]) { if (s->keys[idx] == key) return 0; idx = (idx + 1) % (unsigned int)s->capacity; if ((int)idx == start) return -1; } s->keys[idx] = key; s->seen[idx] = 1; return 1; } int firstRepeating(const int arr[], int n) { IntSet *s = setCreate(TABLE_SIZE); if (s == NULL) return -1; int result = -1; for (int i = 0; i < n; i++) { if (!setInsert(s, arr[i])) { result = arr[i]; break; } } setFree(s); return result; } int main(void) { int arr[] = {10, 5, 3, 4, 3, 5, 6}; printf("%d\n", firstRepeating(arr, 7)); return 0; }

Expected output:

text

3

Problem 12: Reverse String

c

#include <stdio.h> #include <string.h> void reverseString(char s[]) { int i = 0, j = (int)strlen(s) - 1; while (i < j) { char tmp = s[i]; s[i] = s[j]; s[j] = tmp; i++; j--; } } int main(void) { char s[] = "Hello"; reverseString(s); printf("%s\n", s); return 0; }

Expected output:

text

olleH

Problem 13: Anagram Check

c

#include <stdio.h> #include <string.h> #include <stdbool.h> bool isAnagram(const char *a, const char *b) { if (strlen(a) != strlen(b)) return false; int count[256] = {0}; for (int i = 0; a[i]; i++) count[(unsigned char)a[i]]++; for (int i = 0; b[i]; i++) { if (--count[(unsigned char)b[i]] < 0) return false; } return true; } int main(void) { printf("%s\n", isAnagram("listen", "silent") ? "true" : "false"); printf("%s\n", isAnagram("hello", "world") ? "true" : "false"); return 0; }

Expected output:

text

true false

Problem 14: Struct Area (Rectangle)

c

#include <stdio.h> struct Rect { double width, height; }; double area(struct Rect r) { return r.width * r.height; } int main(void) { struct Rect r = {4.5, 3.0}; printf("%.2f\n", area(r)); return 0; }

Expected output:

text

13.50

Problem 15: Add Two Complex Numbers

c

#include <stdio.h> struct Complex { double real, imag; }; struct Complex addComplex(struct Complex a, struct Complex b) { struct Complex c; c.real = a.real + b.real; c.imag = a.imag + b.imag; return c; } int main(void) { struct Complex x = {2.0, 3.0}; struct Complex y = {4.0, -1.0}; struct Complex z = addComplex(x, y); printf("%.2f + %.2fi\n", z.real, z.imag); return 0; }

Expected output:

text

6.00 + 2.00i

Problem 16: Implement Union (Type Punning)

c

#include <stdio.h> #include <stdint.h> union FloatBits { float f; uint32_t bits; }; int main(void) { union FloatBits u; u.f = 3.14f; printf("Float: %f, Bits: 0x%08X\n", u.f, u.bits); return 0; }

Expected output (typical):

text

Float: 3.140000, Bits: 0x4048F5C3

Problem 17: Maximum in Struct Array

c

#include <stdio.h> struct Student { int id; int grade; }; struct Student topStudent(const struct Student arr[], int n) { struct Student top = arr[0]; for (int i = 1; i < n; i++) { if (arr[i].grade > top.grade) top = arr[i]; } return top; } int main(void) { struct Student arr[] = {{1, 85}, {2, 92}, {3, 78}}; struct Student top = topStudent(arr, 3); printf("ID %d, Grade %d\n", top.id, top.grade); return 0; }

Expected output:

text

ID 2, Grade 92

Problem 18: Self-Referencing Structures

c

#include <stdio.h> #include <stdlib.h> struct Node { int value; struct Node *next; }; int main(void) { /* Build a tiny linked list: 10 -> 20 -> 30 */ struct Node *head = malloc(sizeof(struct Node)); if (!head) return 1; head->value = 10; head->next = malloc(sizeof(struct Node)); if (!head->next) { free(head); return 1; } head->next->value = 20; head->next->next = malloc(sizeof(struct Node)); if (!head->next->next) { free(head->next); free(head); return 1; } head->next->next->value = 30; head->next->next->next = NULL; for (struct Node *p = head; p; p = p->next) printf("%d ", p->value); printf("\n"); /* Free */ while (head) { struct Node *tmp = head; head = head->next; free(tmp); } return 0; }

Expected output:

text

10 20 30

Problem 19: Swap Numbers Using a Function

c

#include <stdio.h> void swap(int *a, int *b) { int tmp = *a; *a = *b; *b = tmp; } int main(void) { int x = 5, y = 9; swap(&x, &y); printf("x=%d, y=%d\n", x, y); return 0; }

Expected output:

text

x=9, y=5

Bonus: 2D World Representation

c

#include <stdio.h> #define ROWS 3 #define COLS 4 int main(void) { char grid[ROWS][COLS] = { {'.', '.', '#', '.'}, {'.', '#', '.', '.'}, {'.', '.', '.', '#'} }; for (int r = 0; r < ROWS; r++) { for (int c = 0; c < COLS; c++) putchar(grid[r][c]); putchar('\n'); } return 0; }

Expected output:

text

..#. .#.. ...#

Common Pitfalls, Edge Cases & Memory Considerations

1. Array Bounds

arr[n] is out of bounds for a size-n array. Check bounds before every write.

2. Array Decay

Arrays decay to pointers when passed to functions; sizeof returns pointer size.

c

void f(int arr[]) { size_t s = sizeof(arr); /* pointer size */ }

Pass the length explicitly.

3. String Termination

Every C string must end with '\0'. Forgetting this breaks printf("%s", ...) and all str* functions.

4. strcpy Overflow

Use snprintf or strncpy with explicit bounds and manual null termination.

c

strncpy(dest, src, size - 1); dest[size - 1] = '\0';

5. Struct Padding

sizeof(struct) may exceed the sum of member sizes due to alignment. Use sizeof for allocation.

6. Union Field Access

Reading a union member that wasn’t last written is undefined behavior (except for unsigned char aliasing).

7. Pointer Dereferencing

Check for NULL before dereferencing. Free heap allocations exactly once.

8. Aliasing

Two pointers pointing to the same memory can cause subtle bugs in swap or comparison code.

9. Off-by-One in Loops

i <= n iterates n+1 times. Use < when appropriate.

10. Memory Considerations

  • Prefer stack arrays for small, short-lived data.
  • Use malloc/free for large or dynamic data.
  • Free every allocation; never return pointers to stack data.
  • Zero-initialize structs with {0} when partial initialization is expected.

Summary & Best Practices

Compound data types are the vocabulary of C programming. Mastering them unlocks larger programs, cleaner designs, and better performance.

Key Takeaways

  • Arrays are contiguous and index-based; size is not stored.
  • Pointers store addresses; arithmetic scales by type size.
  • Strings are char arrays terminated by '\0'.
  • Structs group different types; watch out for padding.
  • Unions overlap storage; access only the last-written member.

Best Practices

  1. Always pass array length to functions.
  2. Check every malloc and free every allocation.
  3. Use snprintf for formatted string building.
  4. Prefer sizeof for array and struct sizes.
  5. Zero-initialize structs when only some fields are set.
  6. Never access out-of-bounds array elements.
  7. Cast to unsigned char for character processing.
  8. Return structs by value for small ones; use pointers for large.
  9. Test edge cases: empty arrays, single element, negative numbers.
  10. Compile with -Wall -Wextra -std=c11 to catch issues early.

By working through these 19 problems, you’ll build the practical fluency required for real C programming.

Master C functions with 11 graded practice problems: print, sum, factorial, prime factorization, recursion, Fibonacci. Full C99 code and edge cases.

C Functions Practice Problems: Complete Guide

C Functions Practice Problems: A Comprehensive Tutorial

Functions are the basic building blocks of C programs. They enhance modularity and code reusability by separating the logic of a particular task from the main code and letting you call it whenever required. This tutorial walks through 11 graded practice problems covering function definition, parameters, return values, and recursion—with complete, portable C99/C11 solutions.

Each problem is self-contained, includes inline comments, and shows the exact expected output.


Conceptual Introduction & Fundamentals

Why Practice Functions?

Functions are where beginner C programmers typically plateau. Writing a few functions isn’t enough—you need to internalize:

  • Parameter passing (by value vs. by pointer).
  • Return values vs. output parameters.
  • Recursion and its call-stack footprint.
  • Decomposition: breaking problems into small, testable units.
  • Interface design: naming, signatures, and boundary conditions.

The Function Toolkit

ConceptPurpose
Function prototypeDeclares the signature to the compiler
Function definitionProvides the implementation
ParametersInput values (copied by default)
Return valueSingle output via return
RecursionA function calling itself
voidUsed for “no value”

Function Signature Template

c

return_type function_name(parameter_list);

Example:

c

int add(int a, int b); /* prototype */ int add(int a, int b) { /* definition */ return a + b; }

Progress Plan

The problems below start simple (print, sum) and advance to recursion and number-theory style tasks.


Syntax & Core Mechanics

Parameters and Return Values

C uses pass by value by default. To modify the caller’s variable, pass a pointer:

c

void increment(int *p) { (*p)++; }

Recursion: Two Requirements

  1. Base case: when the function stops calling itself.
  2. Recursive case: a call with a “smaller” problem.

c

int factorial(int n) { if (n <= 1) return 1; /* base */ return n * factorial(n - 1); /* recursive */ }

Function Pointers (Brief)

C lets you store functions in variables, enabling callbacks:

c

int (*op)(int, int) = add; int result = op(3, 4);

Full Practical Code Example with Explanation

Below are 11 problems, each a complete C program.

Problem 1: Print “Hello” Using a Function

c

#include <stdio.h> void printHello(void) { printf("Hello\n"); } int main(void) { printHello(); return 0; }

Expected output:

text

Hello

Problem 2: Function to Print Sum

Prints the sum of two integers (returns void).

c

#include <stdio.h> void printSum(int a, int b) { printf("%d\n", a + b); } int main(void) { printSum(3, 4); return 0; }

Expected output:

text

7

Problem 3: Function to Return Sum

Returns the sum.

c

#include <stdio.h> int add(int a, int b) { return a + b; } int main(void) { printf("%d\n", add(10, 20)); return 0; }

Expected output:

text

30

Problem 4: Factorial Using a Function

Returns n!.

c

#include <stdio.h> unsigned long long factorial(int n) { if (n < 0) return 0; /* error sentinel */ unsigned long long result = 1; for (int i = 2; i <= n; i++) result *= i; return result; } int main(void) { for (int i = 0; i <= 10; i++) { printf("%d! = %llu\n", i, factorial(i)); } return 0; }

Expected output:

text

0! = 1 1! = 1 2! = 2 3! = 6 4! = 24 5! = 120 6! = 720 7! = 5040 8! = 40320 9! = 362880 10! = 3628800

Note: 20! overflows unsigned long long; use a bignum for larger inputs.

Problem 5: Prime Factorization

Print the prime factorization of n.

c

#include <stdio.h> void primeFactorize(long long n) { if (n <= 1) return; for (long long p = 2; p * p <= n; p++) { while (n % p == 0) { printf("%lld ", p); n /= p; } } if (n > 1) printf("%lld", n); printf("\n"); } int main(void) { primeFactorize(60); /* 2 2 3 5 */ primeFactorize(97); /* 97 */ primeFactorize(1); /* (nothing) */ return 0; }

Expected output:

text

2 2 3 5 97

Notes: The loop stops at p * p <= n; if a remainder >1 is left, it’s a prime factor.

Problem 6: Table Difference

Given n, print the difference between n × i and n × (i-1) for i = 1..10.

c

#include <stdio.h> void tableDiff(int n) { for (int i = 1; i <= 10; i++) { printf("%d ", n * i - n * (i - 1)); } printf("\n"); } int main(void) { tableDiff(7); return 0; }

Expected output:

text

7 7 7 7 7 7 7 7 7 7

Notes: Mathematically the difference is always n; the exercise tests loop and function basics.

Problem 7: Decimal to Binary

Convert a non-negative integer to its binary representation.

c

#include <stdio.h> void decimalToBinary(unsigned int n) { if (n == 0) { printf("0\n"); return; } char buf[33]; /* 32 bits + null */ int i = 0; while (n > 0) { buf[i++] = '0' + (n & 1u); n >>= 1; } /* Reverse */ for (int j = i - 1; j >= 0; j--) putchar(buf[j]); putchar('\n'); } int main(void) { decimalToBinary(0); /* 0 */ decimalToBinary(5); /* 101 */ decimalToBinary(255); /* 11111111 */ decimalToBinary(1024); /* 10000000000 */ return 0; }

Expected output:

text

0 101 11111111 10000000000

Problem 8: Binary to Decimal

Parse a binary string into an integer.

c

#include <stdio.h> #include <string.h> unsigned long long binaryToDecimal(const char *s) { unsigned long long value = 0; for (int i = 0; s[i] != '\0'; i++) { if (s[i] != '0' && s[i] != '1') return 0; /* invalid */ value = (value << 1) | (unsigned long long)(s[i] - '0'); } return value; } int main(void) { printf("%llu\n", binaryToDecimal("0")); /* 0 */ printf("%llu\n", binaryToDecimal("101")); /* 5 */ printf("%llu\n", binaryToDecimal("11111111")); /* 255 */ return 0; }

Expected output:

text

0 5 255

Problem 9: Digital Root

The digital root of a non-negative integer is the repeated digit-sum until a single digit remains.

c

#include <stdio.h> int digitalRoot(long long n) { if (n == 0) return 0; if (n < 0) n = -n; return 1 + (int)((n - 1) % 9); } int main(void) { printf("%d\n", digitalRoot(0)); /* 0 */ printf("%d\n", digitalRoot(9875)); /* 2 */ printf("%d\n", digitalRoot(12345)); /* 6 */ printf("%d\n", digitalRoot(9)); /* 9 */ return 0; }

Expected output:

text

0 2 6 9

Notes: The formula 1 + (n-1) % 9 is O(1); the iterative version is O(log n).

Problem 10: Sum of Digits of a Number

c

#include <stdio.h> int digitSum(long long n) { if (n < 0) n = -n; int sum = 0; while (n > 0) { sum += (int)(n % 10); n /= 10; } return sum; } int main(void) { printf("%d\n", digitSum(0)); /* 0 */ printf("%d\n", digitSum(12345)); /* 15 */ printf("%d\n", digitSum(-987)); /* 24 */ return 0; }

Expected output:

text

0 15 24

Problem 11: Fibonacci Using Recursion

c

#include <stdio.h> unsigned long long fib(unsigned int n) { if (n <= 1) return n; return fib(n - 1) + fib(n - 2); } int main(void) { for (unsigned int i = 0; i <= 12; i++) { printf("F(%u) = %llu\n", i, fib(i)); } return 0; }

Expected output:

text

F(0) = 0 F(1) = 1 F(2) = 1 F(3) = 2 F(4) = 3 F(5) = 5 F(6) = 8 F(7) = 13 F(8) = 21 F(9) = 34 F(10) = 55 F(11) = 89 F(12) = 144

Notes: Naive recursion is O(2ⁿ). For n > 40, switch to memoization or iteration.


Common Pitfalls, Edge Cases & Memory Considerations

1. Missing or Wrong Prototype

If a function is called before its definition, a prototype is mandatory. Mismatched prototypes cause link errors.

2. Returning a Pointer to a Local

c

int *bad(void) { int x = 5; return &x; } /* UB */

Return a static variable, allocate on heap, or pass a buffer.

3. Recursion Without Base Case

c

int fact(int n) { return n * fact(n - 1); } /* infinite recursion */

Always define the base case.

4. Overflowing Return Types

int factorial overflows at 13!. Use unsigned long long for n ≤ 20; beyond that, use bignums.

5. Integer Division Truncation

c

double avg = sum / n; /* integer division if both are int */ double avg = (double)sum / n; /* correct */

6. Off-by-One in Loops

for (i = 1; i <= n; i++) iterates n times; for (i = 0; i < n; i++) also n times. Pick per problem.

7. Mutable Global State in Recursion

Recursive functions should be pure. Avoid static counters unless intentional.

8. Uninitialized Pointer Parameters

If a function receives int *p, verify p != NULL before dereferencing.

9. char in <ctype.h>

Cast to unsigned char before calling isdigit, isalpha, etc.

10. Memory Considerations

  • Prefer stack-allocated buffers for small data.
  • Use malloc/free when size is unknown at compile time.
  • Never return pointers to stack-allocated data.
  • Recursion consumes stack proportional to depth — watch out for deep recursion.

Summary & Best Practices

Functions are the atom of good C code. Practice with small, complete examples teaches you the interface and the mechanics simultaneously.

Key Takeaways

  • Functions encapsulate logic and enable reuse.
  • Pass by value by default; use pointers to modify caller state.
  • Recursion needs a base case and progress toward it.
  • Function signatures are contracts — be consistent.
  • Return types limit the range of values safely returned.

Best Practices

  1. Write small, single-purpose functions.
  2. Always provide prototypes (or place definitions before use).
  3. Name functions with verbs (computeSum, printTable).
  4. Handle boundary conditions: n = 0, negatives, overflow.
  5. Use void explicitly for no-return functions and no-parameter signatures.
  6. Prefer iteration over recursion when the recursion isn’t natural.
  7. Use memoization for expensive recursive computations.
  8. Return status via value, output via pointer.
  9. Compile with -Wall -Wextra -pedantic -std=c11.
  10. Test each function in isolation before composing programs.

Suggested Practice Order

  1. Print Hello
  2. Print Sum
  3. Return Sum
  4. Factorial
  5. Prime Factorization
  6. Table Difference
  7. Decimal to Binary
  8. Binary to Decimal
  9. Digital Root
  10. Sum of Digits
  11. Fibonacci Using Recursion

Working through this list with the templates above builds the muscle memory required for larger C projects.

Master C fundamentals with practice problems: variables, data types, I/O, operators, conditionals, and loops. Full C99 code with edge cases.

C Fundamental Practice Problems: Complete Guide

C Fundamental Practice Problems: A Comprehensive Tutorial

Learning C is not about memorizing syntax—it’s about building muscle memory through practice. This tutorial is a structured practice guide covering the fundamental concepts every C programmer must master: variables, data types, input/output, format specifiers, operators, conditional statements, and loops.

Each section explains the concept, then walks through a representative practice problem with a complete, portable C99/C11 solution and expected output.


Conceptual Introduction & Fundamentals

Why Practice Fundamentals?

Fundamentals are the foundation of every C program, whether it’s 20 lines or 20,000. Weak fundamentals show up as:

  • Off-by-one errors in loops.
  • Format specifier mismatches causing undefined behavior.
  • Buffer overflows from unsafe input functions.
  • Silent type conversion bugs in arithmetic.
  • Incorrect edge-case handling in conditionals.

The Fundamental Topics

TopicPurpose
VariablesNamed storage for values
Data TypesSize and interpretation of values
Input / OutputCommunicating with the user
Format SpecifiersControlling how data is printed or read
OperatorsArithmetic, relational, logical, bitwise
Conditional StatementsBranching logic (if, switch)
LoopsRepetition (for, while, do-while)

Difficulty Progression

  • Easy: Printing, variables, simple arithmetic, I/O.
  • Medium: Control flow, digit manipulation, number theory basics.

Syntax & Core Mechanics

1. Variables and Data Types

c

int age = 25; double price = 19.99; char grade = 'A';

Use int for whole numbers, double for decimals, char for single characters. Prefer double over float for computation; use long long when values may exceed int.

2. Input with fgets and Parsing

Never use scanf("%s", ...) or gets for strings. Prefer fgets plus strtol/strtod for robust parsing.

c

char buffer[64]; if (fgets(buffer, sizeof(buffer), stdin) == NULL) return 1; int value = (int)strtol(buffer, NULL, 10);

3. Output with printf

Match the specifier to the type:

TypeSpecifier
int%d
unsigned int%u
long long%lld
double%f, %lf, %g
char%c
String%s
size_t%zu
Pointer%p

4. Operators

c

int a = 7, b = 3; int q = a / b; /* 2 — integer division */ int r = a % b; /* 1 */ double f = (double)a / b; /* 2.333... */

Use casting to force floating-point division.

5. Conditionals and Loops

c

if (a > b) { /* ... */ } else if (a == b) { /* ... */ } else { /* ... */ } for (int i = 0; i < n; i++) { /* ... */ } while (cond) { /* ... */ } do { /* ... */ } while (cond);

Full Practical Code Example with Explanation

Below are eight representative problems covering the fundamentals, each a self-contained C99 program with verified output.

Problem 1: Print “GeeksforGeeks” with Quotes

Goal: Print a string including literal double quotes.

c

#include <stdio.h> int main(void) { /* Escape double quotes with backslash */ printf("\"GeeksforGeeks\"\n"); return 0; }

Expected output:

text

"GeeksforGeeks"

Notes: \" inside a string literal produces a literal ".

Problem 2: Swap Two Numbers (Without Temp for Integers)

Goal: Swap a and b.

c

#include <stdio.h> int main(void) { int a = 5, b = 7; /* Arithmetic swap (works for ints; watch overflow) */ a = a + b; b = a - b; a = a - b; printf("a = %d, b = %d\n", a, b); return 0; }

Expected output:

text

a = 7, b = 5

Notes: For large values, prefer a temporary variable or XOR swap. XOR is safe for equal values but not for aliasing.

Problem 3: Convert double to int

Goal: Truncate a floating-point number.

c

#include <stdio.h> int main(void) { double d = 3.99; int i = (int)d; /* explicit cast truncates toward zero */ printf("%d\n", i); return 0; }

Expected output:

text

3

Notes: (int) truncates; round() from <math.h> rounds to nearest.

Problem 4: Last Digit of a Number

Goal: Extract the last decimal digit.

c

#include <stdio.h> #include <stdlib.h> int main(void) { int n = 12345; int last = n % 10; /* Works for non-negative; see notes for negatives */ printf("%d\n", last); return 0; }

Expected output:

text

5

Notes: For negative n, % in C99 yields a non-positive remainder. Use abs(n) % 10 or n % 10 + 10 (mod 10) for a non-negative digit.

Problem 5: Compare Two Numbers

Goal: Print which of two numbers is greater.

c

#include <stdio.h> int main(void) { int a = 10, b = 20; if (a > b) printf("%d is greater\n", a); else if (b > a) printf("%d is greater\n", b); else printf("Both are equal\n"); return 0; }

Expected output:

text

20 is greater

Problem 6: FizzBuzz (1 to N)

Goal: Print Fizz, Buzz, or FizzBuzz per divisibility rule.

c

#include <stdio.h> void fizzbuzz(int n) { for (int i = 1; i <= n; i++) { if (i % 15 == 0) printf("FizzBuzz "); else if (i % 3 == 0) printf("Fizz "); else if (i % 5 == 0) printf("Buzz "); else printf("%d ", i); } printf("\n"); } int main(void) { fizzbuzz(15); return 0; }

Expected output:

text

1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz

Notes: Check i % 15 == 0 first, otherwise the individual cases shadow the combined one.

Problem 7: Multiplication Table

Goal: Print an n × n multiplication table.

c

#include <stdio.h> int main(void) { int n = 5; for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { printf("%4d", i * j); } printf("\n"); } return 0; }

Expected output:

text

 1 2 3 4 5 2 4 6 8 10 3 6 9 12 15 4 8 12 16 20 5 10 15 20 25

Problem 8: Sum of N Numbers with Safe Input

Goal: Read n integers from stdin, sum them.

c

#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <limits.h> int readInteger(const char *prompt, int *outValue) { char buffer[64]; 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 < INT_MIN || val > INT_MAX) return -1; *outValue = (int)val; return 0; } int main(void) { int n; if (readInteger("How many numbers? ", &n) != 0 || n <= 0) { fprintf(stderr, "Invalid count.\n"); return 1; } long long sum = 0; for (int i = 0; i < n; i++) { int x; char prompt[40]; snprintf(prompt, sizeof(prompt), "Number %d: ", i + 1); if (readInteger(prompt, &x) != 0) { fprintf(stderr, "Invalid number.\n"); return 1; } sum += x; } printf("Sum = %lld\n", sum); return 0; }

Sample run:

text

How many numbers? 3 Number 1: 10 Number 2: 20 Number 3: 30 Sum = 60

Notes: Uses long long for the sum to prevent overflow; strtol for robust parsing.

Bonus Problem: Medium — Count Digits of an Integer

c

#include <stdio.h> #include <stdlib.h> int countDigits(long long n) { if (n == 0) return 1; if (n < 0) n = -n; int count = 0; while (n > 0) { count++; n /= 10; } return count; } int main(void) { printf("%d\n", countDigits(0)); /* 1 */ printf("%d\n", countDigits(12345)); /* 5 */ printf("%d\n", countDigits(-9876)); /* 4 */ return 0; }

Expected output:

text

1 5 4

Bonus Problem: Medium — Leap Year

c

#include <stdio.h> int isLeapYear(int year) { /* A year is leap if divisible by 4 but not 100, unless divisible by 400 */ return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); } int main(void) { int years[] = {1900, 2000, 2024, 2023, 2100}; for (int i = 0; i < 5; i++) { printf("%d → %s\n", years[i], isLeapYear(years[i]) ? "Leap" : "Not Leap"); } return 0; }

Expected output:

text

1900 → Not Leap 2000 → Leap 2024 → Leap 2023 → Not Leap 2100 → Not Leap

Common Pitfalls, Edge Cases & Memory Considerations

1. Integer Division Truncation

5 / 2 is 2, not 2.5. Cast one operand.

c

double r = (double)a / b;

2. Format Specifier Mismatch

printf("%d", 3.14) is undefined behavior. Match specifiers to types.

3. Negative Remainders

In C99, -7 % 3 == -1, not 2. Use (n % m + m) % m for a non-negative result.

4. Off-by-One in Loops

for (i = 0; i <= n; i++) iterates n+1 times. Use < unless you need inclusive bounds.

5. Unsafe Input

Never use gets (removed in C11). Prefer fgets with size limits.

6. Uninitialized Variables

Reading before assignment is undefined behavior.

c

int x; printf("%d\n", x); /* BUG */

7. Signed/Unsigned Confusion

Mixing signed and unsigned in comparisons yields surprising results.

c

int a = -1; unsigned int b = 1; if (a < b) { /* may be FALSE due to conversion */ }

8. Overflow

int overflows for large sums. Use long long.

c

long long sum = 0;

9. Character Classifications

Use <ctype.h> functions for portability.

c

if (isdigit((unsigned char)c)) { /* ... */ }

Cast to unsigned char before passing.

10. Memory Considerations

  • Fundamental programs rarely allocate memory.
  • When they do, prefer dynamic allocation for large inputs.
  • Free everything you malloc.
  • Never return pointers to stack variables.

Summary & Best Practices

Fundamentals are the platform on which all C skill is built. Practice them until they become reflexive.

Key Takeaways

  • Variables and types define what can be stored.
  • Format specifiers define how data is printed/read.
  • Operators and control flow define how programs compute.
  • Safe input requires fgets + strtol/strtod.
  • Every bug you encounter at scale is a fundamentals bug in disguise.

Best Practices

  1. Compile with -Wall -Wextra -pedantic -std=c11 on every exercise.
  2. Match format specifiers to types.
  3. Use long long for sums; double for decimals.
  4. Prefer fgets over scanf("%s", ...).
  5. Cast before division when you need floating-point results.
  6. Initialize all variables.
  7. Handle negative remainders explicitly.
  8. Cast char to unsigned char for <ctype.h> functions.
  9. Test boundary cases: zero, negatives, maxima, empty input.
  10. Practice daily — fundamentals are perishable without repetition.

Recommended Practice Progression

  • Easy: Print GeeksforGeeks; Global Variable; Swap Numbers; Convert double to int; Input Integer; Print with Space/Without/Separator/New Line; Be Precise; Print with Quotes; Last Digit; Compute a/b; Compare Two Numbers; Sign Check; Big Number; Sum of N; Multiplication Table.
  • Medium: AP Term; GP Term; Even Odd Game; Greatest of Three; Leap Year; FizzBuzz; Calculator; First Digit; Count Digits; Days Before n Days; Prime Factorization; Next Prime.

Work through each exercise with the templates and safety rules above. By the time you finish, fundamentals become second nature.

Master Sliding Window interview questions in C: 20 theoretical Q&A plus full code for fixed and variable windows, frequency maps, and edge cases.

Sliding Window Interview Questions in C: 20 Q&A

Sliding Window Technique Interview Questions in C: A Comprehensive Tutorial

The Sliding Window Technique is a top-tier interview topic because it appears in dozens of variations across array, string, and streaming problems. Interviewers test both your conceptual understanding and your ability to implement it correctly in C with safe memory practices.

This tutorial combines 20 curated interview questions with model answers and full C implementations of the most representative problems across easy, medium, and hard levels. Each code block is C99/C11-compliant, includes explicit memory management, and has verified expected output.


Conceptual Introduction & Fundamentals

What is the Sliding Window Technique?

The Sliding Window Technique processes a contiguous range of elements (a “window”) by expanding or shrinking it as it traverses the data, reusing work from the previous window instead of recomputing.

Core properties:

  • Maintains a window using two pointers (usually left and right).
  • Each element enters and leaves the window at most once → O(n) time.
  • Uses O(1) to O(k) auxiliary space depending on the problem.
  • Works on arrays, strings, and streams.

Why Interviewers Love It

  • Discriminating: Separates candidates who can reason about invariants.
  • Practical: Appears in real systems (network buffers, log analysis).
  • Composable: Combines with hashing, deques, and prefix sums.
  • Testable: Easy to design follow-up questions.

The Two Variants

VariantWindow SizeTypical Problems
Fixed-SizeConstant k givenMax sum of size k, averages, anagram counts
Variable-SizeDepends on conditionLongest unique substring, minimum window

When Not to Use Sliding Window

  • Non-contiguous subsets (use DP or brute force).
  • Arbitrary reordering (use sorting or hashing).
  • Conditions that are not monotone over the window.

Syntax & Core Mechanics

Fixed-Size Template

c

/* Compute initial window sum for [0, k-1] */ long long window = 0; for (int i = 0; i < k; i++) window += arr[i]; long long best = window; /* Slide: subtract outgoing, add incoming */ for (int i = k; i < n; i++) { window += arr[i] - arr[i - k]; if (window > best) best = window; }

Variable-Size Template

c

int left = 0; /* Optional: state (freq map, sum, count) */ for (int right = 0; right < n; right++) { /* 1. Include arr[right] into state */ /* 2. While invariant violated, shrink from left */ while (/* invariant violated */) { /* remove arr[left] from state */ left++; } /* 3. Update best answer using [left, right] */ }

Key Invariant

For variable-size windows, we maintain:

All elements in [left, right] satisfy the problem’s condition, and right - left + 1 is the current candidate.

When the condition breaks, shrink left until the condition holds again.


Full Practical Code Example with Explanation

Below are six complete C programs covering the most-asked interview problems. Each is self-contained, C99/C11-compliant, and includes expected output.

Problem 1: Maximum Sum of Subarray of Size k (Easy)

c

#include <stdio.h> #include <limits.h> long long maxSumFixedK(const int arr[], int n, int k) { if (k <= 0 || n < k) return LLONG_MIN; long long window = 0; for (int i = 0; i < k; i++) window += arr[i]; long long best = window; for (int i = k; i < n; i++) { window += arr[i] - arr[i - k]; if (window > best) best = window; } return best; } int main(void) { int arr[] = {5, 2, -1, 0, 3}; int n = sizeof(arr) / sizeof(arr[0]); printf("%lld\n", maxSumFixedK(arr, n, 3)); /* 6 */ return 0; }

Expected output:

text

6

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

Problem 2: Longest Substring Without Repeating Characters (Medium)

c

#include <stdio.h> int longestUnique(const char *s) { int lastSeen[256]; for (int i = 0; i < 256; i++) lastSeen[i] = -1; int left = 0, best = 0; for (int right = 0; s[right] != '\0'; right++) { unsigned char c = (unsigned char)s[right]; if (lastSeen[c] >= left) left = lastSeen[c] + 1; lastSeen[c] = right; int len = right - left + 1; if (len > best) best = len; } return best; } int main(void) { printf("%d\n", longestUnique("abcabcbb")); /* 3 */ printf("%d\n", longestUnique("bbbbb")); /* 1 */ printf("%d\n", longestUnique("pwwkew")); /* 3 */ return 0; }

Expected output:

text

3 1 3

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

Problem 3: Longest Repeating Character Replacement (Medium)

Given a string and integer k, find the longest substring that can be turned into a single repeated character using at most k replacements.

c

#include <stdio.h> int characterReplacement(const char *s, int k) { int count[26] = {0}; int left = 0, maxCount = 0, best = 0; for (int right = 0; s[right] != '\0'; right++) { int idx = s[right] - 'A'; count[idx]++; if (count[idx] > maxCount) maxCount = count[idx]; /* window size = right - left + 1; replacements needed = window size - maxCount */ while ((right - left + 1) - maxCount > k) { count[s[left] - 'A']--; left++; } int len = right - left + 1; if (len > best) best = len; } return best; } int main(void) { printf("%d\n", characterReplacement("AABABBA", 1)); /* 4 */ printf("%d\n", characterReplacement("ABAB", 2)); /* 4 */ return 0; }

Expected output:

text

4 4

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

Problem 4: Minimum Window Substring (Hard)

Find the smallest substring of s that contains all characters of t (with multiplicities).

c

#include <stdio.h> #include <string.h> #include <limits.h> void minWindow(const char *s, const char *t, char *result, int resultSize) { result[0] = '\0'; if (t[0] == '\0' || s[0] == '\0') return; int need[256] = {0}, have[256] = {0}; int required = 0; for (int i = 0; t[i] != '\0'; i++) { if (need[(unsigned char)t[i]] == 0) required++; need[(unsigned char)t[i]]++; } int left = 0, formed = 0; int bestLen = INT_MAX, bestL = -1; int sLen = (int)strlen(s); for (int right = 0; right < sLen; right++) { unsigned char c = (unsigned char)s[right]; have[c]++; if (need[c] > 0 && have[c] == need[c]) formed++; while (formed == required) { int len = right - left + 1; if (len < bestLen) { bestLen = len; bestL = left; } unsigned char lc = (unsigned char)s[left]; have[lc]--; if (need[lc] > 0 && have[lc] < need[lc]) formed--; left++; } } if (bestL != -1 && bestLen < resultSize) { memcpy(result, s + bestL, (size_t)bestLen); result[bestLen] = '\0'; } } int main(void) { char result[256]; minWindow("ADOBECODEBANC", "ABC", result, sizeof(result)); printf("%s\n", result); /* BANC */ minWindow("a", "a", result, sizeof(result)); printf("%s\n", result); /* a */ minWindow("a", "aa", result, sizeof(result)); printf("\"%s\"\n", result); /* "" */ return 0; }

Expected output:

text

BANC a ""

Time: O(|s| + |t|), Space: O(1)

Problem 5: Maximum Consecutive 1s with K Flips (Medium)

Given a binary array and k flips allowed, find the longest subarray of all 1s.

c

#include <stdio.h> int longestOnes(const int arr[], int n, int k) { int left = 0, zeros = 0, best = 0; for (int right = 0; right < n; right++) { if (arr[right] == 0) zeros++; while (zeros > k) { if (arr[left] == 0) zeros--; left++; } int len = right - left + 1; if (len > best) best = len; } return best; } int main(void) { int arr1[] = {1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0}; int arr2[] = {0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1}; printf("%d\n", longestOnes(arr1, 11, 2)); /* 6 */ printf("%d\n", longestOnes(arr2, 19, 3)); /* 10 */ return 0; }

Expected output:

text

6 10

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

Problem 6: Count Distinct Elements in Every Window of Size K (Medium)

For each window of size k, count distinct elements.

c

#include <stdio.h> #include <stdlib.h> #define TABLE_SIZE 4096 typedef struct { int *keys; int *counts; int *occupied; int capacity; } FreqMap; static unsigned int hashFunc(int key, int cap) { return ((unsigned int)key) % (unsigned int)cap; } static FreqMap *mapCreate(int cap) { FreqMap *m = malloc(sizeof(FreqMap)); if (m == NULL) return NULL; m->keys = malloc((size_t)cap * sizeof(int)); m->counts = calloc((size_t)cap, sizeof(int)); m->occupied = calloc((size_t)cap, sizeof(int)); if (m->keys == NULL || m->counts == NULL || m->occupied == NULL) { free(m->keys); free(m->counts); free(m->occupied); free(m); return NULL; } m->capacity = cap; return m; } static void mapFree(FreqMap *m) { if (m) { free(m->keys); free(m->counts); free(m->occupied); free(m); } } static int mapAdd(FreqMap *m, int key) { unsigned int idx = hashFunc(key, m->capacity); int start = (int)idx; while (m->occupied[idx]) { if (m->keys[idx] == key) { m->counts[idx]++; return m->counts[idx]; } idx = (idx + 1) % (unsigned int)m->capacity; if ((int)idx == start) return -1; } m->keys[idx] = key; m->counts[idx] = 1; m->occupied[idx] = 1; return 1; } static void mapRemove(FreqMap *m, int key) { unsigned int idx = hashFunc(key, m->capacity); int start = (int)idx; while (m->occupied[idx]) { if (m->keys[idx] == key) { m->counts[idx]--; return; } idx = (idx + 1) % (unsigned int)m->capacity; if ((int)idx == start) return; } } static int mapDistinct(FreqMap *m) { int d = 0; for (int i = 0; i < m->capacity; i++) if (m->occupied[i] && m->counts[i] > 0) d++; return d; } void countDistinctInWindows(const int arr[], int n, int k) { FreqMap *m = mapCreate(TABLE_SIZE); if (m == NULL) return; for (int i = 0; i < k; i++) mapAdd(m, arr[i]); printf("%d ", mapDistinct(m)); for (int i = k; i < n; i++) { mapRemove(m, arr[i - k]); mapAdd(m, arr[i]); printf("%d ", mapDistinct(m)); } printf("\n"); mapFree(m); } int main(void) { int arr[] = {1, 2, 1, 3, 4, 2, 3}; int n = 7, k = 4; countDistinctInWindows(arr, n, k); /* 3 4 4 3 */ return 0; }

Expected output:

text

3 4 4 3

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


Common Pitfalls, Edge Cases & Memory Considerations

1. Off-by-One in Window Size

The window [left, right] has size right - left + 1. Double-check inclusive bounds.

2. Non-Monotone Invariant

Sliding windows require the condition to monotonically improve as you shrink. Sum-with-negatives problems often violate this — use prefix sums instead.

3. Duplicate Handling in Frequency Maps

When shrinking, decrement counts; do not remove entries entirely (else your map’s occupied markers corrupt probes).

4. Initialization of the First Window

For fixed-size windows, initialize before entering the slide loop. Off-by-one here is the most common bug.

5. Integer Overflow

Use long long for sums and products.

6. Character Arrays and unsigned char

Negative char values (e.g., extended ASCII) index arrays with wraparound. Cast to unsigned char.

7. Empty Inputs

Always guard n <= 0 or s[0] == '\0'.

8. k > n or k = 0

Define and enforce semantics.

9. Variable Window When left > right

Some problems require resetting left = right when shrunk past the right boundary.

10. Memory Considerations

  • Fixed-size windows: O(1) space.
  • Variable-size with frequency map: O(k) or O(alphabet).
  • For substring problems, size frequency arrays to 256 (or larger for Unicode).
  • Free all dynamically allocated memory.

Summary & Best Practices

The Sliding Window Technique is a cornerstone of array and string interview problems. Mastering its two variants—fixed-size and variable-size—unlocks an entire family of linear-time solutions.

Key Takeaways

  • Two variants: fixed-size (constant k) and variable-size (condition-driven).
  • Each element enters and leaves the window at most once → O(n).
  • Invariant must be monotone: shrinking must restore validity.
  • Hash maps and frequency arrays integrate cleanly with the window.
  • Not every subarray problem is a sliding window problem.

Best Practices

  1. Identify the window type before writing code.
  2. Initialize the first window explicitly for fixed-size problems.
  3. Use long long for sums and large counters.
  4. Cast char to unsigned char before indexing arrays.
  5. Keep the invariant simple — one line that captures the goal.
  6. Prefer frequency arrays over hash maps for small alphabets.
  7. Free dynamic memory on all exit paths.
  8. Test with edge cases: empty, single element, k = 0, k = n, k > n.
  9. Verify with a brute-force solution for small inputs.
  10. Master the pattern, not just the specific problem — the interview will vary.

Recommended Practice List (from the source)

  • Easy: Maximum sum subarray of size k; Smallest window containing 0, 1, 2; Permutation substring; Strictly increasing subarrays; Remove consecutive characters.
  • Medium: Longest substring with distinct characters; Substrings with K Distinct; Maximum Consecutive 1s with K Flips; Maximum Fruits in Two Baskets; Minimum Window; Binary Subarray with Sum; Longest Repeating Character Replacement; Count Anagrams.
  • Hard: Maximum of Minimum for Every Window Size; Longest Substring with K Uniques; Minimum Window Substring; Largest Sum Subarray with At Least k Numbers.

Working through these problems with the templates above will solidify the pattern.

Learn the Sliding Window Technique in C: fixed and variable-size windows, max/min subarray sums, longest unique substring, and more with code.

Sliding Window Technique in C: Complete Guide

Sliding Window Technique in C: A Comprehensive Tutorial

The Sliding Window Technique is one of the most powerful and widely used algorithmic patterns for solving subarray and substring problems. Instead of recomputing results from scratch for each window, it maintains a “window” (a contiguous range) that slides across the data, updating the result incrementally. This reduces many O(n²) problems to O(n) — a massive win for large inputs.

This tutorial provides an in-depth exploration of the Sliding Window Technique in C: the two main variants (fixed and variable size), step-by-step mechanics, a full practical implementation of both, common pitfalls, and best practices.


Conceptual Introduction & Fundamentals

What is the Sliding Window Technique?

The Sliding Window Technique maintains a contiguous range (the window) over an array or string. As the window moves forward, we update the result incrementally, reusing the previous window’s state instead of recomputing it.

Key advantages:

  • O(n) time for many problems that would otherwise be O(n²) or O(n × k).
  • O(1) extra space in the fixed-size case.
  • Simple, uniform pattern applicable to arrays, strings, and streams.

Why Sliding Windows Matter

  • Subarray sum problems: Maximum sum of size k, smallest subarray with sum ≥ target.
  • String problems: Longest substring without repeating characters, anagram matching.
  • Streaming data: Smoothing, moving averages, rate limiting.
  • Interview preparation: Extremely common pattern.
  • Performance: Turns quadratic solutions into linear ones.

Two Types of Sliding Window

TypeWhen to UseExample Problem
Fixed SizeWindow size k is givenMax sum of subarray of size k
Variable SizeWindow size depends on a conditionLongest substring without repeats

Identifying Sliding Window Problems

Ask yourself:

  1. Does the problem involve subarrays or substrings?
  2. Is there a maximum/minimum or longest/shortest to find?
  3. Is there a constraint (sum ≥ k, no repeats, distinct elements ≤ k)?

If yes, sliding window is likely the right pattern.


Syntax & Core Mechanics

Fixed-Size Sliding Window

Steps:

  1. Compute the sum (or metric) of the first k elements.
  2. Slide the window one step: window_sum += arr[i] - arr[i - k].
  3. Track the maximum (or minimum) across all windows.

c

#include <stdio.h> #include <limits.h> int maxSumFixedK(const int arr[], int n, int k) { if (n < k || k <= 0) return -1; long long windowSum = 0; for (int i = 0; i < k; i++) windowSum += arr[i]; long long maxSum = windowSum; for (int i = k; i < n; i++) { windowSum += arr[i] - arr[i - k]; if (windowSum > maxSum) maxSum = windowSum; } return (int)maxSum; } int main(void) { int arr[] = {5, 2, -1, 0, 3}; int n = sizeof(arr) / sizeof(arr[0]); int k = 3; printf("%d\n", maxSumFixedK(arr, n, k)); return 0; }

Expected output:

text

6

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

Variable-Size Sliding Window

Steps:

  1. Use left and right pointers starting at 0.
  2. Expand right until the condition is violated.
  3. Shrink left until the condition holds again.
  4. Track the optimum window along the way.

Example: Longest substring without repeating characters.

c

#include <stdio.h> #include <string.h> int longestUniqueSubstr(const char *s) { int lastSeen[256]; for (int i = 0; i < 256; i++) lastSeen[i] = -1; int left = 0, maxLen = 0; for (int right = 0; s[right] != '\0'; right++) { unsigned char c = (unsigned char)s[right]; if (lastSeen[c] >= left) { left = lastSeen[c] + 1; } lastSeen[c] = right; int len = right - left + 1; if (len > maxLen) maxLen = len; } return maxLen; } int main(void) { printf("%d\n", longestUniqueSubstr("abcabcbb")); printf("%d\n", longestUniqueSubstr("bbbbb")); printf("%d\n", longestUniqueSubstr("pwwkew")); return 0; }

Expected output:

text

3 1 3

Time: O(n), Space: O(1) (fixed 256-int array)


Full Practical Code Example with Explanation

This program implements and demonstrates both fixed-size and variable-size sliding window patterns across several classic problems.

c

/** * sliding_window.c * Demonstrates fixed-size and variable-size sliding window patterns. * Problems covered: * 1. Maximum sum of subarray of size k (fixed) * 2. Minimum sum of subarray of size k (fixed) * 3. Longest substring without repeating characters (variable) * 4. Smallest subarray with sum >= target (variable) * 5. Maximum average of any subarray of size k (fixed, floating-point) */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <limits.h> #define MAX_LINE_LEN 100 /* ============================================================ * Fixed-size sliding window: maximum sum of size k * ============================================================ */ long long maxSumFixedK(const int arr[], int n, int k) { if (k <= 0 || n < k) return LLONG_MIN; long long windowSum = 0; for (int i = 0; i < k; i++) windowSum += arr[i]; long long maxSum = windowSum; for (int i = k; i < n; i++) { windowSum += arr[i] - arr[i - k]; if (windowSum > maxSum) maxSum = windowSum; } return maxSum; } /* ============================================================ * Fixed-size sliding window: minimum sum of size k * ============================================================ */ long long minSumFixedK(const int arr[], int n, int k) { if (k <= 0 || n < k) return LLONG_MAX; long long windowSum = 0; for (int i = 0; i < k; i++) windowSum += arr[i]; long long minSum = windowSum; for (int i = k; i < n; i++) { windowSum += arr[i] - arr[i - k]; if (windowSum < minSum) minSum = windowSum; } return minSum; } /* ============================================================ * Variable-size sliding window: * Longest substring without repeating characters * ============================================================ */ int longestUniqueSubstr(const char *s) { int lastSeen[256]; for (int i = 0; i < 256; i++) lastSeen[i] = -1; int left = 0, maxLen = 0; for (int right = 0; s[right] != '\0'; right++) { unsigned char c = (unsigned char)s[right]; if (lastSeen[c] >= left) { left = lastSeen[c] + 1; } lastSeen[c] = right; int len = right - left + 1; if (len > maxLen) maxLen = len; } return maxLen; } /* ============================================================ * Variable-size sliding window: * Smallest subarray with sum >= target (positive integers) * Returns the length or 0 if none exists. * ============================================================ */ int smallestSubarraySumGE(const int arr[], int n, long long target) { if (n <= 0 || target <= 0) return 0; long long windowSum = 0; int left = 0; int minLen = n + 1; for (int right = 0; right < n; right++) { windowSum += arr[right]; /* Shrink while condition still holds */ while (windowSum >= target) { int len = right - left + 1; if (len < minLen) minLen = len; windowSum -= arr[left]; left++; } } return (minLen == n + 1) ? 0 : minLen; } /* ============================================================ * Fixed-size sliding window: maximum average of size k * ============================================================ */ double maxAverageFixedK(const int arr[], int n, int k) { if (k <= 0 || n < k) return 0.0; long long windowSum = 0; for (int i = 0; i < k; i++) windowSum += arr[i]; long long maxSum = windowSum; for (int i = k; i < n; i++) { windowSum += arr[i] - arr[i - k]; if (windowSum > maxSum) maxSum = windowSum; } return (double)maxSum / k; } /* ============================================================ * Utility * ============================================================ */ 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("]"); } static void demoMaxSumFixed(void) { printf("\n=== Max Sum of Subarray of Size k ===\n"); int a1[] = {5, 2, -1, 0, 3}; int n1 = 5, k1 = 3; printf("arr = "); printArray(a1, n1); printf(", k = %d → %lld\n", k1, maxSumFixedK(a1, n1, k1)); int a2[] = {1, 4, 2, 10, 23, 3, 1, 0, 20}; int n2 = 9, k2 = 4; printf("arr = "); printArray(a2, n2); printf(", k = %d → %lld\n", k2, maxSumFixedK(a2, n2, k2)); int a3[] = {-5, -2, -8, -1}; int n3 = 4, k3 = 2; printf("arr = "); printArray(a3, n3); printf(", k = %d → %lld (all negative)\n", k3, maxSumFixedK(a3, n3, k3)); } static void demoMinSumFixed(void) { printf("\n=== Min Sum of Subarray of Size k ===\n"); int a1[] = {5, 2, -1, 0, 3}; int n1 = 5, k1 = 3; printf("arr = "); printArray(a1, n1); printf(", k = %d → %lld\n", k1, minSumFixedK(a1, n1, k1)); int a2[] = {10, 4, 2, 5, 6, 3, 8, 1}; int n2 = 8, k2 = 3; printf("arr = "); printArray(a2, n2); printf(", k = %d → %lld\n", k2, minSumFixedK(a2, n2, k2)); } static void demoLongestUnique(void) { printf("\n=== Longest Substring Without Repeating Chars ===\n"); const char *tests[] = {"abcabcbb", "bbbbb", "pwwkew", "", "abcdef"}; for (int i = 0; i < 5; i++) { printf(" \"%s\" → %d\n", tests[i], longestUniqueSubstr(tests[i])); } } static void demoSmallestSubarray(void) { printf("\n=== Smallest Subarray with Sum >= Target ===\n"); int a1[] = {2, 3, 1, 2, 4, 3}; int n1 = 6; long long target1 = 7; printf("arr = "); printArray(a1, n1); printf(", target = %lld → length %d\n", target1, smallestSubarraySumGE(a1, n1, target1)); int a2[] = {1, 4, 4}; int n2 = 3; long long target2 = 4; printf("arr = "); printArray(a2, n2); printf(", target = %lld → length %d\n", target2, smallestSubarraySumGE(a2, n2, target2)); int a3[] = {1, 1, 1, 1, 1, 1, 1, 1}; int n3 = 8; long long target3 = 11; printf("arr = "); printArray(a3, n3); printf(", target = %lld → length %d (none)\n", target3, smallestSubarraySumGE(a3, n3, target3)); } static void demoMaxAverage(void) { printf("\n=== Maximum Average of Subarray of Size k ===\n"); int a1[] = {1, 12, -5, -6, 50, 3}; int n1 = 6, k1 = 4; printf("arr = "); printArray(a1, n1); printf(", k = %d → %.5f\n", k1, maxAverageFixedK(a1, n1, k1)); } static void demoEdgeCases(void) { printf("\n=== Edge Cases ===\n"); /* k > n */ int a1[] = {1, 2, 3}; printf("k > n: maxSumFixedK = %lld\n", maxSumFixedK(a1, 3, 5)); /* k = 0 */ printf("k = 0: maxSumFixedK = %lld\n", maxSumFixedK(a1, 3, 0)); /* k = n */ printf("k = n: maxSumFixedK = %lld\n", maxSumFixedK(a1, 3, 3)); /* k = 1 */ printf("k = 1: maxSumFixedK = %lld\n", maxSumFixedK(a1, 3, 1)); /* Empty string */ printf("Empty string: %d\n", longestUniqueSubstr("")); /* Target too large */ int a2[] = {1, 2}; printf("Target unreachable: %d\n", smallestSubarraySumGE(a2, 2, 100)); /* Target 0 */ printf("Target 0: %d\n", smallestSubarraySumGE(a2, 2, 0)); } int main(void) { int choice; char buffer[MAX_LINE_LEN]; printf("=== Sliding Window Technique Demo ===\n"); while (1) { printf("\n--- Menu ---\n"); printf("1. Max Sum Fixed Size (k)\n"); printf("2. Min Sum Fixed Size (k)\n"); printf("3. Longest Substring Without Repeats\n"); printf("4. Smallest Subarray with Sum >= Target\n"); printf("5. Maximum Average of Size k\n"); printf("6. Edge Cases\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: demoMaxSumFixed(); break; case 2: demoMinSumFixed(); break; case 3: demoLongestUnique(); break; case 4: demoSmallestSubarray(); break; case 5: demoMaxAverage(); break; case 6: demoEdgeCases(); break; case 7: printf("Exiting.\n"); return EXIT_SUCCESS; default: printf("Invalid choice.\n"); break; } } return EXIT_SUCCESS; }

Explanation of Each Problem

  1. Max Sum Fixed Size: Maintains a running window sum; updates by subtracting the outgoing element and adding the incoming one.
  2. Min Sum Fixed Size: Same as above but tracks the minimum instead.
  3. Longest Unique Substring: Uses lastSeen[256] to know when a duplicate is inside the window; shrinks left past the previous occurrence.
  4. Smallest Subarray with Sum ≥ Target: Expands right; shrinks left while the condition holds. Tracks the minimum length.
  5. Max Average of Size k: Same as max sum, divided by k.

Expected Output (sample run)

text

=== Max Sum of Subarray of Size k === arr = [5, 2, -1, 0, 3], k = 3 → 6 arr = [1, 4, 2, 10, 23, 3, 1, 0, 20], k = 4 → 39 arr = [-5, -2, -8, -1], k = 2 → -3 (all negative) === Min Sum of Subarray of Size k === arr = [5, 2, -1, 0, 3], k = 3 → 1 arr = [10, 4, 2, 5, 6, 3, 8, 1], k = 3 → 9 === Longest Substring Without Repeating Chars === "abcabcbb" → 3 "bbbbb" → 1 "pwwkew" → 3 "" → 0 "abcdef" → 6 === Smallest Subarray with Sum >= Target === arr = [2, 3, 1, 2, 4, 3], target = 7 → length 2 arr = [1, 4, 4], target = 4 → length 1 arr = [1, 1, 1, 1, 1, 1, 1, 1], target = 11 → length 0 (none) === Maximum Average of Subarray of Size k === arr = [1, 12, -5, -6, 50, 3], k = 4 → 12.75000 === Edge Cases === k > n: maxSumFixedK = -9223372036854775808 k = 0: maxSumFixedK = -9223372036854775808 k = n: maxSumFixedK = 6 k = 1: maxSumFixedK = 3 Empty string: 0 Target unreachable: 0 Target 0: 0

Common Pitfalls, Edge Cases & Memory Considerations

1. Off-by-One in Window Bounds

The window of size k starting at i spans [i, i + k - 1]. Sliding to i + 1 adds arr[i + k] and removes arr[i].

c

windowSum += arr[i] - arr[i - k]; /* Correct for i ≥ k */

2. k > n or k <= 0

Return a sentinel (or error). Do not silently produce garbage.

c

if (k <= 0 || n < k) return LLONG_MIN;

3. Integer Overflow

Sums can grow beyond int.

Solution: Use long long for the window sum and result.

4. Variable Window: left > right

If left jumps past right (e.g., duplicate handling), reset it.

c

left = lastSeen[c] + 1;

5. Variable Window with Negative Numbers

The “smallest subarray with sum ≥ target” approach assumes positive numbers. With negatives, the sliding window invariant breaks — use prefix sums + deque instead.

6. Sliding Window is Not Always Applicable

Requires a monotone invariant: when a window violates a condition, shrinking must restore it.

7. Mutating Inputs

All sliding window approaches leave the input untouched.

8. Fixed-Size Initialization

Compute the initial window sum correctly (first k elements).

9. Floating-Point Averages

Dividing a long long by k gives better precision than accumulating floats directly.

10. Memory Considerations

  • Fixed window: O(1) extra space.
  • Variable window: O(1) to O(alphabet) space.
  • Use long long for sums.
  • No heap allocation needed in typical cases.

Summary & Best Practices

The Sliding Window Technique is a fundamental pattern for subarray and substring problems. It reduces O(n²) to O(n) by reusing computation across adjacent windows.

Key Takeaways

  • Two variants: fixed size and variable size.
  • Fixed: window_sum += arr[i] - arr[i - k].
  • Variable: expand right, shrink left as needed.
  • Use long long for sums to prevent overflow.
  • Requires positive numbers for sum-based variants.
  • Applicable to arrays, strings, and streams.

Best Practices

  1. Identify the window type (fixed vs. variable) before coding.
  2. Initialize the first window explicitly.
  3. Slide by subtracting outgoing and adding incoming.
  4. Track optimum (max/min/longest/shortest) during the slide.
  5. Use long long for sums.
  6. Handle k <= 0, k > n, empty inputs.
  7. Ensure the invariant is monotone — sliding windows don’t work for arbitrary conditions.
  8. Prefer fixed-size sliding over nested loops for k-sized queries.
  9. Test with negatives, zeros, duplicates, and boundary sizes.
  10. Profile against naive solutions to verify correctness.

By mastering sliding windows, you’ll solve a large family of subarray and substring problems with elegant linear-time algorithms.

Learn to find the maximum number of collinear points in C: naive O(n³) cross-product and O(n²) slope hashing approaches with code examples.

Count Maximum Points on Same Line in C

Count Maximum Points on the Same Line in C: A Comprehensive Tutorial

Given two arrays x[] and y[] where (x[i], y[i]) represents points on the Cartesian plane, find the maximum number of points that lie on a single straight line. This is a classic computational geometry problem that combines cross-product collinearity checks with hash-based slope grouping.

This tutorial provides an in-depth exploration of two approaches in C: the naive O(n³) brute force and the optimal O(n²) slope-hashing method. Complete, portable C99/C11 implementations are provided with full output examples.


Conceptual Introduction & Fundamentals

What is the Problem?

Given n points, find the largest subset that is collinear (lies on the same straight line).

Examples:

text

Input: x = [1, 2, 3], y = [1, 2, 3] Output: 3 (points (1,1), (2,2), (3,3)) Input: x = [-1, 0, 1, 2, 3, 3], y = [1, 0, 1, 2, 3, 4] Output: 4 (points (3,2), (4,1), (2,3), (1,4) — wait, these are from different x/y)

Let’s verify the second example with the actual arrays:

text

Points: (-1,1), (0,0), (1,1), (2,2), (3,3), (3,4) Line y = x contains: (-1,-1)? No. But (0,0), (1,1), (2,2), (3,3) → 4 points.

Why This Problem Matters

  • Computational geometry: Foundation for convex hull, line fitting, and clustering.
  • Slope normalization: Practical use of GCD for canonical forms.
  • Hash map with structured keys: Custom key encoding.
  • Overlap handling: Duplicate points require special treatment.
  • Interview preparation: Common algorithmic coding interview question.

Approaches Overview

ApproachTimeSpace
Naive cross productO(n³)O(1)
Slope hashingO(n²)O(n)

Key Concepts

  • Collinearity test: Three points A, B, C are collinear iff cross product (B-A) × (C-A) = 0.
  • Slope: dy / dx. Canonicalize by dividing both by gcd(|dx|, |dy|) and ensuring the sign is consistent.
  • Vertical lines: Handle dx == 0 separately (slope is infinite).
  • Overlapping points: Identical points don’t define a slope but always lie on any line through that location.

Syntax & Core Mechanics

Approach 1: Naive Cross-Product

For each pair (i, j), count how many points lie on that line.

c

#include <stdio.h> #include <stdlib.h> int maxPointsNaive(const int x[], const int y[], int n) { if (n <= 2) return n; int ans = 1; for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { if (x[i] == x[j] && y[i] == y[j]) continue; /* Identical */ int cnt = 0; for (int k = 0; k < n; k++) { long long lhs = (long long)(y[j] - y[i]) * (x[k] - x[i]); long long rhs = (long long)(y[k] - y[i]) * (x[j] - x[i]); if (lhs == rhs) cnt++; } if (cnt > ans) ans = cnt; } } return ans; } int main(void) { int x[] = {1, 2, 3}; int y[] = {1, 2, 3}; printf("%d\n", maxPointsNaive(x, y, 3)); return 0; }

Expected output:

text

3

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

Approach 2: Slope Hashing

For each point i, compute the slope to every other point j > i. Group by normalized slope. Track vertical lines and overlaps separately.

c

#include <stdio.h> #include <stdlib.h> #include <string.h> #define MAX_SLOPES 1024 typedef struct { int dy; int dx; int count; int used; } SlopeEntry; static int gcd(int a, int b) { while (b) { int t = a % b; a = b; b = t; } return a < 0 ? -a : a; } static int findOrInsert(SlopeEntry *map, int *size, int dy, int dx) { for (int i = 0; i < *size; i++) { if (map[i].dy == dy && map[i].dx == dx) { map[i].count++; return map[i].count; } } map[*size].dy = dy; map[*size].dx = dx; map[*size].count = 1; (*size)++; return 1; } int maxPointsHash(const int x[], const int y[], int n) { if (n <= 2) return n; int maxPoint = 1; for (int i = 0; i < n; i++) { SlopeEntry map[MAX_SLOPES]; int mapSize = 0; int overlap = 0, vertical = 0, curMax = 0; for (int j = i + 1; j < n; j++) { int xDif = x[j] - x[i]; int yDif = y[j] - y[i]; if (xDif == 0 && yDif == 0) { overlap++; } else if (xDif == 0) { vertical++; } else { int g = gcd(xDif < 0 ? -xDif : xDif, yDif < 0 ? -yDif : yDif); yDif /= g; xDif /= g; /* Normalize sign: make xDif positive */ if (xDif < 0) { xDif = -xDif; yDif = -yDif; } int c = findOrInsert(map, &mapSize, yDif, xDif); if (c > curMax) curMax = c; } if (vertical > curMax) curMax = vertical; } int total = curMax + overlap + 1; if (total > maxPoint) maxPoint = total; } return maxPoint; } int main(void) { int x[] = {-1, 0, 1, 2, 3, 3}; int y[] = {1, 0, 1, 2, 3, 4}; int n = sizeof(x) / sizeof(x[0]); printf("%d\n", maxPointsHash(x, y, n)); return 0; }

Expected output:

text

4

Time: O(n²), Space: O(n) per iteration

Why Slope Normalization Matters

Without normalization, (2, 4) and (1, 2) would be treated as different slopes even though they represent the same line direction.

Normalize:

  1. Divide both dy and dx by gcd(|dy|, |dx|).
  2. Ensure dx > 0; if dx < 0, negate both.

This gives a canonical key for each direction.


Full Practical Code Example with Explanation

This program compares both approaches, verifies their agreement, and exercises edge cases.

c

/** * max_points_on_line.c * Finds maximum number of collinear points. * Compares naive cross-product and slope hashing approaches. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <time.h> #define MAX_LINE_LEN 100 #define MAX_SLOPES 2048 /* ============================================================ * SlopeEntry + GCD * ============================================================ */ typedef struct { int dy; int dx; int count; } SlopeEntry; static int gcdInt(int a, int b) { if (a < 0) a = -a; if (b < 0) b = -b; while (b) { int t = a % b; a = b; b = t; } return a; } static int findOrInsert(SlopeEntry *map, int *size, int dy, int dx) { for (int i = 0; i < *size; i++) { if (map[i].dy == dy && map[i].dx == dx) { map[i].count++; return map[i].count; } } map[*size].dy = dy; map[*size].dx = dx; map[*size].count = 1; (*size)++; return 1; } /* ============================================================ * Approach 1: Naive * ============================================================ */ int maxPointsNaive(const int x[], const int y[], int n) { if (n <= 2) return n; int ans = 1; for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { if (x[i] == x[j] && y[i] == y[j]) continue; int cnt = 0; for (int k = 0; k < n; k++) { long long lhs = (long long)(y[j] - y[i]) * (x[k] - x[i]); long long rhs = (long long)(y[k] - y[i]) * (x[j] - x[i]); if (lhs == rhs) cnt++; } if (cnt > ans) ans = cnt; } } return ans; } /* ============================================================ * Approach 2: Slope Hashing * ============================================================ */ int maxPointsHash(const int x[], const int y[], int n) { if (n <= 2) return n; int maxPoint = 1; for (int i = 0; i < n; i++) { SlopeEntry map[MAX_SLOPES]; int mapSize = 0; int overlap = 0, vertical = 0, curMax = 0; for (int j = i + 1; j < n; j++) { int xDif = x[j] - x[i]; int yDif = y[j] - y[i]; if (xDif == 0 && yDif == 0) { overlap++; } else if (xDif == 0) { vertical++; } else { int g = gcdInt(xDif, yDif); yDif /= g; xDif /= g; if (xDif < 0) { xDif = -xDif; yDif = -yDif; } int c = findOrInsert(map, &mapSize, yDif, xDif); if (c > curMax) curMax = c; } if (vertical > curMax) curMax = vertical; } int total = curMax + overlap + 1; if (total > maxPoint) maxPoint = total; } return maxPoint; } /* ============================================================ * Utility * ============================================================ */ static void demoBasic(void) { printf("\n=== Sample Point Sets ===\n"); int x1[] = {1, 2, 3}; int y1[] = {1, 2, 3}; int x2[] = {-1, 0, 1, 2, 3, 3}; int y2[] = {1, 0, 1, 2, 3, 4}; int x3[] = {1, 1, 1, 1}; int y3[] = {1, 2, 3, 4}; int x4[] = {5, 5, 5, 5, 5}; int y4[] = {1, 2, 3, 4, 5}; struct { int *x; int *y; int n; const char *label; } tests[] = { {x1, y1, 3, "(1,1),(2,2),(3,3)"}, {x2, y2, 6, "6 points"}, {x3, y3, 4, "Vertical line x=1"}, {x4, y4, 5, "Horizontal line y=5"} }; int nt = sizeof(tests) / sizeof(tests[0]); for (int t = 0; t < nt; t++) { printf("\nTest %d: %s\n", t + 1, tests[t].label); int r1 = maxPointsNaive(tests[t].x, tests[t].y, tests[t].n); int r2 = maxPointsHash(tests[t].x, tests[t].y, tests[t].n); printf(" Naive: %d\n Hash: %d\n", r1, r2); printf(" Verify: %s\n", (r1 == r2) ? "PASS" : "FAIL"); } printf("\n"); } static void demoEdgeCases(void) { printf("\n=== Edge Cases ===\n"); /* Empty */ printf("Empty: naive=%d, hash=%d\n", maxPointsNaive(NULL, NULL, 0), maxPointsHash(NULL, NULL, 0)); /* Single point */ int x1[] = {5}; int y1[] = {7}; printf("Single: %d / %d\n", maxPointsNaive(x1, y1, 1), maxPointsHash(x1, y1, 1)); /* Two points */ int x2[] = {1, 2}; int y2[] = {3, 4}; printf("Two: %d / %d\n", maxPointsNaive(x2, y2, 2), maxPointsHash(x2, y2, 2)); /* All identical */ int x3[] = {5, 5, 5, 5}; int y3[] = {7, 7, 7, 7}; printf("All identical: %d / %d (expect 4)\n", maxPointsNaive(x3, y3, 4), maxPointsHash(x3, y3, 4)); /* All on horizontal */ int x4[] = {1, 2, 3, 4}; int y4[] = {5, 5, 5, 5}; printf("All horizontal: %d / %d (expect 4)\n", maxPointsNaive(x4, y4, 4), maxPointsHash(x4, y4, 4)); /* All on vertical */ int x5[] = {3, 3, 3, 3}; int y5[] = {1, 2, 3, 4}; printf("All vertical: %d / %d (expect 4)\n", maxPointsNaive(x5, y5, 4), maxPointsHash(x5, y5, 4)); /* Square corners */ int x6[] = {0, 0, 1, 1}; int y6[] = {0, 1, 0, 1}; printf("Square corners: %d / %d (expect 2)\n", maxPointsNaive(x6, y6, 4), maxPointsHash(x6, y6, 4)); /* Negative coordinates */ int x7[] = {-2, -1, 0, 1, 2}; int y7[] = {-4, -2, 0, 2, 4}; printf("Negatives, slope 2: %d / %d (expect 5)\n", maxPointsNaive(x7, y7, 5), maxPointsHash(x7, y7, 5)); /* Duplicate points mixed in */ int x8[] = {1, 1, 2, 3}; int y8[] = {1, 1, 2, 3}; printf("Duplicate point: %d / %d (expect 4)\n", maxPointsNaive(x8, y8, 4), maxPointsHash(x8, y8, 4)); } static void performanceTest(void) { printf("\n=== Performance Comparison ===\n"); /* Naive small */ int smallN = 300; int *sx = malloc((size_t)smallN * sizeof(int)); int *sy = malloc((size_t)smallN * sizeof(int)); if (!sx || !sy) { free(sx); free(sy); return; } srand(42); for (int i = 0; i < smallN; i++) { sx[i] = rand() % 100; sy[i] = rand() % 100; } clock_t start, end; double t; start = clock(); int r1 = maxPointsNaive(sx, sy, smallN); end = clock(); t = ((double)(end - start)) / CLOCKS_PER_SEC * 1000.0; printf("Naive (n=%d): result=%d, time=%.3f ms\n", smallN, r1, t); start = clock(); int r2 = maxPointsHash(sx, sy, smallN); end = clock(); t = ((double)(end - start)) / CLOCKS_PER_SEC * 1000.0; printf("Hash (n=%d): result=%d, time=%.3f ms\n", smallN, r2, t); printf("Verify: %s\n", (r1 == r2) ? "PASS" : "FAIL"); free(sx); free(sy); /* Hash large */ int largeN = 5000; int *lx = malloc((size_t)largeN * sizeof(int)); int *ly = malloc((size_t)largeN * sizeof(int)); if (!lx || !ly) { free(lx); free(ly); return; } for (int i = 0; i < largeN; i++) { lx[i] = rand() % 1000; ly[i] = rand() % 1000; } start = clock(); int r3 = maxPointsHash(lx, ly, largeN); end = clock(); t = ((double)(end - start)) / CLOCKS_PER_SEC * 1000.0; printf("Hash (n=%d): result=%d, time=%.3f ms\n", largeN, r3, t); free(lx); free(ly); } 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; } static void demoInteractive(void) { int n; if (readInteger("Enter number of points (1-5000): ", &n) != 0 || n < 1 || n > 5000) { printf("Invalid size.\n"); return; } int *x = malloc((size_t)n * sizeof(int)); int *y = malloc((size_t)n * sizeof(int)); if (!x || !y) { free(x); free(y); return; } for (int i = 0; i < n; i++) { char p[40]; snprintf(p, sizeof(p), " Point %d x: ", i); if (readInteger(p, &x[i]) != 0) { free(x); free(y); return; } snprintf(p, sizeof(p), " Point %d y: ", i); if (readInteger(p, &y[i]) != 0) { free(x); free(y); return; } } printf("\nMax points on a line: %d\n", maxPointsHash(x, y, n)); free(x); free(y); } int main(void) { int choice; char buffer[MAX_LINE_LEN]; printf("=== Maximum Points on the Same Line Demo ===\n"); while (1) { printf("\n--- Menu ---\n"); printf("1. Demonstrate Both Approaches\n"); printf("2. Edge Cases\n"); printf("3. Performance Comparison\n"); printf("4. Interactive\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: demoBasic(); break; case 2: demoEdgeCases(); break; case 3: performanceTest(); break; case 4: demoInteractive(); break; case 5: printf("Exiting.\n"); return EXIT_SUCCESS; default: printf("Invalid choice.\n"); break; } } return EXIT_SUCCESS; }

Explanation of the Approaches

  1. Naive: For each pair (i, j), use cross product to test collinearity of every k. O(n³).
  2. Slope Hash: For each anchor i, compute canonical slope to every j. Group by slope. Track maximum group size + overlaps + 1. O(n²).

Expected Output (sample run)

text

=== Sample Point Sets === Test 1: (1,1),(2,2),(3,3) Naive: 3 Hash: 3 Verify: PASS Test 2: 6 points Naive: 4 Hash: 4 Verify: PASS Test 3: Vertical line x=1 Naive: 4 Hash: 4 Verify: PASS Test 4: Horizontal line y=5 Naive: 5 Hash: 5 Verify: PASS === Edge Cases === Empty: naive=0, hash=0 Single: 1 / 1 Two: 2 / 2 All identical: 4 / 4 (expect 4) All horizontal: 4 / 4 (expect 4) All vertical: 4 / 4 (expect 4) Square corners: 2 / 2 (expect 2) Negatives, slope 2: 5 / 5 (expect 5) Duplicate point: 4 / 4 (expect 4)

Common Pitfalls, Edge Cases & Memory Considerations

1. Using Floating-Point Slopes

Floating-point comparison is unreliable; use normalized integer ratios instead.

Solution: Reduce dy/dx by GCD and encode as (dy, dx) pair.

2. Forgetting to Normalize Signs

(1, 2) and (-1, -2) are the same slope.

Solution: Ensure dx > 0 by negating both if needed.

3. Vertical Lines (dx == 0)

Slopes involving vertical lines are undefined.

Solution: Track vertical counts separately.

4. Overlapping (Identical) Points

Identical points lie on every line through them.

Solution: Count overlaps separately and add them to the total.

5. Integer Overflow in Cross Product

(y[j] - y[i]) * (x[k] - x[i]) can overflow int.

Solution: Use long long.

6. Collision in Slope Map

Linear probing handles collisions; use a map or sort-based approach if needed.

7. Empty and Single-Point Cases

Return 0 and 1 respectively.

8. Two Points

Return 2 (trivially collinear).

9. Duplicate Handling in Naive

Skip identical (i, j) pairs.

c

if (x[i] == x[j] && y[i] == y[j]) continue;

10. Memory Considerations

  • Naive: O(1) extra space.
  • Slope hash: O(n) per anchor (cleared between iterations).
  • Use dynamic arrays for large point sets.
  • Free all allocated memory.

Summary & Best Practices

The maximum-collinear-points problem introduces canonical slope representation and its use in hashing. The O(n²) solution is optimal for general point sets.

Key Takeaways

  • Collinearity: cross product (B-A) × (C-A) = 0.
  • Slope: dy / dx reduced by gcd(|dy|, |dx|).
  • Normalize sign: dx > 0.
  • Handle vertical lines and overlaps separately.
  • Naive: O(n³).
  • Slope hash: O(n²) — preferred.

Best Practices

  1. Use slope hashing for O(n²) time.
  2. Normalize slopes by GCD to canonicalize.
  3. Handle vertical lines as a special case.
  4. Count overlapping points and add to the total.
  5. Use long long for cross products.
  6. Size the slope map based on n (avoid fixed limits for large inputs).
  7. Free allocated memory.
  8. Test edge cases: empty, single, duplicates, vertical, horizontal, negatives.
  9. Verify with the naive approach during testing.
  10. Profile for large point sets.

By mastering this problem, you’ll be prepared for line-fitting, clustering, and other geometric algorithms in computer graphics, vision, and data analysis.

Learn to remove minimum elements so two arrays have no common elements in C: two-map and single-map O(n+m) approaches with code examples.

Remove Minimum Elements so No Common Elements in C

Remove Minimum Elements so No Common Elements Exist in Two Arrays (C Tutorial)

Given two arrays, find the minimum number of elements that must be removed (across both arrays combined) so that the resulting arrays have no common elements. This problem is a subtle twist on intersection counting — and the solution is remarkably elegant.

This tutorial covers the insight, two C implementations (two maps vs. single map), edge cases, and best practices.


Conceptual Introduction & Fundamentals

What is the Problem?

Given two arrays arr1[] (size n) and arr2[] (size m), find the minimum number of elements to remove (from either array) so that no value appears in both arrays.

Examples:

text

Input: arr1 = [2, 3, 4, 5, 8], arr2 = [1, 2, 3, 4] Output: 3 (remove 2, 3, 4 from one array) Input: arr1 = [1, 2, 3, 4], arr2 = [5, 6, 7] Output: 0 (no common elements)

Why This Problem Matters

  • Set operations: Deepens understanding of intersection.
  • Multiset reasoning: Frequencies matter, not just presence.
  • Two implementation strategies: Two maps vs. one map.
  • Real-world use: Data deduplication, cache invalidation, conflict resolution.
  • Interview preparation: Common coding interview question.

Key Insight

For each value v present in both arrays with frequencies f1(v) in arr1 and f2(v) in arr2, we must remove all copies from one side (or split removals) so no copy remains in both arrays. The cheapest approach is to remove min(f1(v), f2(v)) copies — remove all copies from the side with fewer.

Summing over all common values:

text

answer = Σ over common values v of min(f1(v), f2(v))

Approaches Overview

ApproachTimeSpace
Two hash mapsO(n + m)O(n + m)
Single hash mapO(n + m)O(n)

Syntax & Core Mechanics

Approach 1: Two Hash Maps

Count frequencies in both arrays; sum min(f1, f2) for common values.

c

#include <stdio.h> #include <stdlib.h> #define TABLE_SIZE 4096 typedef struct { int *keys; int *counts; int *occupied; int capacity; } FreqMap; static unsigned int hashFunc(int key, int cap) { return ((unsigned int)key) % (unsigned int)cap; } FreqMap *createMap(int cap) { FreqMap *m = malloc(sizeof(FreqMap)); if (m == NULL) return NULL; m->keys = malloc((size_t)cap * sizeof(int)); m->counts = calloc((size_t)cap, sizeof(int)); m->occupied = calloc((size_t)cap, sizeof(int)); if (m->keys == NULL || m->counts == NULL || m->occupied == NULL) { free(m->keys); free(m->counts); free(m->occupied); free(m); return NULL; } m->capacity = cap; return m; } void freeMap(FreqMap *m) { if (m) { free(m->keys); free(m->counts); free(m->occupied); free(m); } } void increment(FreqMap *m, int key) { unsigned int idx = hashFunc(key, m->capacity); int start = (int)idx; while (m->occupied[idx]) { if (m->keys[idx] == key) { m->counts[idx]++; return; } idx = (idx + 1) % (unsigned int)m->capacity; if ((int)idx == start) return; } m->keys[idx] = key; m->counts[idx] = 1; m->occupied[idx] = 1; } int getCount(FreqMap *m, int key) { unsigned int idx = hashFunc(key, m->capacity); int start = (int)idx; while (m->occupied[idx]) { if (m->keys[idx] == key) return m->counts[idx]; idx = (idx + 1) % (unsigned int)m->capacity; if ((int)idx == start) break; } return 0; } long long minRemoveTwoMaps(const int a[], int n, const int b[], int m) { FreqMap *ma = createMap(TABLE_SIZE); FreqMap *mb = createMap(TABLE_SIZE); if (ma == NULL || mb == NULL) { freeMap(ma); freeMap(mb); return -1; } for (int i = 0; i < n; i++) increment(ma, a[i]); for (int i = 0; i < m; i++) increment(mb, b[i]); long long res = 0; for (int i = 0; i < ma->capacity; i++) { if (!ma->occupied[i]) continue; int v = ma->keys[i]; int c2 = getCount(mb, v); if (c2 > 0) { int c1 = ma->counts[i]; res += (c1 < c2) ? c1 : c2; } } freeMap(ma); freeMap(mb); return res; } int main(void) { int arr1[] = {2, 3, 4, 5, 8}; int arr2[] = {1, 2, 3, 4}; printf("%lld\n", minRemoveTwoMaps(arr1, 5, arr2, 4)); return 0; }

Expected output:

text

3

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

Approach 2: Single Hash Map (Optimal)

Count arr1 frequencies in one map; iterate arr2; when a value with positive count is found, decrement and add 1.

c

long long minRemoveSingleMap(const int a[], int n, const int b[], int m) { FreqMap *count = createMap(TABLE_SIZE); if (count == NULL) return -1; for (int i = 0; i < n; i++) increment(count, a[i]); long long res = 0; for (int i = 0; i < m; i++) { int c = getCount(count, b[i]); if (c > 0) { /* Decrement manually */ unsigned int idx = hashFunc(b[i], count->capacity); int start = (int)idx; while (count->occupied[idx]) { if (count->keys[idx] == b[i]) { count->counts[idx]--; break; } idx = (idx + 1) % (unsigned int)count->capacity; if ((int)idx == start) break; } res++; } } freeMap(count); return res; }

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

Step-by-Step Trace for arr1 = [1,2,3,4], arr2 = [2,3,4,5,8]

text

Insert into count: {1:1, 2:1, 3:1, 4:1} Iterate arr2: 2 → count[2] = 1 > 0 → decrement to 0, res = 1 3 → count[3] = 1 > 0 → decrement to 0, res = 2 4 → count[4] = 1 > 0 → decrement to 0, res = 3 5 → count[5] = 0 → skip 8 → count[8] = 0 → skip Return 3 ✓

Full Practical Code Example with Explanation

This program implements both approaches, cross-verifies them, and exercises edge cases.

c

/** * min_remove_no_common.c * Minimum elements to remove so no common elements exist. * Compares two-map and single-map approaches. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <time.h> #define MAX_LINE_LEN 100 #define TABLE_SIZE 262144 /* ============================================================ * FreqMap * ============================================================ */ typedef struct { int *keys; int *counts; int *occupied; int capacity; } FreqMap; static unsigned int hashFunc(int key, int cap) { return ((unsigned int)key) % (unsigned int)cap; } FreqMap *createMap(int cap) { FreqMap *m = malloc(sizeof(FreqMap)); if (m == NULL) return NULL; m->keys = malloc((size_t)cap * sizeof(int)); m->counts = calloc((size_t)cap, sizeof(int)); m->occupied = calloc((size_t)cap, sizeof(int)); if (m->keys == NULL || m->counts == NULL || m->occupied == NULL) { free(m->keys); free(m->counts); free(m->occupied); free(m); return NULL; } m->capacity = cap; return m; } void freeMap(FreqMap *m) { if (m) { free(m->keys); free(m->counts); free(m->occupied); free(m); } } void increment(FreqMap *m, int key) { unsigned int idx = hashFunc(key, m->capacity); int start = (int)idx; while (m->occupied[idx]) { if (m->keys[idx] == key) { m->counts[idx]++; return; } idx = (idx + 1) % (unsigned int)m->capacity; if ((int)idx == start) return; } m->keys[idx] = key; m->counts[idx] = 1; m->occupied[idx] = 1; } int getCount(FreqMap *m, int key) { unsigned int idx = hashFunc(key, m->capacity); int start = (int)idx; while (m->occupied[idx]) { if (m->keys[idx] == key) return m->counts[idx]; idx = (idx + 1) % (unsigned int)m->capacity; if ((int)idx == start) break; } return 0; } void decrement(FreqMap *m, int key) { unsigned int idx = hashFunc(key, m->capacity); int start = (int)idx; while (m->occupied[idx]) { if (m->keys[idx] == key) { m->counts[idx]--; return; } idx = (idx + 1) % (unsigned int)m->capacity; if ((int)idx == start) return; } } /* ============================================================ * Approaches * ============================================================ */ long long minRemoveTwoMaps(const int a[], int n, const int b[], int m) { FreqMap *ma = createMap(TABLE_SIZE); FreqMap *mb = createMap(TABLE_SIZE); if (ma == NULL || mb == NULL) { freeMap(ma); freeMap(mb); return -1; } for (int i = 0; i < n; i++) increment(ma, a[i]); for (int i = 0; i < m; i++) increment(mb, b[i]); long long res = 0; for (int i = 0; i < ma->capacity; i++) { if (!ma->occupied[i]) continue; int v = ma->keys[i]; int c2 = getCount(mb, v); if (c2 > 0) { int c1 = ma->counts[i]; res += (c1 < c2) ? c1 : c2; } } freeMap(ma); freeMap(mb); return res; } long long minRemoveSingleMap(const int a[], int n, const int b[], int m) { FreqMap *count = createMap(TABLE_SIZE); if (count == NULL) return -1; for (int i = 0; i < n; i++) increment(count, a[i]); long long res = 0; for (int i = 0; i < m; i++) { if (getCount(count, b[i]) > 0) { decrement(count, b[i]); res++; } } freeMap(count); return res; } /* ============================================================ * Utility * ============================================================ */ 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("]"); } static void demoBasic(void) { printf("\n=== Sample Arrays ===\n"); int a1[] = {2, 3, 4, 5, 8}; int b1[] = {1, 2, 3, 4}; int a2[] = {1, 2, 3, 4}; int b2[] = {5, 6, 7}; int a3[] = {1, 2, 2, 3}; int b3[] = {2, 2, 2, 4}; struct { int *a; int n; int *b; int m; const char *label; } tests[] = { {a1, 5, b1, 4, "arr1=[2,3,4,5,8], arr2=[1,2,3,4]"}, {a2, 4, b2, 3, "arr1=[1,2,3,4], arr2=[5,6,7]"}, {a3, 4, b3, 4, "arr1=[1,2,2,3], arr2=[2,2,2,4]"} }; int nt = sizeof(tests) / sizeof(tests[0]); for (int t = 0; t < nt; t++) { printf("\nTest %d: %s\n", t + 1, tests[t].label); printf(" arr1: "); printArray(tests[t].a, tests[t].n); printf("\n"); printf(" arr2: "); printArray(tests[t].b, tests[t].m); printf("\n"); long long r1 = minRemoveTwoMaps(tests[t].a, tests[t].n, tests[t].b, tests[t].m); long long r2 = minRemoveSingleMap(tests[t].a, tests[t].n, tests[t].b, tests[t].m); printf(" Two-map: %lld\n Single-map: %lld\n", r1, r2); printf(" Verify: %s\n", (r1 == r2) ? "PASS" : "FAIL"); } printf("\n"); } static void demoEdgeCases(void) { printf("\n=== Edge Cases ===\n"); /* Empty arrays */ printf("Both empty: %lld / %lld\n", minRemoveTwoMaps(NULL, 0, NULL, 0), minRemoveSingleMap(NULL, 0, NULL, 0)); /* One empty */ int a1[] = {1, 2, 3}; printf("arr2 empty: %lld / %lld\n", minRemoveTwoMaps(a1, 3, NULL, 0), minRemoveSingleMap(a1, 3, NULL, 0)); /* Disjoint */ int a2[] = {1, 2}; int b2[] = {3, 4}; printf("Disjoint: %lld / %lld (expect 0)\n", minRemoveTwoMaps(a2, 2, b2, 2), minRemoveSingleMap(a2, 2, b2, 2)); /* Identical */ int a3[] = {5, 5, 5}; int b3[] = {5, 5, 5}; printf("Identical [5,5,5]: %lld / %lld (expect 3)\n", minRemoveTwoMaps(a3, 3, b3, 3), minRemoveSingleMap(a3, 3, b3, 3)); /* Different frequencies */ int a4[] = {1, 1, 1, 2}; int b4[] = {1, 2, 2, 2}; printf("Different freqs: %lld / %lld (expect min(3,1)+min(1,3)=2)\n", minRemoveTwoMaps(a4, 4, b4, 4), minRemoveSingleMap(a4, 4, b4, 4)); /* Single common element */ int a5[] = {7, 8, 9}; int b5[] = {10, 7, 11}; printf("Single common: %lld / %lld (expect 1)\n", minRemoveTwoMaps(a5, 3, b5, 3), minRemoveSingleMap(a5, 3, b5, 3)); /* Negatives */ int a6[] = {-1, -2, -3}; int b6[] = {-2, -3, -4}; printf("Negatives: %lld / %lld (expect 2)\n", minRemoveTwoMaps(a6, 3, b6, 3), minRemoveSingleMap(a6, 3, b6, 3)); /* Zero */ int a7[] = {0, 0, 1}; int b7[] = {0, 2}; printf("Zero: %lld / %lld (expect min(2,1)=1)\n", minRemoveTwoMaps(a7, 3, b7, 2), minRemoveSingleMap(a7, 3, b7, 2)); } static void performanceTest(void) { printf("\n=== Performance Comparison ===\n"); int n = 5000000, m = 5000000; int *a = malloc((size_t)n * sizeof(int)); int *b = malloc((size_t)m * sizeof(int)); if (a == NULL || b == NULL) { free(a); free(b); return; } srand(42); for (int i = 0; i < n; i++) a[i] = rand() % 200000; for (int i = 0; i < m; i++) b[i] = rand() % 200000; clock_t start, end; double t; start = clock(); long long r1 = minRemoveTwoMaps(a, n, b, m); end = clock(); t = ((double)(end - start)) / CLOCKS_PER_SEC * 1000.0; printf("Two-map (n=%d, m=%d): result=%lld, time=%.3f ms\n", n, m, r1, t); start = clock(); long long r2 = minRemoveSingleMap(a, n, b, m); end = clock(); t = ((double)(end - start)) / CLOCKS_PER_SEC * 1000.0; printf("Single-map (n=%d, m=%d): result=%lld, time=%.3f ms\n", n, m, r2, t); printf("Verify: %s\n", (r1 == r2) ? "PASS" : "FAIL"); free(a); free(b); } 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; } static void demoInteractive(void) { int n, m; if (readInteger("Enter arr1 size (0-10000): ", &n) != 0 || n < 0 || n > 10000) { printf("Invalid size.\n"); return; } int *a = NULL; if (n > 0) { a = malloc((size_t)n * sizeof(int)); if (a == NULL) return; for (int i = 0; i < n; i++) { char p[40]; snprintf(p, sizeof(p), " arr1[%d]: ", i); if (readInteger(p, &a[i]) != 0) { free(a); return; } } } if (readInteger("Enter arr2 size (0-10000): ", &m) != 0 || m < 0 || m > 10000) { printf("Invalid size.\n"); free(a); return; } int *b = NULL; if (m > 0) { b = malloc((size_t)m * sizeof(int)); if (b == NULL) { free(a); return; } for (int i = 0; i < m; i++) { char p[40]; snprintf(p, sizeof(p), " arr2[%d]: ", i); if (readInteger(p, &b[i]) != 0) { free(a); free(b); return; } } } printf("\nMinimum removals: %lld\n", minRemoveSingleMap(a, n, b, m)); free(a); free(b); } int main(void) { int choice; char buffer[MAX_LINE_LEN]; printf("=== Minimum Removals so No Common Elements Demo ===\n"); while (1) { printf("\n--- Menu ---\n"); printf("1. Demonstrate Both Approaches\n"); printf("2. Edge Cases\n"); printf("3. Performance Comparison\n"); printf("4. Interactive\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: demoBasic(); break; case 2: demoEdgeCases(); break; case 3: performanceTest(); break; case 4: demoInteractive(); break; case 5: printf("Exiting.\n"); return EXIT_SUCCESS; default: printf("Invalid choice.\n"); break; } } return EXIT_SUCCESS; }

Explanation of the Approaches

  1. Two maps: Count frequencies of both arrays; for each shared value, add min(f1, f2).
  2. Single map: Count arr1; iterate arr2 and decrement each found value, adding 1 per match.

Both compute the same sum. The single map saves memory and cache pressure.

Expected Output (sample run)

text

=== Sample Arrays === Test 1: arr1=[2,3,4,5,8], arr2=[1,2,3,4] arr1: [2, 3, 4, 5, 8] arr2: [1, 2, 3, 4] Two-map: 3 Single-map: 3 Verify: PASS Test 2: arr1=[1,2,3,4], arr2=[5,6,7] arr1: [1, 2, 3, 4] arr2: [5, 6, 7] Two-map: 0 Single-map: 0 Verify: PASS Test 3: arr1=[1,2,2,3], arr2=[2,2,2,4] arr1: [1, 2, 2, 3] arr2: [2, 2, 2, 4] Two-map: 2 Single-map: 2 Verify: PASS === Edge Cases === Both empty: 0 / 0 arr2 empty: 0 / 0 Disjoint: 0 / 0 (expect 0) Identical [5,5,5]: 3 / 3 (expect 3) Different freqs: 2 / 2 (expect min(3,1)+min(1,3)=2) Single common: 1 / 1 (expect 1) Negatives: 2 / 2 (expect 2) Zero: 1 / 1 (expect min(2,1)=1)

Common Pitfalls, Edge Cases & Memory Considerations

1. Forgetting Frequencies

Two arrays with the same value but different counts need careful handling.

c

arr1 = [1, 1, 1] arr2 = [1] → remove 1 (min(3, 1) = 1)

2. Using Sets Instead of Maps

A set ignores frequencies and overcounts.

Solution: Use frequency maps.

3. Counting Per Value vs. Total

The answer sums min(f1, f2) across all shared values, not just one.

4. Integer Overflow in Sum

For large arrays with many duplicates, the sum can exceed int.

Solution: Use long long.

5. Empty Arrays

Any of n = 0 or m = 0 yields 0 (no removals needed).

6. Hash Table Sizing

Undersized tables cause probe failures.

Solution: Size ≥ 2× distinct values.

7. Negative Keys

Cast to unsigned int in the hash function.

8. Mutating Input

Both approaches leave inputs intact.

9. decrement Call on Missing Key

Guard with getCount() > 0 before decrementing.

10. Memory Considerations

  • Two maps: O(n + m) space.
  • Single map: O(n) space — preferred.
  • Free allocated memory.
  • Use long long for the total.

Summary & Best Practices

The minimum-removals problem reduces to summing min(f1(v), f2(v)) over all shared values. The single-map approach achieves O(n + m) time and O(n) space.

Key Takeaways

  • Answer = Σ min(f1(v), f2(v)) for v common to both arrays.
  • Two maps: O(n + m) time, O(n + m) space.
  • Single map: O(n + m) time, O(n) space — preferred.
  • Remove from the side with fewer copies per value.
  • Use long long for the total.

Best Practices

  1. Use the single-map approach for better memory usage.
  2. Track frequencies, not just membership.
  3. Sum min(f1, f2) per common value.
  4. Use long long for the answer.
  5. Guard decrements with a positive count check.
  6. Size hash tables ≥ 2× distinct values.
  7. Cast negative keys to unsigned int.
  8. Free all allocated memory.
  9. Test edge cases: empty, disjoint, identical, unequal frequencies.
  10. Profile for large inputs.

By mastering this problem, you’ll deepen your intuition for multiset operations, frequency counting, and set-theoretic reductions.

Learn to find minimum subsets with distinct elements in C: the answer equals the max frequency. Compare naive, sorted, and hash O(n) approaches.

Minimum Subsets with Distinct Elements in C

Minimum Subsets with Distinct Elements in C: A Comprehensive Tutorial

Given an array of integers, partition its elements into the minimum number of subsets so that no subset contains duplicates. At first glance, this looks like a combinatorial optimization problem—but there’s an elegant insight that collapses it into a single statistic: the maximum frequency of any element.

This tutorial explores the reasoning, three implementation approaches (naive O(n²), sort O(n log n), hash O(n)), and everything you need to write robust C code that solves it.


Conceptual Introduction & Fundamentals

What is the Problem?

Given an array arr[], distribute its elements into the fewest possible subsets, where each subset must contain distinct elements (no duplicates inside a subset).

Examples:

text

Input: arr = [1, 2, 3, 4] Output: 1 (all distinct → one subset) Input: arr = [1, 2, 3, 3] Output: 2 ({1,2,3} and {3})

Why This Problem Matters

  • Pattern recognition: Reduces a seemingly complex problem to one statistic.
  • Frequency counting: Foundation for many counting and partition problems.
  • Combinatorial reasoning: Pigeonhole principle in action.
  • Interview preparation: Common coding interview question.
  • Real-world applications: Task scheduling, resource allocation, exam seating.

The Key Insight

Every subset can contain at most one copy of any value. If a value v appears f(v) times, we need at least f(v) subsets to place all its copies. The most frequent value determines the minimum:

text

answer = max over all values of frequency

Why the Answer is Sufficient

Suppose the maximum frequency is m for value x. Create m empty subsets. Distribute the elements round-robin (each value goes to a different subset each time it appears). Since every value appears at most m times, each value fits into distinct subsets, so no subset gets a duplicate.

Approaches Overview

ApproachTimeSpace
Naive nested loopsO(n²)O(1)
SortingO(n log n)O(1) or O(n)
Hash mapO(n) averageO(n)

Syntax & Core Mechanics

Approach 1: Naive Nested Loops

For each element, count its occurrences by scanning the array.

c

#include <stdio.h> int minSubsetsNaive(const int arr[], int n) { if (n <= 0) return 0; int maxCount = 0; for (int i = 0; i < n; i++) { int count = 0; for (int j = 0; j < n; j++) { if (arr[i] == arr[j]) count++; } if (count > maxCount) maxCount = count; } return maxCount; } int main(void) { int arr[] = {40, 50, 30, 40, 50, 30, 30}; int n = sizeof(arr) / sizeof(arr[0]); printf("%d\n", minSubsetsNaive(arr, n)); return 0; }

Expected output:

text

3

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

Approach 2: Sort + Linear Scan

After sorting, equal elements are adjacent. Track the run length.

c

#include <stdio.h> #include <stdlib.h> #include <string.h> static int compareInts(const void *a, const void *b) { int x = *(const int *)a; int y = *(const int *)b; return (x > y) - (x < y); } int minSubsetsSorted(const int arr_in[], int n) { if (n <= 0) return 0; if (n == 1) return 1; int *arr = malloc((size_t)n * sizeof(int)); if (arr == NULL) return -1; memcpy(arr, arr_in, (size_t)n * sizeof(int)); qsort(arr, n, sizeof(int), compareInts); int maxCount = 1; int currCount = 1; for (int i = 1; i < n; i++) { if (arr[i] == arr[i - 1]) currCount++; else currCount = 1; if (currCount > maxCount) maxCount = currCount; } free(arr); return maxCount; } int main(void) { int arr[] = {40, 50, 30, 40, 50, 30, 30}; int n = sizeof(arr) / sizeof(arr[0]); printf("%d\n", minSubsetsSorted(arr, n)); return 0; }

Expected output:

text

3

Time: O(n log n), Space: O(n) for copy

Approach 3: Hash Map (Optimal)

Insert into a hash map (key = value, value = frequency). Return the maximum frequency.

c

#include <stdio.h> #include <stdlib.h> #define TABLE_SIZE 4096 typedef struct { int *keys; int *counts; int *occupied; int capacity; } FreqMap; static unsigned int hashFunc(int key, int cap) { return ((unsigned int)key) % (unsigned int)cap; } FreqMap *createMap(int cap) { FreqMap *m = malloc(sizeof(FreqMap)); if (m == NULL) return NULL; m->keys = malloc((size_t)cap * sizeof(int)); m->counts = calloc((size_t)cap, sizeof(int)); m->occupied = calloc((size_t)cap, sizeof(int)); if (m->keys == NULL || m->counts == NULL || m->occupied == NULL) { free(m->keys); free(m->counts); free(m->occupied); free(m); return NULL; } m->capacity = cap; return m; } void freeMap(FreqMap *m) { if (m) { free(m->keys); free(m->counts); free(m->occupied); free(m); } } void increment(FreqMap *m, int key) { unsigned int idx = hashFunc(key, m->capacity); int start = (int)idx; while (m->occupied[idx]) { if (m->keys[idx] == key) { m->counts[idx]++; return; } idx = (idx + 1) % (unsigned int)m->capacity; if ((int)idx == start) return; } m->keys[idx] = key; m->counts[idx] = 1; m->occupied[idx] = 1; } int minSubsetsHash(const int arr[], int n) { if (n <= 0) return 0; FreqMap *m = createMap(TABLE_SIZE); if (m == NULL) return -1; for (int i = 0; i < n; i++) increment(m, arr[i]); int maxCount = 0; for (int i = 0; i < m->capacity; i++) { if (m->occupied[i] && m->counts[i] > maxCount) { maxCount = m->counts[i]; } } freeMap(m); return maxCount; } int main(void) { int arr[] = {40, 50, 30, 40, 50, 30, 30}; int n = sizeof(arr) / sizeof(arr[0]); printf("%d\n", minSubsetsHash(arr, n)); return 0; }

Expected output:

text

3

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

Round-Robin Distribution Proof

If the maximum frequency is m for value x:

  1. Create m empty subsets.
  2. For each distinct value v appearing f(v) ≤ m times, assign its f(v) copies to f(v) distinct subsets.
  3. Each subset gets at most one copy of v.

This yields a valid partition with exactly m subsets, and no smaller count can work (pigeonhole).


Full Practical Code Example with Explanation

This program implements all three approaches and verifies they agree on multiple datasets.

c

/** * min_subsets.c * Minimum number of subsets with distinct elements. * Compares naive, sorted, and hash map approaches. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <time.h> #define MAX_LINE_LEN 100 #define TABLE_SIZE 262144 /* ============================================================ * FreqMap * ============================================================ */ typedef struct { int *keys; int *counts; int *occupied; int capacity; } FreqMap; static unsigned int hashFunc(int key, int cap) { return ((unsigned int)key) % (unsigned int)cap; } FreqMap *createMap(int cap) { FreqMap *m = malloc(sizeof(FreqMap)); if (m == NULL) return NULL; m->keys = malloc((size_t)cap * sizeof(int)); m->counts = calloc((size_t)cap, sizeof(int)); m->occupied = calloc((size_t)cap, sizeof(int)); if (m->keys == NULL || m->counts == NULL || m->occupied == NULL) { free(m->keys); free(m->counts); free(m->occupied); free(m); return NULL; } m->capacity = cap; return m; } void freeMap(FreqMap *m) { if (m) { free(m->keys); free(m->counts); free(m->occupied); free(m); } } void increment(FreqMap *m, int key) { unsigned int idx = hashFunc(key, m->capacity); int start = (int)idx; while (m->occupied[idx]) { if (m->keys[idx] == key) { m->counts[idx]++; return; } idx = (idx + 1) % (unsigned int)m->capacity; if ((int)idx == start) return; } m->keys[idx] = key; m->counts[idx] = 1; m->occupied[idx] = 1; } /* ============================================================ * Approaches * ============================================================ */ int minSubsetsNaive(const int arr[], int n) { if (n <= 0) return 0; int maxCount = 0; for (int i = 0; i < n; i++) { int cnt = 0; for (int j = 0; j < n; j++) if (arr[i] == arr[j]) cnt++; if (cnt > maxCount) maxCount = cnt; } return maxCount; } static int compareInts(const void *a, const void *b) { int x = *(const int *)a; int y = *(const int *)b; return (x > y) - (x < y); } int minSubsetsSorted(const int arr_in[], int n) { if (n <= 0) return 0; if (n == 1) return 1; int *arr = malloc((size_t)n * sizeof(int)); if (arr == NULL) return -1; memcpy(arr, arr_in, (size_t)n * sizeof(int)); qsort(arr, n, sizeof(int), compareInts); int maxCount = 1, currCount = 1; for (int i = 1; i < n; i++) { if (arr[i] == arr[i - 1]) currCount++; else currCount = 1; if (currCount > maxCount) maxCount = currCount; } free(arr); return maxCount; } int minSubsetsHash(const int arr[], int n) { if (n <= 0) return 0; FreqMap *m = createMap(TABLE_SIZE); if (m == NULL) return -1; for (int i = 0; i < n; i++) increment(m, arr[i]); int maxCount = 0; for (int i = 0; i < m->capacity; i++) { if (m->occupied[i] && m->counts[i] > maxCount) { maxCount = m->counts[i]; } } freeMap(m); return maxCount; } /* ============================================================ * Utility * ============================================================ */ 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("]"); } static void demoBasic(void) { printf("\n=== Sample Arrays ===\n"); int a1[] = {1, 2, 3, 4}; int a2[] = {1, 2, 3, 3}; int a3[] = {40, 50, 30, 40, 50, 30, 30}; int a4[] = {1, 2, 1, 2, 3, 3, 2, 2}; struct { int *arr; int n; const char *label; } tests[] = { {a1, 4, "[1,2,3,4]"}, {a2, 4, "[1,2,3,3]"}, {a3, 7, "[40,50,30,40,50,30,30]"}, {a4, 8, "[1,2,1,2,3,3,2,2]"} }; int nt = sizeof(tests) / sizeof(tests[0]); for (int t = 0; t < nt; t++) { printf("\nTest %d: %s\n", t + 1, tests[t].label); printf(" Array: "); printArray(tests[t].arr, tests[t].n); printf("\n"); int r1 = minSubsetsNaive(tests[t].arr, tests[t].n); int r2 = minSubsetsSorted(tests[t].arr, tests[t].n); int r3 = minSubsetsHash(tests[t].arr, tests[t].n); printf(" Naive: %d\n Sorted: %d\n Hash: %d\n", r1, r2, r3); printf(" Verify: %s\n", (r1 == r2 && r2 == r3) ? "PASS" : "FAIL"); } printf("\n"); } static void demoEdgeCases(void) { printf("\n=== Edge Cases ===\n"); /* Empty */ printf("Empty array: naive=%d, sorted=%d, hash=%d\n", minSubsetsNaive(NULL, 0), minSubsetsSorted(NULL, 0), minSubsetsHash(NULL, 0)); /* Single */ int a1[] = {7}; printf("Single element: %d / %d / %d\n", minSubsetsNaive(a1, 1), minSubsetsSorted(a1, 1), minSubsetsHash(a1, 1)); /* All distinct */ int a2[] = {1, 2, 3, 4, 5}; printf("All distinct: %d / %d / %d (expect 1)\n", minSubsetsNaive(a2, 5), minSubsetsSorted(a2, 5), minSubsetsHash(a2, 5)); /* All same */ int a3[] = {9, 9, 9, 9}; printf("All same: %d / %d / %d (expect 4)\n", minSubsetsNaive(a3, 4), minSubsetsSorted(a3, 4), minSubsetsHash(a3, 4)); /* One dominant value */ int a4[] = {1, 1, 1, 1, 2, 3}; printf("One dominant: %d / %d / %d (expect 4)\n", minSubsetsNaive(a4, 6), minSubsetsSorted(a4, 6), minSubsetsHash(a4, 6)); /* Negatives */ int a5[] = {-1, -1, -2, -2, -3}; printf("Negatives: %d / %d / %d (expect 2)\n", minSubsetsNaive(a5, 5), minSubsetsSorted(a5, 5), minSubsetsHash(a5, 5)); /* Two values alternating */ int a6[] = {5, 6, 5, 6, 5, 6}; printf("Alternating: %d / %d / %d (expect 3)\n", minSubsetsNaive(a6, 6), minSubsetsSorted(a6, 6), minSubsetsHash(a6, 6)); /* Zeros */ int a7[] = {0, 0, 0, 1, 1}; printf("Zeros: %d / %d / %d (expect 3)\n", minSubsetsNaive(a7, 5), minSubsetsSorted(a7, 5), minSubsetsHash(a7, 5)); } static void performanceTest(void) { printf("\n=== Performance Comparison ===\n"); /* Naive small */ int smallN = 3000; int *sa = malloc((size_t)smallN * sizeof(int)); if (sa == NULL) return; srand(42); for (int i = 0; i < smallN; i++) sa[i] = rand() % 500; clock_t start, end; double t; start = clock(); int r1 = minSubsetsNaive(sa, smallN); end = clock(); t = ((double)(end - start)) / CLOCKS_PER_SEC * 1000.0; printf("Naive (n=%d): result=%d, time=%.3f ms\n", smallN, r1, t); start = clock(); int r2 = minSubsetsSorted(sa, smallN); end = clock(); t = ((double)(end - start)) / CLOCKS_PER_SEC * 1000.0; printf("Sorted (n=%d): result=%d, time=%.3f ms\n", smallN, r2, t); start = clock(); int r3 = minSubsetsHash(sa, smallN); end = clock(); t = ((double)(end - start)) / CLOCKS_PER_SEC * 1000.0; printf("Hash (n=%d): result=%d, time=%.3f ms\n", smallN, r3, t); printf("Verify: %s\n", (r1 == r2 && r2 == r3) ? "PASS" : "FAIL"); free(sa); /* Large hash */ int largeN = 5000000; int *la = malloc((size_t)largeN * sizeof(int)); if (la == NULL) return; for (int i = 0; i < largeN; i++) la[i] = rand() % 100000; start = clock(); int r4 = minSubsetsHash(la, largeN); end = clock(); t = ((double)(end - start)) / CLOCKS_PER_SEC * 1000.0; printf("Hash (n=%d): result=%d, time=%.3f ms\n", largeN, r4, t); free(la); } 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; } static void demoInteractive(void) { int n; if (readInteger("Enter array size (0-10000): ", &n) != 0 || n < 0 || n > 10000) { printf("Invalid size.\n"); return; } int *arr = NULL; if (n > 0) { arr = malloc((size_t)n * sizeof(int)); if (arr == NULL) return; for (int i = 0; i < n; i++) { char p[40]; snprintf(p, sizeof(p), " arr[%d]: ", i); if (readInteger(p, &arr[i]) != 0) { printf("Invalid value.\n"); free(arr); return; } } } printf("\nMinimum subsets: %d\n", minSubsetsHash(arr, n)); free(arr); } int main(void) { int choice; char buffer[MAX_LINE_LEN]; printf("=== Minimum Subsets with Distinct Elements Demo ===\n"); while (1) { printf("\n--- Menu ---\n"); printf("1. Demonstrate All Approaches\n"); printf("2. Edge Cases\n"); printf("3. Performance Comparison\n"); printf("4. Interactive\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: demoBasic(); break; case 2: demoEdgeCases(); break; case 3: performanceTest(); break; case 4: demoInteractive(); break; case 5: printf("Exiting.\n"); return EXIT_SUCCESS; default: printf("Invalid choice.\n"); break; } } return EXIT_SUCCESS; }

Explanation of the Approaches

  1. Naive: For each element, count its frequency with a nested loop; track the maximum. O(n²).
  2. Sorted: Sort; scan runs of equal elements. O(n log n).
  3. Hash: Increment frequency in a hash map; find the maximum. O(n) average.

Expected Output (sample run)

text

=== Sample Arrays === Test 1: [1,2,3,4] Array: [1, 2, 3, 4] Naive: 1 Sorted: 1 Hash: 1 Verify: PASS Test 2: [1,2,3,3] Array: [1, 2, 3, 3] Naive: 2 Sorted: 2 Hash: 2 Verify: PASS Test 3: [40,50,30,40,50,30,30] Array: [40, 50, 30, 40, 50, 30, 30] Naive: 3 Sorted: 3 Hash: 3 Verify: PASS Test 4: [1,2,1,2,3,3,2,2] Array: [1, 2, 1, 2, 3, 3, 2, 2] Naive: 4 Sorted: 4 Hash: 4 Verify: PASS === Edge Cases === Empty array: naive=0, sorted=0, hash=0 Single element: 1 / 1 / 1 All distinct: 1 / 1 / 1 (expect 1) All same: 4 / 4 / 4 (expect 4) One dominant: 4 / 4 / 4 (expect 4) Negatives: 2 / 2 / 2 (expect 2) Alternating: 3 / 3 / 3 (expect 3) Zeros: 3 / 3 / 3 (expect 3)

Common Pitfalls, Edge Cases & Memory Considerations

1. Confusing “Distinct” with “Unique Values”

Each subset must have distinct elements within itself; elements may repeat across subsets.

2. Empty Array

Return 0 (no subsets needed). Guard explicitly.

3. Single Element

Exactly 1 subset is required.

4. All Same Values

The answer equals n (each subset holds one copy).

5. All Distinct

The answer is 1 (one subset holds everything).

6. Large Frequencies

The count fits in int for arrays up to INT_MAX elements; use size_t for very large arrays.

7. Hash Table Sizing

Undersized table → probe failures.

Solution: Size ≥ 2× number of distinct values (or ≥ 2× n).

8. Negative Keys

Cast to unsigned int in the hash function.

9. Sorting Mutates Input

Copy before sorting if preservation needed.

10. Memory Considerations

  • Naive: O(1) extra space.
  • Sorted: O(n) for the copy.
  • Hash: O(n) for the frequency map.
  • Free all dynamic allocations.

Summary & Best Practices

The minimum-subsets problem is a beautiful case of “reduce complexity to one statistic.” The answer is simply the maximum frequency of any value in the array.

Key Takeaways

  • Answer = maximum frequency of any element.
  • All distinct → 1 subset.
  • All same → n subsets.
  • Naive: O(n²) — educational.
  • Sorted: O(n log n) — memory-friendly.
  • Hash: O(n) average — optimal.

Best Practices

  1. Use the hash map for O(n) average.
  2. Use sorting when memory is tight.
  3. Track the maximum frequency during the scan.
  4. Handle empty arrays (0) and single elements (1).
  5. Size hash tables ≥ 2× distinct values.
  6. Cast negative keys to unsigned int.
  7. Copy inputs before sorting if preservation matters.
  8. Free allocated memory.
  9. Test edge cases: empty, single, all-same, all-distinct, negatives.
  10. Profile for large inputs.

By mastering this problem, you’ll recognize reductions from combinatorial problems to single statistics—a powerful pattern across algorithms.

Learn to count missing elements of a range in C arrays: sort-based O(n log n) and hash set O(n) approaches with code examples and edge cases.

Missing Elements of a Range in an Array: C Guide

Missing Elements of a Range in an Array in C: A Comprehensive Tutorial

Given an array, let min and max be the smallest and largest elements. The task is to count how many integers must be added so that every value in [min, max] appears at least once. This problem is about counting gaps in a value range, not the indices of the array. It’s a compact exercise in reasoning about sets versus ranges.

This tutorial covers two approaches in C: sorting-based O(n log n) and hash-set-based O(n), each with portable C99/C11 code and complete output examples.


Conceptual Introduction & Fundamentals

What is the Problem?

Given arr[] of size n, let:

  • min = minimum element
  • max = maximum element
  • range_size = max - min + 1

Count how many integers in [min, max] are not present in the array.

Examples:

text

Input: [4, 5, 3, 8, 6] Output: 1 Explanation: Range is [3, 8]; only 7 is missing. Input: [2, 1, 3] Output: 0 Explanation: Range is [1, 3]; nothing is missing.

Why This Problem Matters

  • Set vs range: Distinguishes distinct elements from the covered range.
  • Handling duplicates: Sets collapse duplicates; arrays do not.
  • Two clean solutions: Sorting and hashing with the same asymptotic idea.
  • Real-world use: Completeness of ID ranges, sensor calibration ranges, exam number gaps.
  • Interview preparation: Common coding interview question.

Key Identity

text

missing_count = (max - min + 1) - distinct_count

Where distinct_count is the number of unique values in the array.

Approaches Overview

ApproachTimeSpace
SortingO(n log n)O(1) or O(n)
Hash setO(n) averageO(n)

Syntax & Core Mechanics

Approach 1: Sorting

Sort the array; walk adjacent pairs, adding gaps between distinct consecutive values.

c

#include <stdio.h> #include <stdlib.h> #include <string.h> static int compareInts(const void *a, const void *b) { int x = *(const int *)a; int y = *(const int *)b; return (x > y) - (x < y); } long long countMissingSorted(const int arr_in[], int n) { if (n <= 1) return 0; int *arr = malloc((size_t)n * sizeof(int)); if (arr == NULL) return -1; memcpy(arr, arr_in, (size_t)n * sizeof(int)); qsort(arr, n, sizeof(int), compareInts); long long missing = 0; for (int i = 0; i < n - 1; i++) { int cur = arr[i]; int nxt = arr[i + 1]; if (nxt > cur + 1) { missing += (long long)(nxt - cur - 1); } /* If nxt == cur (duplicate) or nxt == cur+1, add nothing. */ } free(arr); return missing; } int main(void) { int arr[] = {4, 5, 3, 8, 6}; int n = sizeof(arr) / sizeof(arr[0]); printf("%lld\n", countMissingSorted(arr, n)); return 0; }

Expected output:

text

1

Time: O(n log n), Space: O(n) for copy

Why skip duplicates? Adjacent equal values contribute 0 to the gap because nxt - cur - 1 = -1, which would be wrong. The condition nxt > cur + 1 correctly skips both duplicates and consecutive integers.

Approach 2: Hash Set

Track distinct values in a set; also track min and max.

c

#include <stdio.h> #include <stdlib.h> #include <limits.h> #define TABLE_SIZE 8192 typedef struct { int *keys; int *occupied; int capacity; int size; } HashSet; static unsigned int hashFunc(int key, int cap) { return ((unsigned int)key) % (unsigned int)cap; } HashSet *createSet(int cap) { HashSet *s = malloc(sizeof(HashSet)); if (s == NULL) return NULL; s->keys = malloc((size_t)cap * sizeof(int)); s->occupied = calloc((size_t)cap, sizeof(int)); if (s->keys == NULL || s->occupied == NULL) { free(s->keys); free(s->occupied); free(s); return NULL; } s->capacity = cap; s->size = 0; return s; } void freeSet(HashSet *s) { if (s) { free(s->keys); free(s->occupied); free(s); } } int setInsert(HashSet *s, int key) { unsigned int idx = hashFunc(key, s->capacity); int start = (int)idx; while (s->occupied[idx]) { if (s->keys[idx] == key) return 0; /* Already present */ idx = (idx + 1) % (unsigned int)s->capacity; if ((int)idx == start) return -1; /* Full */ } s->keys[idx] = key; s->occupied[idx] = 1; s->size++; return 1; } long long countMissingHash(const int arr[], int n) { if (n <= 1) return 0; HashSet *s = createSet(TABLE_SIZE); if (s == NULL) return -1; int minv = INT_MAX, maxv = INT_MIN; for (int i = 0; i < n; i++) { setInsert(s, arr[i]); if (arr[i] < minv) minv = arr[i]; if (arr[i] > maxv) maxv = arr[i]; } long long range_size = (long long)maxv - (long long)minv + 1; long long result = range_size - (long long)s->size; freeSet(s); return result; } int main(void) { int arr[] = {4, 5, 3, 8, 6}; int n = sizeof(arr) / sizeof(arr[0]); printf("%lld\n", countMissingHash(arr, n)); return 0; }

Expected output:

text

1

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

Why Hash Wins for Large Inputs

The set approach handles unsorted input, duplicates, and arbitrarily wide ranges without allocating per-range memory.


Full Practical Code Example with Explanation

This program implements both approaches and verifies their agreement.

c

/** * missing_range_count.c * Counts how many integers are missing from [min, max] of an array. * Compares sorting and hash set approaches. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <limits.h> #include <time.h> #define MAX_LINE_LEN 100 #define TABLE_SIZE 262144 /* ============================================================ * HashSet * ============================================================ */ typedef struct { int *keys; int *occupied; int capacity; int size; } HashSet; static unsigned int hashFunc(int key, int cap) { return ((unsigned int)key) % (unsigned int)cap; } HashSet *createSet(int cap) { HashSet *s = malloc(sizeof(HashSet)); if (s == NULL) return NULL; s->keys = malloc((size_t)cap * sizeof(int)); s->occupied = calloc((size_t)cap, sizeof(int)); if (s->keys == NULL || s->occupied == NULL) { free(s->keys); free(s->occupied); free(s); return NULL; } s->capacity = cap; s->size = 0; return s; } void freeSet(HashSet *s) { if (s) { free(s->keys); free(s->occupied); free(s); } } int setInsert(HashSet *s, int key) { unsigned int idx = hashFunc(key, s->capacity); int start = (int)idx; while (s->occupied[idx]) { if (s->keys[idx] == key) return 0; idx = (idx + 1) % (unsigned int)s->capacity; if ((int)idx == start) return -1; } s->keys[idx] = key; s->occupied[idx] = 1; s->size++; return 1; } /* ============================================================ * Approaches * ============================================================ */ static int compareInts(const void *a, const void *b) { int x = *(const int *)a; int y = *(const int *)b; return (x > y) - (x < y); } long long countMissingSorted(const int arr_in[], int n) { if (n <= 1) return 0; int *arr = malloc((size_t)n * sizeof(int)); if (arr == NULL) return -1; memcpy(arr, arr_in, (size_t)n * sizeof(int)); qsort(arr, n, sizeof(int), compareInts); long long missing = 0; for (int i = 0; i < n - 1; i++) { int cur = arr[i], nxt = arr[i + 1]; if (nxt > cur + 1) { missing += (long long)(nxt - cur - 1); } } free(arr); return missing; } long long countMissingHash(const int arr[], int n) { if (n <= 1) return 0; HashSet *s = createSet(TABLE_SIZE); if (s == NULL) return -1; int minv = INT_MAX, maxv = INT_MIN; for (int i = 0; i < n; i++) { setInsert(s, arr[i]); if (arr[i] < minv) minv = arr[i]; if (arr[i] > maxv) maxv = arr[i]; } long long range_size = (long long)maxv - (long long)minv + 1; long long result = range_size - (long long)s->size; freeSet(s); return result; } /* ============================================================ * Utility * ============================================================ */ 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("]"); } static void demoBasic(void) { printf("\n=== Sample Arrays ===\n"); int a1[] = {4, 5, 3, 8, 6}; int a2[] = {2, 1, 3}; int a3[] = {3, 5, 8, 6}; int a4[] = {10, 10, 10}; int a5[] = {1, 5, 10}; int a6[] = {-5, -2, 0, 1}; struct { int *arr; int n; const char *label; } tests[] = { {a1, 5, "[4,5,3,8,6]"}, {a2, 3, "[2,1,3]"}, {a3, 4, "[3,5,8,6]"}, {a4, 3, "[10,10,10]"}, {a5, 3, "[1,5,10]"}, {a6, 4, "[-5,-2,0,1]"} }; int nt = sizeof(tests) / sizeof(tests[0]); for (int t = 0; t < nt; t++) { printf("\nTest %d: %s\n", t + 1, tests[t].label); printf(" Array: "); printArray(tests[t].arr, tests[t].n); printf("\n"); long long r1 = countMissingSorted(tests[t].arr, tests[t].n); long long r2 = countMissingHash(tests[t].arr, tests[t].n); printf(" Sorted: %lld\n Hash: %lld\n", r1, r2); printf(" Verify: %s\n", (r1 == r2) ? "PASS" : "FAIL"); } printf("\n"); } static void demoEdgeCases(void) { printf("\n=== Edge Cases ===\n"); /* Empty and single */ printf("Empty array: sorted=%lld, hash=%lld\n", countMissingSorted(NULL, 0), countMissingHash(NULL, 0)); int a1[] = {7}; printf("Single element: sorted=%lld, hash=%lld\n", countMissingSorted(a1, 1), countMissingHash(a1, 1)); /* All same */ int a2[] = {5, 5, 5, 5}; printf("[5,5,5,5]: sorted=%lld, hash=%lld (expect 0)\n", countMissingSorted(a2, 4), countMissingHash(a2, 4)); /* All consecutive */ int a3[] = {1, 2, 3, 4, 5}; printf("[1..5]: sorted=%lld, hash=%lld (expect 0)\n", countMissingSorted(a3, 5), countMissingHash(a3, 5)); /* Big gap */ int a4[] = {1, 1000000000}; printf("[1, 1e9]: sorted=%lld, hash=%lld (expect 999999998)\n", countMissingSorted(a4, 2), countMissingHash(a4, 2)); /* Duplicates + gaps */ int a5[] = {1, 1, 3, 5, 5, 5, 7}; printf("[1,1,3,5,5,5,7]: sorted=%lld, hash=%lld (expect 2: 2,4,6)\n", countMissingSorted(a5, 7), countMissingHash(a5, 7)); /* Negatives */ int a6[] = {-10, -8, -8, -5}; printf("[-10,-8,-8,-5]: sorted=%lld, hash=%lld (expect 3)\n", countMissingSorted(a6, 4), countMissingHash(a6, 4)); } static void performanceTest(void) { printf("\n=== Performance Comparison ===\n"); int n = 500000; int *arr = malloc((size_t)n * sizeof(int)); if (arr == NULL) return; srand(42); for (int i = 0; i < n; i++) arr[i] = rand() % 1000000; clock_t start, end; double t; start = clock(); long long r1 = countMissingSorted(arr, n); end = clock(); t = ((double)(end - start)) / CLOCKS_PER_SEC * 1000.0; printf("Sorted (n=%d): result=%lld, time=%.3f ms\n", n, r1, t); start = clock(); long long r2 = countMissingHash(arr, n); end = clock(); t = ((double)(end - start)) / CLOCKS_PER_SEC * 1000.0; printf("Hash (n=%d): result=%lld, time=%.3f ms\n", n, r2, t); printf("Verify: %s\n", (r1 == r2) ? "PASS" : "FAIL"); free(arr); } 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; } static void demoInteractive(void) { int n; if (readInteger("Enter array size (0-10000): ", &n) != 0 || n < 0 || n > 10000) { printf("Invalid size.\n"); return; } int *arr = NULL; if (n > 0) { arr = malloc((size_t)n * sizeof(int)); if (arr == NULL) return; for (int i = 0; i < n; i++) { char p[40]; snprintf(p, sizeof(p), " arr[%d]: ", i); if (readInteger(p, &arr[i]) != 0) { printf("Invalid value.\n"); free(arr); return; } } } printf("\nMissing count: %lld\n", countMissingHash(arr, n)); free(arr); } int main(void) { int choice; char buffer[MAX_LINE_LEN]; printf("=== Missing Elements of a Range Demo ===\n"); while (1) { printf("\n--- Menu ---\n"); printf("1. Demonstrate Both Approaches\n"); printf("2. Edge Cases\n"); printf("3. Performance Comparison\n"); printf("4. Interactive\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: demoBasic(); break; case 2: demoEdgeCases(); break; case 3: performanceTest(); break; case 4: demoInteractive(); break; case 5: printf("Exiting.\n"); return EXIT_SUCCESS; default: printf("Invalid choice.\n"); break; } } return EXIT_SUCCESS; }

Explanation of the Approaches

  1. Sort: Sort, then sum nxt - cur - 1 for adjacent distinct pairs. Duplicates contribute zero, and nxt == cur + 1 means consecutive.
  2. Hash: Insert into a set; track min and max. The answer is (max - min + 1) - set_size.

Expected Output (sample run)

text

=== Sample Arrays === Test 1: [4,5,3,8,6] Array: [4, 5, 3, 8, 6] Sorted: 1 Hash: 1 Verify: PASS Test 2: [2,1,3] Array: [2, 1, 3] Sorted: 0 Hash: 0 Verify: PASS Test 3: [3,5,8,6] Array: [3, 5, 8, 6] Sorted: 2 Hash: 2 Verify: PASS Test 4: [10,10,10] Array: [10, 10, 10] Sorted: 0 Hash: 0 Verify: PASS Test 5: [1,5,10] Array: [1, 5, 10] Sorted: 6 Hash: 6 Verify: PASS Test 6: [-5,-2,0,1] Array: [-5, -2, 0, 1] Sorted: 3 Hash: 3 Verify: PASS === Edge Cases === Empty array: sorted=0, hash=0 Single element: sorted=0, hash=0 [5,5,5,5]: sorted=0, hash=0 (expect 0) [1..5]: sorted=0, hash=0 (expect 0) [1, 1e9]: sorted=999999998, hash=999999998 (expect 999999998) [1,1,3,5,5,5,7]: sorted=2, hash=2 (expect 2: 2,4,6) [-10,-8,-8,-5]: sorted=3, hash=3 (expect 3)

Common Pitfalls, Edge Cases & Memory Considerations

1. Duplicate Elements Break Naive Gap Counting

Without the nxt > cur + 1 check, adjacent duplicates add -1 per pair, corrupting the count.

c

if (nxt > cur + 1) missing += nxt - cur - 1;

2. Integer Overflow in max - min + 1

For min = INT_MIN and max = INT_MAX, the range overflows int.

Solution: Use long long for range size.

3. Empty and Single-Element Arrays

No range exists; missing count is 0.

4. All Duplicates

The set contains one value; range_size == 1; result 0.

5. All Consecutive

No gap; result 0.

6. Negative Values

Both approaches handle them.

7. Hash Table Sizing

Undersized table → probe failures.

Solution: Size ≥ 2× n.

8. Range Width Overflows long long

For extreme values (min = -9×10¹⁸, max = 9×10¹⁸) even long long may overflow. In such cases, split the difference:

c

unsigned long long width = (unsigned long long)maxv - (unsigned long long)minv + 1;

9. Sorting Mutates Input

Copy before sorting if preservation is needed.

10. Memory Considerations

  • Sorted: O(n) for the copy.
  • Hash: O(n) for the set.
  • Both use long long for the result.

Summary & Best Practices

Counting missing elements in the min–max range is a compact set-versus-range problem. The hash-set solution is the cleanest: insert distinct values, compute (max - min + 1) - distinct_count.

Key Takeaways

  • missing = (max - min + 1) - distinct_count.
  • Sorted: adjacent gap summation, ignoring duplicates.
  • Hash: one pass, set + min/max tracking.
  • Both handle negatives, duplicates, and unsorted input.
  • Use long long for range width.

Best Practices

  1. Use the hash set for O(n) average.
  2. Use sorting when memory is tight.
  3. Use long long for range size arithmetic.
  4. Track min/max in the same pass as insertion.
  5. Do not add gap counts for duplicates.
  6. Size hash tables ≥ 2× n.
  7. Copy inputs before sorting if preservation matters.
  8. Handle empty/single-element arrays as 0.
  9. Test with negatives, duplicates, and wide gaps.
  10. Profile for large inputs.

By mastering this problem, you’ll be equipped for set-difference, range-completeness, and gap-counting tasks.

Learn to find missing elements in a range in C: naive, sorted, hash set, and boolean-array approaches with complete code and edge cases.

Missing Elements in Range in C: Complete Guide

Missing Elements in Range in C: A Comprehensive Tutorial

Given an array and a numeric range [low, high], the task is to find all integers inside that range that are not present in the array, returned in ascending order. This problem appears in data validation, gap detection, and range-based analytics, and it demonstrates a clear progression from brute force to optimal O(n + r) time.

This tutorial explores four approaches in C: naive linear scan, sorted two-pointer, hash set, and boolean array. Each is implemented with portable C99/C11 code and complete output examples.


Conceptual Introduction & Fundamentals

What is the Problem?

Given an integer array arr[] and a range [low, high], output every integer in [low, high] that is not in arr[], in ascending order.

Examples:

text

Input: arr = [10, 12, 11, 15], low = 10, high = 15 Output: [13, 14] Input: arr = [1, 4, 11, 51, 15], low = 50, high = 55 Output: [50, 52, 53, 54, 55]

Why This Problem Matters

  • Set membership: Foundation for range queries and gap detection.
  • Time-space trade-offs: O(r × n) → O(n log n) → O(n + r).
  • Hash vs boolean array: Choosing between sparse and dense representations.
  • Real-world use: Missing sequence IDs, invoice gaps, license plate ranges.
  • Interview preparation: Common coding interview question.

Notation

Let n = length(arr) and r = high - low + 1.

Approaches Overview

ApproachTimeSpace
Naive linear scanO(r × n)O(r) for result
Sorting + two pointersO(n log n + r)O(1) or O(n)
Hash setO(n + r) avgO(n)
Boolean arrayO(n + r)O(r)

Syntax & Core Mechanics

Approach 1: Naive Linear Scan

For each number in [low, high], scan the array to check presence.

c

#include <stdio.h> #include <stdlib.h> int *missingRangeNaive(const int arr[], int n, int low, int high, int *outSize) { if (low > high) { *outSize = 0; return NULL; } int r = high - low + 1; int *res = malloc((size_t)r * sizeof(int)); if (res == NULL) { *outSize = 0; return NULL; } int k = 0; for (int x = low; x <= high; x++) { int found = 0; for (int i = 0; i < n; i++) { if (arr[i] == x) { found = 1; break; } } if (!found) res[k++] = x; } *outSize = k; return res; } int main(void) { int arr[] = {1, 4, 11, 51, 15}; int n = sizeof(arr) / sizeof(arr[0]); int low = 50, high = 55, size = 0; int *res = missingRangeNaive(arr, n, low, high, &size); printf("["); for (int i = 0; i < size; i++) { printf("%d", res[i]); if (i < size - 1) printf(", "); } printf("]\n"); free(res); return 0; }

Expected output:

text

[50, 52, 53, 54, 55]

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

Approach 2: Sort + Two Pointers

Sort the array; scan the range while advancing a pointer.

c

#include <stdio.h> #include <stdlib.h> #include <string.h> static int compareInts(const void *a, const void *b) { int x = *(const int *)a; int y = *(const int *)b; return (x > y) - (x < y); } int *missingRangeSorted(const int arr_in[], int n, int low, int high, int *outSize) { if (low > high) { *outSize = 0; return NULL; } int *arr = malloc((size_t)n * sizeof(int)); if (arr == NULL && n > 0) { *outSize = 0; return NULL; } if (n > 0) memcpy(arr, arr_in, (size_t)n * sizeof(int)); qsort(arr, n, sizeof(int), compareInts); int r = high - low + 1; int *res = malloc((size_t)r * sizeof(int)); if (res == NULL) { free(arr); *outSize = 0; return NULL; } int k = 0, i = 0; for (int x = low; x <= high; x++) { while (i < n && arr[i] < x) i++; if (i == n || arr[i] != x) res[k++] = x; } free(arr); *outSize = k; return res; } int main(void) { int arr[] = {1, 4, 11, 51, 15}; int n = sizeof(arr) / sizeof(arr[0]); int low = 50, high = 55, size = 0; int *res = missingRangeSorted(arr, n, low, high, &size); printf("["); for (int i = 0; i < size; i++) { printf("%d", res[i]); if (i < size - 1) printf(", "); } printf("]\n"); free(res); return 0; }

Expected output:

text

[50, 52, 53, 54, 55]

Time: O(n log n + r), Space: O(n) for copy

Approach 3: Hash Set

Insert all array values into a hash set; check each range number in O(1).

c

#include <stdio.h> #include <stdlib.h> #define TABLE_SIZE 8192 typedef struct { int *keys; int *occupied; int capacity; } HashSet; static unsigned int hashFunc(int key, int cap) { return ((unsigned int)key) % (unsigned int)cap; } HashSet *createSet(int cap) { HashSet *s = malloc(sizeof(HashSet)); if (s == NULL) return NULL; s->keys = malloc((size_t)cap * sizeof(int)); s->occupied = calloc((size_t)cap, sizeof(int)); if (s->keys == NULL || s->occupied == NULL) { free(s->keys); free(s->occupied); free(s); return NULL; } s->capacity = cap; return s; } void freeSet(HashSet *s) { if (s) { free(s->keys); free(s->occupied); free(s); } } int setContains(HashSet *s, int key) { unsigned int idx = hashFunc(key, s->capacity); int start = (int)idx; while (s->occupied[idx]) { if (s->keys[idx] == key) return 1; idx = (idx + 1) % (unsigned int)s->capacity; if ((int)idx == start) break; } return 0; } void setInsert(HashSet *s, int key) { unsigned int idx = hashFunc(key, s->capacity); int start = (int)idx; while (s->occupied[idx]) { if (s->keys[idx] == key) return; idx = (idx + 1) % (unsigned int)s->capacity; if ((int)idx == start) return; } s->keys[idx] = key; s->occupied[idx] = 1; } int *missingRangeHash(const int arr[], int n, int low, int high, int *outSize) { if (low > high) { *outSize = 0; return NULL; } HashSet *s = createSet(TABLE_SIZE); if (s == NULL) { *outSize = 0; return NULL; } for (int i = 0; i < n; i++) setInsert(s, arr[i]); int r = high - low + 1; int *res = malloc((size_t)r * sizeof(int)); if (res == NULL) { freeSet(s); *outSize = 0; return NULL; } int k = 0; for (int x = low; x <= high; x++) { if (!setContains(s, x)) res[k++] = x; } freeSet(s); *outSize = k; return res; } int main(void) { int arr[] = {1, 4, 11, 51, 15}; int n = sizeof(arr) / sizeof(arr[0]); int low = 50, high = 55, size = 0; int *res = missingRangeHash(arr, n, low, high, &size); printf("["); for (int i = 0; i < size; i++) { printf("%d", res[i]); if (i < size - 1) printf(", "); } printf("]\n"); free(res); return 0; }

Expected output:

text

[50, 52, 53, 54, 55]

Time: O(n + r) average, Space: O(n)

Approach 4: Boolean Array (Dense Range)

Allocate r booleans. Mark array elements within [low, high]; then scan.

c

int *missingRangeBool(const int arr[], int n, int low, int high, int *outSize) { if (low > high) { *outSize = 0; return NULL; } int r = high - low + 1; unsigned char *present = calloc((size_t)r, sizeof(unsigned char)); if (present == NULL) { *outSize = 0; return NULL; } for (int i = 0; i < n; i++) { if (arr[i] >= low && arr[i] <= high) { present[arr[i] - low] = 1; } } int *res = malloc((size_t)r * sizeof(int)); if (res == NULL) { free(present); *outSize = 0; return NULL; } int k = 0; for (int i = 0; i < r; i++) { if (!present[i]) res[k++] = low + i; } free(present); *outSize = k; return res; }

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

Recommendation: Prefer the boolean array when r is modest (e.g., ≤ 10⁷). Use the hash set when r is huge but n is small.


Full Practical Code Example with Explanation

This program implements all four approaches and verifies they agree.

c

/** * missing_in_range.c * Finds integers in [low, high] not present in arr[]. * Compares naive, sorted, hash set, and boolean-array approaches. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <time.h> #define MAX_LINE_LEN 100 #define TABLE_SIZE 262144 /* ============================================================ * HashSet * ============================================================ */ typedef struct { int *keys; int *occupied; int capacity; } HashSet; static unsigned int hashFunc(int key, int cap) { return ((unsigned int)key) % (unsigned int)cap; } HashSet *createSet(int cap) { HashSet *s = malloc(sizeof(HashSet)); if (s == NULL) return NULL; s->keys = malloc((size_t)cap * sizeof(int)); s->occupied = calloc((size_t)cap, sizeof(int)); if (s->keys == NULL || s->occupied == NULL) { free(s->keys); free(s->occupied); free(s); return NULL; } s->capacity = cap; return s; } void freeSet(HashSet *s) { if (s) { free(s->keys); free(s->occupied); free(s); } } int setContains(HashSet *s, int key) { unsigned int idx = hashFunc(key, s->capacity); int start = (int)idx; while (s->occupied[idx]) { if (s->keys[idx] == key) return 1; idx = (idx + 1) % (unsigned int)s->capacity; if ((int)idx == start) break; } return 0; } void setInsert(HashSet *s, int key) { unsigned int idx = hashFunc(key, s->capacity); int start = (int)idx; while (s->occupied[idx]) { if (s->keys[idx] == key) return; idx = (idx + 1) % (unsigned int)s->capacity; if ((int)idx == start) return; } s->keys[idx] = key; s->occupied[idx] = 1; } /* ============================================================ * Approaches * ============================================================ */ int *missingRangeNaive(const int arr[], int n, int low, int high, int *outSize) { *outSize = 0; if (low > high) return NULL; int r = high - low + 1; int *res = malloc((size_t)r * sizeof(int)); if (res == NULL) return NULL; int k = 0; for (int x = low; x <= high; x++) { int found = 0; for (int i = 0; i < n; i++) { if (arr[i] == x) { found = 1; break; } } if (!found) res[k++] = x; } *outSize = k; return res; } static int compareInts(const void *a, const void *b) { int x = *(const int *)a; int y = *(const int *)b; return (x > y) - (x < y); } int *missingRangeSorted(const int arr_in[], int n, int low, int high, int *outSize) { *outSize = 0; if (low > high) return NULL; int *arr = NULL; if (n > 0) { arr = malloc((size_t)n * sizeof(int)); if (arr == NULL) return NULL; memcpy(arr, arr_in, (size_t)n * sizeof(int)); } qsort(arr, n, sizeof(int), compareInts); int r = high - low + 1; int *res = malloc((size_t)r * sizeof(int)); if (res == NULL) { free(arr); return NULL; } int k = 0, i = 0; for (int x = low; x <= high; x++) { while (i < n && arr[i] < x) i++; if (i == n || arr[i] != x) res[k++] = x; } free(arr); *outSize = k; return res; } int *missingRangeHash(const int arr[], int n, int low, int high, int *outSize) { *outSize = 0; if (low > high) return NULL; HashSet *s = createSet(TABLE_SIZE); if (s == NULL) return NULL; for (int i = 0; i < n; i++) setInsert(s, arr[i]); int r = high - low + 1; int *res = malloc((size_t)r * sizeof(int)); if (res == NULL) { freeSet(s); return NULL; } int k = 0; for (int x = low; x <= high; x++) { if (!setContains(s, x)) res[k++] = x; } freeSet(s); *outSize = k; return res; } int *missingRangeBool(const int arr[], int n, int low, int high, int *outSize) { *outSize = 0; if (low > high) return NULL; int r = high - low + 1; unsigned char *present = calloc((size_t)r, sizeof(unsigned char)); if (present == NULL) return NULL; for (int i = 0; i < n; i++) { if (arr[i] >= low && arr[i] <= high) { present[arr[i] - low] = 1; } } int *res = malloc((size_t)r * sizeof(int)); if (res == NULL) { free(present); return NULL; } int k = 0; for (int i = 0; i < r; i++) { if (!present[i]) res[k++] = low + i; } free(present); *outSize = k; return res; } /* ============================================================ * Utility * ============================================================ */ 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("]"); } static void demoBasic(void) { printf("\n=== Sample Arrays ===\n"); int a1[] = {10, 12, 11, 15}; int a2[] = {1, 4, 11, 51, 15}; struct { int *arr; int n; int low; int high; const char *label; } tests[] = { {a1, 4, 10, 15, "arr=[10,12,11,15], range=[10,15]"}, {a2, 5, 50, 55, "arr=[1,4,11,51,15], range=[50,55]"} }; int nt = sizeof(tests) / sizeof(tests[0]); for (int t = 0; t < nt; t++) { printf("\nTest %d: %s\n", t + 1, tests[t].label); printf(" Array: "); printArray(tests[t].arr, tests[t].n); printf("\n"); int s1, s2, s3, s4; int *r1 = missingRangeNaive(tests[t].arr, tests[t].n, tests[t].low, tests[t].high, &s1); int *r2 = missingRangeSorted(tests[t].arr, tests[t].n, tests[t].low, tests[t].high, &s2); int *r3 = missingRangeHash(tests[t].arr, tests[t].n, tests[t].low, tests[t].high, &s3); int *r4 = missingRangeBool(tests[t].arr, tests[t].n, tests[t].low, tests[t].high, &s4); printf(" Naive: "); printArray(r1, s1); printf("\n"); printf(" Sorted: "); printArray(r2, s2); printf("\n"); printf(" Hash: "); printArray(r3, s3); printf("\n"); printf(" Boolean: "); printArray(r4, s4); printf("\n"); int ok = (s1 == s2 && s2 == s3 && s3 == s4); if (ok) { for (int i = 0; i < s1; i++) { if (r1[i] != r2[i] || r2[i] != r3[i] || r3[i] != r4[i]) { ok = 0; break; } } } printf(" Verify: %s\n", ok ? "PASS" : "FAIL"); free(r1); free(r2); free(r3); free(r4); } printf("\n"); } static void demoEdgeCases(void) { printf("\n=== Edge Cases ===\n"); int size = 0; /* Empty array, non-empty range */ int *r = missingRangeHash(NULL, 0, 1, 5, &size); printf("Empty arr, [1..5]: "); printArray(r, size); printf("\n"); free(r); /* Empty range (low > high) */ int a1[] = {1, 2, 3}; r = missingRangeHash(a1, 3, 5, 4, &size); printf("Empty range [5,4]: size=%d\n", size); if (r) free(r); /* Single-element range present */ int a2[] = {7, 8, 9}; r = missingRangeHash(a2, 3, 8, 8, &size); printf("[7,8,9] range [8,8]: "); printArray(r, size); printf("\n"); free(r); /* Single-element range missing */ r = missingRangeHash(a2, 3, 5, 5, &size); printf("[7,8,9] range [5,5]: "); printArray(r, size); printf("\n"); free(r); /* All missing */ int a3[] = {100, 200, 300}; r = missingRangeHash(a3, 3, 1, 4, &size); printf("[100,200,300] range [1,4]: "); printArray(r, size); printf("\n"); free(r); /* None missing */ int a4[] = {1, 2, 3, 4, 5}; r = missingRangeHash(a4, 5, 1, 5, &size); printf("[1..5] range [1,5]: size=%d\n", size); free(r); /* Negative range */ int a5[] = {-3, -1, 0}; r = missingRangeHash(a5, 3, -3, 0, &size); printf("[-3,-1,0] range [-3,0]: "); printArray(r, size); printf("\n"); free(r); /* Large sparse range with hash */ int a6[] = {1000000}; r = missingRangeHash(a6, 1, 999998, 1000002, &size); printf("[1000000] range [999998..1000002]: "); printArray(r, size); printf("\n"); free(r); } static void performanceTest(void) { printf("\n=== Performance Comparison ===\n"); int n = 500000; int *arr = malloc((size_t)n * sizeof(int)); if (arr == NULL) return; srand(42); for (int i = 0; i < n; i++) arr[i] = rand() % 1000000; int low = 0, high = 100000; /* r = 100001 */ clock_t start, end; double t; int size = 0; start = clock(); int *r1 = missingRangeNaive(arr, n, low, high, &size); end = clock(); t = ((double)(end - start)) / CLOCKS_PER_SEC * 1000.0; printf("Naive (n=%d, r=%d): size=%d, time=%.3f ms\n", n, high - low + 1, size, t); free(r1); start = clock(); int *r2 = missingRangeSorted(arr, n, low, high, &size); end = clock(); t = ((double)(end - start)) / CLOCKS_PER_SEC * 1000.0; printf("Sorted (n=%d, r=%d): size=%d, time=%.3f ms\n", n, high - low + 1, size, t); free(r2); start = clock(); int *r3 = missingRangeHash(arr, n, low, high, &size); end = clock(); t = ((double)(end - start)) / CLOCKS_PER_SEC * 1000.0; printf("Hash (n=%d, r=%d): size=%d, time=%.3f ms\n", n, high - low + 1, size, t); free(r3); start = clock(); int *r4 = missingRangeBool(arr, n, low, high, &size); end = clock(); t = ((double)(end - start)) / CLOCKS_PER_SEC * 1000.0; printf("Boolean (n=%d, r=%d): size=%d, time=%.3f ms\n", n, high - low + 1, size, t); free(r4); free(arr); } 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; } static void demoInteractive(void) { int n, low, high; if (readInteger("Enter array size (0-10000): ", &n) != 0 || n < 0 || n > 10000) { printf("Invalid size.\n"); return; } int *arr = NULL; if (n > 0) { arr = malloc((size_t)n * sizeof(int)); if (arr == NULL) return; for (int i = 0; i < n; i++) { char p[40]; snprintf(p, sizeof(p), " arr[%d]: ", i); if (readInteger(p, &arr[i]) != 0) { printf("Invalid value.\n"); free(arr); return; } } } if (readInteger("Enter low: ", &low) != 0) { free(arr); return; } if (readInteger("Enter high: ", &high) != 0) { free(arr); return; } int size = 0; int *res = missingRangeHash(arr, n, low, high, &size); printf("Missing: "); printArray(res, size); printf("\n"); free(res); free(arr); } int main(void) { int choice; char buffer[MAX_LINE_LEN]; printf("=== Missing Elements in Range Demo ===\n"); while (1) { printf("\n--- Menu ---\n"); printf("1. Demonstrate All Approaches\n"); printf("2. Edge Cases\n"); printf("3. Performance Comparison\n"); printf("4. Interactive\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: demoBasic(); break; case 2: demoEdgeCases(); break; case 3: performanceTest(); break; case 4: demoInteractive(); break; case 5: printf("Exiting.\n"); return EXIT_SUCCESS; default: printf("Invalid choice.\n"); break; } } return EXIT_SUCCESS; }

Explanation of the Approaches

  1. Naive: For each candidate in the range, scan the array. Quadratic in r × n.
  2. Sorted: Sort once, then advance a pointer while scanning the range.
  3. Hash Set: Insert all values, then check each range number in O(1) average.
  4. Boolean Array: Mark present indices directly; iterate over r.

Expected Output (sample run)

text

=== Sample Arrays === Test 1: arr=[10,12,11,15], range=[10,15] Array: [10, 12, 11, 15] Naive: [13, 14] Sorted: [13, 14] Hash: [13, 14] Boolean: [13, 14] Verify: PASS Test 2: arr=[1,4,11,51,15], range=[50,55] Array: [1, 4, 11, 51, 15] Naive: [50, 52, 53, 54, 55] Sorted: [50, 52, 53, 54, 55] Hash: [50, 52, 53, 54, 55] Boolean: [50, 52, 53, 54, 55] Verify: PASS === Edge Cases === Empty arr, [1..5]: [1, 2, 3, 4, 5] Empty range [5,4]: size=0 [7,8,9] range [8,8]: [] [7,8,9] range [5,5]: [5] [100,200,300] range [1,4]: [1, 2, 3, 4] [1..5] range [1,5]: size=0 [-3,-1,0] range [-3,0]: [-2] [1000000] range [999998..1000002]: [999998, 999999, 1000001, 1000002]

Common Pitfalls, Edge Cases & Memory Considerations

1. Empty Range (low > high)

Return an empty result; never allocate a negative or zero-sized array.

c

if (low > high) { *outSize = 0; return NULL; }

2. Range Size Overflow

high - low + 1 can overflow int.

Solution: Use long long for the range width.

c

long long r = (long long)high - (long long)low + 1;

3. Boolean Array for Very Large Ranges

Allocating r bytes for r = 10⁹ is impractical.

Solution: Use the hash set when r is huge and n is small.

4. Hash Table Sizing

Undersized table causes probe failures.

Solution: Size ≥ 2× n.

5. Negative Values

Cast to unsigned int in the hash function.

6. Duplicate Array Values

Hash insert is idempotent; boolean marking is idempotent. Correct by construction.

7. Mutating Inputs

Sorted approach copies inputs before sorting.

8. Out-of-Range Array Values

The boolean array checks bounds before indexing:

c

if (arr[i] >= low && arr[i] <= high) present[arr[i] - low] = 1;

9. Freeing on Error Paths

Free present and arr copies before returning NULL.

10. Memory Considerations

  • Naive: O(r) for result, no extras.
  • Sorted: O(n) for the copy.
  • Hash: O(n) for the set.
  • Boolean: O(r) for the marker array.

Summary & Best Practices

Finding missing elements in a range is a clean set-difference problem with multiple efficient solutions. Choose the hash set for sparse large ranges and the boolean array for dense moderate ranges.

Key Takeaways

  • Range size r = high - low + 1.
  • Naive: O(r × n).
  • Sorted: O(n log n + r).
  • Hash: O(n + r) average.
  • Boolean: O(n + r) with O(r) space.
  • Handle low > high as empty.
  • Use long long for large ranges.

Best Practices

  1. Use the boolean array when r is modest (< 10⁷).
  2. Use the hash set when r is huge but n is small.
  3. Sort approach when memory is constrained.
  4. Guard bounds before indexing in the boolean approach.
  5. Cast to unsigned in the hash function for negative keys.
  6. Handle low > high without allocating.
  7. Use long long for range width when in doubt.
  8. Free all dynamic memory.
  9. Test edge cases: empty array, empty range, negative range, out-of-range values.
  10. Profile for large inputs.

By mastering this problem, you’ll be prepared for gap detection, missing ID resolution, and range-based analytics.