This course is designed to help learners master the core concepts of Data Structures and Algorithms (DSA) using the C programming language. Starting from the basics and advancing to complex topics like graphs, dynamic programming, and memory optimization, this course is ideal for students, job seekers, and aspiring developers. You’ll learn how to structure and manipulate data efficiently, solve real-world coding problems, and prepare for technical interviews at top companies. The content is structured step-by-step, combining theory with hands-on coding examples and practice problems to reinforce understanding. Whether you're preparing for university exams, campus placements, or competitive programming, this course provides a strong foundation in logic building, code efficiency, and problem-solving using C. Key Highlights: Covers all major DSA topics from beginner to advanced level 100+ coding examples with explanations Focus on time and space complexity optimization Designed for coding interviews, competitive exams, and CS fundamentals
So far, we've explored comparison-based sorting algorithms like Bubble, Selection, Insertion, Merge, and Quick Sort. These algorithms sort elements by comparing them to each other. However, there's a powerful class of sorting algorithms known as non-comparison sorts that can achieve linear time complexity, O(n), under specific conditions. They do this by leveraging information about the distribution or structure of the input numbers themselves, rather than just their relative order. In this chapter, we'll dive into three prominent non-comparison sorts: Counting Sort, Radix Sort, and Bucket Sort.
Counting Sort is an efficient sorting algorithm for integers when the range of input numbers (the difference between the maximum and minimum values) is not significantly larger than the number of items to be sorted. It works by counting the frequency of each distinct element in the input array, and then using that information to place the elements in their correct sorted positions.
// Counting Sort in C
void countingSort(int arr[], int n) {
// Find the maximum element to determine the range
int max = arr[0];
for (int i = 1; i < n; i++) {
if (arr[i] > max)
max = arr[i];
}
int count[max + 1]; // Create count array (size max + 1)
int output[n]; // Create output array
// Initialize count array with all zeros
for (int i = 0; i <= max; ++i) {
count[i] = 0;
}
// Store the count of each element
for (int i = 0; i < n; i++) {
count[arr[i]]++;
}
// Store cumulative count of each array (prefix sum)
// This gives the actual position of elements in the output array
for (int i = 1; i <= max; i++) {
count[i] += count[i - 1];
}
// Place the elements in output array
// Iterate from right to left to ensure stability
for (int i = n - 1; i >= 0; i--) {
output[count[arr[i]] - 1] = arr[i];
count[arr[i]]--;
}
// Copy the output array to arr[], so that arr[] now contains sorted elements
for (int i = 0; i < n; i++) {
arr[i] = output[i];
}
}
Time Complexity: O(n + k), where 'n' is the number of elements and 'k' is the range of input (max_value - min_value + 1). If 'k' is proportional to 'n' (i.e., k = O(n)), then the time complexity becomes O(n), which is linear.
Space Complexity: O(n + k) for the `count` and `output` arrays.
Radix Sort is a non-comparison sorting algorithm that sorts numbers by processing individual digits (or bits) from least significant to most significant (LSD Radix Sort) or vice versa (MSD Radix Sort). It requires a stable sorting algorithm (like Counting Sort) as a subroutine for sorting digits. This makes it particularly effective for integers or strings with a fixed length.
// Radix Sort in C (using Counting Sort as a subroutine)
// A utility function to get maximum value in arr[]
int getMax(int arr[], int n) {
int max = arr[0];
for (int i = 1; i < n; i++)
if (arr[i] > max)
max = arr[i];
return max;
}
// A function to do counting sort of arr[] according to
// the digit represented by 'exp'.
void countSortForRadix(int arr[], int n, int exp) {
int output[n]; // output array
int i, count[10] = {0}; // 0-9 for digits
// Store count of occurrences in count[]
for (i = 0; i < n; i++)
count[(arr[i] / exp) % 10]++;
// Change count[i] so that count[i] now contains actual
// position of this digit in output[]
for (i = 1; i < 10; i++)
count[i] += count[i - 1];
// Build the output array
for (i = n - 1; i >= 0; i--) {
output[count[(arr[i] / exp) % 10] - 1] = arr[i];
count[(arr[i] / exp) % 10]--;
}
// Copy the output array to arr[], so that arr[] now
// contains sorted numbers according to current digit
for (i = 0; i < n; i++)
arr[i] = output[i];
}
// The main function to that sorts arr[] of size n using Radix Sort
void radixSort(int arr[], int n) {
// Find the maximum number to know number of digits
int m = getMax(arr, n);
// Do counting sort for every digit. Note that instead of passing digit number,
// exp is passed. exp is 10^i where i is current digit number
for (int exp = 1; m / exp > 0; exp *= 10)
countSortForRadix(arr, n, exp);
}
Time Complexity: O(d * (n + b)), where 'd' is the number of digits (or passes), 'n' is the number of elements, and 'b' is the base (radix) of the numbers (usually 10 for decimal numbers). If 'd' and 'b' are constants, this simplifies to O(n), making it linear.
Space Complexity: O(n + b) due to the auxiliary space used by Counting Sort (output array and count array).
Bucket Sort, also known as bin sort, is a distribution sort algorithm that distributes elements of an array into a number of buckets. Each bucket is then sorted individually, either using a different sorting algorithm (like Insertion Sort, which is efficient for small inputs) or by recursively applying the bucket sort algorithm. Finally, the elements are gathered from the buckets in order. Bucket Sort works best when the input is uniformly distributed over a range.
// Bucket Sort in C (simplified for integers, real-world often uses floating points)
// This example assumes numbers are in range [0, 100] for simplicity.
#include <stdio.h>
#include <stdlib.h>
#include <string.h> // For memset
// Function to sort an array using insertion sort (for individual buckets)
void insertionSortForBucket(int arr[], int n) {
int i, key, j;
for (i = 1; i < n; i++) {
key = arr[i];
j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j = j - 1;
}
arr[j + 1] = key;
}
}
void bucketSort(int arr[], int n) {
// 1. Create n buckets. For simplicity, let's assume max value is 100
// and elements are distributed across these buckets.
// In a real scenario, you'd determine the number of buckets and range per bucket
// based on max_val and min_val of the array.
int max_val = arr[0];
for(int i=1; i max_val) max_val = arr[i];
}
// Let's create 'num_buckets' buckets.
// A common approach is num_buckets = n or sqrt(n)
// For integers, usually elements are mapped to buckets using (value * num_buckets) / (max_val + 1)
int num_buckets = 10; // Example: 10 buckets
if (n > 0) num_buckets = (max_val / n) + 1; // A more dynamic way to determine buckets
// Create an array of vectors (or linked lists) for buckets
// This is a common way to represent dynamic arrays in C, but typically
// would be done with dynamic memory or standard library containers in C++
// For simplicity, let's use a 2D array and track sizes. This is not ideal for real-world.
// In a production C environment, you'd use dynamic arrays (e.g., `struct Vector { int* data; int size; int capacity; }`).
// For demonstration, let's use a simplified approach assuming max items per bucket.
// THIS IS A SIMPLIFIED, ILLUSTRATIVE C IMPLEMENTATION FOR THE CONCEPT.
// A robust C implementation would be more complex (e.g., using linked lists for buckets).
// Simulate buckets using a simple array of arrays (fixed size for illustration)
// In real C, you'd typically use linked lists or dynamically resizing arrays for buckets.
int buckets[num_buckets][n]; // Max n elements per bucket
int bucket_sizes[num_buckets];
memset(bucket_sizes, 0, sizeof(bucket_sizes)); // Initialize all sizes to 0
// 2. Put array elements into appropriate buckets
for (int i = 0; i < n; i++) {
int bucket_idx = (arr[i] * num_buckets) / (max_val + 1); // Map to bucket index
buckets[bucket_idx][bucket_sizes[bucket_idx]++] = arr[i];
}
// 3. Sort individual buckets
for (int i = 0; i < num_buckets; i++) {
insertionSortForBucket(buckets[i], bucket_sizes[i]);
}
// 4. Concatenate all buckets into arr[]
int index = 0;
for (int i = 0; i < num_buckets; i++) {
for (int j = 0; j < bucket_sizes[i]; j++) {
arr[index++] = buckets[i][j];
}
}
}
Time Complexity: O(n + k), where 'n' is the number of elements and 'k' is the number of buckets. If elements are uniformly distributed, making the bucket sorting efficient (e.g., O(1) or small constant), then it approaches O(n).
Space Complexity: O(n + k) for the buckets.
These algorithms offer a compelling alternative to comparison sorts:
However, they come with crucial limitations:
These non-comparison sorts, given their specialized nature, find practical applications in specific niches within a platform like UdaanPath:
We've seen the power of both comparison-based and non-comparison based sorts. In the next chapter, we'll explore Heap Sort, another efficient comparison-based sorting algorithm that leverages a specialized data structure called a "Heap" to achieve an O(n log n) time complexity with the significant advantage of being an in-place sort.