CoreTechED

Loading

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

Binary Search in C: Iterative & Recursive Guide

Binary Search in C: A Comprehensive Tutorial

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

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


Conceptual Introduction & Fundamentals

What is Binary Search?

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

Why Binary Search Matters

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

Prerequisites

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

How Binary Search Works

For a sorted array and target x:

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

Example:

text

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

Complexity Analysis

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

Space:

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

Syntax & Core Mechanics

Iterative Binary Search

c

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

Expected output:

text

Element is present at index 3

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

Recursive Binary Search

c

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

Expected output:

text

Element is present at index 3

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

Finding First and Last Occurrence

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

c

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

Full Practical Code Example with Explanation

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

Program: Binary Search Complete Demo

c

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

Explanation of Binary Search Variants

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

Expected Output (sample run)

text

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

Common Pitfalls, Edge Cases & Memory Considerations

1. Integer Overflow in Mid Calculation

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

c

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

2. Infinite Loop from Wrong Update

Forgetting to update low or high causes infinite loops.

c

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

3. Off-by-One in Loop Condition

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

c

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

4. Unsorted Array

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

Solution: Sort the array first with qsort.

5. Empty Array

Return -1 for empty arrays.

c

if (n <= 0) return -1;

6. Duplicates

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

7. Recursion Depth

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

8. Descending Arrays

Adapt comparison logic for descending order.

c

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

9. Search on Monotonic Functions

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

10. Memory Considerations

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

Summary & Best Practices

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

Key Takeaways

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

Best Practices

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

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

Learn to remove duplicates from sorted arrays using two-pointer technique O(n) time O(1) space. Master in-place array manipulation in C.

Remove Duplicates from Sorted Array: Complete Guide

Remove Duplicates from Sorted Array: A Comprehensive Tutorial

Given a sorted array, the goal is to rearrange the array so that all distinct elements appear at the beginning in sorted order. This problem demonstrates efficient in-place array manipulation using the two-pointer technique.

This tutorial provides an in-depth exploration of removing duplicates from a sorted array: the hash set approach (works for unsorted), the optimal in-place approach, practical implementations in C, edge cases, common pitfalls, and best practices.


Conceptual Introduction & Fundamentals

What is Removing Duplicates?

Removing duplicates means keeping only one instance of each distinct element in a sorted array, placing them at the beginning of the array.

Example:

text

Input: [1, 2, 2, 3, 4, 4, 4, 5, 5] Output: [1, 2, 3, 4, 5] (first 5 positions)

Why This Problem Matters

  • In-place manipulation: Modify array without extra space.
  • Two-pointer technique: Foundation for many array problems.
  • Algorithm optimization: O(n) time with O(1) space.
  • Interview preparation: Common coding question.

Approaches

ApproachTime ComplexitySpace ComplexityWorks for Unsorted?
Hash SetO(n)O(n)Yes
Two PointerO(n)O(1)No (requires sorted)

Syntax & Core Mechanics

Optimal Approach: Two Pointers (O(n) Time, O(1) Space)

Since the array is sorted, duplicates are consecutive. Use two pointers: idx for the next distinct position and i for iteration.

c

#include <stdio.h> /* Removes duplicates in-place; returns new length */ int removeDuplicates(int arr[], int n) { if (n <= 1) { return n; // No duplicates possible } int idx = 1; // Position for next distinct element for (int i = 1; i < n; i++) { if (arr[i] != arr[i - 1]) { arr[idx++] = arr[i]; // Place distinct element } } return idx; // New length } int main(void) { int arr[] = {1, 2, 2, 3, 4, 4, 4, 5, 5}; int n = sizeof(arr) / sizeof(arr[0]); int newSize = removeDuplicates(arr, n); printf("Array after removing duplicates: "); for (int i = 0; i < newSize; i++) { printf("%d ", arr[i]); } printf("\n"); printf("New length: %d\n", newSize); return 0; }

Expected output:

text

Array after removing duplicates: 1 2 3 4 5 New length: 5

Hash Set Approach (Works for Unsorted)

For unsorted arrays, use a hash set to track seen elements.

c

