Interview Prep
Problems modelled on what technical interviews ask. Solve them in JavaScript, TypeScript, Python or SQL.
Recommended next
Easy · System Design · Hash Map · ~10 min
Difficulty
Topics
Language
solve(s) — convert a valid Roman numeral (1..3999) to an integer. The symbols are I=1 V=5 X=10 L=50 C=100 D=500 M=1000. Symbols are normally written largest to smallest and added, except in six subtractive cases (IV=4 IX=9 XL=40 XC=90 CD=400 CM=900) where a smaller symbol before a larger one is subtracted. Scan left to right: when a symbol is smaller than the one after it, subtract it, otherwise add it. Examples
solve(s) — return true if s can be built by repeating a shorter substring two or more times ("abab" is "ab" twice). Trying every divisor of the length is fine, but the failure table of KMP answers it in one pass: let n = s.length and b = lps[n - 1] (the longest border of the whole string). The string is periodic exactly when b > 0 and n is divisible by n - b (the length of the smallest period). Examples
solve(strs) — return the longest string that is a prefix of every string in strs (non-empty array), or "" if there is none. Take the first string as the candidate prefix and shrink it until each other string starts with it. Or compare column by column: the prefix ends at the first index where any string is shorter or disagrees with the first one. Either way is O(total characters). Examples
solve(s, t) — two strings of the same length are isomorphic if the characters of s can be replaced to get t, with a one-to-one mapping: every character of s always maps to the same character of t, and two different characters of s never map to the same character. One map is not enough: "ab" → "aa" is a valid function but not a valid bijection. Keep two maps (s → t and t → s), or compare the index of each character's first occurrence. Examples
Replace a positive integer by the sum of the squares of its digits, and repeat. If the process reaches 1 the number is happy; otherwise it loops forever in a cycle that never contains 1. solve(n) — return true when n (1 <= n < 2^31) is happy. The sequence cannot grow without bound (a 10-digit number maps to at most 810), so it either reaches 1 or repeats a value. Detect the repeat with a Set of seen values, or in O(1) space with Floyd's slow / fast pointers on the "next number" function. Examples
solve(columnNumber) — convert a positive column number (1 <= columnNumber < 2^31) to its spreadsheet title: 1 → "A", 26 → "Z", 27 → "AA", 28 → "AB", 702 → "ZZ", 703 → "AAA". It looks like base 26, but it is bijective: there is no digit for zero (A is 1, Z is 26). The usual n % 26 loop breaks on multiples of 26. The fix is to shift by one before each division: take the letter for (n - 1) % 26, then continue with floor((n - 1) / 26). Examples
Partial application pre-fills some arguments of a function, returning a new function that accepts the rest. Implement partial(fn, ...presetArgs) that returns a new function pre-filled with presetArgs. solve(label, ...args) dispatches to predefined partials: 'add5' → partial of (a,b)=>a+b with first arg 5 'multiply3' → partial of (a,b)=>a*b with first arg 3 'greet' → partial of (greeting,name)=>greeting+' '+name with 'Hello'
compose applies functions right-to-left: the last function runs first. Implement compose(...fns) that returns a function applying the array of functions right-to-left. solve(x, label) dispatches to a predefined composition: 'double_addOne' → compose(double, addOne) 'square_double' → compose(square, double) 'addOne_square_double' → compose(addOne, square, double)
Define a family of interchangeable algorithms, encapsulate each one, and select between them at runtime — a sorter that can switch between ascending and descending order without changing Sorter itself. Implement AscendingStrategy/DescendingStrategy (sort(arr)) and Sorter.sort(arr), which delegates to whichever strategy it holds. solve(arr, strategyName) picks the strategy by name and returns the sorted array (the original arr must be left untouched).
Ensure a class has only one instance, and provide a global point of access to it — every call to getInstance() must return the exact same object, so state persists across "modules" that each grab their own reference. Implement Counter.getInstance() so it lazily creates the instance once and reuses it forever after, plus increment()/getValue() on that instance. solve(ops) runs a sequence of 'increment' | 'getValue' operations against Counter.getInstance(), collecting the results of every 'getValue' call. solve(['increment', 'increment', 'getValue']) → [2] — both increments landed on the same instance.
Create new objects by cloning an existing instance rather than instantiating from scratch — the clone must be fully independent: mutating it must never affect the original. Implement ShapePrototype.clone(), returning a new, independent ShapePrototype with the same type/color. solve(type, color, newColor) clones a shape, changes the clone's color to newColor, and returns both { original, clone } — proving the original is untouched.
Let a subject notify a list of subscribed observers whenever its state changes, without the subject knowing anything concrete about who's listening. Implement Subject.subscribe(name)/notify(value): every subscribed observer's update(value) should log { observer: name, value } when notified. solve(names, values) subscribes every name, then calls notify for every value, and returns the full log. solve(['a', 'b'], [1]) → [{ observer: 'a', value: 1 }, { observer: 'b', value: 1 }]
A password-reset token generator is meant to build a token from a caller-supplied source of cryptographically-random integers. The bug: the buggy version ignores the secure source entirely and calls Math.random() instead — Math.random is a fast, statistically-predictable PRNG never intended for security purposes; tokens generated from it can be predicted or brute-forced far more easily than a real attacker should need. Your task: fix solve(randomInts) so it builds the token from the provided randomInts array (mapping each integer to a character via % alphabet.length), never by generating its own randomness internally.
The signup form below accepts any string as an email address without validation. The bug: malformed input flows downstream into emails, database records and third-party APIs — at best causing bounced emails, at worst becoming an injection vector in systems that trust the "email" field is actually shaped like one. Your task: fix solve(email) to return true only for strings shaped like local@domain.tld (non-empty local part, @, non-empty domain, a dot, non-empty TLD) — false otherwise.
The function below builds a SQL query by concatenating user input directly into the string. The bug: an attacker can pass admin' OR '1'='1 as the username and the resulting query becomes SELECT * FROM users WHERE username = 'admin' OR '1'='1' — always true, bypassing authentication entirely. Your task: fix solve(username) so it returns a parameterized query — a placeholder (?) in the SQL string, with the user input passed separately as a bind parameter, never concatenated.
The file-download endpoint below serves whatever path the client asks for, relative to an uploads folder. The bug: a request for ../../etc/passwd (or an absolute path like /etc/passwd) escapes the intended uploads directory entirely, exposing arbitrary files on the server. Your task: fix solve(userPath) so it returns the path unchanged when it's a safe relative path, or null when it contains .. or starts with /.
A post-login "redirect back to where you came from" feature takes a returnTo URL from the query string and redirects there unchecked. The bug: ?returnTo=https://evil-lookalike.com/login sends a freshly-authenticated user straight to a phishing page — and because the redirect started on the real, trusted domain, it's far more convincing than a cold phishing link. Protocol-relative URLs (//evil.com) are an easy-to-miss bypass of a naive check too. Your task: fix solve(url) so it returns url unchanged only when it's a same-site relative path (starts with / but not //) — otherwise it returns the safe fallback '/'.
The core of a bit.ly-style service: turn a long URL into a short code and back. solve(args, ops) builds new Shortener(startId) — args = [startId] — and replays ops: shorten(url) — return the short code for url. The first new URL gets id startId, the next new URL startId + 1, and so on. The same URL always returns the same code and does not consume an id. expand(code) — return the original URL, or null for an unknown code. The code is the id written in base 62 with the alphabet 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ (so 10 → "a", 61 → "Z", 62 → "10"). Using a counter instead of a hash guarantees there are no collisions; encode it yourself, without toString(62) (which does not exist for 62). Example (startId = 61)
A fixed-size circular buffer: the structure behind log tails, audio buffers and metric windows. Writing to a full buffer overwrites the oldest value. solve(args, ops) builds new RingBuffer(capacity) — args = [capacity] — and replays ops: write(value) — append value; if the buffer is full, drop the oldest value first. Returns nothing. read() — remove and return the oldest value, or null if the buffer is empty. snapshot() — return all values as an array, oldest first, without removing them. size() — the number of values currently stored. All operations except snapshot must be O(1): allocate the array once and move a start index and a size counter around it with modulo arithmetic — no shift(). Example (capacity = 3)
Build the counter behind a "requests in the last 5 minutes" metric. solve(args, ops) builds new HitCounter() (no constructor arguments) and replays ops: hit(timestamp) — record one hit at timestamp (seconds). Returns nothing. count(timestamp) — return how many hits happened in the last 300 seconds, that is hits with timestamp - 300 < hitTime <= timestamp. Timestamps never go backwards. Several hits can share a timestamp. Keep memory bounded: drop hits that have left the window instead of keeping the whole history (a queue works). Example