Toolmingo
Guides16 min read

Sorting Algorithms Cheat Sheet: Complexity, Code & When to Use

Complete sorting algorithms guide — all major algorithms compared by time/space complexity, stability, and use case. Includes working code in JavaScript, Python, Go, and PHP, plus a quick reference cheat sheet.

Sorting is one of the most studied problems in computer science. The right algorithm can cut your runtime from minutes to milliseconds — and the wrong one can grind production to a halt. This cheat sheet covers every major sorting algorithm with complexity tables, working code, and concrete guidance on when to use each.

Quick reference

Algorithm Best Average Worst Space Stable In-place
Bubble sort O(n) O(n²) O(n²) O(1)
Selection sort O(n²) O(n²) O(n²) O(1)
Insertion sort O(n) O(n²) O(n²) O(1)
Merge sort O(n log n) O(n log n) O(n log n) O(n)
Quicksort O(n log n) O(n log n) O(n²) O(log n)
Heap sort O(n log n) O(n log n) O(n log n) O(1)
Counting sort O(n+k) O(n+k) O(n+k) O(k)
Radix sort O(nk) O(nk) O(nk) O(n+k)
Bucket sort O(n+k) O(n+k) O(n²) O(n+k)
Tim sort O(n) O(n log n) O(n log n) O(n)
Shell sort O(n log n) O(n log²n) O(n²) O(1)

k = range of input values. Stable = equal elements preserve original order.

TL;DR for 80% of cases: use your language's built-in sort (Tim sort in Python/Java, introsort in C++, V8's Tim sort in JavaScript). Only reach for a custom algorithm when you have a specific constraint — huge n, tiny range of values, nearly-sorted input, or memory limits.


Bubble sort

The simplest sorting algorithm. Repeatedly steps through the list, swaps adjacent elements that are out of order, and repeats until no swaps are needed.

Use when: teaching sorting concepts. Never in production for large n.

// JavaScript
function bubbleSort(arr) {
  const a = [...arr];
  let n = a.length;
  for (let i = 0; i < n - 1; i++) {
    let swapped = false;
    for (let j = 0; j < n - i - 1; j++) {
      if (a[j] > a[j + 1]) {
        [a[j], a[j + 1]] = [a[j + 1], a[j]]; // swap
        swapped = true;
      }
    }
    if (!swapped) break; // already sorted — O(n) best case
  }
  return a;
}

bubbleSort([64, 34, 25, 12, 22, 11, 90]);
// [11, 12, 22, 25, 34, 64, 90]
# Python
def bubble_sort(arr):
    a = arr[:]
    n = len(a)
    for i in range(n - 1):
        swapped = False
        for j in range(n - i - 1):
            if a[j] > a[j + 1]:
                a[j], a[j + 1] = a[j + 1], a[j]
                swapped = True
        if not swapped:
            break
    return a
// Go
func bubbleSort(arr []int) []int {
    a := append([]int{}, arr...)
    n := len(a)
    for i := 0; i < n-1; i++ {
        swapped := false
        for j := 0; j < n-i-1; j++ {
            if a[j] > a[j+1] {
                a[j], a[j+1] = a[j+1], a[j]
                swapped = true
            }
        }
        if !swapped {
            break
        }
    }
    return a
}
// PHP
function bubbleSort(array $arr): array {
    $n = count($arr);
    for ($i = 0; $i < $n - 1; $i++) {
        $swapped = false;
        for ($j = 0; $j < $n - $i - 1; $j++) {
            if ($arr[$j] > $arr[$j + 1]) {
                [$arr[$j], $arr[$j + 1]] = [$arr[$j + 1], $arr[$j]];
                $swapped = true;
            }
        }
        if (!$swapped) break;
    }
    return $arr;
}

Selection sort

Finds the minimum element in the unsorted portion and places it at the beginning. Makes the minimum number of swaps (O(n)), but always O(n²) comparisons — can't short-circuit like bubble sort.

Use when: write operations are expensive (e.g., flash memory) and n is small.

