Big-O and the real cost of JS built-ins
Count what the loop body does per item; a scan or a shift inside a loop multiplies, a Set lookup does not.
Predict first
Before you read: predict
ExamHall checks each submitted attempt ID against the list of registered IDs. None of the 2,000 submitted IDs is registered:
By the spec algorithm, how many elements of registered do the 2,000 includes calls read in total?
const registered = Array.from({ length: 2000 }, (_, i) => 'R-' + i);const submitted = Array.from({ length: 2000 }, (_, i) => 'S-' + i);let unknown = 0;for (const id of submitted) { if (!registered.includes(id)) unknown++;}export { registered, unknown };The 30-second version
Big-O says how work grows with input size. A built-in call is not one step: includes, indexOf and find read element by element; shift and splice move elements to close the gap. Inside a loop each is a hidden inner loop: n × m. Build a Set once and look up: the loop turns linear.
Count what the loop body does per item; a scan or a shift inside a loop multiplies, a Set lookup does not.
How it works
Step 1 of 10
Analogy: A snooker hall's front desk on a league night (cue sports)
On league night the desk checks members in, hands out cue lockers and keeps a waiting list for tables on a chalkboard.
| At the snooker hall | In the code |
|---|---|
| reading down the paper booking sheet from the top for each arriving member | includes in a loop (steps 2, 4) |
| a member not on the sheet costs the whole sheet; one near the top, a line or two | a miss reads everything (step 3) |
| labelling the cue lockers, one per member, by one walk along the membership list | building the Set once (step 8) |
| each member then goes straight to a numbered locker | one lookup per item (step 7) |
| when the first pair gets a table, every name below is rubbed out and written one line up | shift (step 5) |
| a pair leaving from the middle rewrites every name below them | splice (step 6) |
In real terms: cost is the number of elements a call touches; a scan touches up to all of them, a hash lookup touches a bounded number on average.
Breaks down when: the desk can glance down a short sheet; code reads one slot at a time. And two lockers never share a number, while two keys can land in one hash bucket: that is why the spec says "on average".
Contrasting case: a pub with one table and a two-name queue. Reading the list costs nothing worth fixing: Big-O matters as n grows.
Examples, run for real
4.1 ExamHall EXH-4210: 40,000 IDs checked with includes
ExamHall checked submitted attempt IDs against an array of registered IDs with includes in a loop. At exam size, 40,000 against 40,000, the check crawled (EXH-4210). Counted at three smaller sizes:
4.1 EXH-4210 includes, 1,250 IDs against 1,250: 1,562,500 element reads
4.1 EXH-4210 includes, 2,500 IDs against 2,500: 6,250,000 element reads
4.1 EXH-4210 includes, 5,000 IDs against 5,000: 25,000,000 element reads
4.1 each doubling multiplies the reads by 4; reads = size² at all three sizes, so 40,000 against 40,000 is 1,600,000,000 (arithmetic, not run)
4.1 MR !4510 new Set(registered) once, then has(): 160,000 counted operations in all at 40,000
Every submitted ID is a miss, so each call reads the whole array (step 3), and doubling both lists quadruples the work (step 4). MR !4510 builds the Set once (step 8) and looks each ID up (step 7): 160,000 counted operations in all, where includes needed 1.6 billion reads.
4.2 Ticketing: a waiting list drained with shift()
A box office serves its waiting list from the front:
while (waiting.length > 0) served.push(waiting.shift());
4.2 shift() drains 1,000 fans: 499,500 element writes on the waiting list
4.2 shift() drains 2,000 fans: 1,999,000 element writes on the waiting list
4.2 doubling the list multiplies the writes by 4.00
Each shift copies every remaining fan one place down (step 5), and doubling the list quadruples the writes. What to use instead is dsa-stack-queue's lane.
Try it
Answer first, then check. Nothing is saved.
Question 1 of 6
A support chat gives each new conversation to the agent at the front of its rota, then moves that agent to the back. The rota holds 500 agents, and 200 conversations arrive:
By the spec algorithm, how many element writes do the 200 shift calls and the 200 push calls make on rota in total?
const rota = Array.from({ length: 500 }, (_, i) => 'agent-' + i);const assigned = [];for (let c = 0; c < 200; c++) { const agent = rota.shift(); assigned.push(agent); rota.push(agent);}export { rota, assigned };Question 2 of 6
A gate scanner drops repeat scans of the same ticket. Someone added a Set, yet the gate still slows down as the queue grows:
The harness counts every array element read or written and every Set call, on 1,000 and then 2,000 distinct ticket IDs.
Which one change keeps the output and makes the count at most double when the IDs double?
export function dedupe(ids) { const out = []; const seen = new Set(); for (const id of ids) { if (!out.includes(id)) { seen.add(id); out.push(id); } } return out;}Question 3 of 6
A ledger matches each incoming payment to its open invoice. Today 160 payments arrive and 1,250 invoices are open; none of the payments matches an invoice:
By the spec algorithm, how many elements of invoices do the find calls read?
const invoices = Array.from({ length: 1250 }, (_, i) => ({ ref: 'INV-' + i }));const refs = Array.from({ length: 160 }, (_, i) => 'PAY-' + i);const matched = [];for (const ref of refs) { const inv = invoices.find((x) => x.ref === ref); if (inv) matched.push(inv);}export { invoices };Question 4 of 6
A video app removes the skipped tracks from a 1,000-track playlist, walking from the end; every even-numbered track was skipped:
By the spec algorithm, how many element writes do the 500 splice calls make on tracks?
const tracks = Array.from({ length: 1000 }, (_, i) => ({ id: i, skipped: i % 2 === 0 }));for (let i = tracks.length - 1; i >= 0; i--) { if (tracks[i].skipped) tracks.splice(i, 1);}export { tracks };Question 5 of 6
ExamHall's nightly job checks 10,000 late submissions against 25,000 registered IDs; none is registered. The harness counts every array element read and every Set call:
How many counted operations does the job make, from new Set to the end of the loop?
const registered = Array.from({ length: 25000 }, (_, i) => 'R-' + i);const late = Array.from({ length: 10000 }, (_, i) => 'L-' + i);const known = new Set(registered);let unknown = 0;for (const id of late) if (!known.has(id)) unknown++;export { unknown };Question 6 of 6
A chat app resolves the @mentions in a busy channel. There are 800 members, and the 320 mentions name members 1, 3, 5, … 639, each of whom is in the list:
By the spec algorithm, how many elements of members do the 320 indexOf calls read in total?
const members = Array.from({ length: 800 }, (_, i) => 'user' + i);const mentions = Array.from({ length: 320 }, (_, i) => 'user' + (i * 2 + 1));let resolved = 0;for (const name of mentions) if (members.indexOf(name) !== -1) resolved++;export { members, resolved };Go deeper
Pro tips
- Scan the loop body for
includes,indexOf,find,shiftorsplice. When: reviewing any loop over a list that can grow (step 9). Each is O(n) per call; a Set or Map built once usually removes it. Verify (RAN,tips.mjs): (The curriculum row's tip, [SELF].)tip 1 the same membership check at 3,000 IDs: includes 9,000,000 element reads · a Set built once 12,000 counted operations - Measure growth by counting, not by timing: run the check at n and 2n and compare the counts. When: you need evidence for a reviewer that a loop is quadratic (step 1). A ratio near 4 is quadratic, near 2 is linear. Verify (RAN,
tips.mjs):tip 2 includes check at 1,500 and 3,000: ×4.00 · Set check at 1,500 and 3,000: ×2.00
Source
Vendor pages change, so their claims are labelled as of 2026-10-14; every count this lesson grades is executed on Node 24. The curriculum row's first source, Cormen et al., is a book and keys nothing.
- MDN, Map (the curriculum row's second source), https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map (
mdn-map.txt): "it could be represented internally as a hash table (with O(1) lookup), a search tree (with O(log(N)) lookup), or any other data structure, as long as the complexity is better than O(N)." (step 7). As of 2026-10-14. - MDN, Set, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set (
mdn-set.txt): "The specification requires sets to be implemented "that, on average, provide access times that are sublinear on the number of elements in the collection"." (step 7). As of 2026-10-14. - MDN, Array.prototype.shift(), https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/shift (
mdn-array-shift.txt): "The shift() method shifts all values to the left by 1 and decrements the length by 1, resulting in the first element being removed." (step 5). As of 2026-10-14. - ECMAScript 2026, Keyed Collections, https://tc39.es/ecma262/multipage/keyed-collections.html (
ecma-keyed-collections.txt): "Set objects must be implemented using either hash tables or other mechanisms that, on average, provide access times that are sublinear on the number of elements in the collection." (step 7). - ECMAScript 2026, Indexed Collections, https://tc39.es/ecma262/multipage/indexed-collections.html (
ecma-indexed-collections.txt), §23.1.3.27Array.prototype.shift: "Repeat, while k < length,Let from be ! ToString(𝔽(k))." then "Perform ? Set(obj, to, fromValue, true)." (step 5); the same page'sincludes,indexOf,findandsplicesteps are whatlib/counter.mjslets run on a Proxy (steps 2, 6).
Related lessons
Checked against the tools and docs as of 2026-10-15.