#include <stdio.h> #include <stdlib.h> #include <stdbool.h> /* Simple hash set for demonstration (limited range) */ #define MAX_VALUE 10000 int removeDuplicatesUnsorted(int arr[], int n) { bool seen[MAX_VALUE] = {false}; int idx = 0; for (int i = 0; i < n; i++) { if (!seen[arr[i]]) { seen[arr[i]] = true; arr[idx++] = arr[i]; } } return idx; } int main(void) { int arr[] = {5, 2, 1, 2, 4, 1, 5, 3}; int n = sizeof(arr) / sizeof(arr[0]); int newSize = removeDuplicatesUnsorted(arr, n); printf("Unsorted array after removing duplicates: "); for (int i = 0; i < newSize; i++) { printf("%d ", arr[i]); } printf("\n"); return 0; }

Full Practical Code Example with Explanation

This program demonstrates removing duplicates with comprehensive examples and edge cases.

Program: Remove Duplicates from Sorted Array

c

/** * remove_duplicates.c * Demonstrates removing duplicates from sorted array. * Includes optimal two-pointer approach and edge cases. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <stdbool.h> #define MAX_LINE_LEN 100 /* Function prototypes */ int removeDuplicatesSorted(int arr[], int n); int removeDuplicatesUnsorted(int arr[], int n); void demonstrateSortedArray(void); void demonstrateUnsortedArray(void); void demonstrateEdgeCases(void); void demonstratePerformance(void); void printArray(const int arr[], int n); int readInteger(const char *prompt, int *outValue); int main(void) { int choice; char buffer[MAX_LINE_LEN]; printf("=== Remove Duplicates from Array ===\n"); while (1) { printf("\n--- Menu ---\n"); printf("1. Sorted Array (Two Pointers)\n"); printf("2. Unsorted Array (Hash Set)\n"); printf("3. Edge Cases\n"); printf("4. Exit\n"); printf("Enter choice: "); fflush(stdout); if (fgets(buffer, sizeof(buffer), stdin) == NULL) { fprintf(stderr, "Error reading choice.\n"); return EXIT_FAILURE; } choice = atoi(buffer); switch (choice) { case 1: demonstrateSortedArray(); break; case 2: demonstrateUnsortedArray(); break; case 3: demonstrateEdgeCases(); break; case 4: printf("Exiting.\n"); return EXIT_SUCCESS; default: printf("Invalid choice. Please try again.\n"); break; } } return EXIT_SUCCESS; } /* Removes duplicates from sorted array: O(n) time, O(1) space */ int removeDuplicatesSorted(int arr[], int n) { if (n <= 1) { return n; } int idx = 1; for (int i = 1; i < n; i++) { if (arr[i] != arr[i - 1]) { arr[idx++] = arr[i]; } } return idx; } /* Removes duplicates from unsorted array: O(n) time, O(n) space */ int removeDuplicatesUnsorted(int arr[], int n) { bool *seen = calloc(10000, sizeof(bool)); // For values 0-9999 if (seen == NULL) { return n; } int idx = 0; for (int i = 0; i < n; i++) { if (arr[i] >= 0 && arr[i] < 10000 && !seen[arr[i]]) { seen[arr[i]] = true; arr[idx++] = arr[i]; } } free(seen); return idx; } /* Demonstrates sorted array */ void demonstrateSortedArray(void) { printf("\n=== Sorted Array ===\n"); int arr[] = {1, 2, 2, 3, 4, 4, 4, 5, 5}; int n = sizeof(arr) / sizeof(arr[0]); printf("Original: "); printArray(arr, n); int newSize = removeDuplicatesSorted(arr, n); printf("After removing duplicates: "); printArray(arr, newSize); printf("New length: %d\n", newSize); printf("Time: O(n), Space: O(1)\n"); } /* Demonstrates unsorted array */ void demonstrateUnsortedArray(void) { printf("\n=== Unsorted Array ===\n"); int arr[] = {5, 2, 1, 2, 4, 1, 5, 3}; int n = sizeof(arr) / sizeof(arr[0]); printf("Original: "); printArray(arr, n); int newSize = removeDuplicatesUnsorted(arr, n); printf("After removing duplicates: "); printArray(arr, newSize); printf("Time: O(n), Space: O(n)\n"); } /* Demonstrates edge cases */ void demonstrateEdgeCases(void) { printf("\n=== Edge Cases ===\n"); /* Case 1: Empty array */ int emptyArr[] = {}; int emptyN = 0; printf("Empty array: "); int newSize = removeDuplicatesSorted(emptyArr, emptyN); printf("(length = %d)\n", newSize); /* Case 2: Single element */ int singleArr[] = {42}; int singleN = sizeof(singleArr) / sizeof(singleArr[0]); newSize = removeDuplicatesSorted(singleArr, singleN); printf("Single element: "); printArray(singleArr, newSize); /* Case 3: All duplicates */ int allDupArr[] = {2, 2, 2, 2, 2}; int allDupN = sizeof(allDupArr) / sizeof(allDupArr[0]); newSize = removeDuplicatesSorted(allDupArr, allDupN); printf("All duplicates: "); printArray(allDupArr, newSize); /* Case 4: No duplicates */ int noDupArr[] = {1, 2, 3, 4, 5}; int noDupN = sizeof(noDupArr) / sizeof(noDupArr[0]); newSize = removeDuplicatesSorted(noDupArr, noDupN); printf("No duplicates: "); printArray(noDupArr, newSize); /* Case 5: Two elements */ int twoArr[] = {1, 1}; int twoN = sizeof(twoArr) / sizeof(twoArr[0]); newSize = removeDuplicatesSorted(twoArr, twoN); printf("Two same elements: "); printArray(twoArr, newSize); /* Case 6: Negative numbers */ int negArr[] = {-5, -5, -2, -1, -1, 0, 3, 3}; int negN = sizeof(negArr) / sizeof(negArr[0]); newSize = removeDuplicatesSorted(negArr, negN); printf("Negative numbers: "); printArray(negArr, newSize); } /* Prints array */ void printArray(const int arr[], int n) { for (int i = 0; i < n; i++) { printf("%d ", arr[i]); } printf("\n"); }

Explanation of Approaches

  1. Sorted array: Two-pointer technique, O(n) time, O(1) space.
  2. Unsorted array: Hash set, O(n) time, O(n) space.
  3. Edge cases: Empty, single, all duplicates, no duplicates, negative.

Expected Output (partial)

text

=== Remove Duplicates from Array === --- Menu --- 1. Sorted Array (Two Pointers) 2. Unsorted Array (Hash Set) 3. Edge Cases 4. Exit Enter choice: 1 === Sorted Array === Original: 1 2 2 3 4 4 4 5 5 After removing duplicates: 1 2 3 4 5 New length: 5 Time: O(n), Space: O(1) --- Menu --- ... Enter choice: 3 === Edge Cases === Empty array: (length = 0) Single element: 42 All duplicates: 2 No duplicates: 1 2 3 4 5 Two same elements: 1 Negative numbers: -5 -2 -1 0 3

Common Pitfalls, Edge Cases & Memory Considerations

1. Not Handling Empty Array

Accessing arr[0] on empty array is undefined.

Solution: Check n <= 1 and return n.

2. Forgetting idx Starts at 1

First element is always distinct.

Solution: Initialize idx = 1 for sorted arrays.

3. Not Using Sorted Property

Using hash set on sorted array wastes space.

Solution: Use two-pointer technique for sorted arrays.

4. Memory Leaks in Hash Set

Forgetting to free hash set memory.

Solution: Always free allocated memory.

5. Hash Set Size Limitations

Fixed-size hash set may not cover all values.

Solution: Use dynamic hash table for arbitrary values.

6. Not Handling Negative Numbers

Hash set with only positive indices fails for negatives.

Solution: Use proper hash function or handle negatives.

7. Modifying Original Array

The function modifies the array in-place.

Solution: Document that the array is modified.

8. Returning Wrong Length

Forgetting to return the new length.

Solution: Return idx as the new size.

9. Not Testing with Edge Cases

Missing tests for empty, single, all duplicates.

Solution: Test thoroughly.

10. Memory Considerations

  • Sorted array: O(1) extra space.
  • Unsorted array: O(n) for hash set.
  • In-place modification: no additional array needed.
  • No recursion: no stack space.

Summary & Best Practices

Removing duplicates from a sorted array uses the two-pointer technique for O(n) time and O(1) space. For unsorted arrays, a hash set provides O(n) time with O(n) space.

Key Takeaways

  • Sorted array: consecutive duplicates.
  • Two-pointer technique: idx and i.
  • O(n) time, O(1) space for sorted.
  • Hash set for unsorted: O(n) time, O(n) space.
  • Return the new length.
  • Array is modified in-place.

Best Practices

  1. Use two-pointer for sorted arrays.
  2. Check for empty/single element arrays.
  3. Use const where appropriate.
  4. Document in-place modification.
  5. Free hash set memory.
  6. Test with edge cases.
  7. Return new length correctly.
  8. Handle negative numbers.
  9. Consider space-time trade-offs.
  10. Profile performance for large arrays.

By mastering this problem, you’ll develop the two-pointer technique essential for many array manipulation tasks.

Learn to count elements smaller than X in C arrays using linear scan and binary search. Master O(n) and O(log n) approaches with edge cases.

Count Smaller Than X in Array: C Complete Guide

Count Smaller Than X in an Array: A Comprehensive Tutorial

Counting elements smaller than a given value X in an array is a fundamental operation in data analysis and algorithm design. This problem tests understanding of array traversal, comparison operations, and optimization techniques. It serves as a building block for more complex algorithms like rank finding, percentile calculation, and data filtering.

This tutorial provides an in-depth exploration of counting elements smaller than X in C: multiple approaches, handling sorted and unsorted arrays, performance optimization, edge cases, and best practices.


Conceptual Introduction & Fundamentals

What Does “Count Smaller Than X” Mean?

Given an array of integers and a target value X, count how many elements in the array are strictly less than X.

Example:

text

Array: [10, 5, 20, 15, 25, 30] X = 20 Elements smaller than 20: 10, 5, 15 Count = 3

Problem Variations

  1. Strictly less than: Elements < X
  2. Less than or equal to: Elements <= X
  3. Greater than: Elements > X
  4. Within range: Elements between [lower, upper]

Why This Problem Matters

  • Data filtering: Find items below a threshold (price, score, temperature).
  • Rank calculation: Determine position of an element in sorted order.
  • Percentile computation: Find what percentage of data falls below a value.
  • Algorithm foundation: Building block for selection algorithms and order statistics.
  • Database queries: COUNT WHERE value < X.

Approaches Overview

ApproachTime ComplexitySpace ComplexityRequirement
Linear scanO(n)O(1)None (works on unsorted)
Binary search (sorted)O(log n)O(1)Sorted array
Sorting + binary searchO(n log n)O(1) or O(n)Sorting cost
Frequency countO(n + range)O(range)Small value range

Syntax & Core Mechanics

Approach 1: Linear Scan (Unsorted Array)

Traverse the array and count elements less than X.

c

#include <stdio.h> int countSmallerThanX(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, 30}; int n = sizeof(arr) / sizeof(arr[0]); int x = 20; int count = countSmallerThanX(arr, n, x); printf("Elements smaller than %d: %d\n", x, count); return 0; }

Expected output:

text

Elements smaller than 20: 3

Time complexity: O(n)

Approach 2: Binary Search (Sorted Array)

If the array is sorted, use binary search to find the first element >= X. The count of smaller elements equals that index.

c

#include <stdio.h> int countSmallerSorted(const int arr[], int n, int x) { int left = 0; int right = n - 1; int result = n; /* Default: all elements are smaller */ while (left <= right) { int mid = left + (right - left) / 2; if (arr[mid] >= x) { result = mid; /* Record position */ right = mid - 1; /* Search left */ } else { left = mid + 1; /* Search right */ } } return result; /* Number of elements < X */ } int main(void) { int arr[] = {5, 10, 15, 20, 25, 30}; int n = sizeof(arr) / sizeof(arr[0]); int x = 20; int count = countSmallerSorted(arr, n, x); printf("Elements smaller than %d: %d\n", x, count); return 0; }

Expected output:

text

Elements smaller than 20: 3

Time complexity: O(log n)

Approach 3: Count Less Than or Equal To

Variation that counts elements <= X.

c

#include <stdio.h> int countLessEqual(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, 20, 30, 40, 50}; int n = sizeof(arr) / sizeof(arr[0]); int x = 30; int count = countLessEqual(arr, n, x); printf("Elements <= %d: %d\n", x, count); return 0; }

Expected output:

text

Elements <= 30: 3

Approach 4: Count Within Range

Count elements in range [lower, upper].

c

#include <stdio.h> int countInRange(const int arr[], int n, int lower, int upper) { int count = 0; for (int i = 0; i < n; i++) { if (arr[i] >= lower && arr[i] <= upper) { count++; } } return count; } int main(void) { int arr[] = {10, 20, 30, 40, 50, 60}; int n = sizeof(arr) / sizeof(arr[0]); int lower = 20, upper = 50; int count = countInRange(arr, n, lower, upper); printf("Elements in [%d, %d]: %d\n", lower, upper, count); return 0; }

Expected output:

text

Elements in [20, 50]: 4

Approach 5: Count Smaller for Each Element

For each element, count how many elements are smaller than it.

c

#include <stdio.h> #include <stdlib.h> int *countSmallerForEach(const int arr[], int n) { int *result = malloc(n * sizeof(int)); if (result == NULL) return NULL; for (int i = 0; i < n; i++) { int count = 0; for (int j = 0; j < n; j++) { if (arr[j] < arr[i]) { count++; } } result[i] = count; } return result; } int main(void) { int arr[] = {10, 20, 30, 40}; int n = sizeof(arr) / sizeof(arr[0]); int *counts = countSmallerForEach(arr, n); if (counts != NULL) { for (int i = 0; i < n; i++) { printf("Elements smaller than %d: %d\n", arr[i], counts[i]); } free(counts); } return 0; }

Expected output:

text

Elements smaller than 10: 0 Elements smaller than 20: 1 Elements smaller than 30: 2 Elements smaller than 40: 3

Full Practical Code Example with Explanation

This program demonstrates counting elements smaller than X in a realistic scenario: a student score analyzer that counts students below a passing threshold.

Program: Student Score Analyzer

c

/** * count_smaller_than_x.c * Counts elements smaller than X in arrays. * Demonstrates linear scan, binary search, and range counting. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <time.h> #define MAX_LINE_LEN 100 #define MAX_STUDENTS 100 /* Function prototypes */ int countSmallerLinear(const int arr[], int n, int x); int countSmallerSorted(const int arr[], int n, int x); int countLessEqual(const int arr[], int n, int x); int countInRange(const int arr[], int n, int lower, int upper); void sortArray(int arr[], int n); int readInteger(const char *prompt, int *outValue); void printArray(const int arr[], int n); void demonstrateApproach(const char *name, int result, clock_t start, clock_t end); int main(void) { int choice; char buffer[MAX_LINE_LEN]; /* Test arrays */ int unsortedScores[] = {85, 45, 92, 60, 78, 35, 70, 55}; int nUnsorted = sizeof(unsortedScores) / sizeof(unsortedScores[0]); int sortedScores[] = {35, 45, 55, 60, 70, 78, 85, 92}; int nSorted = sizeof(sortedScores) / sizeof(sortedScores[0]); printf("=== Student Score Analyzer ===\n"); while (1) { printf("\n--- Menu ---\n"); printf("1. Count Scores Below Passing (Linear)\n"); printf("2. Count Scores Below Passing (Binary Search)\n"); printf("3. Count Scores in Grade Range\n"); printf("4. Count <= Threshold\n"); printf("5. Performance Comparison\n"); printf("6. 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: { int passingScore; if (readInteger("Enter passing score: ", &passingScore) != 0) { fprintf(stderr, "Invalid score.\n"); break; } printf("\nUnsorted scores: "); printArray(unsortedScores, nUnsorted); int count = countSmallerLinear(unsortedScores, nUnsorted, passingScore); printf("Students below %d: %d\n", passingScore, count); break; } case 2: { int passingScore; if (readInteger("Enter passing score: ", &passingScore) != 0) { fprintf(stderr, "Invalid score.\n"); break; } printf("\nSorted scores: "); printArray(sortedScores, nSorted); int count = countSmallerSorted(sortedScores, nSorted, passingScore); printf("Students below %d: %d\n", passingScore, count); break; } case 3: { int lower, upper; if (readInteger("Enter lower bound: ", &lower) != 0 || readInteger("Enter upper bound: ", &upper) != 0) { fprintf(stderr, "Invalid bounds.\n"); break; } printf("\nScores: "); printArray(unsortedScores, nUnsorted); int count = countInRange(unsortedScores, nUnsorted, lower, upper); printf("Students in [%d, %d]: %d\n", lower, upper, count); break; } case 4: { int threshold; if (readInteger("Enter threshold: ", &threshold) != 0) { fprintf(stderr, "Invalid threshold.\n"); break; } printf("\nScores: "); printArray(unsortedScores, nUnsorted); int count = countLessEqual(unsortedScores, nUnsorted, threshold); printf("Students <= %d: %d\n", threshold, count); break; } case 5: { printf("\n=== Performance Comparison ===\n"); int largeSize = 10000000; int *largeArr = malloc(largeSize * sizeof(int)); if (largeArr == NULL) { fprintf(stderr, "Memory allocation failed.\n"); break; } /* Fill with random values */ srand(42); for (int i = 0; i < largeSize; i++) { largeArr[i] = rand() % 1000; } int x = 500; clock_t start, end; /* Linear scan on unsorted */ start = clock(); int linearResult = countSmallerLinear(largeArr, largeSize, x); end = clock(); demonstrateApproach("Linear scan", linearResult, start, end); /* Sort for binary search */ start = clock(); sortArray(largeArr, largeSize); end = clock(); printf("Sorting time: %.3f ms\n", ((double)(end - start)) / CLOCKS_PER_SEC * 1000.0); /* Binary search on sorted */ start = clock(); int binaryResult = countSmallerSorted(largeArr, largeSize, x); end = clock(); demonstrateApproach("Binary search", binaryResult, start, end); free(largeArr); break; } case 6: printf("Exiting.\n"); return EXIT_SUCCESS; default: printf("Invalid choice. Please try again.\n"); break; } } return EXIT_SUCCESS; } /* Linear scan: O(n), works on unsorted arrays */ int countSmallerLinear(const int arr[], int n, int x) { int count = 0; for (int i = 0; i < n; i++) { if (arr[i] < x) { count++; } } return count; } /* Binary search: O(log n), requires sorted array */ int countSmallerSorted(const int arr[], int n, int x) { int left = 0; int right = n - 1; int result = n; while (left <= right) { int mid = left + (right - left) / 2; if (arr[mid] >= x) { result = mid; right = mid - 1; } else { left = mid + 1; } } return result; } /* Count elements <= x: O(n) */ int countLessEqual(const int arr[], int n, int x) { int count = 0; for (int i = 0; i < n; i++) { if (arr[i] <= x) { count++; } } return count; } /* Count elements in range [lower, upper]: O(n) */ int countInRange(const int arr[], int n, int lower, int upper) { int count = 0; for (int i = 0; i < n; i++) { if (arr[i] >= lower && arr[i] <= upper) { count++; } } return count; } /* Sorts array using bubble sort (simple, O(n^2)) */ void sortArray(int arr[], int n) { for (int i = 0; i < n - 1; i++) { for (int j = 0; j < n - i - 1; j++) { if (arr[j] > arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } } /* Displays approach result and time */ void demonstrateApproach(const char *name, int result, clock_t start, clock_t end) { double timeTaken = ((double)(end - start)) / CLOCKS_PER_SEC * 1000.0; printf("%-20s: Count = %d, Time = %.3f ms\n", name, result, timeTaken); } /* Prints an array */ void printArray(const int arr[], int n) { if (n == 0) { printf("[]"); return; } printf("["); for (int i = 0; i < n; i++) { printf("%d", arr[i]); if (i < n - 1) printf(", "); } printf("]"); } /* Reads an integer safely */ int readInteger(const char *prompt, int *outValue) { char buffer[MAX_LINE_LEN]; char *endPtr; long val; printf("%s", prompt); fflush(stdout); if (fgets(buffer, sizeof(buffer), stdin) == NULL) { return -1; } errno = 0; val = strtol(buffer, &endPtr, 10); if (endPtr == buffer || (*endPtr != '\n' && *endPtr != '\0')) { return -1; } if (errno == ERANGE || val < -2147483647 - 1 || val > 2147483647) { return -1; } *outValue = (int)val; return 0; }

Explanation of Approaches

  1. countSmallerLinear: O(n) linear scan for unsorted arrays.
  2. countSmallerSorted: O(log n) binary search for sorted arrays.
  3. countLessEqual: Counts elements <= X.
  4. countInRange: Counts elements within [lower, upper].
  5. sortArray: Sorts array (bubble sort for simplicity).
  6. Performance comparison: Shows trade-off between linear scan and sorting + binary search.

Expected Output (sample run)

text

=== Student Score Analyzer === --- Menu --- 1. Count Scores Below Passing (Linear) 2. Count Scores Below Passing (Binary Search) 3. Count Scores in Grade Range 4. Count <= Threshold 5. Performance Comparison 6. Exit Enter choice: 1 Enter passing score: 60 Unsorted scores: [85, 45, 92, 60, 78, 35, 70, 55] Students below 60: 3 --- Menu --- ... Enter choice: 2 Enter passing score: 60 Sorted scores: [35, 45, 55, 60, 70, 78, 85, 92] Students below 60: 3 --- Menu --- ... Enter choice: 3 Enter lower bound: 50 Enter upper bound: 80 Scores: [85, 45, 92, 60, 78, 35, 70, 55] Students in [50, 80]: 4 --- Menu --- ... Enter choice: 4 Enter threshold: 70 Scores: [85, 45, 92, 60, 78, 35, 70, 55] Students <= 70: 5 --- Menu --- ... Enter choice: 5 === Performance Comparison === Linear scan : Count = 5000000, Time = 8.234 ms Sorting time: 0.000 ms Binary search : Count = 5000000, Time = 0.001 ms

Common Pitfalls, Edge Cases & Memory Considerations

1. Strict vs. Non-Strict Comparison

Using < vs <= changes the result. Be clear about the requirement.

c

/* Strictly less than */ if (arr[i] < x) count++; /* Less than or equal to */ if (arr[i] <= x) count++;

Solution: Document which comparison is required.

2. Binary Search on Unsorted Array

Using binary search on an unsorted array gives incorrect results.

c

int arr[] = {50, 10, 40, 20, 30}; // Unsorted countSmallerSorted(arr, 5, 25); // Incorrect

Solution: Sort first or use linear scan.

3. Empty Array

Accessing arr[0] or arr[n-1] on an empty array is undefined behavior.

c

int countSmallerSorted(const int arr[], int n, int x) { if (arr[n - 1] < x) return n; // BUG: n == 0 }

Solution: Check n == 0 first.

4. All Elements Smaller or Larger

Handle cases where all elements are < X or all are >= X.

c

/* All smaller: returns n */ /* All larger: returns 0 */

Solution: Initialize result appropriately in binary search.

5. Integer Overflow in Mid Calculation

(left + right) / 2 can overflow for large arrays.

c

int mid = (left + right) / 2; // Potential overflow

Solution: Use left + (right - left) / 2.

6. Sorting Cost

Sorting for binary search costs O(n log n). For single queries on unsorted arrays, linear scan is better.

Solution: Use linear scan for single queries; sort only for multiple queries.

7. Duplicate Elements

Binary search for < X should return the first index where arr[index] >= X.

c

/* For duplicates: [1, 2, 2, 3], X = 2 */ /* Count smaller than 2 = 1 (only index 0) */

Solution: Use >= in binary search condition.

8. Negative Numbers

Comparisons work correctly with negative numbers, but ensure the array is sorted for binary search.

c

int arr[] = {-10, -5, 0, 5, 10}; countSmallerSorted(arr, 5, 0); // Returns 2 (-10, -5)

9. Large Arrays and Performance

Linear scan is O(n) per query. For many queries, sorting + binary search or frequency count may be better.

Solution: Consider preprocessing (sorting, prefix sums) for repeated queries.

10. Memory Considerations

  • Linear scan: O(1) space.
  • Binary search: O(1) space (iterative) or O(log n) (recursive).
  • Sorting: O(1) to O(n) space depending on algorithm.
  • Frequency count: O(range) space.

Summary & Best Practices

Counting elements smaller than X is a fundamental operation with applications in filtering, ranking, and data analysis. Linear scan works on unsorted arrays in O(n); binary search achieves O(log n) on sorted arrays.

Key Takeaways

  • Linear scan: O(n), works on any array.
  • Binary search: O(log n), requires sorted array.
  • Count smaller than X = index of first element >= X in sorted array.
  • Handle edge cases: empty array, all smaller, all larger, duplicates.
  • Use < for strict, <= for inclusive comparison.

Best Practices

  1. Use linear scan for single queries on unsorted arrays.
  2. Sort + binary search for multiple queries on the same array.
  3. Check for empty arrays before processing.
  4. Use left + (right - left) / 2 to avoid overflow.
  5. Document strict vs. non-strict comparison requirements.
  6. Handle duplicates correctly in binary search.
  7. Consider sorting cost when choosing approach.
  8. Use size_t for array sizes.
  9. Test with edge cases: empty, single element, all equal, all smaller/larger.
  10. Profile performance for large arrays and many queries.

By mastering this operation, you’ll build a foundation for more complex algorithms like selection, ranking, and order statistics.