Skip to main content

Sorting Algorithms

Why Sorting Matters

Sorting is fundamental to efficient data processing—enabling binary search, duplicate detection, and data analysis:

  • Database queries: ORDER BY uses optimized sort
  • Search optimization: Binary search requires sorted data
  • Data analysis: Finding median, percentiles, outliers
  • Efficiency: Many algorithms assume sorted input

Real-world impact:

  • Sorting 1 million integers:
    • Bubble sort: ~1 hour (O(n²))
    • QuickSort: ~50ms (O(n log n))—70,000x faster
  • Java's Arrays.sort() uses hybrid TimSort for optimal performance

Core Concepts

Comparison-Based Sorting

All comparison sorts have lower bound of O(n log n):

Non-Comparison Sorting

Can achieve O(n) by exploiting data properties:

AlgorithmTimeSpaceStableConstraints
Counting SortO(n + k)O(k)YesSmall integer range
Radix SortO(d × n)O(n + k)YesInteger/string keys
Bucket SortO(n + k)O(n)YesUniform distribution

Where:

  • k = range of values (for counting/bucket)
  • d = number of digits (for radix)

Stability

Stable sort preserves relative order of equal elements:

Before: [(Alice, 25), (Bob, 20), (Charlie, 25)]
After stable sort by age:
[(Bob, 20), (Alice, 25), (Charlie, 25)]

Alice before Charlie (stable)

Why stability matters:

  • Sort by multiple criteria (e.g., department, then salary)
  • Maintain meaningful relative ordering

Deep Dive

Merge Sort

Divide array in half, recursively sort, merge sorted halves:

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

mergeSort(arr, left, mid); // Sort left half
mergeSort(arr, mid + 1, right); // Sort right half

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

private void merge(int[] arr, int left, int mid, int right) {
// Create temp arrays
int[] leftArr = Arrays.copyOfRange(arr, left, mid + 1);
int[] rightArr = Arrays.copyOfRange(arr, mid + 1, right + 1);

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

while (i < leftArr.length && j < rightArr.length) {
if (leftArr[i] <= rightArr[j]) {
arr[k++] = leftArr[i++];
} else {
arr[k++] = rightArr[j++];
}
}

// Copy remaining elements
while (i < leftArr.length) arr[k++] = leftArr[i++];
while (j < rightArr.length) arr[k++] = rightArr[j++];
}

Complexity: O(n log n) time, O(n) space Stable: Yes (due to <= comparison)

Advantages:

  • Guaranteed O(n log n) performance
  • Stable
  • Excellent for linked lists (O(1) extra space)

Disadvantages:

  • Not in-place (requires O(n) extra space)
  • Slower than QuickSort in practice due to copying

QuickSort

Partition array around pivot, recursively sort partitions:

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

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

private int partition(int[] arr, int low, int high) {
int pivot = arr[high]; // Choose last element as pivot
int i = low - 1; // Index of smaller element

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

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

private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}

Complexity: Average O(n log n), Worst O(n²) time, O(log n) space Stable: No (due to partitioning)

Advantages:

  • In-place (O(log n) stack space for recursion)
  • Fast in practice (cache-friendly)
  • Can be optimized with randomized pivot

Disadvantages:

  • Worst case O(n²) on already sorted array
  • Not stable
  • Vulnerable to poor pivot selection

Heap Sort

Build max heap, repeatedly extract maximum:

public void heapSort(int[] arr) {
int n = arr.length;

// Build max heap
for (int i = n / 2 - 1; i >= 0; i--) {
heapify(arr, n, i);
}

// Extract elements from heap
for (int i = n - 1; i > 0; i--) {
swap(arr, 0, i); // Move current max to end
heapify(arr, i, 0); // Heapify reduced heap
}
}

private void heapify(int[] arr, int n, int i) {
int largest = i; // Initialize largest as root
int left = 2 * i + 1;
int right = 2 * i + 2;

if (left < n && arr[left] > arr[largest]) {
largest = left;
}

if (right < n && arr[right] > arr[largest]) {
largest = right;
}

if (largest != i) {
swap(arr, i, largest);
heapify(arr, n, largest);
}
}

Complexity: O(n log n) time, O(1) space Stable: No

Advantages:

  • Guaranteed O(n log n) performance
  • In-place
  • No additional memory needed

Disadvantages:

  • Not stable
  • Slower than QuickSort in practice
  • Poor cache locality

Non-Comparison Sorts

Counting Sort

Count occurrences, reconstruct sorted array:

public void countingSort(int[] arr) {
int max = Arrays.stream(arr).max().getAsInt();
int min = Arrays.stream(arr).min().getAsInt();
int range = max - min + 1;

int[] count = new int[range];
int[] output = new int[arr.length];

// Count occurrences
for (int num : arr) {
count[num - min]++;
}

// Calculate cumulative count
for (int i = 1; i < range; i++) {
count[i] += count[i - 1];
}

// Build output array (stable)
for (int i = arr.length - 1; i >= 0; i--) {
output[count[arr[i] - min] - 1] = arr[i];
count[arr[i] - min]--;
}

System.arraycopy(output, 0, arr, 0, arr.length);
}

