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
| Algorithm | Best | Average | Worst | Space | Stable | In place |
|---|---|---|---|---|---|---|
| Bubble sort | O(n) | O(n²) | O(n²) | O(1) | yes | yes |
| Insertion sort | O(n) | O(n²) | O(n²) | O(1) | yes | yes |
| Selection sort | O(n²) | O(n²) | O(n²) | O(1) | no | yes |
| Merge sort | O(n log n) | O(n log n) | O(n log n) | O(n) | yes | no |
| Quicksort | O(n log n) | O(n log n) | O(n²) | O(log n) stack | no | yes |
| Heapsort | O(n log n) | O(n log n) | O(n log n) | O(1) | no | yes |
| Timsort | O(n) | O(n log n) | O(n log n) | O(n) | yes | no |
| Counting sort | O(n + k) | O(n + k) | O(n + k) | O(k) | yes | no |
| Radix sort (LSD) | O(d(n + b)) | O(d(n + b)) | O(d(n + b)) | O(n + b) | yes | no |
| Bucket sort | O(n + k) | O(n + k) | O(n²) | O(n) | yes | no |
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
| Language | Objects | Primitives |
|---|---|---|
| Python | Timsort, stable | same, Timsort |
| Java | Timsort, stable (Arrays.sort(Object[])) | dual-pivot quicksort, not stable, O(n²) adversarial |
| C++ | std::sort is introsort, not stable | same |
| C++ | std::stable_sort is merge sort, O(n log n) guaranteed | same |
| Go | sort.Slice is pattern-defeating quicksort, not stable | slices.SortStableFunc for stable |
| JavaScript | Array.prototype.sort is stable since ES2019 | default 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
truefor equal elements (a <= b) causes out-of-bounds reads insidestd::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
| Situation | Choice |
|---|---|
| general purpose, just sort it | the library sort |
| stability required (multi-pass sorting) | Timsort, stable_sort, Arrays.sort(Object[]) |
| guaranteed O(n log n), no worst case | merge sort or heapsort |
| strict O(1) extra space | heapsort |
| nearly sorted input | insertion 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 only | quickselect O(n), or a size-k heap O(n log k) |
| small known integer range | counting sort O(n + k) |
| fixed-width keys, huge n | radix sort |
| data larger than memory | external merge sort |
| linked list | merge sort, no random access needed |
Gotchas
a.sort()returnsNone.b = a.sort()assignsNone. Usesorted(a)for a new list.- Subtraction comparators overflow.
(x, y) -> x - ybreaks when the values straddleInteger.MIN_VALUE. UseInteger.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=Truestill stable. Python reverses the comparison, not the output, so equal elements keep their original relative order in both directions.