Data Structures & Algorithms Cheatsheet

Sorting

Use this Data Structures & Algorithms reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

Complexity Comparison

AlgorithmBestAverageWorstSpaceStableIn place
Bubble sortO(n)O(n²)O(n²)O(1)yesyes
Insertion sortO(n)O(n²)O(n²)O(1)yesyes
Selection sortO(n²)O(n²)O(n²)O(1)noyes
Merge sortO(n log n)O(n log n)O(n log n)O(n)yesno
QuicksortO(n log n)O(n log n)O(n²)O(log n) stacknoyes
HeapsortO(n log n)O(n log n)O(n log n)O(1)noyes
TimsortO(n)O(n log n)O(n log n)O(n)yesno
Counting sortO(n + k)O(n + k)O(n + k)O(k)yesno
Radix sort (LSD)O(d(n + b))O(d(n + b))O(d(n + b))O(n + b)yesno
Bucket sortO(n + k)O(n + k)O(n²)O(n)yesno

k is the value range, d the digit count, b the base. Comparison sorts cannot beat Ω(n log n), because n! orderings need at least log₂(n!) ≈ n log n comparisons to distinguish.

What The Standard Libraries Actually Use

LanguageObjectsPrimitives
PythonTimsort, stablesame, Timsort
JavaTimsort, stable (Arrays.sort(Object[]))dual-pivot quicksort, not stable, O(n²) adversarial
C++std::sort is introsort, not stablesame
C++std::stable_sort is merge sort, O(n log n) guaranteedsame
Gosort.Slice is pattern-defeating quicksort, not stableslices.SortStableFunc for stable
JavaScriptArray.prototype.sort is stable since ES2019default comparator is lexicographic
// Java defense against quicksort worst case on int[]
Integer[] boxed = Arrays.stream(a).boxed().toArray(Integer[]::new);
Arrays.sort(boxed);            // now Timsort, O(n log n) guaranteed
// JavaScript default sorts as strings: [10, 9, 1] -> [1, 10, 9]
nums.sort((x, y) => x - y);    // always pass a comparator for numbers

Sorting With Keys And Comparators

a.sort()                                  # in place, stable
b = sorted(a)                             # new list
sorted(a, reverse=True)
sorted(words, key=len)
sorted(words, key=str.lower)
sorted(points, key=lambda p: (p[0], -p[1]))     # asc x, then desc y
sorted(d.items(), key=lambda kv: (-kv[1], kv[0]))  # count desc, name asc

# multi-key with mixed directions and non-numeric keys: sort twice,
# least significant first. Stability preserves the earlier order.
people.sort(key=lambda p: p.name)
people.sort(key=lambda p: p.age, reverse=True)

# custom comparator, when a key function cannot express the rule
from functools import cmp_to_key
def cmp(x, y):
    if x + y > y + x:
        return -1        # x first
    if x + y < y + x:
        return 1
    return 0
largest = "".join(sorted(map(str, nums), key=cmp_to_key(cmp)))
list.sort(Comparator.comparingInt(P::age)
                    .thenComparing(P::name)
                    .reversed());
Arrays.sort(intervals, (x, y) -> Integer.compare(x[0], y[0]));
// Never write (x, y) -> x[0] - y[0], it overflows on large values.
sort(v.begin(), v.end(), [](const P& a, const P& b) {
    if (a.age != b.age) return a.age > b.age;   // strict weak ordering
    return a.name < b.name;
});

A C++ comparator must be a strict weak ordering. Returning true for equal elements (a <= b) causes out-of-bounds reads inside std::sort, not just a wrong order.

Merge Sort

Predictable O(n log n), stable, and the base of counting inversions and external sorting.

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

def merge(x, y):
    out, i, j = [], 0, 0
    while i < len(x) and j < len(y):
        if x[i] <= y[j]:        # <= keeps it stable
            out.append(x[i])
            i += 1
        else:
            out.append(y[j])
            j += 1
    out.extend(x[i:])
    out.extend(y[j:])
    return out

