Skip to content

Sorting Algorithms

PropertyDefinitionWhy It Matters
StableEqual elements retain their relative orderPreserves secondary sort keys, needed for multi-key sorting
In-placeUses O(1)O(1) extra memory (or O(logn)O(\log n) for recursion)Critical when memory is constrained
AdaptiveRuns faster on partially sorted inputCommon in practice — incremental updates, nearly-sorted logs
OnlineCan sort elements as they arriveStreaming scenarios where the full input is not available

A comparison-based sort can only determine the relative order of elements by comparing pairs. The Information-theoretic lower bound applies: sorting nn elements requires Ω(nlogn)\Omega(n \log n) Comparisons because there are n!n! possible orderings and each comparison provides at most 1 bit of Information.

Non-comparison sorts bypass this bound by exploiting structure in the input (integer keys, bounded Ranges, known distributions).

Repeatedly swap adjacent elements that are out of order. After ii passes, the last ii elements are In their final position.

def bubble_sort(arr):
"""
Bubble sort — adjacent swap.
Time: O(n^2) worst/average, O(n) best (already sorted with early termination)
Space: O(1)
Stable: Yes
"""
n = len(arr)
for i in range(n):
swapped = False
for j in range(0, n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
swapped = True
if not swapped:
break
return arr

Bubble sort is primarily of educational value. Its only practical advantage is that it can detect Whether the input is already sorted in a single pass (O(n)O(n)), but insertion sort does this better.

Find the minimum element in the unsorted portion and swap it into place.

def selection_sort(arr):
"""
Selection sort — find minimum, swap.
Time: O(n^2) all cases
Space: O(1)
Stable: No (swapping can change relative order of equal elements)
"""
n = len(arr)
for i in range(n):
min_idx = i
for j in range(i + 1, n):
if arr[j] < arr[min_idx]:
min_idx = j
arr[i], arr[min_idx] = arr[min_idx], arr[i]
return arr

Selection sort makes exactly n(n1)/2n(n-1)/2 comparisons regardless of input — it is never adaptive. Its Only advantage is that it does at most nn swaps, which matters when writes are expensive (e.g., Flash memory with limited write cycles).

Build the sorted array one element at a time by inserting each element into its correct position.

def insertion_sort(arr):
"""
Insertion sort — insert each element into sorted prefix.
Time: O(n^2) worst/average, O(n) best (already sorted)
Space: O(1)
Stable: Yes
"""
for i in range(1, len(arr)):
key = arr[i]
j = i - 1
while j >= 0 and arr[j] > key:
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = key
return arr

Sorting is the most fundamental algorithmic building block — it prestructures data so that subsequent operations (searching, merging, deduplication) become efficient. The theoretical lower bound of O(n log n) for comparison-based sorts comes from an information-theoretic argument: distinguishing among n! permutations requires at least log₂(n!) ≈ n log n bits of information, and each comparison provides at most one bit. Merge sort and heapsort achieve this bound in the worst case, while quicksort achieves it on average. The choice between them comes down to practical trade-offs: quicksort is fastest in practice due to cache locality, but has O(n²) worst case; heapsort guarantees O(n log n) but is slower due to poor cache behavior.

The real insight in modern sorting is that no single algorithm is best for all inputs. TimSort (used in Python, Java, and Rust) exploits existing order in the data — on already-sorted input it runs in O(n), just scanning for runs. Introsort (used in C++ std::sort) starts with quicksort and switches to heapsort if recursion gets too deep, combining quicksort’s speed with heapsort’s worst-case guarantee. Non-comparison sorts like counting sort and radix sort bypass the n log n lower bound entirely by exploiting properties of the data (small integer ranges or fixed-width keys), achieving O(n) time at the cost of additional memory.

Stability matters more than most developers realize. A stable sort preserves the relative order of equal elements, which is essential when sorting by multiple keys. If you sort employees by salary first and then by department, an unstable second sort could destroy the salary ordering within each department. The practical rule: when sorting records by multiple fields, sort by the least significant key first using a stable sort, then by more significant keys. Alternatively, use a compound comparison key that encodes all sort criteria in a single comparison.