Coding Interview Patterns Module 2 – Interview toolkits in five languages
JavaScript and TypeScript toolkit
Map, Set, sorting, numbers, typed arrays and a heap template for coding rounds in JavaScript and TypeScript, with every trap run on Node.js 24.
What you will learn
- Use Map, Set, typed arrays and modern array methods safely in coding-round solutions
- Avoid the default-sort, comparator, 32-bit bitwise, number-precision and recursion-depth traps
- Write a binary heap with a comparator from memory and extend it
- Run TypeScript on Node.js and check what a judge's TypeScript target accepts
Before you start
What you’ll learn · Before you start
What you will learn
- Use Map, Set, typed arrays and modern array methods safely in coding-round solutions
- Avoid the default-sort, comparator, 32-bit bitwise, number-precision and recursion-depth traps
- Write a binary heap with a comparator from memory and extend it
- Run TypeScript on Node.js and check what a judge's TypeScript target accepts
Before you start
On this page
JavaScript can solve every coding-round problem, but its standard library gives you less than Python’s or Java’s, and
several of its defaults are traps. Map and Set cover hashing, and arrays cover stacks and sorting. There is no heap
and no sorted map, numbers are 64-bit floating point, bitwise operators cut values to 32 bits, and sort() compares
numbers as text unless you say otherwise. This lesson shows the parts of JavaScript that interview problems use, runs
every trap on Node.js 24.21.0 next to its fix, gives you a heap template to write from memory, and covers what changes
when you write TypeScript on Node.js or on a judge.
Map and Set before plain objects
An object can count and look things up, but its keys are always strings, so obj[1] and obj["1"] are the same
entry. A Map keeps the type of each key, has a size, and iterates in insertion order, which the language
specification defines, so output built from it repeats from run to run.
// Object keys are always strings, so 1 and "1" are the same key; a Map keeps the type of its keys.
const obj = {};
obj[1] = "number one";
obj["1"] = "text one";
console.log(Object.keys(obj).length, JSON.stringify(obj));
const map = new Map([[1, "number one"], ["1", "text one"]]);
console.log(map.size, map.get(1), "|", map.get("1"));
// Counting with a Map: ?? supplies 0 for a word not seen yet. A Map iterates in insertion order.
const words = "pear fig pear apple fig kiwi apple pear".split(" ");
const counts = new Map();
for (const w of words) counts.set(w, (counts.get(w) ?? 0) + 1);
console.log(JSON.stringify([...counts]));
// Grouping in one call (ES2024).
console.log(JSON.stringify(Object.groupBy(words, (w) => `${w.length} letters`)));
// Set operations (ES2025).
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
console.log(JSON.stringify([...a.intersection(b)]), JSON.stringify([...a.union(b)]), JSON.stringify([...a.difference(b)]));
// Arrays are compared by reference, so a Set of pairs needs a key you build, such as "row,col".
const visited = new Set();
visited.add([2, 3]);
console.log(visited.has([2, 3]));
const cells = new Set();
cells.add(`${2},${3}`);
console.log(cells.has(`${2},${3}`)); Output
1 {"1":"text one"}
2 number one | text one
[["pear",3],["fig",2],["apple",2],["kiwi",1]]
{"4 letters":["pear","pear","kiwi","pear"],"3 letters":["fig","fig"],"5 letters":["apple","apple"]}
[2,3] [1,2,3,4] [1]
false
true
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node maps_and_sets.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
The counting line is the pattern to keep: counts.set(w, (counts.get(w) ?? 0) + 1), where ?? supplies 0 for a key
that is not there yet. Object.groupBy (ECMAScript 2024) builds the lists of a “group by” in one call, and Set has
had intersection, union and difference since ECMAScript 2025; on an older engine, write them with filter and
has.
The last two lines are the trap that costs the most time in grid problems. Arrays are objects, and a Set compares
objects by reference, so a new [2, 3] is never “in” the set, even when an equal pair was added. Encode a cell as a
string such as "2,3", or as one number, row * cols + col, which is faster to build and compare. The
DSA toolkit lesson lists what the specification promises about the cost of
these structures.
Sorting: the comparator decides
sort() without a comparator converts the elements to strings and compares those, so [10, 9, 1, 25, 100] comes out
as [1, 10, 100, 25, 9]. Pass a comparator for numbers. The specification expects it to return a negative number, zero
or a positive number consistently, and when a comparator is not consistent it leaves the resulting order to the
engine.
const nums = [10, 9, 1, 25, 100];
// Without a comparator, sort() converts the numbers to strings and compares those.
console.log(JSON.stringify([...nums].sort()));
console.log(JSON.stringify([...nums].sort((a, b) => a - b)));
// A comparator must return a negative number, zero or a positive number. A boolean is read as 1 or 0, never
// negative, so the engine is told "not smaller" for every pair and the result is not sorted.
const twelve = [5, 1, 4, 2, 8, 3, 9, 7, 6, 12, 11, 10];
console.log(JSON.stringify([...twelve].sort((a, b) => a > b)));
console.log(JSON.stringify([...twelve].sort((a, b) => a - b)));
// Sorting is stable, so equal keys keep their order; || chains a second key.
const people = [["ravi", 31], ["asha", 25], ["meena", 31], ["dev", 25]];
const byAgeThenName = people.toSorted((p, q) => q[1] - p[1] || (p[0] < q[0] ? -1 : p[0] > q[0] ? 1 : 0));
console.log(JSON.stringify(byAgeThenName));
// toSorted (ES2023) returned a new array and left the original alone.
console.log(JSON.stringify(people[0])); Output
[1,10,100,25,9] [1,9,10,25,100] [5,1,4,2,8,3,9,7,6,12,11,10] [1,2,3,4,5,6,7,8,9,10,11,12] [["meena",31],["ravi",31],["asha",25],["dev",25]] ["ravi",31]
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node sorting.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Version note
The Run button executes this file with QuickJS (quickjs-emscripten 0.32.0), whose sort algorithm is not Node.js’s.
The boolean comparator gives a different wrong order there, [5,1,2,3,4,7,6,8,9,10,11,12], while every correct
comparator gives the same result in both engines.
In your browser, QuickJS (quickjs-emscripten 0.32.0) prints:
[1,10,100,25,9] [1,9,10,25,100] [5,1,2,3,4,7,6,8,9,10,11,12] [1,2,3,4,5,6,7,8,9,10,11,12] [["meena",31],["ravi",31],["asha",25],["dev",25]] ["ravi",31]
The third line is the comparator (a, b) => a > b. It returns true or false, which the sort reads as 1 or 0, never
as a negative number, so the engine is never told that one element comes before another. On Node.js the array came back
exactly as it went in; QuickJS, as the note says, produced a different wrong order. A comparator that only works on the
sample input is worse than an error, because nothing fails until a hidden test does.
Sorting has been stable by specification since ECMAScript 2019, so elements that compare equal keep their order, and
|| chains a second key: q[1] - p[1] || byName sorts by age from oldest, then by name. toSorted (ECMAScript 2023)
returns a sorted copy and leaves the original array alone, which matters when the caller’s array must not change.
Numbers: doubles, 32-bit bitwise operators and BigInt
Every Number is an IEEE 754 double. Integers are exact up to Number.MAX_SAFE_INTEGER, 2⁵³ − 1, and bitwise
operators first convert their operands to 32-bit integers. Several common shortcuts break because of these rules.
// Bitwise operators work on 32-bit signed integers.
console.log(1 << 31, (1 << 31) >>> 0);
// So "x | 0" and "~~x" truncate only while x fits in 32 bits; Math.trunc works on any Number.
const big = 2 ** 32 + 5.75;
console.log(big | 0, ~~big, Math.trunc(big));
// % keeps the sign of the left operand; add m once more for a remainder in 0 .. m - 1.
const m = 5;
console.log(-17 % m, ((-17 % m) + m) % m);
// A product of two values below 1,000,000,007 can pass 2^53, where Numbers stop being exact.
const MOD = 1_000_000_007;
const x = 999_999_999;
const y = 999_999_998;
console.log((x * y) % MOD, Number((BigInt(x) * BigInt(y)) % BigInt(MOD)));
// Splitting one factor into 15-bit halves keeps every partial product below 2^45, so plain Numbers stay exact.
function mulMod(a, b, mod) {
const hi = Math.floor(b / 32768);
const lo = b % 32768;
return ((((a * hi) % mod) * 32768) % mod + ((a * lo) % mod)) % mod;
}
console.log(mulMod(x, y, MOD));
// Mixing BigInt and Number throws (each engine words the message its own way).
try {
console.log(1n + 1);
} catch (error) {
console.log(error.name);
} Output
-2147483648 2147483648 5 5 4294967301 -2 3 70 72 72 TypeError
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node numbers.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
1 << 31is negative. The result is a signed 32-bit integer, -2147483648;>>> 0reads the same bits as unsigned. Bit tricks on masks of more than 31 bits need BigInt or two numbers.x | 0and~~xtruncate only small numbers. Above 2³¹ they wrap around: 4,294,967,301.75 became 5. UseMath.truncorMath.floor.%keeps the sign of the left operand, so-17 % 5is -2. For a remainder from 0 to m − 1, write((x % m) + m) % m.- A product modulo 10⁹ + 7 is wrong with plain Numbers. 999,999,999 × 999,999,998 is about 10¹⁸, far past 2⁵³, so
the product is rounded before
%sees it: 70 instead of 72. BigInt gives the exact 72. So doesmulMod, which splits one factor into two 15-bit halves so that no partial product passes 2⁴⁵ and every step stays exact in a Number.
Mixing BigInt and Number in one expression throws a TypeError, so convert every operand. The message itself differs
between engines, which is why the example prints only the error’s name.
A common piece of advice says that BigInt is too slow for a loop of modular products and that the split is the fast way. Measured on Node.js 24, it is the other way round:
// A million products modulo 1,000,000,007, chained so that each one uses the last: with BigInt, and with
// plain Numbers split into 15-bit halves. Both must end on the same value.
const MOD = 1_000_000_007;
const N = 1_000_000;
function mulMod(a, b, mod) {
const hi = Math.floor(b / 32768);
const lo = b % 32768;
return ((((a * hi) % mod) * 32768) % mod + ((a * lo) % mod)) % mod;
}
let start = performance.now();
let big = 123_456_789n;
const bigMod = BigInt(MOD);
for (let i = 0; i < N; i++) big = (big * 987_654_321n) % bigMod;
const bigMs = performance.now() - start;
start = performance.now();
let split = 123_456_789;
for (let i = 0; i < N; i++) split = mulMod(split, 987_654_321, MOD);
const splitMs = performance.now() - start;
console.log(`BigInt: ${String(big)}, ${Math.round(bigMs)} ms`);
console.log(`split Numbers: ${split}, ${Math.round(splitMs)} ms`);
console.log(`same result: ${String(big) === String(split)}`); Output
BigInt: 239439686, 15 ms split Numbers: 239439686, 603 ms same result: true
This output changes from run to run: the times depend on the computer and on what else it is running
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node mulmod_speed.mjs
Both chains end on the same value, and BigInt took a small fraction of the split version’s time, because each
mulMod call performs four floating-point remainders. Use BigInt for modular products, converting once on the way in
and once on the way out, and keep the split for code that must stay in Numbers, such as values stored in a typed
array.
Arrays: shared rows, typed arrays and queues
Three array habits decide whether a solution is right and fast enough.
// fill() puts the same object in every slot: three references to one array.
const shared = Array(3).fill([]);
shared[0].push("x");
console.log(JSON.stringify(shared));
// Array.from calls the function once per slot, so every row is a new array.
const rows = Array.from({ length: 3 }, () => []);
rows[0].push("x");
console.log(JSON.stringify(rows));
// A typed array starts filled with zeros and has a fixed size, good for DP tables and counts.
const dp = new Int32Array(5);
dp[4] = 7;
console.log(JSON.stringify([...dp]));
// But it stores every value in its type: 2^31 does not fit in an Int32Array and wraps around, and a
// Uint8Array keeps only the lowest 8 bits.
dp[0] = 2 ** 31;
console.log(dp[0], new Uint8Array([300])[0]); Output
[["x"],["x"],["x"]] [["x"],[],[]] [0,0,0,0,7] -2147483648 44
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node arrays.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Array(3).fill([]) puts one array into all three slots, so a push to one row shows in every row: MDN’s page on fill
warns that an object value makes every slot reference the same object. Array.from({ length: n }, () => []) calls the
function once per slot. Typed arrays such as Int32Array start filled with zeros and are compact, which suits DP tables
and counters, but they store each value in their own type: 2³¹ does not fit in an Int32Array and wraps to
-2147483648, and a Uint8Array keeps only the lowest 8 bits of 300, which is 44. Use a Float64Array or a plain array
when sums can grow.
Queues are the third habit. JavaScript has no deque, and shift() moves every remaining element down by one place:
// Draining a queue of 50,000 items: shift() moves every remaining item down one place on each call,
// while a head index only moves a number.
const n = 50_000;
function drainWithShift() {
const queue = Array.from({ length: n }, (_, i) => i);
let total = 0;
while (queue.length > 0) total += queue.shift();
return total;
}
function drainWithHead() {
const queue = Array.from({ length: n }, (_, i) => i);
let total = 0;
for (let head = 0; head < queue.length; head++) total += queue[head];
return total;
}
for (const drain of [drainWithShift, drainWithHead]) {
const start = performance.now();
const total = drain();
console.log(`${drain.name}: total ${total}, ${Math.round(performance.now() - start)} ms`);
} Output
drainWithShift: total 1249975000, 773 ms drainWithHead: total 1249975000, 3 ms
This output changes from run to run: the times depend on the computer and on what else it is running
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node queue_cost.mjs
Draining the queue with shift() takes time that grows with the square of its length, so the two lines differ by a
factor in the hundreds. A breadth-first search over 10⁵ nodes whose queue grows long can time out this way. Keep the
array and an index of its front instead: read queue[head++] and never remove anything. Memory grows to the total
number of pushes, which a search uses anyway.
A heap you can write from memory
There is no priority queue in JavaScript, and Dijkstra’s algorithm, top-k problems and k-way merges all need one. This template is short enough to write in five minutes: an array, a comparator, a loop that moves a new item up, and a loop that moves the last item down after a pop.
// A binary min-heap with a comparator: the template to keep for coding rounds in JavaScript.
class MinHeap {
constructor(compare = (a, b) => a - b) {
this.items = [];
this.compare = compare;
}
get size() {
return this.items.length;
}
peek() {
return this.items[0];
}
push(value) {
const a = this.items;
a.push(value);
let i = a.length - 1;
while (i > 0) {
const parent = (i - 1) >> 1;
if (this.compare(a[i], a[parent]) >= 0) break;
[a[i], a[parent]] = [a[parent], a[i]];
i = parent;
}
}
pop() {
const a = this.items;
if (a.length === 0) return undefined;
const top = a[0];
const last = a.pop();
if (a.length > 0) {
a[0] = last;
let i = 0;
for (;;) {
const left = 2 * i + 1;
const right = left + 1;
let best = i;
if (left < a.length && this.compare(a[left], a[best]) < 0) best = left;
if (right < a.length && this.compare(a[right], a[best]) < 0) best = right;
if (best === i) break;
[a[i], a[best]] = [a[best], a[i]];
i = best;
}
}
return top;
}
}
const drain = (heap) => {
const out = [];
while (heap.size) out.push(heap.pop());
return out;
};
const numbers = new MinHeap();
for (const x of [5, 1, 4, 1, 3]) numbers.push(x);
console.log(JSON.stringify(drain(numbers)));
// Largest first: reverse the comparator.
const largest = new MinHeap((a, b) => b - a);
for (const x of [5, 1, 4, 1, 3]) largest.push(x);
console.log(JSON.stringify(drain(largest)));
// Records: earliest day first, then name from A to Z.
const byName = (p, q) => (p.name < q.name ? -1 : p.name > q.name ? 1 : 0);
const tasks = new MinHeap((p, q) => p.day - q.day || byName(p, q));
tasks.push({ name: "report", day: 3 });
tasks.push({ name: "invoice", day: 3 });
tasks.push({ name: "slides", day: 1 });
console.log(drain(tasks).map((t) => t.name).join(", "));
// Checked against sorting on 1,000 random arrays, with a seeded generator so every run is the same.
function mulberry32(seed) {
return () => {
seed = (seed + 0x6d2b79f5) | 0;
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
const random = mulberry32(42);
let agree = 0;
for (let trial = 0; trial < 1000; trial++) {
const values = Array.from({ length: Math.floor(random() * 40) }, () => Math.floor(random() * 100) - 50);
const heap = new MinHeap();
for (const v of values) heap.push(v);
if (JSON.stringify(drain(heap)) === JSON.stringify(values.toSorted((a, b) => a - b))) agree++;
}
console.log(`heap order equals sorted order on ${agree} of 1000 random arrays`);
console.log("pop on an empty heap:", String(new MinHeap().pop())); Output
[1,1,3,4,5] [5,4,3,1,1] slides, invoice, report heap order equals sorted order on 1000 of 1000 random arrays pop on an empty heap: undefined
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node min_heap.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
The comparator makes the template general: (a, b) => b - a turns it into a max-heap, and a comparator on records
orders tasks by day and then by name. The last lines check the heap the way to check any data structure you write
under pressure: against something simpler that is surely right. On 1,000 random arrays from a seeded generator, the
heap’s order equals the sorted order every time, and popping an empty heap returns undefined instead of throwing.
Two details are easy to get wrong when you write it from memory. The parent of index i is (i - 1) >> 1, and the
children are 2i + 1 and 2i + 2. And in pop(), the last item must be moved to the root only if the heap is not now
empty; otherwise popping the only item puts it straight back.
Recursion depth and the iterative rewrite
Deep inputs, such as a path-shaped tree of 10⁵ nodes, are a standard hidden test, and a JavaScript engine’s call stack is small.
// How deep can a trivial function recurse before the engine stops it?
let calls = 0;
function dive() {
calls++;
dive();
}
try {
dive();
} catch (error) {
console.log(`${error.name} after about ${Math.round(calls / 100) * 100} calls`);
}
// A path of 100,000 nodes, as a deep test case of a tree or graph problem would give.
const n = 100_000;
const next = Array.from({ length: n }, (_, i) => (i + 1 < n ? i + 1 : -1));
function countRecursive(node) {
return node === -1 ? 0 : 1 + countRecursive(next[node]);
}
try {
console.log("recursive:", countRecursive(0));
} catch (error) {
console.log("recursive:", error.name);
}
// The same walk with an explicit stack: an array holds what the call stack held.
function countIterative(start) {
const stack = [start];
let count = 0;
while (stack.length > 0) {
const node = stack.pop();
if (node === -1) continue;
count++;
stack.push(next[node]);
}
return count;
}
console.log("iterative:", countIterative(0)); Output
RangeError after about 10400 calls recursive: RangeError iterative: 100000
This output changes from run to run: the depth at which the engine stops depends on its stack size and on the machine
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node depth.mjs
On Node.js 24.21.0, a trivial function stopped with a RangeError after about 10,400 calls, so the recursive walk over
100,000 nodes failed. The iterative version keeps the nodes still to visit in an array and has no such limit. Write
depth-first searches iteratively in JavaScript when the input can be deep. The in-browser runner of this site has an
even smaller stack, which is why this example shows recorded output only.
TypeScript on Node.js and on judges
TypeScript’s types disappear before the code runs, so everything above applies to TypeScript unchanged. Node.js 24
runs .ts files directly: type stripping became the default in v23.6.0 and stable in v24.12.0. Its documentation
says that Node.js replaces the type syntax with whitespace and performs no type checking.
TypeScript (top_words.ts) · top_words.ts
type Pair = [word: string, count: number];
function topWords(words: string[], k: number): string[] {
const counts = new Map<string, number>();
for (const w of words) counts.set(w, (counts.get(w) ?? 0) + 1);
const pairs: Pair[] = [...counts];
pairs.sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1));
return pairs.slice(0, k).map(([w]) => w);
}
console.log(topWords("pear fig pear apple fig kiwi apple pear".split(" "), 2).join(" ")); TypeScript (no_type_check.ts) · no_type_check.ts
// tsc rejects the first line: a string is not a number. Node.js removes the annotation and runs it anyway.
const limit: number = "10";
console.log(limit + 5); TypeScript (colour_enum.ts) · colour_enum.ts
enum Colour {
White,
Grey,
Black,
}
console.log(Colour.Grey); TypeScript (colour_const.ts) · colour_const.ts
// An object with "as const" and a type made from it: plain JavaScript once the types are removed.
const Colour = { White: 0, Grey: 1, Black: 2 } as const;
type Colour = (typeof Colour)[keyof typeof Colour];
const state: Colour = Colour.Grey;
console.log(state); // Runs Node.js on each TypeScript file, as you would in a terminal, and prints what each run printed.
import { spawnSync } from "node:child_process";
for (const file of ["top_words.ts", "no_type_check.ts", "colour_enum.ts", "colour_const.ts"]) {
console.log(`$ node ${file}`);
const run = spawnSync(process.execPath, [file], { encoding: "utf8" });
if (run.stdout) process.stdout.write(run.stdout);
// Only the line that names the error: the rest of the report is Node's own call stack.
const error = run.stderr.split("\n").find((line) => /Error/.test(line));
if (error) console.log(error);
if (run.status !== 0) console.log(`[exit status ${run.status}]`);
} Output
$ node top_words.ts pear apple $ node no_type_check.ts 105 $ node colour_enum.ts SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode [exit status 1] $ node colour_const.ts 1
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node ts_session.mjs
- Type annotations, tuple types and generics such as
Map<string, number>run as they are. - A type error does not stop the run. The TypeScript compiler rejects
const limit: number = "10", but Node.js printed105, the string"10"joined with 5. Only a type checker such astsc --noEmitreports the mistake. - Syntax that needs JavaScript to be generated, such as an
enum, stops withERR_UNSUPPORTED_TYPESCRIPT_SYNTAX. An object withas constand a type derived from it does the same job and strips cleanly.
Judges that compile TypeScript first may reject methods the language target does not list. HackerRank’s execution
environment page lists TypeScript 5.6.2 on Node.js v20.15.1 with the target ES2020. With an ES2020 target and no
lib setting, the compiler’s built-in declarations are those of ES2020, so toSorted (ES2023) and Object.groupBy
(ES2024) are type errors there, and Node.js 20 has no Object.groupBy at all. Keep to sort on a copy and to loops
with a Map when you cannot check.
Not affiliated
Based on public information about HackerRank. MySmartCoPilot is not affiliated with HackerRank, and HackerRank’s real environments may differ from what this lesson describes.
Practise: extend the heap
The exercise starts from the heap template above. Add pushPop, replace and a static from that builds a heap in
O(n). The sample tests check each method on small cases and then run 10,000 random operations against a sorted array.
Exercise · Medium · JavaScript
Add pushPop, replace and from to the heap template
heap.mjs holds the lesson's MinHeap template: push, pop, peek and size work. Add the three methods that Python's heapq also offers and that top-k and k-way merge solutions use:
- pushPop(value) pushes value and then pops the smallest item, in one step: when value is not larger than the top (or the heap is empty), it returns value and leaves the heap as it was; otherwise it returns the old top and keeps value in the heap. - replace(value) pops the smallest item and then pushes value, in one step, and returns the item it popped. The returned item may be larger than value. On an empty heap it returns undefined and the heap holds value. - MinHeap.from(values, compare) builds a heap from an array in O(n) by sifting down from the last parent to the root, instead of n pushes. It must not change the array it is given, and compare is optional, as in the constructor.
Each method sifts at most once, so pushPop and replace cost O(log n). The tests compare your heap with a sorted array on 10,000 random operations.
Starter code · heap.mjs
/** A binary min-heap with a comparator (the lesson's template). */
export class MinHeap {
constructor(compare = (a, b) => a - b) {
this.items = [];
this.compare = compare;
}
get size() {
return this.items.length;
}
peek() {
return this.items[0];
}
push(value) {
const a = this.items;
a.push(value);
let i = a.length - 1;
while (i > 0) {
const parent = (i - 1) >> 1;
if (this.compare(a[i], a[parent]) >= 0) break;
[a[i], a[parent]] = [a[parent], a[i]];
i = parent;
}
}
pop() {
const a = this.items;
if (a.length === 0) return undefined;
const top = a[0];
const last = a.pop();
if (a.length > 0) {
a[0] = last;
let i = 0;
for (;;) {
const left = 2 * i + 1;
const right = left + 1;
let best = i;
if (left < a.length && this.compare(a[left], a[best]) < 0) best = left;
if (right < a.length && this.compare(a[right], a[best]) < 0) best = right;
if (best === i) break;
[a[i], a[best]] = [a[best], a[i]];
i = best;
}
}
return top;
}
/** Pushes value, then pops and returns the smallest item. */
pushPop(value) {
throw new Error("pushPop is not written yet");
}
/** Pops and returns the smallest item (undefined when empty), then pushes value. */
replace(value) {
throw new Error("replace is not written yet");
}
/** A heap of the values, built in O(n); the array itself is not changed. */
static from(values, compare) {
throw new Error("from is not written yet");
}
} The sample tests · heap.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { MinHeap } from './heap.mjs';
const drain = (heap) => {
const out = [];
while (heap.size) out.push(heap.pop());
return out;
};
test('pushPop on an empty heap returns the value and keeps the heap empty', () => {
const h = new MinHeap();
assert.equal(h.pushPop(4), 4);
assert.equal(h.size, 0);
});
test('pushPop with a value not larger than the top returns it and changes nothing', () => {
const h = MinHeap.from([5, 7, 9]);
assert.equal(h.pushPop(2), 2);
assert.equal(h.pushPop(5), 5);
assert.deepEqual(drain(h), [5, 7, 9]);
});
test('pushPop with a larger value returns the old top and keeps the value', () => {
const h = MinHeap.from([5, 7, 9]);
assert.equal(h.pushPop(8), 5);
assert.deepEqual(drain(h), [7, 8, 9]);
});
test('replace returns the old top even when the new value is smaller', () => {
const h = MinHeap.from([5, 7, 9]);
assert.equal(h.replace(1), 5);
assert.deepEqual(drain(h), [1, 7, 9]);
});
test('replace on an empty heap returns undefined and keeps the value', () => {
const h = new MinHeap();
assert.equal(h.replace(3), undefined);
assert.deepEqual(drain(h), [3]);
});
test('from builds a heap that pops in sorted order and leaves the array alone', () => {
const values = [9, 4, 7, 1, 8, 2, 2, 6];
const h = MinHeap.from(values);
assert.equal(h.peek(), 1);
assert.deepEqual(drain(h), [1, 2, 2, 4, 6, 7, 8, 9]);
assert.deepEqual(values, [9, 4, 7, 1, 8, 2, 2, 6]);
});
test('from uses the comparator it is given', () => {
const h = MinHeap.from([3, 10, 1, 7], (a, b) => b - a);
assert.deepEqual(drain(h), [10, 7, 3, 1]);
});
test('from an empty array gives an empty heap', () => {
const h = MinHeap.from([]);
assert.equal(h.size, 0);
assert.equal(h.pop(), undefined);
});
test('10,000 random operations agree with a sorted array', () => {
let seed = 7;
const random = () => {
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0;
return seed / 4294967296;
};
const heap = MinHeap.from([50, 20, 80]);
const model = [20, 50, 80];
const insert = (v) => {
let i = 0;
while (i < model.length && model[i] <= v) i++;
model.splice(i, 0, v);
};
for (let step = 0; step < 10000; step++) {
const v = Math.floor(random() * 1000);
const op = Math.floor(random() * 4);
if (op === 0) {
heap.push(v);
insert(v);
} else if (op === 1) {
assert.equal(heap.pop(), model.shift(), `pop at step ${step}`);
} else if (op === 2) {
insert(v);
assert.equal(heap.pushPop(v), model.shift(), `pushPop(${v}) at step ${step}`);
} else {
const expected = model.length ? model.shift() : undefined;
insert(v);
assert.equal(heap.replace(v), expected, `replace(${v}) at step ${step}`);
}
assert.equal(heap.size, model.length, `size at step ${step}`);
}
}); A hint
All three methods reuse the loop at the end of pop(), which moves the item at one index down until neither child is smaller. Move that loop into a method of its own, #siftDown(i), call it from pop(), and the new methods become a few lines each: replace puts value at index 0 and sifts it down, pushPop does the same only when value is larger than the top, and from sifts down every index from (n >> 1) - 1 to 0.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Interview questions
Fresher · Why does [10, 9, 1].sort() return [1, 10, 9] in JavaScript, and how do you sort numbers?
Without a comparator, Array.prototype.sort converts each element to a string and compares the strings by their UTF-16
code units, so "10" comes before "9", because the character 1 comes before 9. The numbers are only compared as
text; the array still holds numbers afterwards. To sort numbers, pass a comparator that returns a negative number, zero
or a positive number: (a, b) => a - b for ascending order and (a, b) => b - a for descending. A comparator that
returns a boolean, such as (a, b) => a > b, is a common mistake: it never returns a negative value, so the result
depends on the engine and is usually not sorted. Sorting is stable since ECMAScript 2019, and toSorted returns a
sorted copy instead of changing the array.
Key takeaways
- Use
MapandSetfor hashing; encode pairs and cells as strings or single numbers, because arrays compare by reference. - Always pass a numeric comparator to
sort, never a boolean one; chain keys with||. - Bitwise operators work on 32 bits,
| 0breaks above 2³¹,%keeps the sign of the left operand, and products modulo 10⁹ + 7 need BigInt or a split multiplication. Array(n).fill([])shares one array; typed arrays wrap values that do not fit their type.- Never use
shift()on a long queue: keep a head index. Write deep searches iteratively. - Keep a tested heap template; Node.js runs TypeScript without checking types, and a judge’s target decides which methods compile.
Check yourself
6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
References
- ECMAScript language specification: Array.prototype.sort and SortCompare (Ecma International (TC39))
- ECMAScript language specification: Map and Set objects (Ecma International (TC39))
- Finished proposals (the ECMAScript edition each feature joined) (Ecma International (TC39))
- Array.prototype.sort() (MDN Web Docs)
- Array.prototype.fill() (MDN Web Docs)
- Array.prototype.shift() (MDN Web Docs)
- Left shift (<<) and the 32-bit conversion of bitwise operators (MDN Web Docs)
- Remainder (%) (MDN Web Docs)
- Number.MAX_SAFE_INTEGER (MDN Web Docs)
- BigInt (MDN Web Docs)
- Typed arrays (Int32Array, Uint8Array) (MDN Web Docs)
- Getting things sorted in V8 (The V8 project)
- Node.js 24 documentation: Modules: TypeScript (OpenJS Foundation (Node.js))
- TSConfig reference: lib (Microsoft (TypeScript))
- quickjs-emscripten (quickjs-emscripten project)
- Execution environment (HackerRank)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress