Skip to content

Sorting Algorithms

Comprehensive reference for sorting algorithms covering comparison-based sorts (insertion, bubble, selection, merge, quick, heap), non-comparison sorts (counting, radix, bucket), and their properties. The theoretical lower bound for comparison-based sorting is Omega(n log n).

Comparison Table

Algorithm Best Average Worst Space Stable In-place
Insertion sort O(n) O(n^2) O(n^2) O(1) Yes Yes
Bubble sort O(n) O(n^2) O(n^2) O(1) Yes Yes
Selection sort O(n^2) O(n^2) O(n^2) O(1) No Yes
Heap sort O(n log n) O(n log n) O(n log n) O(1) No Yes
Merge sort O(n log n) O(n log n) O(n log n) O(n) Yes No
Quick sort O(n log n) O(n log n) O(n^2) O(log n) No Yes
Counting sort O(n+k) O(n+k) O(n+k) O(k) Yes No
Radix sort O(d(n+k)) O(d(n+k)) O(d(n+k)) O(n+k) Yes No
Bucket sort O(n+k) O(n+k) O(n^2) O(n+k) Yes No

Classification Properties

Property Definition
Stable Equal elements maintain original relative order
In-place Uses O(1) auxiliary space
Adaptive Faster on nearly-sorted input
Online Can process input sequentially
Comparison-based Uses only element comparisons to determine order

Key Facts

  • Lower bound for comparison sorts: Omega(n log n) - proof via decision tree (n! leaves, tree height >= log2(n!) = Omega(n log n))
  • Non-comparison sorts break this barrier using key structure (counting, radix, bucket)
  • Python's sorted() and .sort() use Timsort (hybrid merge+insertion): O(n log n) worst, O(n) best, stable, adaptive

Patterns

Insertion Sort

def insertion_sort(arr):
    for i in range(1, len(arr)):
        j = i
        while j > 0 and arr[j] < arr[j - 1]:
            arr[j], arr[j - 1] = arr[j - 1], arr[j]
            j -= 1

Best for: small arrays, nearly-sorted data, online sorting. O(n) best case on sorted input.

Selection Sort

def selection_sort(arr):
    for i in range(len(arr)):
        pos_min = i
        for j in range(i + 1, len(arr)):
            if arr[j] < arr[pos_min]:
                pos_min = j
        arr[i], arr[pos_min] = arr[pos_min], arr[i]

Always O(n^2) regardless of input order. Not adaptive, not stable.

Merge Sort

def merge_sort(arr):
    if len(arr) <= 1:
        return arr
    mid = len(arr) // 2
    left = merge_sort(arr[:mid])
    right = merge_sort(arr[mid:])
    return merge(left, right)

Recurrence: T(n) = 2T(n/2) + O(n) -> O(n log n). Recursion tree has log2(n) levels, each doing O(n) merge work. Stable but requires O(n) extra space.

Quick Sort

def quick_sort(arr, low, high):
    if low < high:
        pivot_idx = partition(arr, low, high)
        quick_sort(arr, low, pivot_idx - 1)
        quick_sort(arr, pivot_idx + 1, high)

O(n^2) worst case when pivot always worst (sorted array + always pick first/last). O(n log n) average. In practice faster than merge sort due to cache locality and small constants. Worst case rare with random or median-of-three pivot.

Counting Sort

For integers in range [0, k). Time O(n + k), space O(k). Useful when k = O(n).

Radix Sort

Sort by digits LSD to MSD using counting sort as subroutine. Time O(d(n+k)) where d = digits, k = digit range. For fixed-width integers: O(n).

Python Built-in Sorting

sorted(strs)                          # lexicographic
sorted(strs, key=len)                 # by length
sorted(strs, key=lambda s: s.count('c'))  # by custom criterion
sorted(products, key=lambda p: p['price'])  # by dict field

Gotchas

  • Quicksort worst case O(n^2) on already-sorted arrays with naive pivot (first/last element)
  • Selection sort is NOT stable - long-range swaps displace equal elements
  • Bubble sort with early termination flag becomes adaptive (O(n) on sorted input)
  • Shell sort (gap-based insertion sort) achieves O(n log^2 n) with Hibbard gaps
  • Counting sort requires non-negative integers in known range
  • Bucket sort degrades to O(n^2) if all elements land in same bucket

See Also

  • [[complexity-analysis]] - understanding O-notation
  • [[searching-algorithms]] - binary search requires sorted input
  • [[python:built-in-functions]] - sorted() and sort() details