// JavaScript
function selectionSort(arr) {
  const a = [...arr];
  const n = a.length;
  for (let i = 0; i < n - 1; i++) {
    let minIdx = i;
    for (let j = i + 1; j < n; j++) {
      if (a[j] < a[minIdx]) minIdx = j;
    }
    if (minIdx !== i) [a[i], a[minIdx]] = [a[minIdx], a[i]];
  }
  return a;
}
# Python
def selection_sort(arr):
    a = arr[:]
    n = len(a)
    for i in range(n - 1):
        min_idx = i
        for j in range(i + 1, n):
            if a[j] < a[min_idx]:
                min_idx = j
        a[i], a[min_idx] = a[min_idx], a[i]
    return a

Insertion sort

Builds the sorted array one item at a time by inserting each new element into its correct position among already-sorted elements. Excellent for small or nearly-sorted arrays — it's the algorithm Tim sort falls back to for small runs.

Use when: n < 20, or the array is nearly sorted (mostly O(n)).

// JavaScript
function insertionSort(arr) {
  const a = [...arr];
  for (let i = 1; i < a.length; i++) {
    const key = a[i];
    let j = i - 1;
    while (j >= 0 && a[j] > key) {
      a[j + 1] = a[j];
      j--;
    }
    a[j + 1] = key;
  }
  return a;
}
# Python
def insertion_sort(arr):
    a = arr[:]
    for i in range(1, len(a)):
        key = a[i]
        j = i - 1
        while j >= 0 and a[j] > key:
            a[j + 1] = a[j]
            j -= 1
        a[j + 1] = key
    return a
// Go
func insertionSort(arr []int) []int {
    a := append([]int{}, arr...)
    for i := 1; i < len(a); i++ {
        key := a[i]
        j := i - 1
        for j >= 0 && a[j] > key {
            a[j+1] = a[j]
            j--
        }
        a[j+1] = key
    }
    return a
}

Merge sort

Divide-and-conquer algorithm that splits the array in half, recursively sorts both halves, then merges them. Guaranteed O(n log n) in all cases — never degrades. The downside is O(n) auxiliary space.

Use when: you need guaranteed O(n log n), stable sort, or sorting linked lists (merge sort works naturally on linked lists with O(1) extra space).

// JavaScript
function mergeSort(arr) {
  if (arr.length <= 1) return arr;
  const mid = Math.floor(arr.length / 2);
  const left = mergeSort(arr.slice(0, mid));
  const right = mergeSort(arr.slice(mid));
  return merge(left, right);
}

function merge(left, right) {
  const result = [];
  let i = 0, j = 0;
  while (i < left.length && j < right.length) {
    if (left[i] <= right[j]) result.push(left[i++]);
    else result.push(right[j++]);
  }
  return result.concat(left.slice(i)).concat(right.slice(j));
}

mergeSort([38, 27, 43, 3, 9, 82, 10]);
// [3, 9, 10, 27, 38, 43, 82]
# Python
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)

def merge(left, right):
    result = []
    i = j = 0
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            result.append(left[i]); i += 1
        else:
            result.append(right[j]); j += 1
    result.extend(left[i:])
    result.extend(right[j:])
    return result
// Go
func mergeSort(arr []int) []int {
    if len(arr) <= 1 {
        return append([]int{}, arr...)
    }
    mid := len(arr) / 2
    left := mergeSort(arr[:mid])
    right := mergeSort(arr[mid:])
    return mergeSorted(left, right)
}

func mergeSorted(left, right []int) []int {
    result := make([]int, 0, len(left)+len(right))
    i, j := 0, 0
    for i < len(left) && j < len(right) {
        if left[i] <= right[j] {
            result = append(result, left[i]); i++
        } else {
            result = append(result, right[j]); j++
        }
    }
    result = append(result, left[i:]...)
    result = append(result, right[j:]...)
    return result
}

Quicksort

Divide-and-conquer algorithm that picks a pivot, partitions the array so elements smaller than the pivot are left and larger are right, then recursively sorts both partitions. Average O(n log n) with excellent cache performance in practice — often faster than merge sort on real data despite same asymptotic complexity.

Use when: general-purpose sorting of arrays in memory where you don't need stability. Avoid on nearly-sorted data with naive pivot selection.

// JavaScript — Lomuto partition with random pivot
function quickSort(arr, lo = 0, hi = arr.length - 1) {
  if (lo < hi) {
    const p = partition(arr, lo, hi);
    quickSort(arr, lo, p - 1);
    quickSort(arr, p + 1, hi);
  }
  return arr;
}

