Sorting Algorithms

Efficiently organize data with algorithms like Quick Sort, Merge Sort, and Heap Sort, explained step-by-step with C++ implementations and complexity analysis.


Sorting algorithms are fundamental techniques used to rearrange data in a specific order. Let's explore the most important sorting algorithms, their implementations, and when to use each one.

1. Quick Sort

Quick Sort is a divide-and-conquer algorithm that picks an element as a pivot and partitions the array around it.

void quickSort(int arr[], int low, int high) {
    if (low < high) {
        // Partition the array and get the pivot position
        int pivot = partition(arr, low, high);
 
        // Recursively sort elements before and after pivot
        quickSort(arr, low, pivot - 1);
        quickSort(arr, pivot + 1, high);
    }
}
 
int partition(int arr[], int low, int high) {
    int pivot = arr[high];  // Choose the rightmost element as pivot
    int i = low - 1;        // Index of smaller element
 
    for (int j = low; j < high; j++) {
        // If current element is smaller than the pivot
        if (arr[j] < pivot) {
            i++;
            swap(arr[i], arr[j]);
        }
    }
 
    // Place the pivot in its correct position
    swap(arr[i + 1], arr[high]);
    return i + 1;
}

Time Complexity:

  • Best Case: O(n log n)
  • Average Case: O(n log n)
  • Worst Case: O(n²) - occurs when the pivot is always the smallest or largest element

Space Complexity: O(log n) - for the recursive call stack

When to use:

  • When average case performance matters more than worst-case
  • When space is a concern (in-place sorting)
  • For arrays with many duplicates (with proper implementation)

2. Merge Sort

Merge Sort divides the array into halves, sorts them recursively, and then merges the sorted halves.

void mergeSort(int arr[], int left, int right) {
    if (left < right) {
        // Find the middle point
        int mid = left + (right - left) / 2;
 
        // Sort first and second halves
        mergeSort(arr, left, mid);
        mergeSort(arr, mid + 1, right);
 
        // Merge the sorted halves
        merge(arr, left, mid, right);
    }
}
 
void merge(int arr[], int left, int mid, int right) {
    int n1 = mid - left + 1;
    int n2 = right - mid;
 
    // Create temporary arrays
    int* L = new int[n1];
    int* R = new int[n2];
 
    // Copy data to temporary arrays
    for (int i = 0; i < n1; i++)
        L[i] = arr[left + i];
    for (int j = 0; j < n2; j++)
        R[j] = arr[mid + 1 + j];
 
    // Merge the temporary arrays back into arr[left..right]
    int i = 0, j = 0, k = left;
    while (i < n1 && j < n2) {
        if (L[i] <= R[j]) {
            arr[k] = L[i];
            i++;
        } else {
            arr[k] = R[j];
            j++;
        }
        k++;
    }
 
    // Copy the remaining elements of L[], if any
    while (i < n1) {
        arr[k] = L[i];
        i++;
        k++;
    }
 
    // Copy the remaining elements of R[], if any
    while (j < n2) {
        arr[k] = R[j];
        j++;
        k++;
    }
 
    delete[] L;
    delete[] R;
}

Time Complexity:

  • Best Case: O(n log n)
  • Average Case: O(n log n)
  • Worst Case: O(n log n)

Space Complexity: O(n) - for the temporary arrays during merging

