Three problems, worked end to end
The full process applied — clarify, brute force, improve, code, test.
Problem 1 — longest substring without repeating characters
Clarify. Substring means contiguous. Return the length. Characters could be any Unicode. Empty string returns 0.
Brute force. Every substring, checking each for duplicates: O(n³), or O(n²) with a set per start. Say it, then improve.
The insight. As the right edge advances, the left edge never needs to move backwards — so it is a sliding window, O(n).
function longestUnique(text) {
const lastSeen = new Map();
let start = 0;
let best = 0;
for (let end = 0; end < text.length; end++) {
const ch = text[end];
if (lastSeen.has(ch) && lastSeen.get(ch) >= start) start = lastSeen.get(ch) + 1;
lastSeen.set(ch, end);
best = Math.max(best, end - start + 1);
}
return best;
}
Test. "" → 0. "aaaa" → 1. "abcabcbb" → 3. "tmmzuxt" → 5 — that last one catches the missing >= start guard, which is the standard bug.
Problem 2 — number of islands
Clarify. Four-directional adjacency. May I mutate the grid? (If yes, no visited set is needed.)
Approach. Every unvisited land cell starts a flood fill; count the fills.
function countIslands(grid) {
let islands = 0;
const sink = (r, c) => {
if (r < 0 || c < 0 || r >= grid.length || c >= grid[0].length) return;
if (grid[r][c] !== '1') return;
grid[r][c] = '0'; // mark visited by sinking it
sink(r + 1, c); sink(r - 1, c); sink(r, c + 1); sink(r, c - 1);
};
for (let r = 0; r < grid.length; r++) {
for (let c = 0; c < grid[0].length; c++) {
if (grid[r][c] === '1') { islands++; sink(r, c); }
}
}
return islands;
}
O(rows × cols) time and, in the worst case, the same in recursion depth — mention that a BFS queue avoids the stack overflow on a large all-land grid.
Problem 3 — k-th largest in a stream
Clarify. Repeated queries as values arrive, so this is a design question, not a one-off scan.
Approach. A min-heap of size k. The root is always the k-th largest; anything smaller than the root is irrelevant forever.
class KthLargest {
constructor(k, initial = []) {
this.k = k;
this.heap = new MinHeap();
for (const value of initial) this.add(value);
}
add(value) {
if (this.heap.size < this.k) this.heap.push(value);
else if (value > this.heap.peek()) { this.heap.pop(); this.heap.push(value); }
return this.heap.peek();
}
}
O(log k) per insertion, O(k) memory regardless of stream length. Compare aloud with sorting (O(n log n) per query, and needs the whole stream) — naming the alternative and why you rejected it is the part that scores.