function partition(arr, lo, hi) {
  // Random pivot to avoid O(n²) on sorted input
  const pivotIdx = lo + Math.floor(Math.random() * (hi - lo + 1));
  [arr[pivotIdx], arr[hi]] = [arr[hi], arr[pivotIdx]];
  const pivot = arr[hi];
  let i = lo - 1;
  for (let j = lo; j < hi; j++) {
    if (arr[j] <= pivot) {
      i++;
      [arr[i], arr[j]] = [arr[j], arr[i]];
    }
  }
  [arr[i + 1], arr[hi]] = [arr[hi], arr[i + 1]];
  return i + 1;
}

const data = [10, 7, 8, 9, 1, 5];
quickSort(data);
// [1, 5, 7, 8, 9, 10]
# Python
import random

def quick_sort(arr, lo=0, hi=None):
    if hi is None:
        arr = arr[:]
        hi = len(arr) - 1
    if lo < hi:
        p = partition(arr, lo, hi)
        quick_sort(arr, lo, p - 1)
        quick_sort(arr, p + 1, hi)
    return arr

def partition(arr, lo, hi):
    pivot_idx = random.randint(lo, hi)
    arr[pivot_idx], arr[hi] = arr[hi], arr[pivot_idx]
    pivot = arr[hi]
    i = lo - 1
    for j in range(lo, hi):
        if arr[j] <= pivot:
            i += 1
            arr[i], arr[j] = arr[j], arr[i]
    arr[i + 1], arr[hi] = arr[hi], arr[i + 1]
    return i + 1
// Go
func quickSort(arr []int, lo, hi int) {
    if lo < hi {
        p := partitionGo(arr, lo, hi)
        quickSort(arr, lo, p-1)
        quickSort(arr, p+1, hi)
    }
}

func partitionGo(arr []int, lo, hi int) int {
    // median-of-three pivot
    mid := lo + (hi-lo)/2
    if arr[lo] > arr[mid] { arr[lo], arr[mid] = arr[mid], arr[lo] }
    if arr[lo] > arr[hi]  { arr[lo], arr[hi]  = arr[hi],  arr[lo] }
    if arr[mid] > arr[hi] { arr[mid], arr[hi] = arr[hi], arr[mid] }
    arr[mid], arr[hi] = arr[hi], arr[mid]
    pivot := arr[hi]
    i := lo - 1
    for j := lo; j < hi; j++ {
        if arr[j] <= pivot {
            i++
            arr[i], arr[j] = arr[j], arr[i]
        }
    }
    arr[i+1], arr[hi] = arr[hi], arr[i+1]
    return i + 1
}

Heap sort

Uses a binary max-heap to sort. First builds a heap in O(n), then extracts the maximum element n times, each in O(log n). Guaranteed O(n log n) worst case with O(1) space — better space than merge sort, better worst case than quicksort.

Use when: you need guaranteed O(n log n) and O(1) extra space, and don't need stability.

// JavaScript
function heapSort(arr) {
  const a = [...arr];
  const n = a.length;
  // Build max heap
  for (let i = Math.floor(n / 2) - 1; i >= 0; i--) heapify(a, n, i);
  // Extract elements one by one
  for (let i = n - 1; i > 0; i--) {
    [a[0], a[i]] = [a[i], a[0]]; // move current root to end
    heapify(a, i, 0);
  }
  return a;
}

