All tutorials Mighty Professional
Build a Game Engine ยท Foundations

Big O Notation

A hundred objects in the test scene, ten thousand in the shipping one. The build runs at 60 fps on the laptop and 4 fps on the target box. The animation that took 1 ยตs on day one takes 18 ms once the encounter is populated. Big O is the language game programmers use to predict that, before the QA report comes in. This tutorial starts from the 1894 definition and works up through the cousin notations, the master theorem, and four worked engine case studies (broad-phase collision, BVH ray traversal, A*, draw-call sorting), with a live demo at every step.

Time~55 min LevelJunior to mid; review for senior PrereqsYou can read C++ or pseudocode at intermediate level. You've heard of arrays, hash tables, and trees. HardwareNone, but knowing that CPUs have caches helps in ยง6 and ยง13.
โ—‚ Build a Game Engine Phase 0 ยท Foundations Next ยท Bit Shifting โ–ธ

01Why a notation, not a stopwatch

A naive collision check on the entities in a scene is two nested loops: for every pair of entities, test whether they overlap. On the four-object test scene that's six comparisons; the function takes 200 nanoseconds and nobody profiles it. On the eight-hundred-object shipping scene that's roughly 320,000 comparisons, the function takes 6 milliseconds, and a third of a 60 fps frame is gone before any contact has been resolved. A stopwatch on day one tells you nothing about day three hundred. A notation that says this function does work proportional to nยฒ does, because the multiplier the player will pay was visible the moment the function was written.

is that language. It strips a function down to its dominant growth term and throws away everything else (constants, lower-order terms, the specific hardware). What's left is the shape of the curve, which largely decides whether a system survives the jump from test scene to shipping content. The shape is independent of the CPU: it describes how the cost grows as the workload does.

What you'll have by the end

A working understanding of asymptotic notation strong enough to read a published algorithm description and predict its runtime curve; the discipline to spot accidental quadratic behavior in a code review; the math for the standard divide-and-conquer recurrences via the master theorem; a feel for when Big O lies (cache effects, constant factors); and four worked engine case studies (broad-phase collision, BVH ray traversal, A*, draw-call sorting) where the asymptotic choice is the design choice. By the end you'll know how sweep-and-prune and spatial hashing take the broad phase from nยฒ to near-linear, why ray-traced reflections are tractable at all[1], why many renderers sort draw calls with one bit-packed 64-bit key[2], and why Tetris with a known piece sequence is NP-complete[3].

The shape that ate the encounter

The widget below plots three implementations of "find all pairs of objects that might be touching": brute force at O(nยฒ), sweep-and-prune at roughly O(n log n), and a uniform spatial hash at O(n + k). Drag the object-count slider:

Live ยท Pair-test cost as the scene scales
brute (O(nยฒ))
ยทยทยท
sweep+prune
ยทยทยท
spatial hash
ยทยทยท
Comparisons-per-frame is a proxy for cost; on a modern CPU each pair test is a handful of cycles, so the absolute milliseconds depend on hardware, but the ratios between the curves are robust. Sweep-and-prune's average is O(n log n) with a worst case of O(nยฒ) when every box overlaps every other[4]; the spatial hash is approximately O(n + k) where k is the number of actually-overlapping pairs, as long as the cell density stays bounded. The y axis is logarithmic: each gridline is ten times the one below it.

02A short history of writing down growth rates

Big O was invented for analytic number theory, not computer science, and its modern shape took about 80 years to settle. The current usage makes more sense with the history in view:

1877
Paul du Bois-Reymond compares growth rates symbolically.[6] His "infinitary calculus" writes f โ‰บ g when g grows faster than f, and ranks functions such as x2 and ex by their rate of increase. Hardy later adopted the โ‰บ symbol; Big O came from a different line.
1894
Paul Bachmann introduces the O symbol.[7] Volume II of his Analytische Zahlentheorie writes "f(x) = O(g(x))" with O standing for Ordnung ("order"). This is the first appearance of the notation that survived. Bachmann uses it for the error terms in his estimates of number-theoretic functions such as the number and sum of divisors.
1909
Edmund Landau adds little-o.[8] Landau's Handbuch der Lehre von der Verteilung der Primzahlen ("Handbook of the Theory of the Distribution of Prime Numbers") adopts Bachmann's big-O and introduces the lower-case o for strict asymptotic dominance. The pair is still called the Landau symbols in the math literature.
1914
Hardy and Littlewood extend the family.[9] G.H. Hardy and J.E. Littlewood introduce big-ฮฉ as a number-theoretic lower bound (a non-vanishing remainder, "f is not little-o of g"). The symbol is borrowed later by computer science with a different and stricter definition.
1976
Knuth standardizes Big O, Big ฮฉ, and Big ฮ˜ for algorithm analysis.[10] Donald Knuth's "Big Omicron and Big Omega and Big Theta" in SIGACT News formalizes the trio with the definitions algorithms researchers use today. Knuth wrote it because authors kept using O() for lower bounds (his example: rejecting a sorting method "because its running time is O(nยฒ)") and he wanted a notation that kept the distinction visible. The modern ฮฉ is stricter than Hardy and Littlewood's: it requires a uniform lower bound, not just an infinitely-often one.
1990
CLRS canonizes the asymptotic toolkit for teaching.[11] The first edition of Cormen, Leiserson, and Rivest (Stein joins for the second) puts asymptotic notation in an early "Growth of Functions" chapter of what becomes the most widely used algorithms textbook. Generations of undergraduates learn Big O from this presentation.
1997
Musser publishes introsort.[12] David Musser's "Introspective Sorting and Selection Algorithms" introduces the hybrid that runs quicksort but switches to heapsort when recursion depth exceeds 2 logโ‚‚ n. It turns the "worst case versus expected case" distinction into a library design decision: introsort guarantees O(n log n) worst case while keeping quicksort's speed on common inputs.
1999
Frigo, Leiserson, Prokop, Ramachandran formalize cache-obliviousness.[13] Counting block transfers between memory levels instead of CPU operations was already standard in the external-memory model of Aggarwal and Vitter (1988)[31]. Frigo et al. add the ideal-cache model and algorithms that hit the optimal transfer bounds without knowing the cache size, so one analysis covers every level of the hierarchy. Both models make the same point: Big O over memory transfers is sometimes the bound that matters, not Big O over arithmetic operations.
2010s
The "galactic algorithm" idea enters common usage.[14] A 2010 post on Lipton and Regan's blog names asymptotically superior algorithms whose constants are so vast they never beat the simpler approach on any realistically sized input (the name is Regan's). Coppersmith-Winograd matrix multiplication at O(n2.376) is a standard example; high-performance linear-algebra libraries implement the O(nยณ) product with cache blocking instead.
2026
Asymptotics live alongside cache, branch, and parallelism cost models. Engine work typically treats Big O as the first filter (it catches a quadratic before it ships) and then applies cache-line, branch-prediction, and SIMD reasoning to the candidates that pass it.

03The definition, picked apart

Big O is defined for functions from non-negative integers (or reals) to non-negative reals. The formal statement, the way Knuth wrote it[10] and CLRS teaches it[11]:

eq. 1 ยท big-O f(n) = O ( g(n) )   โ‡”   โˆƒ c > 0, nโ‚€ โ‰ฅ 0 : โˆ€ n โ‰ฅ nโ‚€, 0 โ‰ค f(n) โ‰ค c ยท g(n)

Read aloud: "f(n) is O(g(n)) if there exist constants c and nโ‚€ such that for every n at least nโ‚€, f(n) is sandwiched between zero and c times g(n)." The c is what lets you throw away constant factors; the nโ‚€ is what lets you throw away lower-order terms. Both are existential, which means the definition is satisfied as soon as you can find any pair that works.

The three things this definition does not say are at least as important as what it does say:

Drag the widget below. The orange curve is f(n); the violet curve is c ยท g(n) for the chosen c. The vertical line is nโ‚€. Big O holds when the orange curve stays under the violet one for every n to the right of the line:

Live ยท Find c and nโ‚€ that prove the bound
f(n)
3nยฒ + 12n + 50
status
ยทยทยท
For f(n) = 3nยฒ + 12n + 50, the bound f = O(nยฒ) holds with c = 4, nโ‚€ = 16: f(16) = 3ยท256 + 192 + 50 = 1010 and 4ยท16ยฒ = 1024, so f(16) โ‰ค 4ยทg(16), and the inequality survives for every larger n. Push the slider down to nโ‚€ = 13 (still at c = 4) and the bound fails: f(13) = 713 beats 4ยท169 = 676. Either bump c up or nโ‚€ back up to recover. With g = n, no constant c works asymptotically, because the nยฒ term eventually beats any linear cap. Toggle to ฮฉ or ฮ˜ to test the lower-bound and tight-bound definitions from ยง4. ฮ˜ needs two constants; the widget uses c for the upper curve and draws a second curve at c/2 as the lower one, so with g = nยฒ and nโ‚€ = 16 the sandwich (c/2)ยทg โ‰ค f โ‰ค cยทg holds for any c from 4 to 6. The check is numerical: it samples n from nโ‚€ up to about nโ‚€ + 250.
Why is nโ‚€ existentially quantified?

The point of nโ‚€ is to let you ignore noise at small n. A function might do weird things for the first ten or hundred inputs (a startup cost, a one-time table fill, a cache that hasn't warmed up) and then settle into its real growth shape. Big O cares about the real growth shape, not the noise.

Choosing nโ‚€ = 0 is a stronger claim ("the bound holds for every input"); choosing nโ‚€ = 106 is a weaker one ("the bound holds for every input I care about"). The definition says you only need to find any nโ‚€, which is the right tradeoff for ignoring small-input artifacts without throwing away the bound.

04Omega, Theta, and the rest of the family

Big O is an upper bound. The full asymptotic toolkit has four cousins. Each is one inequality flip away from the others:

Name Symbol Meaning Definition Reads as
Big O O(g) Upper bound (not necessarily tight) โˆƒc, nโ‚€ โˆ€nโ‰ฅnโ‚€: f โ‰ค cยทg "grows no faster than"
Big Omega ฮฉ(g) Lower bound (not necessarily tight) โˆƒc, nโ‚€ โˆ€nโ‰ฅnโ‚€: f โ‰ฅ cยทg "grows no slower than"
Big Theta ฮ˜(g) Tight bound, same growth f = O(g) and f = ฮฉ(g) "grows exactly like"
Little o o(g) Strictly slower โˆ€c > 0 โˆƒnโ‚€ โˆ€nโ‰ฅnโ‚€: f < cยทg "grows strictly slower than"
Little omega ฯ‰(g) Strictly faster โˆ€c > 0 โˆƒnโ‚€ โˆ€nโ‰ฅnโ‚€: f > cยทg "grows strictly faster than"

The flip from "big" to "little" is the flip from existential to universal on the constant: big-O finds one c that bounds f above; little-o requires that every positive c eventually bounds f above. That's a much stronger statement: f can't just be no-bigger-than g by some constant, it has to grow strictly slower.

eq. 2 ยท concrete examples 3nยฒ + 5n = O(nยฒ), ฮ˜(nยฒ), ฮฉ(n), o(nยณ), ฯ‰(n)

The same function fits five bounds. ฮ˜(nยฒ) is the tightest description (matching upper and lower); the others are looser. "Merge sort is ฮ˜(n log n)" says more than "merge sort is O(n log n)": the ฮ˜ statement also rules out merge sort running faster than n log n, which the O statement leaves open.

What about little-o?

Little-o is what you reach for when you want to claim that one function is fundamentally faster-growing than another, not just that one happens to be bigger. The statement 1/n = o(1) says 1/n shrinks to zero, which is the asymptotic version of "1/n vanishes." The statement n log n = o(nยฒ) says no constant multiple of n log n can keep up with nยฒ: the gap between them grows without bound.

Knuth's 1976 paper introduced ฮ˜ so that authors could state a tight bound instead of stretching O to cover it[10]. The looser habit survives anyway: "an O(n log n) sort" almost always means "a ฮ˜(n log n) sort" in practice. Read O() that way unless the author explicitly distinguishes the two.

A practical rule of thumb

Algorithm analysis usually wants ฮ˜ (matching upper and lower bounds) for the algorithm you wrote, and ฮฉ for the problem you're solving (the best possible algorithm). "Merge sort is O(n log n)" and "comparison-based sorting is ฮฉ(n log n)" combine to say "merge sort is worst-case optimal among comparison sorts, up to a constant factor."

05The growth zoo, racing

A handful of complexity classes cover most of the algorithms an engine programmer meets. Knowing their shapes lets you read most papers without redoing the algebra:

Class Name Where you'll see it At n=106 (1 ns/op)
O(1)ConstantHash table lookup (average), array index, push to a stack1 ns
O(log n)LogarithmicBinary search, balanced BST operation, typical BVH ray query20 ns
O(โˆšn)Square-rootTrial-division primality test, sqrt decomposition1 ยตs
O(n)LinearLinear search, single pass over an array, building a histogram1 ms
O(n log n)LinearithmicComparison-based sort, FFT, sweep-and-prune broad phase20 ms
O(nยฒ)QuadraticNested-loop pair test, bubble sort, naive matrix-vector product17 min
O(nยณ)CubicNaive matrix multiply, Floyd-Warshall all-pairs shortest paths32 years
O(2n)ExponentialBrute-force SAT over n variables, subset enumerationโ‰ซ age of universe
O(n!)FactorialBrute-force TSP, permutation enumerationโ‰ซโ‰ซ age of universe

The "at n=106" column makes the spacing concrete: six orders of magnitude separate O(n) from O(nยฒ), and another six separate O(nยฒ) from O(nยณ). At a million elements that is the difference between a millisecond, a quarter of an hour, and three decades.

The widget below races them on a log-log plot. On log-log axes a polynomial nk is a straight line with slope k, which is why log-log is the standard way to compare growth rates by eye:

Live ยท Common growth rates on log-log axes
O(1) ยทยทยท O(log n) ยทยทยท O(n) ยทยทยท O(n log n) ยทยทยท O(nยฒ) ยทยทยท O(nยณ) ยทยทยท O(2โฟ) ยทยทยท
The n slider moves in powers of ten to the tenth (1, 1.26, 1.58, โ€ฆ) so each decade gets equal travel; the key below the chart gives each class's total time at that n, marked on the chart by the vertical line. The "per-operation time" slider sets how many nanoseconds one elementary step costs; the dashed line marks how many operations fit in a 16.6 ms frame at that cost, so a curve crossing it shows the largest n that fits in a 60 fps frame. 1 ns per operation is a middle-of-the-road figure: a modern core retires several simple instructions per cycle at 3 to 5 GHz, but a cache miss that goes to DRAM costs on the order of 100 ns.

06When constants lie

Big O drops constants on purpose: a notation that cared about constants couldn't compare implementations across hardware, compilers, or decades. The price is that two algorithms with the same Big O can differ by orders of magnitude in real-world cost, and an algorithm with worse Big O can beat one with better Big O on inputs you'll actually see.

The textbook example is 100n versus nยฒ / 100. The first is O(n); the second is O(nยฒ). By any asymptotic measure, the linear algorithm wins. Solve for the crossover:

eq. 3 ยท crossover point 100n = n2 / 100 โ‡’ n = 10,000

Below n = 10,000 the "worse" algorithm is genuinely faster. With these constants, a system that never exceeds 10,000 elements is better off with the quadratic implementation.

Live ยท Where does the "worse" algorithm cross over?
crossover n
ยทยทยท
linear wins if
ยทยทยท
Both curves are plotted on linear axes so the crossover is visible as the intersection, at n = a ยท b. The linear curve always wins eventually; whether it wins on the inputs you ship is the question.

What constants stand for in practice

An algorithm's hidden constant absorbs all the per-operation work the abstraction throws away. Three common sources of large constants:

The external-memory model[31] and the cache-oblivious model that builds on it[13] formalize the first of these: they count block transfers between cache and memory instead of CPU operations. For data far larger than cache, the transfer count is often the bound that matters and the operation count is the secondary term. Big O over operations stays correct; a serious analysis often needs a second count next to it.

Constants live forever

An algorithm with hidden constant 106 is in the same Big O class as one with hidden constant 1, and a million times slower at every n. The Coppersmith-Winograd matrix-multiply algorithm hides a constant so large that it would only win on matrices far too big for any computer; no shipping linear-algebra library uses it[14]. Big O says nothing about the size of the constant.

07Worst, best, average, amortized

An algorithm's runtime is a function of its input. Different inputs produce different runtimes. Big O over an algorithm has to specify which inputs you mean:

Quicksort: the canonical worst-versus-average gap

Quicksort partitions the array around a pivot and recurses on the two sides; the partition leaves both sides in place, so there is no combine step. A pivot at the median splits the array in half, giving the recurrence T(n) = 2T(n/2) + ฮ˜(n), which the master theorem (ยง8) solves to ฮ˜(n log n). A random pivot is rarely the exact median, but the expected cost still works out to ฮ˜(n log n), about 1.39 n logโ‚‚ n comparisons. On an adversarial pivot (always-smallest or always-largest), it produces partitions of sizes 0 and nโˆ’1, giving T(n) = T(nโˆ’1) + ฮ˜(n), which sums to ฮ˜(nยฒ).

A sorted-or-reverse-sorted input plus a naive first-element pivot triggers the worst case on every recursion. The widget runs a first-element-pivot quicksort on three input patterns:

Live ยท Quicksort comparisons under three input patterns
comparisons at n
ยทยทยท
vs n log n
ยทยทยท
The "sorted" and "reverse" curves are the quadratic worst case; they grow as nยฒ/2. Random input gives the expected n log n curve. The major C++ standard libraries avoid the quadratic case with Musser's introsort design[12]: they switch to heapsort once the recursion depth passes a limit (2 logโ‚‚ n in libstdc++ and libc++, about 1.5 logโ‚‚ n in MSVC), which guarantees O(n log n) in the worst case.

Amortized: std::vector::push_back

A dynamic array doubles its capacity when full. Most pushes are O(1) (write to the next slot). The pushes that trigger a resize are O(n) (copy every element to a bigger buffer). Worst-case per push is O(n), which badly overstates what a long sequence of pushes costs. Amortized per push, summed over a sequence and divided by length, is O(1).

The proof is a direct sum (the "aggregate method" in the amortized-analysis framework Tarjan formalized in 1985[17]): n pushes do n single-slot writes plus resize-copies of sizes 1, 2, 4, ..., 2โŒˆlogโ‚‚ nโŒ‰โˆ’1, each smaller than n. That sum is less than 2n. Total work is less than 3n; per-push amortized cost is at most 3 = O(1).

Live ยท The cost of each push, with running average
peak push cost
ยทยทยท
amortized average
ยทยทยท
The growth factor controls the resize-cost-versus-memory-overhead tradeoff. The C++ standard leaves it to the implementation: libstdc++ and libc++ double, which can leave up to half the buffer unused; MSVC's std::vector and Facebook's folly::fbvector grow by 1.5ร—, which wastes less but resizes more often. All of them have amortized O(1) push; the constant differs. The dashed line is the bound 1 + g/(gโˆ’1) for growth factor g.

08The Master Theorem

Most divide-and-conquer algorithms produce a recurrence of the form:

eq. 4 ยท the standard form T(n) = a ยท T(n / b ) + f(n)

"To solve a problem of size n, do a recursive subsolves each of size n/b, plus f(n) of additional work to combine them." Merge sort is a = 2, b = 2, f(n) = ฮ˜(n). Top-down binary BVH construction with binned SAH splits has the same shape when the splits come out balanced.

The Master Theorem[11] classifies these recurrences in three cases, depending on whether the recursive work or the combine work dominates. Let ccrit = logb a:

Worked examples

Six standard divide-and-conquer recurrences and what the theorem says about them:

Algorithm Recurrence ccrit Case Result
Binary searchT(n) = T(n/2) + ฮ˜(1)0Case 2 (c=0)ฮ˜(log n)
Merge sortT(n) = 2T(n/2) + ฮ˜(n)1Case 2ฮ˜(n log n)
Karatsuba multiplicationT(n) = 3T(n/2) + ฮ˜(n)logโ‚‚3 โ‰ˆ 1.585Case 1ฮ˜(nlogโ‚‚3) โ‰ˆ ฮ˜(n1.585)
Strassen matrix multiplyT(n) = 7T(n/2) + ฮ˜(nยฒ)logโ‚‚7 โ‰ˆ 2.807Case 1ฮ˜(nlogโ‚‚7) โ‰ˆ ฮ˜(n2.807)
Naive matrix multiply (recursive)T(n) = 8T(n/2) + ฮ˜(nยฒ)3Case 1ฮ˜(nยณ)
BVH top-down (binned SAH, balanced splits)T(n) = 2T(n/2) + ฮ˜(n)1Case 2ฮ˜(n log n)

The lever is the ratio between how many subproblems you make and how fast they shrink. Strassen's algorithm beats the naive cubic matrix multiply by doing 7 half-size multiplications instead of 8, paid for with extra matrix additions that are still ฮ˜(nยฒ) per level; that one fewer recursive call drops the exponent from 3 to logโ‚‚ 7. Later results (Coppersmith-Winograd 1990 and the refinements since, down to ฯ‰ < 2.371339[18]) lower the exponent with heavier algebraic machinery rather than a small recursion like Strassen's. Of these, only Strassen sees practical use; the rest are galactic (ยง14).

The cases that aren't in the Master Theorem

The classical Master Theorem doesn't cover every recurrence. Three gaps:

  • Logs in f(n). If f(n) = ฮ˜(nccrit logk n), the three cases above don't apply. An extended case 2 gives ฮ˜(nccrit logk+1 n), and the Akra-Bazzi method covers it too.
  • Uneven splits. T(n) = T(n/5) + T(7n/10) + O(n) (median-of-medians selection) requires Akra-Bazzi or direct analysis; it solves to ฮ˜(n).
  • Subtractive recurrences. T(n) = T(n-1) + O(n) (quicksort worst case, naive recursion) isn't divide-and-conquer; it's "shrink by 1." The answer is the sum ฮ˜(nยฒ), found by direct summation.

For the recurrences this tutorial cares about, the three-case Master Theorem above is sufficient. The Akra-Bazzi version is in CLRS chapter 4[11].

09Measuring growth empirically

Sometimes the algorithm is too complex to analyze by hand and you have to measure. The classical trick: run it at sizes n, 2n, 4n, 8n, plot on log-log axes, and read the slope.

On log-log axes, T(n) = c ยท nk becomes log T = log c + k ยท log n, which is a straight line with slope k and intercept log c. Eyeballing the slope is a coarse diagnostic: a slope of 1 means O(n), a slope of 2 means O(nยฒ), anything between 1 and 2 is suspicious (could be O(n log n), could be O(n1.5), hard to tell from slope alone).

A more discriminating test: the doubling ratio. Measure T(n) and T(2n); the ratio T(2n) / T(n) diagnoses the class:

Doubling ratioDiagnosisSlope on log-log
~1O(1)0
~1 + 1/log nO(log n)tends to 0
~2O(n)1
~2.1โ€“2.3O(n log n)slightly above 1
~4O(nยฒ)2
~8O(nยณ)3

The widget below shows a simulated running-time curve and lets you guess the complexity. The points carry ยฑ9% random noise, standing in for the cache effects, GC pauses, and scheduler hiccups of real measurements, so the diagnosis isn't always exact.

Live ยท Guess the complexity from measured data
slope (log-log fit)
ยทยทยท
doubling ratio
ยทยทยท
verdict
ยทยทยท
Click "new mystery curve" to randomize the underlying algorithm; pick a complexity class to check. The slope is the least-squares fit of log T vs log n; the doubling ratio is computed from the two largest measured points (for the exponential case the sizes step by 1, so the ratio is per +1). Real profiling data is noisier than this. A slope fit separates n from nยฒ easily but struggles to separate n from n log n over a narrow range of sizes.

10Case study: broad-phase collision

Broad-phase collision is the classic engine-side O(nยฒ) trap. Physics simulations need to find every pair of objects whose bounding volumes overlap. A naive double-loop checks n(nโˆ’1)/2 pairs; at n = 1000 that's 500,000 pairs every frame, or 30 million pair tests per second at 60 fps. At a few nanoseconds per AABB test that is one to a few milliseconds per frame on one core, a large share of a physics budget that is often only a few milliseconds in total, and it quadruples every time n doubles.

Three standard fixes, with different complexity in different regimes:

Sweep-and-prune (sort and sweep)

Sort the bounding-box endpoints along one axis (say X). Two boxes can only overlap on the X axis if neither's max-X is before the other's min-X. Sweep through the sorted endpoints with an active list; when you hit a min-X, add the box to the active list; when you hit a max-X, remove it. Every pair of boxes simultaneously active is a candidate. Repeat on Y and Z; take the intersection.

The first frame's cost is dominated by the initial sort at O(n log n); the sweep itself is O(n + k) where k is the number of active-pair encounters. In subsequent frames the endpoint lists are nearly sorted (objects move a little, not a lot, per step), and insertion sort on a nearly-sorted list runs in O(n + s) where s is the number of out-of-order pair swaps. In steady state that puts per-frame cost at O(n + k + s), which is close to linear when both k and s are O(n). The pathological case (every box overlaps every other) blows k up to O(nยฒ) and you're back to the naive cost. Cohen et al.'s 1995 I-COLLIDE paper is the standard reference for sweep-and-prune with this temporal-coherence trick[4].

Uniform spatial hash

Partition space into a regular grid. Hash each object's AABB into the cells it touches. Two objects can only collide if they share a cell. Build cost is O(n); query cost is the sum over cells of the per-cell pair-test count.

If average density is d objects per cell and the cell size is at least as large as the biggest object's AABB (so each object touches O(1) cells), the per-cell pair-test count averages dยฒ/2, and total cost is O(n ยท d), which collapses to O(n) when d is bounded. The catch: d is bounded only when objects are spread out. A pile of crates on the same tile reduces to brute force inside that tile. GPU Gems 3 builds the same kind of grid on the GPU: it radix-sorts (cell, object) pairs so each cell's occupants end up contiguous, then tests them in passes that keep neighboring cells from double-counting a pair[5].

BVH for moving objects

Build a bounding volume hierarchy over the objects' AABBs and query each object against the tree. Construction is O(n log n); a query costs roughly O(log n) plus the pairs it reports in well-spread scenes, so a full pass is roughly O(n log n + k). Real engines update the tree incrementally as objects move instead of rebuilding it: Bullet's btDbvtBroadphase and Box2D's b2DynamicTree are dynamic AABB trees of this kind. PhysX 5 takes a different route, shipping sweep-and-prune plus a family of box-pruning broad phases, with automatic box pruning (eABP) recommended as the default[30].

The widget runs all three on the same simulated scene. At low object counts the three work counts are close, and brute force's tiny per-test constant can make it the fastest in wall-clock terms; at high counts it loses by a wide margin:

Live ยท Three broad-phase algorithms on the same scene
brute work/frame
ยทยทยท
SAP work/frame
ยทยทยท
hash work/frame
ยทยทยท
Each counter is one frame of broad-phase work. Brute force counts every pair test. Sweep-and-prune counts the pairs whose x-intervals overlap during the sweep, plus an n logโ‚‚ n charge for sorting the endpoints. The spatial hash counts one insertion per cell an object touches plus the pair tests inside each cell. Small cells make each circle register in up to four cells, so insertions climb; large cells pack more circles into each cell and push the hash toward brute force inside the cell. In this scene the total bottoms out with cells two to three circle diameters wide, near the left end of the slider.

11Case study: BVH ray traversal

Ray tracing a scene of n triangles by brute force means testing the ray against every triangle: O(n) per ray, O(n ยท pixels) per image. At n = 107 triangles and a 1080p image that's 2ร—1013 ray-triangle tests per frame, which is several orders of magnitude past tractable.

A typically brings this down to roughly logarithmic cost per ray. Triangles are organized into a binary tree where each node's AABB encloses every triangle in its subtree. To intersect a ray with the scene, test the ray against the root's AABB; if it misses, the whole subtree is irrelevant. If it hits, visit the children, nearer one first. At a leaf, test the ray against the small bucket of triangles inside. For the closest hit, which is what a renderer usually wants, any node whose box starts farther along the ray than the best hit found so far can be skipped.

The logarithmic behavior is empirical rather than guaranteed. A tree built with the surface-area heuristic (the build Physically Based Rendering walks through[1]) over reasonably distributed geometry visits a number of nodes per ray that grows roughly with log n. The worst case is still O(n): a ray grazing many overlapping boxes, or geometry such as long thin triangles whose boxes all overlap, can force a visit to most of the tree.

Live ยท Brute force vs BVH, traced live
brute tests
ยทยทยท
BVH tests
ยทยทยท
speedup
ยทยทยท
Both methods look for each ray's closest hit. The BVH is built with a median split on the longer axis at each level, with up to four objects per leaf. Brute force has to test all n objects to be sure which hit is closest; the BVH tests the two child boxes of every node it enters, visits the nearer child first, and skips any box that starts beyond the best hit found so far. Going from 16 to 400 objects, the average BVH cost here rises from about 9 tests to about 19 while brute force rises 25-fold, so the speedup grows with n (from under 2ร— to about 20ร—). Rays that pass close to many boxes before their first hit cost the most. NVIDIA's RT cores (Turing and later) run this traversal and the ray-triangle tests in fixed-function hardware; that shrinks the constant, not the growth rate[19].

12Case study: A* and the branching factor

A* searches a graph for the shortest path from start to goal. At each step it picks the frontier node with the lowest estimated total cost f(n) = g(n) + h(n), where g is the known cost so far and h is a heuristic estimate to the goal. With an admissible heuristic, A* returns an optimal path[20]; with no heuristic, it degenerates to Dijkstra; with a perfect heuristic (and ties broken toward the goal), it expands only nodes on an optimal path.

The textbook worst case for A* as a tree search is O(bd), where b is the branching factor (how many neighbors a node has) and d is the depth of the solution. With a closed set that never expands a node twice, the search is also bounded by the size of the graph: O(E log V) with a binary-heap open list. On a 2D grid b = 4 (von Neumann) or b = 8 (Moore neighborhood); on a navmesh b is the number of neighbors per polygon, 3 for a triangle mesh and up to 6 for Recast's default convex polygons.

The exponential isn't in n (the size of the graph); it's in d (how far the goal is). On a 100ร—100 grid with the goal 50 steps away, a tree search that didn't track visited nodes would expand on the order of 450 nodes; with a visited set, BFS expands at most the grid's n = 104 cells (an unobstructed diamond of radius 50 holds about 5,100). A* with a good heuristic on an open map expands a few nodes per step of path, a few hundred in total: the visited region is a corridor along the optimal path, not a diamond.

The heuristic's quality is the lever. A perfect heuristic (the true remaining distance) keeps the search on an optimal path. An admissible but loose heuristic (Manhattan distance on a grid with obstacles) makes A* explore a fan along the path. An inadmissible heuristic (one that can overestimate) usually makes A* faster, but the result is no longer guaranteed optimal:

Live ยท A* frontier under different heuristics
nodes expanded
ยทยทยท
path length
ยทยทยท
optimal?
ยทยทยท
The grid is 4-connected with unit step costs. Dijkstra is A* with h = 0; it expands every reachable cell closer than the goal, which on this grid is a diamond clipped by obstacles. The Manhattan heuristic is admissible here; A* explores a wedge biased toward the goal. Weighted A* multiplies h by 2.5 to push harder toward the goal; it usually expands fewer cells, but the path is no longer guaranteed optimal. The shape change is the improvement: Dijkstra's cost grows with the area it floods, while a heuristic that tracks the true distance closely keeps the expanded set near the path itself. The usual measure is the effective branching factor b*: the search expands roughly (b*)d nodes, and a better heuristic pushes b* toward 1.

13Case study: sorting draw calls

A renderer can issue thousands of draw calls per frame. Reordering them to group calls that share a render target, shader, or material cuts the number of state changes, which cost CPU time in the driver and can stall the GPU[2].

A common pattern: every draw call gets a 32- or 64-bit sort key packing render target, shader, material, depth, and so on into bit fields in priority order. Sort the array of keys. Issue the calls in the sorted order. The sort's cost matters because the renderer runs it every frame:

Why isn't every sort a radix sort? Two reasons. First, comparison sorts work on any type with an ordering; radix sorts need keys that break into digits (integers, floats after a sign-bit flip, strings). Second, a naive radix sort scatters writes across many buckets and misses cache, so the constant is larger than the textbook complexity suggests; the implementations that beat std::sort handle that explicitly. A 2022 Bevy engine issue measured radix against comparison sorting for its render phases, with a large win on one benchmark and regressions on two others[21]; Christer Ericson's blog post lays out the widely used bit-packing pattern[2].

Radix sort being O(n) (treating d as a constant for fixed-width keys) sounds like it violates the ฮฉ(n log n) lower bound for sorting. It doesn't: the lower bound's proof counts the leaves of a binary tree of comparison outcomes[11], so it only covers sorts that learn about the keys through comparisons. Radix sort reads the digits directly.

draw_key.hpp ยท packing a sort key
#include <cstdint>  // uint64_t

// A 64-bit draw-call sort key. Field order is priority order: the most
// significant bits drive the coarsest grouping, so a plain integer
// compare on packed keys yields the draw order.
// Packed with shifts rather than C++ bit-fields: bit-field placement is
// implementation-defined (MSVC and GCC on x86 put the first field in the low
// bits), and a struct of bit-fields has no ordering to sort by anyway.
constexpr int kPassBits = 2, kShaderBits = 10, kMaterialBits = 14, kMeshBits = 14, kDepthBits = 24;
static_assert(kPassBits + kShaderBits + kMaterialBits + kMeshBits + kDepthBits == 64, "fields must fill the key");

constexpr uint64_t fieldMask(int bits) { return (uint64_t{1} << bits) - 1; }

constexpr uint64_t packDrawKey(uint64_t renderPass,     // 0=shadow, 1=opaque, 2=transparent, 3=postfx
                               uint64_t shaderProgram,  // up to 1024 programs
                               uint64_t materialId,     // up to 16K materials
                               uint64_t meshId,         // up to 16K meshes
                               uint64_t depth)          // 24-bit quantized view depth: store it as-is for
                                                        // front-to-back, or fieldMask(24) - depth for back-to-front
{
  // Mask every field so an out-of-range id can't bleed into its neighbor.
  return ((renderPass    & fieldMask(kPassBits))     << (64 - kPassBits))
       | ((shaderProgram & fieldMask(kShaderBits))   << (kMaterialBits + kMeshBits + kDepthBits))
       | ((materialId    & fieldMask(kMaterialBits)) << (kMeshBits + kDepthBits))
       | ((meshId        & fieldMask(kMeshBits))     << kDepthBits)
       |  (depth         & fieldMask(kDepthBits));
}

// Sorting a std::vector<uint64_t> of these keys is one std::sort, or eight
// 8-bit LSD radix passes (fewer if the high bytes never vary).

The layout decides the draw order. With the render pass in the high bits, every draw in the same pass is contiguous after sorting; with depth in the low bits, draws that share a shader, material, and mesh come out front to back. Transparent draws are the exception: blending needs back-to-front order to outrank shader and material, so renderers either move the depth field above them for that pass (Ericson's approach[2]) or sort the transparent pass with its own key. Either way, the sort routine only ever orders 64-bit integers.

14Galactic algorithms

An algorithm is galactic if its asymptotic behavior is excellent but its hidden constants are so large that it loses to an asymptotically worse algorithm on every input anyone would actually run, so it is never used to compute anything[14]. The name comes from a 2010 post on Richard Lipton and Ken Regan's blog, which credits it to Regan.

The flagship example is matrix multiplication. The naive triple-loop algorithm is O(nยณ). Improvements to the exponent have kept coming since 1969 (a selection):

YearAuthor(s)Exponent ฯ‰Practical?
1969Strassenlogโ‚‚7 โ‰ˆ 2.807Sometimes, for large matrices
1978Pan2.795No; constants too large
1981Schรถnhage2.522No
1990Coppersmith-Winograd2.376No; canonical galactic algorithm
2014Le Gall2.3728639No
2024Vassilevska Williams, Y. Xu, Z. Xu, Zhou[18]2.371552No
2025Alman, Duan, Vassilevska Williams, Y. Xu, Z. Xu, Zhou[18]2.371339No

No entry past Strassen is used in practice. The Coppersmith-Winograd family would only overtake a cache-blocked cubic multiply on matrices far larger than any machine can hold, so the matrix products in graphics, audio, physics, and scientific libraries are O(nยณ) (occasionally Strassen at the top levels of very large products). Game engines mostly multiply 4ร—4 matrices, where none of this applies.

Galactic algorithms are still worth studying, for two reasons. First, they narrow the gap between what is achievable and the best general lower bound, which is still essentially the trivial ฮฉ(nยฒ) (every entry of the output has to be written). Second, techniques sometimes transfer down: Strassen's algorithm is practical, and it began the chain. For engineering purposes, "asymptotically faster" does not mean "faster on the inputs you have."

15NP-hard games

Big O can describe problems for which no polynomial-time algorithm is known, and which are believed to require exponential time. The class NP is the set of decision problems whose "yes" answers can be checked in polynomial time given a witness. The class P is the subset whose answers can also be computed in polynomial time. The P vs NP question is whether these are the same class.

A problem is NP-hard if every problem in NP reduces to it in polynomial time; NP-complete if it's both NP-hard and in NP. The Cook-Levin theorem (Cook 1971, Levin 1973 independently) showed that SAT is NP-complete, the first such proof[22]; thousands of problems have since been shown NP-complete by chains of polynomial reductions that start from SAT.

Several games are known to be NP-hard in their generalized forms, and some are harder still:

GameProblem statementComplexityReference
Generalized TetrisMaximize survival on a known sequence of piecesNP-complete[3]
SokobanPush boxes onto target tilesPSPACE-complete[23]
Super Mario BrosReach the end of a generalized levelNP-hard[24]
Minesweeper consistencyIs a given board consistent?NP-complete[25]
Generalized SudokuSolve an nยฒ ร— nยฒ SudokuNP-complete[26]

These results apply to generalized versions (Tetris on an n ร— m board, Sudoku on nยฒ ร— nยฒ). The fixed-size game you play in your browser is solvable by brute force in principle; the asymptotic claim is about the family. The practical takeaway is that exact solvers for these problems can't be expected to scale, so game AI for them leans on heuristic search with aggressive pruning and on learned policies.

Procedural generation hits the same wall from the other side. Deciding whether an arbitrary Sokoban level is solvable is PSPACE-complete[23]. Generators ship anyway: some build levels backward from a solved state by pulling boxes, so solvability holds by construction; others restrict the rules to a tractable subset, or run a solver with a bounded search budget and discard levels that don't certify within it.

16Beyond polynomial

Past exponential are growth rates so vast they have no operational meaning. The number of atoms in the observable universe is roughly 1080; 2n crosses that threshold at n = 266, and n! crosses it at n = 59. The grows faster than any primitive-recursive function; already at A(4, 3) it produces a number far too large to write out in decimal[27].

These aren't useful complexities for algorithms ("the algorithm runs in A(n, n) time" is functionally indistinguishable from "the algorithm doesn't terminate"), but they show up as inverse functions in tight upper bounds. The classic example: union-find with union-by-rank and path compression has an amortized cost per operation of O(ฮฑ(n)), where ฮฑ is the inverse Ackermann function; Tarjan proved the bound in 1975[32]. For any n that fits in any computer ever built, ฮฑ(n) โ‰ค 4, so in practice the operation is constant-time. Technically it is not constant: ฮฑ grows without bound, just extraordinarily slowly.

Live ยท How big is big? Magnitude at small n
n!
ยทยทยท
2โฟ
ยทยทยท
nยณ
ยทยทยท
All values are shown as their log10: a bar at 80 represents a number with 81 digits. The "atoms in universe" reference line (logโ‚โ‚€ โ‰ˆ 80) and the "nanoseconds since big bang" line (logโ‚โ‚€ โ‰ˆ 26.6) make the difference between "expensive" and "not finishable" visually concrete. n! crosses atoms-in-universe at n = 59 (59! โ‰ˆ 1.4 ร— 1080); 2n doesn't until n = 266 (2266 โ‰ˆ 1.2 ร— 1080), past the slider's range. Both set the limit of brute-force search.

17Try it yourself

The playground lets you pick one of six common data structures and one of four operations, then highlights the elements a single run of that operation touches next to its asymptotic cost.

Live ยท Data structures versus operations
cost (this run)
ยทยทยท
asymptotic
ยทยทยท
"Cost" is the number of structural visits (comparisons, pointer follows, hash probes) the operation performed. The asymptotic line shows the Big O for this (structure, operation) combo. Note that the playground's hash table assumes a good hash; degenerate hashes degrade to O(n) worst case.

18How engines actually use this

Asymptotic complexity drives many of the core data-structure choices in shipped engines, with constant-factor refinements layered on top. Concrete examples:

The shared pattern: pick the asymptotic class first, then tune for cache and parallelism. Constant-factor work on an accidental O(nยฒ) only moves the content size at which it breaks.

19Pitfalls

Mistakes that come up repeatedly in code review:

20What's next

The natural follow-ups are the algorithms whose complexity this tutorial only sketched:

The most useful next step is empirical: profile at shipping content scale, look for costs that grow faster than the content does, and replace the ones that will actually see large n.

Sources

  1. Matt Pharr, Wenzel Jakob, Greg Humphreys. Physically Based Rendering: From Theory to Implementation, 3rd ed., 2018, ยง4.3 "Bounding Volume Hierarchies." pbr-book.org. BVH construction with the surface-area heuristic, and BVH traversal.
  2. Christer Ericson. "Order your graphics draw calls around!" realtimecollisiondetection.net, October 2008. realtimecollisiondetection.net. The widely cited write-up of bit-packed draw-call sort keys, including swapping the depth and material fields for translucent draws.
  3. Erik D. Demaine, Susan Hohenberger, David Liben-Nowell. "Tetris is Hard, Even to Approximate." COCOON 2003. arXiv:cs/0210020. Original NP-completeness proof for generalized Tetris.
  4. Jonathan D. Cohen, Ming C. Lin, Dinesh Manocha, Madhav K. Ponamgi. "I-COLLIDE: An Interactive and Exact Collision Detection System for Large-Scale Environments." Proc. ACM Interactive 3D Graphics, 1995. The standard reference for sweep-and-prune (sort-and-sweep) with incremental re-sorting that exploits frame-to-frame coherence.
  5. Scott Le Grand. "Broad-Phase Collision Detection with CUDA." GPU Gems 3, ch. 32, NVIDIA, 2007. developer.nvidia.com. A uniform-grid broad phase on the GPU: cells at least as large as the largest object, a parallel radix sort of cell IDs, and multi-pass processing of cell types so no pair is tested twice.
  6. Erik R. Tou. "Math Origins: Orders of Growth." MAA Convergence, January 2018. archived copy (the old.maa.org site is offline). Historical survey covering du Bois-Reymond's 1877 infinitary calculus, Bachmann 1894, Landau 1909, and Hardy 1910.
  7. Paul Bachmann. Die analytische Zahlentheorie, vol. II, Teubner, 1894. archive.org scan. First published use of the O symbol, in the chapter on divisor functions; "O" is usually read as "Ordnung."
  8. Edmund Landau. Handbuch der Lehre von der Verteilung der Primzahlen, Teubner, 1909. archive.org scan. Introduces the little-o notation; the source of "Landau symbols."
  9. G. H. Hardy, J. E. Littlewood. "Some Problems of Diophantine Approximation: Part II. The Trigonometrical Series Associated with the Elliptic ฮธ-Functions." Acta Mathematica 37, 1914. Introduces the original ฮฉ notation as a non-vanishing-remainder marker; predates Knuth's stricter algorithmic ฮฉ.
  10. Donald E. Knuth. "Big Omicron and Big Omega and Big Theta." ACM SIGACT News 8(2):18โ€“24, 1976. dl.acm.org. The standardization paper for algorithm analysis: defines Big O, Big ฮฉ, Big ฮ˜ as used today.
  11. Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein. Introduction to Algorithms, 4th ed., MIT Press, 2022, ch. 3โ€“4 and ยง8.1. The textbook reference for asymptotic notation, the master theorem (with Akra-Bazzi in ยง4.7), and the comparison-sort lower bound.
  12. David R. Musser. "Introspective Sorting and Selection Algorithms." Software: Practice and Experience 27(8):983โ€“993, 1997. PDF. The introsort design (quicksort with a depth-limited heapsort fallback and a final insertion-sort pass) that libstdc++, libc++, and MSVC's std::sort build on.
  13. Matteo Frigo, Charles E. Leiserson, Harald Prokop, Sridhar Ramachandran. "Cache-Oblivious Algorithms." Proc. 40th IEEE FOCS, 1999. PDF. The original cache-oblivious paper: the ideal-cache model and algorithms (including funnelsort) that meet the optimal transfer bounds without knowing the cache parameters.
  14. Richard J. Lipton, Kenneth W. Regan. "Galactic Algorithms." Gรถdel's Lost Letter and P=NP, 23 October 2010. rjlipton.com. The post that introduced the name, credited to Regan, for algorithms that are wonderful asymptotically but never used to compute anything. Discusses Coppersmith-Winograd matrix multiplication.
  15. Peter Norvig. "Teach Yourself Programming in Ten Years," "Approximate timing for various operations" table. norvig.com/21-days.html. Memory-hierarchy latency ballpark numbers (L1 ~0.5 ns, L2 ~7 ns, DRAM ~100 ns).
  16. Agner Fog. "The microarchitecture of Intel, AMD and VIA CPUs," chapter on branch prediction. PDF. Source for the 15โ€“20 cycle branch-misprediction penalty on modern x86 cores.
  17. Robert E. Tarjan. "Amortized Computational Complexity." SIAM Journal on Algebraic Discrete Methods 6(2):306โ€“318, 1985. Formalizes amortized analysis (the banker's and physicist's views).
  18. Virginia Vassilevska Williams, Yinzhan Xu, Zixuan Xu, Renfei Zhou. "New Bounds for Matrix Multiplication: from Alpha to Omega." Proc. SODA 2024 (arXiv:2307.07970); result ฯ‰ < 2.371552. Subsequently improved by Josh Alman, Ran Duan, and the same group in "More Asymmetry Yields Faster Matrix Multiplication," Proc. SODA 2025 (arXiv:2404.16349), to ฯ‰ < 2.371339. Currently the best published exponents. arXiv 2307.07970, arXiv 2404.16349.
  19. NVIDIA. "NVIDIA Turing GPU Architecture Whitepaper," 2018, "RT Core" section. PDF. Hardware-accelerated BVH traversal in fixed-function silicon.
  20. Peter E. Hart, Nils J. Nilsson, Bertram Raphael. "A Formal Basis for the Heuristic Determination of Minimum Cost Paths." IEEE Transactions on Systems Science and Cybernetics 4(2):100โ€“107, 1968. Original A* paper; the optimality result under admissible heuristics.
  21. Bevy Engine. "Use radix sort for sort phase and sprite sorting." GitHub issue #4291, 2022. github.com. Benchmarks of the radsort crate against Rust's comparison sorts for Bevy's render-phase sorts: 0.728 ms to 0.094 ms on many_cubes, but roughly 10ร— slower on two sprite benchmarks. Bevy 0.8 shipped radsort for its 3D phase sorts; later releases moved opaque meshes to unsorted bins and sort transparent phases with Rust's stable comparison sort, and radsort remains in sprite picking.
  22. Stephen A. Cook. "The Complexity of Theorem-Proving Procedures." Proc. ACM STOC, 1971, pp. 151โ€“158. dl.acm.org. The original NP-completeness theorem; SAT is NP-complete. Leonid Levin proved an equivalent result independently in "Universal Sequential Search Problems," Problemy Peredachi Informatsii 9(3):115โ€“116, 1973 (English translation: Problems of Information Transmission 9:265โ€“266), hence the joint Cook-Levin name.
  23. Joseph Culberson. "Sokoban is PSPACE-complete." Technical Report TR97-02, University of Alberta, 1997; also in Proc. Fun with Algorithms, 1998. archived copy. PSPACE-completeness proof for generalized Sokoban.
  24. Greg Aloupis, Erik D. Demaine, Alan Guo, Giovanni Viglietta. "Classic Nintendo Games are (Computationally) Hard." Theoretical Computer Science 586:135โ€“160, 2015. NP-hardness proofs for generalized Super Mario Bros, Donkey Kong, Legend of Zelda, Metroid, and Pokรฉmon.
  25. Richard Kaye. "Minesweeper is NP-Complete." The Mathematical Intelligencer 22(2):9โ€“15, 2000. NP-completeness proof for the Minesweeper Consistency Problem.
  26. Takayuki Yato, Takahiro Seta. "Complexity and Completeness of Finding Another Solution and Its Application to Puzzles." IEICE Transactions E86-A(5):1052โ€“1060, 2003. NP-completeness proof for generalized nยฒร—nยฒ Sudoku.
  27. Wilhelm Ackermann. "Zum Hilbertschen Aufbau der reellen Zahlen." Mathematische Annalen 99:118โ€“133, 1928. Original Ackermann function (later simplified by Pรฉter and Robinson to the two-argument form used today).
  28. Epic Games. "TMap Reference," Unreal Engine 5 documentation. dev.epicgames.com. Documentation of Unreal's hash-map data structure and its design choices.
  29. Matthias Teschner, Bruno Heidelberger, Matthias Mรผller, Danat Pomeranets, Markus Gross. "Optimized Spatial Hashing for Collision Detection of Deformable Objects." Proc. Vision, Modeling, Visualization (VMV) 2003. PDF. Cell-size analysis for uniform spatial hashing applied to collision detection; the canonical reference on the tradeoff between query cost and per-cell occupancy.
  30. NVIDIA. "Rigid Body Collision," PhysX 5.4.0 SDK documentation, Broad-phase section. nvidia-omniverse.github.io. Names the available broad-phase algorithms (eSAP, eMBP, eABP, ePABP, eGPU) and calls eABP "a good default choice for the broadphase."
  31. Alok Aggarwal, Jeffrey Scott Vitter. "The Input/Output Complexity of Sorting and Related Problems." Communications of the ACM 31(9):1116โ€“1127, 1988. doi.org/10.1145/48529.48535. The external-memory model that counts block transfers, and the ฮ˜((N/B) logM/B(N/B)) transfer bound for sorting.
  32. Robert E. Tarjan. "Efficiency of a Good But Not Linear Set Union Algorithm." Journal of the ACM 22(2):215โ€“225, 1975. doi.org/10.1145/321879.321884. Proves the inverse-Ackermann bound for union-find with path compression and union by rank.

See also