def count_inversions(a):
    """Pairs i < j with a[i] > a[j]. O(n log n) via merge sort."""
    def sort_count(arr):
        if len(arr) <= 1:
            return arr, 0
        mid = len(arr) // 2
        left, cl = sort_count(arr[:mid])
        right, cr = sort_count(arr[mid:])
        merged, i, j, cross = [], 0, 0, 0
        while i < len(left) and j < len(right):
            if left[i] <= right[j]:
                merged.append(left[i])
                i += 1
            else:
                merged.append(right[j])
                j += 1
                cross += len(left) - i     # left[i:] all exceed right[j]
        merged += left[i:] + right[j:]
        return merged, cl + cr + cross
    return sort_count(a)[1]

Quicksort And Quickselect

Partition around a pivot, recurse on the parts. Randomize the pivot or a sorted input drives it to O(n²).

import random

def quicksort(a, lo=0, hi=None):
    if hi is None:
        hi = len(a) - 1
    while lo < hi:                      # tail-call elimination by hand
        p = partition(a, lo, hi)
        if p - lo < hi - p:             # recurse on the smaller side
            quicksort(a, lo, p - 1)     # keeps the stack at O(log n)
            lo = p + 1
        else:
            quicksort(a, p + 1, hi)
            hi = p - 1

def partition(a, lo, hi):
    """Lomuto partition with a random pivot."""
    r = random.randint(lo, hi)
    a[r], a[hi] = a[hi], a[r]
    pivot = a[hi]
    i = lo
    for j in range(lo, hi):
        if a[j] < pivot:
            a[i], a[j] = a[j], a[i]
            i += 1
    a[i], a[hi] = a[hi], a[i]
    return i

def quickselect(a, k):
    """kth smallest, 0-indexed. O(n) average, O(n^2) worst. Mutates a."""
    lo, hi = 0, len(a) - 1
    while True:
        p = partition(a, lo, hi)
        if p == k:
            return a[p]
        if p < k:
            lo = p + 1
        else:
            hi = p - 1

Quickselect is the reason to know partitioning: kth largest in O(n) average beats sorting's O(n log n) and beats a heap's O(n log k) when k is large.

Linear-Time Sorts

Available only when the key space is small and known.

def counting_sort(a, k):
    """Values in 0..k. O(n + k), stable as written."""
    count = [0] * (k + 1)
    for x in a:
        count[x] += 1
    for i in range(1, k + 1):
        count[i] += count[i - 1]        # running positions
    out = [0] * len(a)
    for x in reversed(a):               # reversed keeps it stable
        count[x] -= 1
        out[count[x]] = x
    return out

def radix_sort(a):
    """Non-negative ints, LSD base 256. O(d(n + b))."""
    if not a:
        return a
    out = a[:]
    for shift in range(0, 32, 8):
        buckets = [[] for _ in range(256)]
        for x in out:
            buckets[(x >> shift) & 255].append(x)
        out = [x for b in buckets for x in b]
    return out

Use counting sort when the range is small (ages, ASCII, scores 0–100). Use radix for fixed-width integers or strings. Both need extra space and neither works on arbitrary comparators.

Choosing A Sort

SituationChoice
general purpose, just sort itthe library sort
stability required (multi-pass sorting)Timsort, stable_sort, Arrays.sort(Object[])
guaranteed O(n log n), no worst casemerge sort or heapsort
strict O(1) extra spaceheapsort
nearly sorted inputinsertion sort O(n), or Timsort which detects runs
tiny arrays (n < 16)insertion sort, lower constants than any divide and conquer
kth smallest or largest onlyquickselect O(n), or a size-k heap O(n log k)
small known integer rangecounting sort O(n + k)
fixed-width keys, huge nradix sort
data larger than memoryexternal merge sort
linked listmerge sort, no random access needed

Gotchas

  • a.sort() returns None. b = a.sort() assigns None. Use sorted(a) for a new list.
  • Subtraction comparators overflow. (x, y) -> x - y breaks when the values straddle Integer.MIN_VALUE. Use Integer.compare.
  • JavaScript sorts lexicographically by default. Always pass a numeric comparator.
  • Stability matters when you sort twice. The multi-key trick only works because the second sort preserves the first sort's order among equals.
  • Sorting loses original indices. Sort (value, index) pairs when the answer needs positions.
  • Sorting strings is O(n·k log n), not O(n log n), because each comparison scans up to k characters.
  • Recursion depth. Naive quicksort on sorted input recurses n deep. Recurse into the smaller partition and loop on the larger.
  • reverse=True still stable. Python reverses the comparison, not the output, so equal elements keep their original relative order in both directions.