function heapify(arr, n, i) {
  let largest = i;
  const l = 2 * i + 1, r = 2 * i + 2;
  if (l < n && arr[l] > arr[largest]) largest = l;
  if (r < n && arr[r] > arr[largest]) largest = r;
  if (largest !== i) {
    [arr[i], arr[largest]] = [arr[largest], arr[i]];
    heapify(arr, n, largest);
  }
}
# Python
def heap_sort(arr):
    a = arr[:]
    n = len(a)

    def heapify(arr, n, i):
        largest = i
        l, r = 2*i+1, 2*i+2
        if l < n and arr[l] > arr[largest]: largest = l
        if r < n and arr[r] > arr[largest]: largest = r
        if largest != i:
            arr[i], arr[largest] = arr[largest], arr[i]
            heapify(arr, n, largest)

    for i in range(n // 2 - 1, -1, -1):
        heapify(a, n, i)
    for i in range(n - 1, 0, -1):
        a[0], a[i] = a[i], a[0]
        heapify(a, i, 0)
    return a

Counting sort

Non-comparison sort that counts occurrences of each value, then reconstructs the sorted array. O(n+k) where k is the range of values. Incredibly fast when k is small relative to n (e.g., sorting grades 0–100 for a million students).

Use when: elements are integers (or can be mapped to integers) with a small known range. Sorting 10M array elements where values are 0–1000 → counting sort wins decisively.

// JavaScript
function countingSort(arr, maxVal) {
  const count = new Array(maxVal + 1).fill(0);
  for (const x of arr) count[x]++;
  // Prefix sum for stable output
  for (let i = 1; i <= maxVal; i++) count[i] += count[i - 1];
  const output = new Array(arr.length);
  for (let i = arr.length - 1; i >= 0; i--) {
    output[count[arr[i]] - 1] = arr[i];
    count[arr[i]]--;
  }
  return output;
}

countingSort([4, 2, 2, 8, 3, 3, 1], 8);
// [1, 2, 2, 3, 3, 4, 8]
# Python
def counting_sort(arr, max_val):
    count = [0] * (max_val + 1)
    for x in arr:
        count[x] += 1
    # prefix sum
    for i in range(1, max_val + 1):
        count[i] += count[i - 1]
    output = [0] * len(arr)
    for i in range(len(arr) - 1, -1, -1):
        output[count[arr[i]] - 1] = arr[i]
        count[arr[i]] -= 1
    return output

Radix sort

Sorts integers digit by digit, from least significant digit (LSD) to most significant digit (MSD), using counting sort as a subroutine at each digit position. O(nk) where k is the number of digits. Stable and very fast for integers.

Use when: sorting large arrays of integers (or fixed-length strings). Sorting 10M 32-bit integers → radix sort (4 passes of counting sort on 8-bit digits) is very fast.

// JavaScript — LSD Radix Sort (base 10)
function radixSort(arr) {
  const max = Math.max(...arr);
  for (let exp = 1; Math.floor(max / exp) > 0; exp *= 10) {
    arr = countingSortByDigit(arr, exp);
  }
  return arr;
}

function countingSortByDigit(arr, exp) {
  const output = new Array(arr.length);
  const count = new Array(10).fill(0);
  for (const x of arr) count[Math.floor(x / exp) % 10]++;
  for (let i = 1; i < 10; i++) count[i] += count[i - 1];
  for (let i = arr.length - 1; i >= 0; i--) {
    const digit = Math.floor(arr[i] / exp) % 10;
    output[count[digit] - 1] = arr[i];
    count[digit]--;
  }
  return output;
}
# Python
def radix_sort(arr):
    max_val = max(arr)
    exp = 1
    while max_val // exp > 0:
        arr = counting_sort_by_digit(arr, exp)
        exp *= 10
    return arr

def counting_sort_by_digit(arr, exp):
    n = len(arr)
    output = [0] * n
    count = [0] * 10
    for x in arr:
        count[(x // exp) % 10] += 1
    for i in range(1, 10):
        count[i] += count[i - 1]
    for i in range(n - 1, -1, -1):
        d = (arr[i] // exp) % 10
        output[count[d] - 1] = arr[i]
        count[d] -= 1
    return output

Built-in sorts (what you should actually use)

Language Sort function Algorithm Stable Notes
Python sorted() / list.sort() Tim sort Best general choice
JavaScript Array.prototype.sort() Tim sort (V8) Sort is stable since ES2019
Java Arrays.sort() (objects) Tim sort sort() for primitives = dual-pivot quicksort
Java Arrays.sort() (primitives) Dual-pivot quicksort Faster for primitives
Go slices.Sort() Pattern-defeating quicksort Go 1.21+; use slices.SortStableFunc for stable
C++ std::sort Introsort Hybrid: quicksort + heapsort + insertion
C++ std::stable_sort Merge sort Use when stability required
PHP sort() / usort() Quick sort (Zend) usort compares by callback

Custom comparator examples:

// Sort objects by age descending, then name ascending
users.sort((a, b) => b.age - a.age || a.name.localeCompare(b.name));
# Sort tuples by second element, stable
data.sort(key=lambda x: x[1])
# Multiple keys: sort by category asc, price desc
products.sort(key=lambda p: (p['category'], -p['price']))
// Go 1.21+
import "slices"
slices.SortFunc(users, func(a, b User) int {
    if a.Age != b.Age {
        return b.Age - a.Age // descending age
    }
    return strings.Compare(a.Name, b.Name)
})
// PHP
usort($products, fn($a, $b) => $a['price'] <=> $b['price']);

When to use which algorithm

Situation Best choice Why
General purpose, any type Built-in (sort()) Tim sort is hard to beat
Integers, small range (0–10k) Counting sort O(n) — range is irrelevant overhead
Large integers, many elements Radix sort O(nk) beats O(n log n) for large n
Nearly sorted data Insertion sort / Tim sort O(n) on sorted runs
Linked list Merge sort No random access needed
Memory constrained Heap sort O(1) extra space, O(n log n) guaranteed
Stability required Merge sort / Tim sort Preserves equal-element order
Unknown input (defensive) Merge sort No O(n²) worst case
Small n (< 20) Insertion sort Fastest in practice for tiny arrays
External sort (disk) Merge sort Sequential access patterns

Common mistakes

Mistake Problem Fix
Naive quicksort pivot (always first/last) O(n²) on sorted/reverse-sorted input Use random pivot or median-of-three
Assuming sort() is always stable Pre-ES2019 JS engines weren't stable Check language version; use Tim sort variants
Using bubble sort "because it's simple" O(n²) — slow even at n=10,000 Use insertion sort for tiny n
Forgetting counting sort needs non-negative ints Negative indices crash Shift values: min_val as offset
Mutating original array Breaks callers Return sorted copy, or document mutation
Comparing floats for equality in sort NaN propagation, unstable results Sort integers where possible; handle NaN explicitly
Choosing merge sort for large primitive arrays Extra allocation overhead Use dual-pivot quicksort (Java) or introsort (C++)

Complexity summary

Algorithm Best Average Worst Space Stable
Bubble O(n) O(n²) O(n²) O(1)
Selection O(n²) O(n²) O(n²) O(1)
Insertion O(n) O(n²) O(n²) O(1)
Merge O(n log n) O(n log n) O(n log n) O(n)
Quicksort O(n log n) O(n log n) O(n²) O(log n)
Heap O(n log n) O(n log n) O(n log n) O(1)
Counting O(n+k) O(n+k) O(n+k) O(k)
Radix O(nk) O(nk) O(nk) O(n+k)
Bucket O(n+k) O(n+k) O(n²) O(n+k)
Tim O(n) O(n log n) O(n log n) O(n)

FAQ

Why is quicksort often faster than merge sort despite the same O(n log n) average? Cache locality. Quicksort works in-place, operating on contiguous memory with fewer cache misses. Merge sort allocates auxiliary arrays, causing more cache pressure. In practice, quicksort's constants are smaller.

When does quicksort hit O(n²)? When the pivot is consistently the smallest or largest element — worst case for naive pivot selection on already-sorted or reverse-sorted data. Random pivot selection or median-of-three reduces this to astronomically unlikely.

Is JavaScript's Array.sort() stable? Yes, since ES2019 (ECMAScript 2019) the specification requires stable sort. All modern engines (V8/SpiderMonkey/JavaScriptCore) implement this with Tim sort. Pre-2019 V8 used unstable quicksort for arrays > 10 elements.

Can I sort strings with these algorithms? Yes — replace numeric comparisons with string comparison (localeCompare, strcmp, strings.Compare). For locale-aware sorting (handles accented characters, language rules), always use localeCompare in JavaScript or locale.strxfrm in Python.

What is Tim sort? Tim sort (named after Tim Peters who invented it for Python in 2002) is a hybrid algorithm that uses insertion sort for small runs and merge sort to combine them. It detects existing sorted runs in the input, making it O(n) on nearly-sorted data. It's now the default in Python, Java, and modern JavaScript engines.

Radix sort has O(nk) complexity — isn't that worse than O(n log n)? It depends on k (the number of digits). For 32-bit integers, k = 4 (sorting by 8-bit digits in 4 passes). So radix sort runs in effectively 4 × O(n) = O(n) for typical integers, which beats O(n log n) at large n. At n = 10 million, radix sort is typically 2–4× faster than comparison sorts.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools