Sorting Algorithms

Chapter 4: Sorting Algorithms

Introduction

Sorting is the process of arranging elements in a specific order (ascending or descending). It's one of the most studied problems in computer science because sorted data enables efficient searching, data analysis, and optimization. Understanding sorting algorithms teaches fundamental algorithmic techniques like divide-and-conquer, recursion, and complexity trade-offs.

Why This Matters

Sorting appears everywhere:

  • Databases organizing query results
  • E-commerce sites showing products by price
  • File systems displaying files by name/date
  • Search engines ranking results
  • Operating systems scheduling processes

Different sorting algorithms have different characteristics:

  • Performance: O(n²) vs O(n log n) matters for large datasets
  • Stability: Preserving relative order of equal elements
  • Memory: In-place vs requiring extra space
  • Adaptivity: Performance on partially sorted data

How to Study This Chapter

  1. Visualize each algorithm - Draw array transformations step by step
  2. Implement from scratch - Don't just copy; understand why it works
  3. Compare performance - Time algorithms with different input sizes
  4. Understand trade-offs - Speed vs memory vs stability
  5. Practice variations - Solve problems requiring sorting knowledge

Sorting Algorithm Categories

Simple Sorts - O(n²)

  • Bubble Sort
  • Selection Sort
  • Insertion Sort

Efficient Sorts - O(n log n)

  • Merge Sort
  • Quick Sort
  • Heap Sort

Special Purpose

  • Counting Sort - O(n + k)
  • Radix Sort - O(d × n)
  • Bucket Sort - O(n + k)

Bubble Sort

Bubble sort repeatedly steps through the list, compares adjacent elements, and swaps them if they're in the wrong order.

Algorithm

1. For i from 0 to n-1:
   a. For j from 0 to n-i-1:
      - If arr[j] > arr[j+1], swap them
   b. After each pass, largest element "bubbles" to the end

Visualization

Initial: [64, 34, 25, 12, 22, 11, 90]

Pass 1:
[34, 64, 25, 12, 22, 11, 90]  (64 > 34, swap)
[34, 25, 64, 12, 22, 11, 90]  (64 > 25, swap)
[34, 25, 12, 64, 22, 11, 90]  (64 > 12, swap)
[34, 25, 12, 22, 64, 11, 90]  (64 > 22, swap)
[34, 25, 12, 22, 11, 64, 90]  (64 > 11, swap)
                        ^^^ 90 in final position

Pass 2:
[25, 34, 12, 22, 11, 64, 90]
...
                    ^^^ 64 in final position

C Implementation

#include <stdio.h>

void bubbleSort(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]) {
                // Swap
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

void printArray(int arr[], int n) {
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main() {
    int arr[] = {64, 34, 25, 12, 22, 11, 90};
    int n = 7;

    printf("Original: ");
    printArray(arr, n);

    bubbleSort(arr, n);

    printf("Sorted: ");
    printArray(arr, n);

    return 0;
}

C++ Implementation

#include <iostream>
#include <vector>
using namespace std;

void bubbleSort(vector<int>& arr) {
    int n = arr.size();
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                swap(arr[j], arr[j + 1]);
            }
        }
    }
}

int main() {
    vector<int> arr = {64, 34, 25, 12, 22, 11, 90};

    bubbleSort(arr);

    for (int num : arr) {
        cout << num << " ";
    }
    cout << endl;

    return 0;
}

Java Implementation

import java.util.Arrays;

public class BubbleSort {
    public static void bubbleSort(int[] arr) {
        int n = arr.length;
        for (int i = 0; i < n - 1; i++) {
            for (int j = 0; j < n - i - 1; j++) {
                if (arr[j] > arr[j + 1]) {
                    // Swap
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
    }

    public static void main(String[] args) {
        int[] arr = {64, 34, 25, 12, 22, 11, 90};
        bubbleSort(arr);
        System.out.println(Arrays.toString(arr));
    }
}

Optimized Bubble Sort

Add a flag to detect if array is already sorted:

void bubbleSortOptimized(int arr[], int n) {
    for (int i = 0; i < n - 1; i++) {
        int swapped = 0;

        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;
                swapped = 1;
            }
        }

        // If no swaps, array is sorted
        if (!swapped) break;
    }
}

Complexity Analysis

  • Time Complexity:
    • Best case: O(n) - already sorted (with optimization)
    • Average case: O(n²)
    • Worst case: O(n²) - reverse sorted
  • Space Complexity: O(1)
  • Stable: Yes - equal elements maintain relative order
  • In-place: Yes

Selection Sort

Selection sort finds the minimum element and places it at the beginning, then repeats for the remaining elements.

Algorithm

1. For i from 0 to n-1:
   a. Find minimum element in arr[i...n-1]
   b. Swap it with arr[i]

C Implementation

#include <stdio.h>

void selectionSort(int arr[], int n) {
    for (int i = 0; i < n - 1; i++) {
        int minIndex = i;

        // Find minimum in remaining array
        for (int j = i + 1; j < n; j++) {
            if (arr[j] < arr[minIndex]) {
                minIndex = j;
            }
        }

        // Swap minimum with current position
        if (minIndex != i) {
            int temp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = temp;
        }
    }
}

int main() {
    int arr[] = {64, 25, 12, 22, 11};
    int n = 5;

    selectionSort(arr, n);

    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    return 0;
}

C++ Implementation

#include <iostream>
#include <vector>
using namespace std;

void selectionSort(vector<int>& arr) {
    int n = arr.size();
    for (int i = 0; i < n - 1; i++) {
        int minIndex = i;

        for (int j = i + 1; j < n; j++) {
            if (arr[j] < arr[minIndex]) {
                minIndex = j;
            }
        }

        if (minIndex != i) {
            swap(arr[i], arr[minIndex]);
        }
    }
}

int main() {
    vector<int> arr = {64, 25, 12, 22, 11};
    selectionSort(arr);

    for (int num : arr) {
        cout << num << " ";
    }
    cout << endl;

    return 0;
}

Java Implementation

public class SelectionSort {
    public static void selectionSort(int[] arr) {
        int n = arr.length;
        for (int i = 0; i < n - 1; i++) {
            int minIndex = i;

            for (int j = i + 1; j < n; j++) {
                if (arr[j] < arr[minIndex]) {
                    minIndex = j;
                }
            }

            if (minIndex != i) {
                int temp = arr[i];
                arr[i] = arr[minIndex];
                arr[minIndex] = temp;
            }
        }
    }

    public static void main(String[] args) {
        int[] arr = {64, 25, 12, 22, 11};
        selectionSort(arr);

        for (int num : arr) {
            System.out.print(num + " ");
        }
        System.out.println();
    }
}

Complexity Analysis

  • Time Complexity: O(n²) for all cases - always does the same comparisons
  • Space Complexity: O(1)
  • Stable: No (can be made stable with modifications)
  • In-place: Yes
  • Good for: Small arrays or when swap cost is high

Insertion Sort

Insertion sort builds sorted array one element at a time by inserting each element into its correct position.

Algorithm

1. For i from 1 to n-1:
   a. key = arr[i]
   b. j = i - 1
   c. Shift elements greater than key to the right
   d. Insert key at correct position

Visualization

[12, 11, 13, 5, 6]

i=1: key=11
[12, 12, 13, 5, 6]  (shift 12)
[11, 12, 13, 5, 6]  (insert 11)

i=2: key=13
[11, 12, 13, 5, 6]  (already in place)

i=3: key=5
[11, 12, 13, 13, 6] (shift 13)
[11, 12, 12, 13, 6] (shift 12)
[11, 11, 12, 13, 6] (shift 11)
[5, 11, 12, 13, 6]  (insert 5)

i=4: key=6
[5, 11, 12, 13, 13] (shift 13)
[5, 11, 12, 12, 13] (shift 12)
[5, 11, 11, 12, 13] (shift 11)
[5, 6, 11, 12, 13]  (insert 6)

C Implementation

#include <stdio.h>

void insertionSort(int arr[], int n) {
    for (int i = 1; i < n; i++) {
        int key = arr[i];
        int j = i - 1;

        // Shift elements greater than key to the right
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }

        arr[j + 1] = key;
    }
}

int main() {
    int arr[] = {12, 11, 13, 5, 6};
    int n = 5;

    insertionSort(arr, n);

    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    return 0;
}

C++ Implementation

#include <iostream>
#include <vector>
using namespace std;

void insertionSort(vector<int>& arr) {
    int n = arr.size();
    for (int i = 1; i < n; i++) {
        int key = arr[i];
        int j = i - 1;

        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }

        arr[j + 1] = key;
    }
}

int main() {
    vector<int> arr = {12, 11, 13, 5, 6};
    insertionSort(arr);

    for (int num : arr) {
        cout << num << " ";
    }
    cout << endl;

    return 0;
}

Java Implementation

public class InsertionSort {
    public static void insertionSort(int[] arr) {
        int n = arr.length;
        for (int i = 1; i < n; i++) {
            int key = arr[i];
            int j = i - 1;

            while (j >= 0 && arr[j] > key) {
                arr[j + 1] = arr[j];
                j--;
            }

            arr[j + 1] = key;
        }
    }

    public static void main(String[] args) {
        int[] arr = {12, 11, 13, 5, 6};
        insertionSort(arr);

        for (int num : arr) {
            System.out.print(num + " ");
        }
        System.out.println();
    }
}

Complexity Analysis

  • Time Complexity:
    • Best case: O(n) - already sorted
    • Average case: O(n²)
    • Worst case: O(n²) - reverse sorted
  • Space Complexity: O(1)
  • Stable: Yes
  • In-place: Yes
  • Adaptive: Works well on partially sorted data
  • Good for: Small arrays, nearly sorted data, online sorting

Merge Sort

Merge sort uses divide-and-conquer: divide array in half, recursively sort halves, merge sorted halves.

Algorithm

1. If array has 1 element, return (base case)
2. Divide array into two halves
3. Recursively sort left half
4. Recursively sort right half
5. Merge the two sorted halves

Visualization

[38, 27, 43, 3, 9, 82, 10]

Divide:
[38, 27, 43, 3] | [9, 82, 10]
[38, 27] [43, 3] | [9, 82] [10]
[38] [27] [43] [3] | [9, 82] [10]
[38] [27] [43] [3] | [9] [82] [10]

Merge:
[27, 38] [3, 43] | [9, 82] [10]
[3, 27, 38, 43] | [9, 10, 82]
[3, 9, 10, 27, 38, 43, 82]

C Implementation

#include <stdio.h>
#include <stdlib.h>

void merge(int arr[], int left, int mid, int right) {
    int n1 = mid - left + 1;
    int n2 = right - mid;

    // Create temp arrays
    int *L = (int*)malloc(n1 * sizeof(int));
    int *R = (int*)malloc(n2 * sizeof(int));

    // Copy data
    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 back
    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 remaining
    while (i < n1) {
        arr[k] = L[i];
        i++;
        k++;
    }

    while (j < n2) {
        arr[k] = R[j];
        j++;
        k++;
    }

    free(L);
    free(R);
}

void mergeSort(int arr[], int left, int right) {
    if (left < right) {
        int mid = left + (right - left) / 2;

        mergeSort(arr, left, mid);
        mergeSort(arr, mid + 1, right);

        merge(arr, left, mid, right);
    }
}

int main() {
    int arr[] = {38, 27, 43, 3, 9, 82, 10};
    int n = 7;

    mergeSort(arr, 0, n - 1);

    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    return 0;
}

C++ Implementation

#include <iostream>
#include <vector>
using namespace std;

void merge(vector<int>& arr, int left, int mid, int right) {
    int n1 = mid - left + 1;
    int n2 = right - mid;

    vector<int> L(n1), R(n2);

    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];
    }

    int i = 0, j = 0, k = left;

    while (i < n1 && j < n2) {
        if (L[i] <= R[j]) {
            arr[k++] = L[i++];
        } else {
            arr[k++] = R[j++];
        }
    }

    while (i < n1) {
        arr[k++] = L[i++];
    }

    while (j < n2) {
        arr[k++] = R[j++];
    }
}

void mergeSort(vector<int>& arr, int left, int right) {
    if (left < right) {
        int mid = left + (right - left) / 2;

        mergeSort(arr, left, mid);
        mergeSort(arr, mid + 1, right);

        merge(arr, left, mid, right);
    }
}

int main() {
    vector<int> arr = {38, 27, 43, 3, 9, 82, 10};

    mergeSort(arr, 0, arr.size() - 1);

    for (int num : arr) {
        cout << num << " ";
    }
    cout << endl;

    return 0;
}

Java Implementation

public class MergeSort {
    public static void merge(int[] arr, int left, int mid, int right) {
        int n1 = mid - left + 1;
        int n2 = right - mid;

        int[] L = new int[n1];
        int[] R = new int[n2];

        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];
        }

        int i = 0, j = 0, k = left;

        while (i < n1 && j < n2) {
            if (L[i] <= R[j]) {
                arr[k++] = L[i++];
            } else {
                arr[k++] = R[j++];
            }
        }

        while (i < n1) {
            arr[k++] = L[i++];
        }

        while (j < n2) {
            arr[k++] = R[j++];
        }
    }

    public static void mergeSort(int[] arr, int left, int right) {
        if (left < right) {
            int mid = left + (right - left) / 2;

            mergeSort(arr, left, mid);
            mergeSort(arr, mid + 1, right);

            merge(arr, left, mid, right);
        }
    }

    public static void main(String[] args) {
        int[] arr = {38, 27, 43, 3, 9, 82, 10};
        mergeSort(arr, 0, arr.length - 1);

        for (int num : arr) {
            System.out.print(num + " ");
        }
        System.out.println();
    }
}

Complexity Analysis

  • Time Complexity: O(n log n) for all cases
  • Space Complexity: O(n) - requires temporary arrays
  • Stable: Yes
  • Not in-place: Requires extra memory
  • Good for: Large datasets, linked lists, external sorting

Quick Sort

Quick sort picks a pivot, partitions array so elements < pivot are left, > pivot are right, then recursively sorts partitions.

Algorithm

1. Choose pivot (often last element)
2. Partition: rearrange so elements < pivot are left, > pivot are right
3. Recursively quicksort left partition
4. Recursively quicksort right partition

C Implementation

#include <stdio.h>