Complexity: O(n + k) time, O(k) space

Use when: Range k is O(n)

Radix Sort

Sort by each digit, least significant to most:

public void radixSort(int[] arr) {
int max = Arrays.stream(arr).max().getAsInt();

// Sort by each digit
for (int exp = 1; max / exp > 0; exp *= 10) {
countingSortByDigit(arr, exp);
}
}

private void countingSortByDigit(int[] arr, int exp) {
int[] output = new int[arr.length];
int[] count = new int[10];

// Count occurrences
for (int num : arr) {
count[(num / exp) % 10]++;
}

// Cumulative count
for (int i = 1; i < 10; i++) {
count[i] += count[i - 1];
}

// Build output (stable)
for (int i = arr.length - 1; i >= 0; i--) {
output[count[(arr[i] / exp) % 10] - 1] = arr[i];
count[(arr[i] / exp) % 10]--;
}

System.arraycopy(output, 0, arr, 0, arr.length);
}

Complexity: O(d × (n + k)) time, O(n + k) space

Java's Sorting Algorithms

Arrays.sort() for primitives

Uses Dual-Pivot Quicksort:

  • Two pivots instead of one
  • Better cache locality
  • Faster on average than standard QuickSort

Arrays.sort() for objects

Uses TimSort (hybrid of Merge Sort and Insertion Sort):

  • Divide array into runs
  • Sort small runs with Insertion Sort
  • Merge runs adaptively
  • Stable sort
  • Optimized for real-world (partially sorted) data

Practical Applications

Custom Object Sorting

class Person {
String name;
int age;
int salary;
}

// Sort by age
Arrays.sort(people, Comparator.comparingInt(p -> p.age));

// Sort by name, then age (stable for equals names)
Arrays.sort(people, Comparator
.comparing((Person p) -> p.name)
.thenComparingInt(p -> p.age));

// Reverse order
Arrays.sort(people, Comparator.comparingInt(Person::getAge).reversed());

Sort Binary Array

public void sortBinary(int[] arr) {
int left = 0, right = arr.length - 1;

while (left < right) {
while (left < right && arr[left] == 0) left++;
while (left < right && arr[right] == 1) right--;

if (left < right) {
arr[left] = 0;
arr[right] = 1;
left++;
right--;
}
}
}

Sort by Frequency

public int[] frequencySort(int[] nums) {
Map<Integer, Integer> freq = new HashMap<>();
for (int num : nums) {
freq.merge(num, 1, Integer::sum);
}

return Arrays.stream(nums)
.boxed()
.sorted((a, b) -> {
int freqCompare = freq.get(b) - freq.get(a);
return freqCompare != 0 ? freqCompare : b - a;
})
.mapToInt(Integer::intValue)
.toArray();
}

Interview Questions

Q1: Merge Sorted Array (Easy)

Problem: Merge two sorted arrays in-place.

Approach: Merge from end

Complexity: O(n + m) time, O(1) space

public void merge(int[] nums1, int m, int[] nums2, int n) {
int p1 = m - 1, p2 = n - 1, p = m + n - 1;

while (p1 >= 0 && p2 >= 0) {
if (nums1[p1] > nums2[p2]) {
nums1[p--] = nums1[p1--];
} else {
nums1[p--] = nums2[p2--];
}
}

while (p2 >= 0) {
nums1[p--] = nums2[p2--];
}
}

Q2: Sort an Array (Medium)

Problem: Implement QuickSort.

Approach: Partition around pivot

Complexity: Average O(n log n), worst O(n²)

public int[] sortArray(int[] nums) {
quickSort(nums, 0, nums.length - 1);
return nums;
}

private void quickSort(int[] nums, int low, int high) {
if (low < high) {
int pivot = partition(nums, low, high);
quickSort(nums, low, pivot - 1);
quickSort(nums, pivot + 1, high);
}
}

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

Q3: Sort Colors (Medium)

Problem: Sort array of 0s, 1s, 2s (Dutch National Flag).

Approach: Three-way partition

Complexity: O(n) time, O(1) space

public void sortColors(int[] nums) {
int low = 0, mid = 0, high = nums.length - 1;

while (mid <= high) {
if (nums[mid] == 0) {
swap(nums, low++, mid++);
} else if (nums[mid] == 1) {
mid++;
} else {
swap(nums, mid, high--);
}
}
}

Further Reading

  • Binary Search: Requires sorted input
  • Heaps: Heap sort uses priority queues
  • Divide & Conquer: Merge sort strategy
  • LeetCode: Sorting problems