When to use:

  • When stable sorting is required (preserves the relative order of equal elements)
  • When worst-case performance is important
  • For linked lists (can be implemented with O(1) extra space)
  • For external sorting (when data doesn't fit in memory)

3. Heap Sort

Heap Sort uses a binary heap data structure to sort elements.

void heapSort(int arr[], int n) {
    // Build a max heap
    for (int i = n / 2 - 1; i >= 0; i--)
        heapify(arr, n, i);
 
    // Extract elements from heap one by one
    for (int i = n - 1; i > 0; i--) {
        // Move current root to end
        swap(arr[0], arr[i]);
 
        // Call max heapify on the reduced heap
        heapify(arr, i, 0);
    }
}
 
void heapify(int arr[], int n, int i) {
    int largest = i;      // Initialize largest as root
    int left = 2 * i + 1; // Left child
    int right = 2 * i + 2; // Right child
 
    // If left child is larger than root
    if (left < n && arr[left] > arr[largest])
        largest = left;
 
    // If right child is larger than largest so far
    if (right < n && arr[right] > arr[largest])
        largest = right;
 
    // If largest is not root
    if (largest != i) {
        swap(arr[i], arr[largest]);
 
        // Recursively heapify the affected sub-tree
        heapify(arr, n, largest);
    }
}

Time Complexity:

  • Best Case: O(n log n)
  • Average Case: O(n log n)
  • Worst Case: O(n log n)

Space Complexity: O(1) - in-place sorting

When to use:

  • When space is a concern (in-place sorting)
  • When consistent performance is needed (same time complexity for all cases)
  • When you need to find the k largest/smallest elements (can stop after k iterations)

4. Insertion Sort

Insertion Sort builds the sorted array one item at a time.

void insertionSort(int arr[], int n) {
    for (int i = 1; i < n; i++) {
        int key = arr[i];
        int j = i - 1;
 
        // Move elements greater than key to one position ahead
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = key;
    }
}

Time Complexity:

  • Best Case: O(n) - when array is already sorted
  • Average Case: O(n²)
  • Worst Case: O(n²) - when array is reverse sorted

Space Complexity: O(1) - in-place sorting

When to use:

  • For small datasets (typically n < 20)
  • When array is nearly sorted
  • When memory usage is a concern
  • As part of more complex algorithms (like Timsort)

5. Selection Sort

Selection Sort repeatedly finds the minimum element from the unsorted part and puts it at the beginning.

void selectionSort(int arr[], int n) {
    for (int i = 0; i < n - 1; i++) {
        // Find the minimum element in unsorted array
        int min_idx = i;
        for (int j = i + 1; j < n; j++) {
            if (arr[j] < arr[min_idx])
                min_idx = j;
        }
 
        // Swap the found minimum element with the first element
        swap(arr[min_idx], arr[i]);
    }
}

Time Complexity:

  • Best Case: O(n²)
  • Average Case: O(n²)
  • Worst Case: O(n²)

Space Complexity: O(1) - in-place sorting

When to use:

  • When memory usage is a concern
  • When simplicity is more important than efficiency
  • In situations where the cost of swapping elements is high (makes minimum number of swaps: O(n))

6. Bubble Sort

Bubble Sort repeatedly steps through the list and compares adjacent elements, swapping them if they are in the wrong order.

void bubbleSort(int arr[], int n) {
    for (int i = 0; i < n - 1; i++) {
        bool swapped = false;
 
        // Last i elements are already in place
        for (int j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                swap(arr[j], arr[j + 1]);
                swapped = true;
            }
        }
 
        // If no swapping occurred in this pass, array is sorted
        if (!swapped)
            break;
    }
}

Time Complexity:

  • Best Case: O(n) - when array is already sorted
  • Average Case: O(n²)
  • Worst Case: O(n²) - when array is reverse sorted

Space Complexity: O(1) - in-place sorting

When to use:

  • For educational purposes
  • When simplicity is more important than efficiency
  • When the array is nearly sorted (optimized version with swapped flag)

7. Counting Sort

Counting Sort is a non-comparison based sorting algorithm that works well when the range of input values is not significantly greater than the number of elements.

void countingSort(int arr[], int n, int range) {
    int* output = new int[n];
    int* count = new int[range + 1];
 
    // Initialize count array with zeros
    memset(count, 0, sizeof(int) * (range + 1));
 
    // Store count of each element
    for (int i = 0; i < n; i++)
        count[arr[i]]++;
 
    // Change count[i] to contain actual position of this element in output
    for (int i = 1; i <= range; i++)
        count[i] += count[i - 1];
 
    // Build the output array
    for (int i = n - 1; i >= 0; i--) {
        output[count[arr[i]] - 1] = arr[i];
        count[arr[i]]--;
    }
 
    // Copy the output array to arr
    for (int i = 0; i < n; i++)
        arr[i] = output[i];
 
    delete[] output;
    delete[] count;
}

Time Complexity: O(n + k) where k is the range of input values Space Complexity: O(n + k)

When to use:

  • When range of input values is not significantly greater than the number of elements
  • When stability is important (preserves relative order of equal elements)
  • For integer or data that can be mapped to integers with a small range

8. Radix Sort

Radix Sort is a non-comparison based sorting algorithm that sorts data based on individual digits or groups of digits.

// A function to get the 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 countSort(int arr[], int n, int exp) {
    int* output = new int[n];
    int count[10] = {0};
 
    // Store count of occurrences in count[]
    for (int 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 (int i = 1; i < 10; i++)
        count[i] += count[i - 1];
 
    // Build the output array
    for (int 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[]
    for (int i = 0; i < n; i++)
        arr[i] = output[i];
 
    delete[] output;
}
 
// 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
    for (int exp = 1; m / exp > 0; exp *= 10)
        countSort(arr, n, exp);
}

Time Complexity: O(d * (n + k)) where d is the number of digits and k is the range of digits (usually 10) Space Complexity: O(n + k)

When to use:

  • When elements are within a reasonable range
  • For integer or string data
  • When stability is important
  • When the keys have fixed length (like words, fixed length integers)

9. Bucket Sort

Bucket Sort distributes elements into a number of buckets, which are then sorted individually.

void bucketSort(float arr[], int n) {
    // Create n empty buckets
    vector<float>* buckets = new vector<float>[n];
 
    // Put array elements in different buckets
    for (int i = 0; i < n; i++) {
        int bucketIndex = n * arr[i]; // Assuming arr[i] is in range [0, 1)
        buckets[bucketIndex].push_back(arr[i]);
    }
 
    // Sort individual buckets
    for (int i = 0; i < n; i++)
        sort(buckets[i].begin(), buckets[i].end());
 
    // Concatenate all buckets into arr[]
    int index = 0;
    for (int i = 0; i < n; i++) {
        for (size_t j = 0; j < buckets[i].size(); j++) {
            arr[index++] = buckets[i][j];
        }
    }
 
    delete[] buckets;
}

Time Complexity:

  • Average Case: O(n + k) where k is the number of buckets
  • Worst Case: O(n²) when all elements fall into a single bucket

Space Complexity: O(n + k)

When to use:

  • When input is uniformly distributed over a range
  • For floating-point numbers in range [0.0, 1.0)
  • When data distribution is known

10. Timsort

Timsort is a hybrid sorting algorithm derived from merge sort and insertion sort. It's the default sorting algorithm in Python, Java, and many other languages.

// Insertion sort for small portions
void insertionSort(int arr[], int left, int right) {
    for (int i = left + 1; i <= right; i++) {
        int temp = arr[i];
        int j = i - 1;
        while (j >= left && arr[j] > temp) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = temp;
    }
}
 
// Merge function like in merge sort
void merge(int arr[], int l, int m, int r) {
    // Same as the merge function shown in Merge Sort section
    // ...
}
 
// The main Timsort function
void timSort(int arr[], int n) {
    const int RUN = 32;  // Size of a "run" - typically between 32 and 64
 
    // Sort individual subarrays of size RUN
    for (int i = 0; i < n; i += RUN)
        insertionSort(arr, i, min((i + RUN - 1), (n - 1)));
 
    // Start merging from size RUN
    for (int size = RUN; size < n; size = 2 * size) {
        for (int left = 0; left < n; left += 2 * size) {
            int mid = left + size - 1;
            int right = min((left + 2 * size - 1), (n - 1));
 
            if (mid < right)
                merge(arr, left, mid, right);
        }
    }
}

Time Complexity:

  • Best Case: O(n) when array is already sorted
  • Average Case: O(n log n)
  • Worst Case: O(n log n)

Space Complexity: O(n)

When to use:

  • As a general-purpose sorting algorithm
  • When stability is important
  • When data may have some pre-existing order
  • For real-world data with various patterns

11. Comparison of Sorting Algorithms

AlgorithmBestAverageWorstSpaceStableIn-Place
Quick SortO(n log n)O(n log n)O(n²)O(log n)NoYes
Merge SortO(n log n)O(n log n)O(n log n)O(n)YesNo
Heap SortO(n log n)O(n log n)O(n log n)O(1)NoYes
Insertion SortO(n)O(n²)O(n²)O(1)YesYes
Selection SortO(n²)O(n²)O(n²)O(1)NoYes
Bubble SortO(n)O(n²)O(n²)O(1)YesYes
Counting SortO(n + k)O(n + k)O(n + k)O(n + k)YesNo
Radix SortO(n * k)O(n * k)O(n * k)O(n + k)YesNo
Bucket SortO(n + k)O(n + k)O(n²)O(n + k)YesNo
TimsortO(n)O(n log n)O(n log n)O(n)YesNo

12. When to Choose Which Algorithm

Here's a guide for choosing the right sorting algorithm based on your requirements:

  • For general-purpose sorting:

    • Timsort or Quick Sort are excellent defaults
  • When memory is limited:

    • Heap Sort (in-place with O(1) extra space)
    • Quick Sort (in-place with O(log n) call stack)
  • For nearly sorted data:

    • Insertion Sort (fastest for small or nearly sorted arrays)
    • Timsort (optimized for partially sorted data)
  • When stability is required (preserve order of equal elements):

    • Merge Sort
    • Insertion Sort
    • Bubble Sort
    • Counting Sort (with proper implementation)
  • For small arrays (n < 20):

    • Insertion Sort
  • For integer data with limited range:

    • Counting Sort (when range is not much larger than n)
    • Radix Sort (for larger ranges)
  • For floating point data in range [0,1):

    • Bucket Sort

Understanding these algorithms and their characteristics helps you make the right choice for specific data patterns and requirements, leading to more efficient and effective code!