Sorting Algorithms โ Ultimate Expert Reference Guide
For Practitioners in Data Science, ML, AI & LLM Pipelinesโ
This guide assumes you already know what sorting is. The goal here is to give you the engineering judgment to know which algorithm the standard library is actually running under your
sorted()call, when a hand-rolled sort beats it, and where sorting quietly becomes the bottleneck (or the fix) inside real ML systems.
1. Merge Sortโ
Conceptโ
Merge Sort is a divide-and-conquer algorithm. It recursively splits the array in half until each piece is trivially sorted (length 0 or 1), then merges the sorted pieces back together in linear time. The intelligence is not in the splitting โ it's in the merge step, which walks two already-sorted lists with two pointers and interleaves them in order.
The defining property: because the merge always takes the left element when there's a tie (<=), relative order of equal keys is preserved โ Merge Sort is stable. Its runtime is O(n log n) regardless of input distribution โ there is no "bad input" that degrades it.
Analogyโ
Imagine two colleagues each hand you a pre-alphabetized stack of rรฉsumรฉs. To combine them, you don't re-sort everything โ you glance at the top card of each stack, take whichever name comes first, and repeat. You never look deeper than the top of each pile. Merge Sort is the recursive version of "everyone alphabetizes their own small pile, then we zipper the piles together."
Algorithm Walkthroughโ
Given [38, 27, 43, 3, 9, 82, 10]:
- Split into
[38, 27, 43]and[3, 9, 82, 10]. - Recurse on each half until you reach single elements (a single element is sorted by definition).
- Merge upward:
[27, 38]+[43]โ[27, 38, 43];[3, 9]+[10, 82]โ[3, 9, 10, 82]. - Final merge:
[27, 38, 43]+[3, 9, 10, 82]โ[3, 9, 10, 27, 38, 43, 82].
Each level of recursion touches all n elements once during merging, and there are log n levels โ O(n log n).
Pseudocodeโ
function merge_sort(A):
if length(A) <= 1: return A
mid = length(A) / 2
left = merge_sort(A[0:mid])
right = merge_sort(A[mid:])
return merge(left, right)
function merge(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]: # <= preserves stability
append left[i]; i += 1
else:
append right[j]; j += 1
append remaining of left, then right
return result
Python Implementationโ
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]: # '<=' keeps equal keys stable
result.append(left[i]); i += 1
else:
result.append(right[j]); j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
# Usage
print(merge_sort([38, 27, 43, 3, 9, 82, 10]))
# -> [3, 9, 10, 27, 38, 43, 82]
Complexityโ
| Case | Time | Space |
|---|---|---|
| Best | O(n log n) | O(n) |
| Average | O(n log n) | O(n) |
| Worst | O(n log n) | O(n) |
Space is O(n) for the auxiliary merge buffers (the array copies). An in-place variant exists but is complex and rarely worth it. Recursion adds O(log n) stack depth.
When to Use / When NOT to Useโ
- โ
Use when you need guaranteed
O(n log n)(no worst-case blowup), when stability matters (sorting records by a secondary key without disturbing the primary order), or when data lives on disk/streams (external merge sort powers "sort 100GB with 8GB RAM"). - โ
Use for linked lists โ merging needs only pointer rewiring, so the
O(n)extra space vanishes. - โ Avoid when memory is tight and data is array-based โ the
O(n)buffer is a real cost. - โ Avoid for small arrays โ the constant factors and allocation overhead lose to insertion sort (which is exactly why hybrid sorts exist).
๐ก Pro Tip: Merge Sort is the backbone of external sorting. When your dataset doesn't fit in RAM (think: sorting a 500GB feature dump before a join), you sort chunks that do fit, write them to disk as sorted "runs", then k-way merge them. This is precisely how databases and Spark's shuffle-sort operate under the hood.
2. Quick Sortโ
Conceptโ
Quick Sort is also divide-and-conquer, but it front-loads the work. It picks a pivot, then partitions the array so everything smaller sits left of the pivot and everything larger sits right. The pivot is now in its final position. Recurse on both sides. Unlike Merge Sort, there is no merge step โ the sorting happens during partitioning, and it's done in place (O(log n) extra space for the call stack, not O(n) for buffers).
The catch: performance hinges entirely on pivot quality. A pivot that splits the array roughly in half gives O(n log n). A consistently terrible pivot (e.g., always the smallest element on already-sorted data) degrades to O(nยฒ).
Analogyโ
Picture organizing a crowd of people by height for a photo. You grab one person as a reference (the pivot) and shout: "Shorter than me? Go left. Taller? Go right." Now the reference person is standing in their correct final spot, and you've split the crowd into two smaller groups. Each group repeats the process with its own reference. Nobody gets re-measured against the whole crowd โ only against the current reference.
Algorithm Walkthroughโ
Given [3, 6, 1, 8, 2, 9, 4], pivot = last element (4):
- Partition around
4: elements< 4go left โ[3, 1, 2], elements> 4go right โ[6, 8, 9]. Result:[3, 1, 2, 4, 6, 8, 9]. The4is now permanently placed. - Recurse on
[3, 1, 2](pivot2) โ[1, 2, 3]. - Recurse on
[6, 8, 9](pivot9) โ[6, 8, 9]. - Combine (no work needed โ already in place) โ
[1, 2, 3, 4, 6, 8, 9].
Pseudocodeโ
function quick_sort(A, lo, hi):
if lo < hi:
p = partition(A, lo, hi)
quick_sort(A, lo, p - 1)
quick_sort(A, p + 1, hi)
function partition(A, lo, hi): # Lomuto scheme
pivot = A[hi]
i = lo - 1
for j = lo to hi - 1:
if A[j] <= pivot:
i += 1
swap A[i], A[j]
swap A[i+1], A[hi]
return i + 1
Python Implementationโ
import random
def quick_sort(arr, lo=0, hi=None):
if hi is None:
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):
# Randomized pivot: swap a random element into the hi slot.
# This defeats adversarial/already-sorted inputs -> avoids O(n^2).
rand = random.randint(lo, hi)
arr[rand], arr[hi] = arr[hi], arr[rand]
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
# Usage
print(quick_sort([3, 6, 1, 8, 2, 9, 4]))
# -> [1, 2, 3, 4, 6, 8, 9]
A concise (but not in-place, and less efficient) "teaching" version:
def quick_sort_simple(arr):
if len(arr) <= 1:
return arr
pivot = arr[len(arr) // 2]
left = [x for x in arr if x < pivot]
mid = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quick_sort_simple(left) + mid + quick_sort_simple(right)
This is elegant but allocates new lists at every level (O(n)* space, not in-place) and does 3 passes per level. Use it to explain the idea, not in production.*
Complexityโ
| Case | Time | Space (stack) |
|---|---|---|
| Best | O(n log n) | O(log n) |
| Average | O(n log n) | O(log n) |
| Worst | O(nยฒ) | O(n) |
Worst case (O(nยฒ)) happens with pathological pivots โ e.g., always picking the min/max on sorted data. Randomized or median-of-three pivots make this astronomically unlikely in practice. Quick Sort is not stable in its standard in-place form.
When to Use / When NOT to Useโ
- โ Use when you want the fastest in-place comparison sort in practice โ excellent cache locality (it works on contiguous memory rather than allocating buffers) and small constant factors make it the real-world speed king for in-memory arrays.
- โ Use when memory is constrained and you don't need stability.
- โ Avoid when you need guaranteed
O(n log n)(e.g., adversarial input, real-time systems) โ theO(nยฒ)tail risk is real. Use Merge Sort or Heap Sort instead. - โ Avoid when stability is required.
๐ก Pro Tip: Production sorts are almost never pure Quick Sort. C++
std::sortuses introsort โ Quick Sort that watches its own recursion depth and switches to Heap Sort if it detects it's heading towardO(nยฒ), guaranteeingO(n log n). Both introsort and Python's Timsort fall back to insertion sort on small partitions (typically โค 16 elements) because insertion sort's tiny constants win at that scale. The lesson: hybridize.
3. Counting Sortโ
Conceptโ
Counting Sort breaks the O(n log n) comparison barrier because it doesn't compare elements at all. It works only on integers (or values mappable to a small integer range [0, k]). It counts how many times each value appears, computes a prefix sum of those counts to determine each value's final position, then places elements directly. Runtime is O(n + k) โ linear when k (the value range) is not much larger than n.
It is stable when implemented correctly (iterating the input right-to-left when placing), which is the reason it serves as the inner loop of Radix Sort.
Analogyโ
Imagine tallying exam scores from 0โ100 for a lecture hall of students. You don't line students up and compare scores pairwise โ you make 101 mailboxes labeled 0 to 100 and drop each student's card into the matching box. Then you empty the boxes in order. You never compared two students to each other; you just knew, by the label, exactly where each card belongs.
Algorithm Walkthroughโ
Given [4, 2, 2, 8, 3, 3, 1], max value k = 8:
- Count occurrences โ index = value:
count[1]=1, count[2]=2, count[3]=2, count[4]=1, count[8]=1. - Prefix sum the counts so
count[v]= number of elementsโค vโ this tells each value its ending position. - Place elements into the output using those positions, iterating the input right-to-left to keep it stable.
- Result:
[1, 2, 2, 3, 3, 4, 8].
Pseudocodeโ
function counting_sort(A, k): # values in [0, k]
count = array of (k+1) zeros
for x in A: count[x] += 1
for i = 1 to k: count[i] += count[i-1] # prefix sums
output = array of len(A)
for x in reverse(A): # reverse => stable
count[x] -= 1
output[count[x]] = x
return output
Python Implementationโ
def counting_sort(arr):
if not arr:
return arr
lo, hi = min(arr), max(arr)
k = hi - lo # offset so negatives work too
count = [0] * (k + 1)
for x in arr:
count[x - lo] += 1
# Prefix sums -> each count[i] becomes an ending index
for i in range(1, k + 1):
count[i] += count[i - 1]
output = [0] * len(arr)
for x in reversed(arr): # right-to-left keeps it STABLE
count[x - lo] -= 1
output[count[x - lo]] = x
return output
# Usage
print(counting_sort([4, 2, 2, 8, 3, 3, 1]))
# -> [1, 2, 2, 3, 3, 4, 8]
Complexityโ
| Case | Time | Space |
|---|---|---|
| Best | O(n + k) | O(n + k) |
| Average | O(n + k) | O(n + k) |
| Worst | O(n + k) | O(n + k) |
n* = number of elements, k = size of the value range. If k = O(n), this is linear time. If k >> n (e.g., sorting seven 64-bit integers), the O(k) space and time make it catastrophic โ you'd allocate an array of billions of buckets.*
When to Use / When NOT to Useโ
- โ Use when you're sorting integers (or small enumerable keys) over a bounded, modest range โ ages, pixel intensities (0โ255), grades, categorical codes, character codes.
- โ Use as the digit-sort inside Radix Sort to sort large integers or fixed-length strings in linear time.
- โ Avoid when the value range
kis large or unbounded (floats, arbitrary 64-bit IDs) โ memory explodes. - โ Avoid for general comparable objects โ it needs an integer key mapping.
๐ก Pro Tip: Counting Sort's stability is load-bearing for Radix Sort. Radix sorts numbers digit-by-digit from least-significant to most-significant, and each pass relies on the previous pass's order being preserved. Break stability and Radix Sort silently produces garbage. This is the classic "why does stability matter?" answer that separates people who memorized the definition from those who understand it.
4. Python sorted() Key Tricksโ
Python's built-in
sorted()andlist.sort()both run Timsort โ a hybrid, stable, adaptive Merge Sort + insertion sort that exploits pre-existing ordered "runs" in real-world data. It'sO(n log n)worst case andO(n)* on already-sorted or nearly-sorted input*. You will almost never beat it with a hand-rolled Python sort โ the value you add is in the key function.
Basic Syntax & Parametersโ
sorted(iterable, *, key=None, reverse=False) # returns a NEW list
list.sort(*, key=None, reverse=False) # sorts IN PLACE, returns None
key: a function of one argument that extracts the comparison key from each element. Called exactly once per element (this is the "Schwartzian transform" / decorate-sort-undecorate, done for you).reverse:Truesorts descending.- Gotcha:
list.sort()returnsNone.x = mylist.sort()setsx = Noneโ a classic bug. Usesorted()if you need the return value.
Lambda-Based Sortingโ
words = ["banana", "kiwi", "watermelon", "fig"]
# Sort by length
sorted(words, key=lambda w: len(w))
# -> ['fig', 'kiwi', 'banana', 'watermelon']
# Sort by last character
sorted(words, key=lambda w: w[-1])
๐ก Pro Tip: For attribute/index/method extraction,
operator.itemgetter,operator.attrgetter, andoperator.methodcallerare faster than lambdas (they're implemented in C and skip Python-level function-call overhead). On large lists this is a measurable win:
from operator import itemgetter, attrgetter
sorted(rows, key=itemgetter(2)) # by 3rd column
sorted(objs, key=attrgetter("score")) # by .score attribute
sorted(rows, key=itemgetter(1, 0)) # multi-key, tuple order
Multi-Key Sortingโ
Return a tuple from the key function โ Python compares tuples lexicographically (first element, then second as tiebreaker, ...).
people = [
{"name": "Alice", "age": 30},
{"name": "Bob", "age": 30},
{"name": "Carol", "age": 25},
]
# Primary: age ascending. Tiebreaker: name ascending.
sorted(people, key=lambda p: (p["age"], p["name"]))
Mixed directions (some keys ascending, some descending): negate numeric keys, or exploit Timsort's stability with chained sorts (sort by the least significant key first):
# Numeric: negate for descending
sorted(people, key=lambda p: (-p["age"], p["name"])) # age DESC, name ASC
# Non-numeric descending (can't negate a string) -> stable chained sort:
data.sort(key=lambda p: p["name"]) # secondary first
data.sort(key=lambda p: p["age"], reverse=True) # primary last wins
๐ก Pro Tip: The stable-chained-sort trick (sort by secondary key, then by primary) is the only clean way to mix ascending/descending on non-numeric keys, since you can't put a
reverseon just one tuple element. It works because Timsort is stable.
Sorting Custom Objectsโ
from dataclasses import dataclass, field
from functools import total_ordering
@dataclass
class Model:
name: str
accuracy: float
params_millions: float
models = [
Model("bert-base", 0.91, 110),
Model("distilbert", 0.89, 66),
Model("bert-large", 0.92, 340),
]
# Best accuracy first; break ties by smaller model (fewer params)
sorted(models, key=lambda m: (-m.accuracy, m.params_millions))
To make objects intrinsically sortable (so bare sorted(objs) works), define __lt__ โ or use @total_ordering to derive the rest from __eq__ + __lt__:
@total_ordering
class Version:
def __init__(self, major, minor): self.major, self.minor = major, minor
def __eq__(self, o): return (self.major, self.minor) == (o.major, o.minor)
def __lt__(self, o): return (self.major, self.minor) < (o.major, o.minor)
Reverse Sorting Patternsโ
sorted(nums, reverse=True) # simplest descending
sorted(preds, key=lambda x: x["score"], reverse=True) # descending by key
# Descending primary, ascending secondary (numeric): negate the primary
sorted(preds, key=lambda x: (-x["score"], x["label"]))
# reverse=True reverses the WHOLE comparison, including tiebreakers,
# but because Timsort is STABLE, equal keys retain original input order.
Real-World DS/ML Use Casesโ
# 1) Rank model predictions by confidence (top-k retrieval / re-ranking)
predictions = [{"label": "cat", "score": 0.82},
{"label": "dog", "score": 0.91},
{"label": "fox", "score": 0.91}]
ranked = sorted(predictions, key=lambda x: (-x["score"], x["label"]))
top_k = ranked[:2]
# 2) Sort experiment runs: best val-loss first, then fewest epochs
runs = [{"id": "a", "val_loss": 0.31, "epochs": 40},
{"id": "b", "val_loss": 0.31, "epochs": 25}]
sorted(runs, key=lambda r: (r["val_loss"], r["epochs"]))
# 3) Rank tokens by frequency to build a vocabulary (NLP)
from collections import Counter
freqs = Counter(corpus_tokens)
vocab = sorted(freqs.items(), key=lambda kv: (-kv[1], kv[0])) # freq DESC, token ASC
# 4) Sort feature importances for a report
importances = {"age": 0.12, "income": 0.30, "tenure": 0.30}
sorted(importances.items(), key=lambda kv: kv[1], reverse=True)
# 5) nlargest without a full sort (faster for top-k on huge lists)
import heapq
heapq.nlargest(5, predictions, key=lambda x: x["score"])
๐ก Pro Tip: For top-k on a large collection, don't sort the whole thing.
heapq.nlargest(k, data, key=...)isO(n log k)vs.O(n log n)for a full sort โ a big win whenk << n(e.g., top-10 retrieval hits out of a million candidates).
5. Comparative Summary Tableโ
| Algorithm | Best | Average | Worst | Space | Stable? | In-place? | Best Use-Case Domain |
|---|---|---|---|---|---|---|---|
| Merge Sort | O(n log n) | O(n log n) | O(n log n) | O(n) | โ Yes | โ No | Guaranteed bound; stability; external/on-disk sort; linked lists |
| Quick Sort | O(n log n) | O(n log n) | O(nยฒ) | O(log n) | โ No | โ Yes | Fastest in-memory array sort; cache-friendly; memory-constrained |
| Counting Sort | O(n + k) | O(n + k) | O(n + k) | O(n + k) | โ Yes | โ No | Small-range integers; radix-sort inner loop; histograms |
Timsort (sorted) | O(n) | O(n log n) | O(n log n) | O(n) | โ Yes | โ No* | General-purpose default; exploits partially-sorted real data |
*list.sort() is in-place from the caller's view but Timsort uses O(n) temp merge space internally. sorted() returns a new list.
6. DS/ML/AI/LLM Practitioner Spotlightโ
Sorting is rarely the headline of an ML system, but it's woven through the plumbing โ and it's frequently where a naive implementation quietly loses you 10ร in latency.
Where each algorithm shows up:
- Merge Sort (external merge) โ The engine behind shuffle-sort in distributed compute (Spark, MapReduce) and **database
ORDER BY/ **JOINon datasets larger than RAM. When you sort a multi-TB feature table before a join or a windowed aggregation, you're running external merge sort. Its stability + guaranteed bound are why it's trusted for reproducible, large-scale ETL. - Quick Sort / Introsort โ The default in NumPy (
np.sort(kind='quicksort')โ actually introsort) and C++ STL. This is what runs when you sort an in-memory feature vector, dedupe an array, or prepare data for a binary search. Cache locality makes it the fastest option once your data is a contiguous array. - Counting Sort / Radix Sort โ Linear-time sorting of bounded integer keys: bucketing token IDs, sorting pixel values in image preprocessing, or ordering categorical codes. Radix sort (built on stable counting sort) is used in GPU-accelerated pipelines (e.g., CUDA Thrust) where it dramatically outperforms comparison sorts on integer keys.
- Timsort (
sorted) โ Everywhere in glue code: ranking predictions, ordering retrieval results, building vocabularies, sorting hyperparameter-search results. Its adaptivity to partially-sorted input is a real advantage because ML data is often already semi-ordered (e.g., time-series, appended logs).
How sorting impacts performance across the pipeline:
- Data preprocessing: Sorting enables
O(log n)binary search for deduplication, joins, and range queries. Sorting once to enable many fast lookups amortizes beautifully. Sorted data also compresses better (run-length / delta encoding) โ relevant for columnar formats like Parquet. - Ranking & recommendation: The final stage of nearly every search / recommender / retrieval system is a sort (or partial sort) of candidates by score. Use
heapq.nlargest/np.argpartitionfor top-k instead of a full sort โ this is the single most common sorting optimization in production ML serving. - NLP / tokenization: Building a vocabulary means sorting tokens by frequency (then truncating to vocab size). BPE/WordPiece tokenizer training sorts merge candidates by frequency each iteration. Sorted vocab IDs also let tokenizers use fast lookups.
- LLM inference: Top-k / top-p (nucleus) sampling requires sorting (or partially sorting) the logits/probabilities over a ~50k-token vocabulary at every generated token. Because vocab is fixed-size, this is a bounded sort โ but doing it naively (full
O(V log V)sort per token) vs.argpartitionfor top-k is a measurable latency difference across millions of tokens. Batch scheduling (e.g., sorting requests by sequence length to minimize padding in a batch) is another quiet but impactful sort. - Vector search / embeddings: ANN indices (FAISS, HNSW) ultimately return candidates that get sorted by distance/similarity for the final ranking.
np.argpartition+ partial sort on the top-k is standard.
๐ก Pro Tip: In NumPy,
np.argsortreturns indices โ the ML-idiomatic pattern. You sort once and use the index array to reorder features, labels, and metadata in parallel (X[idx], y[idx]), keeping everything aligned. And for top-k,np.argpartition(scores, -k)[-k:]isO(n)vs.np.argsort'sO(n log n)โ use it whenever you don't need the top-k internally sorted.
7. Must-Know Gotchas & Pro Tipsโ
list.sort()** returnsNone.**sorted_x = mylist.sort()is a bug that bites everyone once. Usesorted()when you want a return value; use.sort()only for the in-place side effect.sorted()** copies,.sort()mutates.** On a 10GB array,sorted()doubles your memory. Choose deliberately.- The
cmpparameter is gone (Python 3). Old comparator functions must be wrapped withfunctools.cmp_to_key(cmp). Prefer akeyfunction โ it's called once per element (O(n)key extractions) vs. a comparator'sO(n log n)calls, sokeyis both faster and cleaner. key** is evaluated once per element.** If your key is expensive (regex, DB lookup, model call), that's fine โ it won't be recomputed during comparisons. But precompute it once yourself if you also need the values elsewhere.- Mixing incomparable types raises
TypeErrorin Python 3.sorted([1, "a"])throws. There's no silent coercion like Python 2. Normalize types (or key-map them) first. NaN** breaks sorting silently.**float('nan')comparesFalseagainst everything, so aNaNin your data produces a non-deterministic, non-sorted result with no error. Filter or replace NaNs before sorting model scores/losses:```python clean = [x for x in scores if not math.isnan(x)]
- **Stability is a feature โ use it.** To sort by multiple criteria with mixed directions on non-numeric keys, do successive stable sorts from least- to most-significant key. Timsort guarantees earlier order survives.
- **Don't full-sort for top-k.** `heapq.nlargest(k, ...)` (`O(n log k)`) and `np.argpartition` (`O(n)`) crush a full sort when `k << n`. This is the highest-ROI sorting optimization in ML serving.
- `operator.itemgetter`**/**`attrgetter`** > **`lambda`**.** For large lists, the C-level getters measurably beat Python lambdas as key functions.
- **Reverse โ stability loss.** `reverse=True` reverses the comparison but Timsort *stays stable* โ equal elements keep their original relative order (they are **not** reversed). This surprises people who expect `reverse=True` to be "sort then reverse the list."
- **Recursion depth in hand-rolled Quick/Merge Sort.** Python's default recursion limit (~1000) means a naive recursive sort can hit `RecursionError` on large or degenerate inputs. Real implementations recurse on the smaller partition and loop on the larger (tail-call elimination) to cap stack depth at `O(log n)`.
- **Know what your library actually runs.** `sorted()` โ Timsort (stable). `np.sort()` default โ introsort (**not** stable); pass `kind='stable'` (Timsort/radix) when you need stability. `pandas.DataFrame.sort_values(kind='mergesort')` is the stable option. Picking the wrong `kind` silently reorders equal keys and can corrupt downstream joins.
> ๐ก **Final Expert Takeaway:** In modern practice you will almost never *write* a sort โ you'll *choose* one and, critically, *design the key*. The engineering skill is (1) knowing your library's default and its stability guarantee, (2) reaching for **partial sorts** (`nlargest`, `argpartition`) whenever you only need top-k, and (3) recognizing when sorting is a *preprocessing investment* that unlocks `O(log n)` lookups downstream. Get those three right and sorting stops being a bottleneck and becomes a lever.
---
## Related Guides
**Prerequisites:** [Big-O Notation & Complexity Analysis](/docs/big-o-complexity) ยท [Recursion & The Call Stack](/docs/recursion-and-call-stack)
**See also:** [Binary Search & Search on Answer](/docs/binary-search) ยท [Heaps & Priority Queues](/docs/heaps-and-priority-queues)
*Section: [Core DSA](/docs/category/02-core-dsa) ยท [All guides](/)*