int partition(int arr[], int low, int high) {
    int pivot = arr[high];
    int i = low - 1;

    for (int j = low; j < high; j++) {
        if (arr[j] < pivot) {
            i++;
            // Swap
            int temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
    }

    // Place pivot in correct position
    int temp = arr[i + 1];
    arr[i + 1] = arr[high];
    arr[high] = temp;

    return i + 1;
}

void quickSort(int arr[], int low, int high) {
    if (low < high) {
        int pi = partition(arr, low, high);

        quickSort(arr, low, pi - 1);
        quickSort(arr, pi + 1, high);
    }
}

int main() {
    int arr[] = {10, 7, 8, 9, 1, 5};
    int n = 6;

    quickSort(arr, 0, n - 1);

    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    return 0;
}

C++ Implementation

#include <iostream>
#include <vector>
using namespace std;

int partition(vector<int>& arr, int low, int high) {
    int pivot = arr[high];
    int i = low - 1;

    for (int j = low; j < high; j++) {
        if (arr[j] < pivot) {
            i++;
            swap(arr[i], arr[j]);
        }
    }

    swap(arr[i + 1], arr[high]);
    return i + 1;
}

void quickSort(vector<int>& arr, int low, int high) {
    if (low < high) {
        int pi = partition(arr, low, high);

        quickSort(arr, low, pi - 1);
        quickSort(arr, pi + 1, high);
    }
}

int main() {
    vector<int> arr = {10, 7, 8, 9, 1, 5};

    quickSort(arr, 0, arr.size() - 1);

    for (int num : arr) {
        cout << num << " ";
    }
    cout << endl;

    return 0;
}

Java Implementation

public class QuickSort {
    public static int partition(int[] arr, int low, int high) {
        int pivot = arr[high];
        int i = low - 1;

        for (int j = low; j < high; j++) {
            if (arr[j] < pivot) {
                i++;
                int temp = arr[i];
                arr[i] = arr[j];
                arr[j] = temp;
            }
        }

        int temp = arr[i + 1];
        arr[i + 1] = arr[high];
        arr[high] = temp;

        return i + 1;
    }

    public static void quickSort(int[] arr, int low, int high) {
        if (low < high) {
            int pi = partition(arr, low, high);

            quickSort(arr, low, pi - 1);
            quickSort(arr, pi + 1, high);
        }
    }

    public static void main(String[] args) {
        int[] arr = {10, 7, 8, 9, 1, 5};
        quickSort(arr, 0, arr.length - 1);

        for (int num : arr) {
            System.out.print(num + " ");
        }
        System.out.println();
    }
}

Complexity Analysis

  • Time Complexity:
    • Best/Average: O(n log n)
    • Worst case: O(n²) - already sorted with bad pivot choice
  • Space Complexity: O(log n) - recursion stack
  • Stable: No
  • In-place: Yes
  • Good for: Large datasets, often fastest in practice

Optimizations

Random Pivot: Choose random pivot to avoid worst case

int randomPivot = low + rand() % (high - low + 1);
swap(&arr[randomPivot], &arr[high]);

Comparison of Sorting Algorithms

AlgorithmBestAverageWorstSpaceStableIn-place
BubbleO(n)O(n²)O(n²)O(1)YesYes
SelectionO(n²)O(n²)O(n²)O(1)NoYes
InsertionO(n)O(n²)O(n²)O(1)YesYes
MergeO(n log n)O(n log n)O(n log n)O(n)YesNo
QuickO(n log n)O(n log n)O(n²)O(log n)NoYes

Common Mistakes

  1. Off-by-one errors - Wrong loop bounds
  2. Not handling empty/single element - Missing base cases
  3. Integer overflow in merge - Calculating mid incorrectly
  4. Inefficient pivot choice - Always choosing first/last in quicksort
  5. Memory leaks in C - Not freeing temporary arrays
  6. Modifying during comparison - Unstable sorts
  7. Wrong partition logic - Infinite recursion in quicksort

Debugging Tips

  1. Print array after each pass - Visualize sorting progress
  2. Test edge cases - Empty, single element, duplicates, sorted, reverse
  3. Verify stability - Track equal elements
  4. Check recursion depth - Prevent stack overflow
  5. Use assertions - Verify partitioning correctness
  6. Test with small inputs first - Easy to trace manually
  7. Use debugger - Step through recursive calls

Mini Exercises

  1. Implement bubble sort with early termination
  2. Sort array of strings using insertion sort
  3. Count inversions using merge sort
  4. Implement 3-way quicksort for duplicates
  5. Find kth largest element using quickselect
  6. Sort linked list using merge sort
  7. Implement in-place merge sort
  8. Sort array with only 0s, 1s, and 2s (Dutch flag)
  9. Implement cocktail shaker sort
  10. Compare sorting algorithm performance experimentally

Review Questions

  1. Why is merge sort stable but quicksort is not?
  2. When would you choose insertion sort over quicksort?
  3. How does pivot selection affect quicksort performance?
  4. Why does merge sort require O(n) extra space?
  5. What makes an algorithm adaptive?

Reference Checklist

By the end of this chapter, you should be able to:

  • Implement bubble, selection, insertion sort
  • Implement merge sort and quick sort
  • Analyze time and space complexity
  • Understand stability and in-place sorting
  • Choose appropriate algorithm for the problem
  • Recognize worst-case scenarios
  • Optimize sorting algorithms
  • Debug sorting implementations

Next Steps

Now that you understand sorting algorithms, Chapter 5 explores linked lists—a dynamic data structure that excels where arrays struggle. You'll learn singly linked lists, doubly linked lists, and circular lists with their operations and applications.


Key Takeaway: Different sorting algorithms have different trade-offs. Simple sorts (O(n²)) work well for small or nearly sorted data. Efficient sorts (O(n log n)) scale to large datasets. Understanding these trade-offs helps you choose the right algorithm for your specific needs.