The DSA round
Your background shows no competitive programming, so this is your weakest round and the reason to sequence mid-size product companies later in your campaign. The good news: at six years on a full-stack profile they ask easy and medium problems from a narrow set of patterns, not hard graph theory. Pattern recognition beats volume.
Do not attempt breadth. Do the first four patterns only — hash map, two pointers, sliding window, stack — about eight problems each, until recognition is instant. Those four cover the large majority of mediums given to full-stack candidates. Add intervals as a fifth, because your booking-marketplace background means an interviewer may reach for it deliberately.
| Pattern | Recognise it by | Practise |
|---|---|---|
| Hash map counting | "how many", "duplicate", "anagram", "frequency", "seen before" | Two Sum · Group Anagrams · Top K Frequent · Valid Anagram · Longest Consecutive Sequence |
| Two pointers | Sorted input, a pair or triplet, in-place rearrangement | Container With Most Water · 3Sum · Remove Duplicates · Valid Palindrome · Trapping Rain Water |
| Sliding window | "longest/shortest substring or subarray such that…" | Longest Substring Without Repeating Characters · Minimum Window Substring · Max Consecutive Ones III |
| Stack | Matching pairs, "next greater", parsing, undo | Valid Parentheses · Daily Temperatures · Min Stack · Largest Rectangle in Histogram |
| Binary search | Sorted input, or "smallest value that satisfies a monotonic predicate" | Search in Rotated Sorted Array · First and Last Position · Koko Eating Bananas |
| Tree / BFS-DFS | Anything with a tree, a grid, or nesting | Level Order · Max Depth · Validate BST · Lowest Common Ancestor · Number of Islands |
| Intervals your domain | Start and end times, booking, calendars, merging | Merge Intervals · Insert Interval · Meeting Rooms II · Non-overlapping Intervals |
| Basic DP | "how many ways", "min cost", overlapping subproblems | Climbing Stairs · House Robber · Coin Change · Longest Increasing Subsequence |
The shape generalises: expand end every iteration, shrink start while the window is invalid, record the answer. The only thing that changes between problems is what "invalid" means and what state you keep — a count map, a sum, a set.
function longest(s) {
const last = new Map() // char → last index seen
let start = 0, best = 0
for (let end = 0; end < s.length; end++) {
const c = s[end]
// only move start forward, never back
if (last.has(c) && last.get(c) >= start) start = last.get(c) + 1
last.set(c, end)
best = Math.max(best, end - start + 1)
}
return best
}
// O(n) time, O(min(n, alphabet)) space- Now do it for at most K distinct characters.
- What if the string is a stream and you cannot index backwards?
The tie-break in that sort is the whole problem: at the same timestamp, an ending meeting must be processed before a starting one, or a room that just freed up gets double-counted. That is the same half-open [start, end) reasoning from the machine coding round — say so, and connect it to the booking system you built.
function merge(intervals) {
if (!intervals.length) return []
intervals.sort((a, b) => a[0] - b[0]) // sort by start — always step one
const out = [intervals[0]]
for (const [s, e] of intervals.slice(1)) {
const last = out[out.length - 1]
if (s <= last[1]) last[1] = Math.max(last[1], e) // overlap → extend
else out.push([s, e])
}
return out
}
// O(n log n) — the sort dominates// sweep line: +1 at every start, -1 at every end, take the running max
function minRooms(meetings) {
const events = []
for (const [s, e] of meetings) { events.push([s, 1]); events.push([e, -1]) }
events.sort((a, b) => a[0] - b[0] || a[1] - b[1]) // end before start at equal time
let cur = 0, best = 0
for (const [, d] of events) { cur += d; best = Math.max(best, cur) }
return best
}- What if meetings can be cancelled dynamically?
- How would you do this with a heap instead?
Say out loud why this is O(1): a JavaScript Map guarantees insertion order, and delete plus set is the cheapest way to move a key to the most-recent end. That is language fluency, not a shortcut — but be ready to give the canonical version too, because some interviewers want it.
The canonical answer: a hash map from key to node, plus a doubly linked list with sentinel head and tail. The map gives O(1) lookup; the list gives O(1) move-to-front and O(1) eviction from the tail. The sentinels exist so you never write a null check for the empty case.
class LRU {
constructor(cap) { this.cap = cap; this.m = new Map() }
get(k) {
if (!this.m.has(k)) return -1
const v = this.m.get(k)
this.m.delete(k); this.m.set(k, v) // re-insert → moves to the end
return v
}
put(k, v) {
if (this.m.has(k)) this.m.delete(k)
this.m.set(k, v)
if (this.m.size > this.cap) {
this.m.delete(this.m.keys().next().value) // first key = least recent
}
}
}- Now make it an LFU cache.
- How would you make it thread-safe? (Trick question in JS — but ask about worker threads.)
- How does this relate to Redis eviction policies?
- Deep flatten to a given depth, without
Array.prototype.flat. Recursive, plus the iterative stack version for when they say "no recursion". - Deep clone with circular references — the
WeakMapof seen objects from R3. Promise.allfrom scratch — asked constantly. Index order and the empty array.- A promise pool with concurrency N — the highest-value one to have ready.
- Retry with exponential backoff and jitter.
- An event emitter with
on,off,once,emit. Watch the bug: removing a listener duringemitwhile iterating the live array skips the next one — iterate a copy. memoizewith a configurable key resolver and aMapcache.pipe/compose—const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x).- Debounce and throttle, with cancel and trailing edge.
- A deep equality function — handle arrays, dates, NaN, and different key counts.
- Chunk an array and group by — trivial, but asked as warm-ups.
- Curry a function of arbitrary arity —
fn.lengthis how you know when it is saturated.
class Emitter {
#m = new Map()
on(e, fn) { (this.#m.get(e) ?? this.#m.set(e, new Set()).get(e)).add(fn); return () => this.off(e, fn) }
off(e, fn) { this.#m.get(e)?.delete(fn) }
once(e, fn) { const w = (...a) => { this.off(e, w); fn(...a) }; this.on(e, w) }
emit(e, ...a) { for (const fn of [...(this.#m.get(e) ?? [])]) fn(...a) }
// ^ copy, so off() during emit is safe
}- Restate the problem and confirm one edge case. Buys thirty seconds and stops you solving the wrong thing.
- Give the brute force out loud with its complexity. Never sit in silence — an interviewer cannot score thinking they cannot hear.
- Name what makes it slow. "The inner loop re-scans what I have already seen — that is usually a hash map." "The input is sorted and I am scanning linearly — that is usually two pointers or binary search."
- State the approach and the complexity before you type, and get agreement. If they say "can you do better than O(n log n)", you have just saved fifteen minutes.
- Write it, then dry-run it out loud on a small input — including an empty input and a single element. Finding your own off-by-one is worth more than not having one.
Following that script with a working brute force scores better than a silent optimal solution. And if you are genuinely stuck, say so and ask for a hint — it costs a little, and burning ten minutes in silence costs the round.
Complexities you should be able to state without pausing: hash map operations O(1) average; sorting O(n log n); binary search O(log n); BFS/DFS O(V+E); a nested loop O(n²); recursion depth is space. Getting a complexity wrong after solving the problem is a surprisingly common way to lose the round.