R7 · Part 2 · The technical rounds
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.
- saas
- product
- service (less often)
- agency (less often)
Everything open, top to bottom.
10On this page
- The eight patterns that cover most of what you will be asked
- Sliding window — the template to internalise
- Intervals — merge, and why this matters for you
- Write an LRU cache with O(1) get and put.
- JavaScript-specific implementations they ask senior candidates for
- How to run the round when you do not know the answer
- What is the time and space complexity of this — and how do you know?
- Count the subarrays that sum to k. The array can contain negative numbers.
- Write a binary search for the first index whose value is at least the target.
- Count the islands in a grid. Then: why does your recursive version crash on a large one?
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.
The eight patterns that cover most of what you will be asked
What they are really testingRecognition speed. At six years they expect you to name the pattern before you write code.
| 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 |
“This is asking for a contiguous subarray, so I think it is a sliding window. Let me say the brute force first — two nested loops, check every window, that is O(n²) and it will be too slow at the sizes they usually mean. I think I can drop the inner loop by keeping a running sum and moving a left pointer. Let me try that and I will talk through it as I go.”
Why it passes here: At two years nobody expects the optimal solution to arrive fully formed. What is being graded is whether you go silent when stuck. Silence is the fail; “I am working out whether a hash map removes the inner loop” is not — it is the interviewer’s cue to nudge you, which they are allowed to do.
“Contiguous subarray with a constraint — sliding window. Expand right while the window is valid, shrink from the left when it breaks. O(n) time, O(k) space for whatever I keep about the window. Before I code it: empty input, a single element, and the case where no window is valid, which should return zero rather than minus one — I will confirm that with you. Then I will write it and dry-run it on [2, 1, 3] out loud.”
Why it lands: Stating the complexity before implementing proves you chose the approach rather than stumbled into it. Volunteering the edge cases removes the interviewer’s next question, which is worth real minutes in a forty-five minute round, and it is the behaviour that gets written down as “rigorous”.
“Before I pick — how large is the input, and does it fit in memory? If it is a stream the window still works but anything that sorts first does not, and that changes the answer. Assuming it fits: sliding window at O(n) versus sorting first at O(n log n). The sort is meaningfully simpler to get right, and for n in the low thousands I would genuinely ship it — but this is asking for the linear one, so I will do the window. If we have time afterwards I would like to talk about what happens when the constraint becomes two-dimensional, because that is where this pattern stops being enough.”
Why this is the level above: Two things happen here that do not at five years. You made the input scale a question instead of an assumption, and you named a case where the simpler, asymptotically worse algorithm would be the right call. Senior DSA rounds are looking for that judgement more than for the trick — the trick they assume.
- Which pattern is this, and what in the wording told you?
- What is the brute force, and where does it waste work?
- Which of these eight are you least comfortable with?
Sliding window — the template to internalise
What they are really testingOne template solves a whole class. Knowing it cold buys you thinking time for the variation.
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)) spaceMoving start to last.get(c) + 1 without checking that the old index is inside the window. On "abba" the second a sends start backwards to index 1, the window suddenly contains bb again, and the answer comes out as 3 instead of 2. The >= start check is not decoration.
- Now do it for at most K distinct characters.
- What if the string is a stream and you cannot index backwards?
Intervals — merge, and why this matters for you
What they are really testingYour booking background makes this the problem an interviewer is most likely to pick for you deliberately.
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
}intervals.sort() with no comparator. JavaScript sorts arrays as strings, so [[10, 12], [9, 11]] stays in that order — "10" sorts before "9" — and the merge silently misses the overlap.
- What if meetings can be cancelled dynamically?
- How would you do this with a heap instead?
Write an LRU cache with O(1) get and put.
What they are really testingThe most-asked design-flavoured DSA problem, and JavaScript has an elegant answer most candidates miss.
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
}
}
}An array of keys with indexOf and splice to track recency. It is correct and reads cleanly, and it is O(n) per operation — the question said O(1), so it answers a different question.
- 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?
JavaScript-specific implementations they ask senior candidates for
What they are really testingThese are the "DSA" questions a frontend-leaning interviewer actually asks. They are more likely for you than a graph problem.
- 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
}How to run the round when you do not know the answer
What they are really testingThis is the actual skill being assessed at six years. They are hiring your process at least as much as your recall.
- 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.
- You have been quiet for a minute. Where are you?
- Can you do better than O(n²)?
- What input would break your solution?
- Walk me through it on an empty array.
What is the time and space complexity of this — and how do you know?
What they are really testingWhether you can read complexity off real JavaScript, including the cost hidden inside built-in methods.
Most complexity mistakes in this round are not in your loops. They are in a built-in method you called inside a loop and assumed was free. Know these without pausing:
| Operation | Cost | The trap |
|---|---|---|
push, pop | O(1) amortised | None — the backing store grows geometrically, so the occasional copy averages out. |
shift, unshift | O(n) | Every element moves. queue.shift() inside a BFS turns O(V + E) into something quadratic. |
includes, indexOf, find | O(n) | Inside a loop, quadratic. A Set makes the check O(1). |
slice, spread, concat | O(k) for k elements copied | [...acc, x] inside a reduce is O(n²) overall. |
sort | O(n log n) | With no comparator it compares as strings: [10, 9, 1].sort() gives [1, 10, 9]. |
Map / Set get, set, has | O(1) average | Average, not worst case — say the word. |
Space is the half people forget. Recursion uses a stack frame per level, so a recursive DFS down a path-shaped tree is O(n) space even though you allocated nothing. And if they ask about auxiliary space, the output array does not count — say which one you mean.
// 1. looks O(n). Is O(n²) — includes is a scan.
const unique = []
for (const x of arr) if (!unique.includes(x)) unique.push(x)
// 2. looks O(n). Is O(n²) — the spread copies acc every time.
const doubled = arr.reduce((acc, x) => [...acc, x * 2], [])
// 3. looks O(n²). Is O(n) — start only ever moves forward.
let start = 0, sum = 0
for (let end = 0; end < arr.length; end++) {
sum += arr[end]
while (sum > k) sum -= arr[start++] // non-negative input
}Counting loops. "One loop, so O(n)" misses the O(n) method inside it; "two loops, so O(n²)" gets the sliding window wrong the other way. Count the work, not the brackets.
The third one is worth being able to argue: the inner while looks like a nested loop, but start only moves forward, so across the whole run it moves at most n times. Counting how many times each element is touched in total, rather than per iteration, is amortised analysis — and it is exactly the sentence they want to hear.
- What does sort cost in V8, and is it stable?
- Why is push O(1) if the array sometimes has to copy itself?
- What is the space complexity of your recursive solution?
- Can a hash map lookup ever be O(n)?
Count the subarrays that sum to k. The array can contain negative numbers.
What they are really testingWhether you notice that negatives break the sliding window, and reach for prefix sums and a hash map instead.
The instinct is a sliding window, and here it is wrong. A window only works when growing it moves the sum in one direction; with negative numbers, adding an element can make the sum smaller, so you never know whether to shrink. Say that out loud — spotting why the obvious pattern fails is most of the marks.
The move is prefix sums. If the running sum up to index j is P[j], the subarray from i + 1 to j sums to P[j] − P[i]. So a subarray ending at j sums to k exactly when some earlier prefix equals P[j] − k. Count every prefix you have seen, and each step is one lookup.
function countBrute(nums, k) {
let count = 0
for (let i = 0; i < nums.length; i++) {
let sum = 0
for (let j = i; j < nums.length; j++) {
sum += nums[j]
if (sum === k) count++
}
}
return count
}function subarraySum(nums, k) {
const seen = new Map([[0, 1]]) // the empty prefix: subarrays that start at index 0
let sum = 0, count = 0
for (const x of nums) {
sum += x
count += seen.get(sum - k) ?? 0 // earlier prefixes that make this one work
seen.set(sum, (seen.get(sum) ?? 0) + 1)
}
return count
}
// [1, -1, 1], k = 1 → 3A sliding window. It passes every example with positive numbers, which is exactly why the interviewer put a negative in the input. If your approach does not explain what happens on [3, -2, 1], it is not an approach yet.
Two details to volunteer. The [0, 1] seed is the one everybody forgets, and without it you miss every subarray that starts at index 0. And the order of the two lines inside the loop matters: look up first, then record the current prefix — the other way round, with k = 0, you count an empty subarray at every step.
The same idea solves a family: the longest subarray with sum k (store the first index of each prefix instead of a count), subarrays divisible by k (store sum % k, normalised because % can be negative in JavaScript), and the longest run with equal zeros and ones (count 0 as −1). Naming the family shows recognition rather than memory.
- Now return the length of the longest such subarray.
- If every number is positive, is there an O(1)-space answer?
- Why does the map start with 0 already in it?
- Count the subarrays whose sum is divisible by k.
Write a binary search for the first index whose value is at least the target.
What they are really testingBoundary discipline. Everyone knows binary search; most people write one that loops forever or misses by one under pressure.
Do not memorise five variants. Memorise one — lower bound, the first index where arr[i] >= target — and derive the rest from it. Exact match, last occurrence, insert position and count of a value are each a line on top of it.
Use a half-open range [lo, hi) and a single rule: if mid could be the answer, keep it (hi = mid); if it cannot, discard it (lo = mid + 1). The loop ends when lo === hi, and that is the answer, with nothing to patch afterwards.
function lowerBound(arr, target) {
let lo = 0, hi = arr.length // half-open: hi is one past the end
while (lo < hi) {
const mid = (lo + hi) >>> 1 // integer midpoint — / 2 gives a float in JS
if (arr[mid] < target) lo = mid + 1 // too small, cannot be the answer
else hi = mid // might be the answer — keep it
}
return lo // arr.length if everything is smaller
}
// everything else is lowerBound plus one line
const indexOf = (a, t) => { const i = lowerBound(a, t); return a[i] === t ? i : -1 }
const upperBound = (a, t) => lowerBound(a, t + 1) // integers only
const countOf = (a, t) => upperBound(a, t) - lowerBound(a, t)const mid = (lo + hi) / 2 from a C or Java habit. In JavaScript that is a float, arr[2.5] is undefined, every comparison against it is false, and the search quietly returns the wrong index. Use Math.floor or >>> 1.
Say why each boundary is what it is, because that is what they are checking. hi starts at arr.length, not length − 1, so "insert at the end" is a possible answer. lo = mid + 1 always moves, and hi = mid always shrinks the range because mid < hi, so the loop cannot stall. Then dry-run it on an empty array and on a target larger than everything.
Rotated sorted arrays, peak finding and binary search on the answer (in the ₹50L section) are the same loop with a different test inside the if. The test changes; the boundaries do not.
- Now find the last occurrence of the target.
- Search a rotated sorted array.
- Why would lo = mid make this loop forever?
- What changes when the array has duplicates?
Count the islands in a grid. Then: why does your recursive version crash on a large one?
What they are really testingGrid traversal, the most common graph problem at this level — and whether you know what JavaScript does to deep recursion.
Walk the grid. Every time you meet land you have not visited, that is a new island: count it, then flood-fill everything connected so it is never counted again. O(rows × cols) time, because each cell is touched a constant number of times.
Recursive DFS is the shortest to write and the first thing to break. A grid that is one long snake of land recurses once per cell, and Node's stack gives out somewhere around ten thousand frames — a 200 × 200 grid can do it. Say that before they ask, then write the iterative version:
function numIslands(grid) {
const R = grid.length, C = grid[0]?.length ?? 0
const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]]
let islands = 0
for (let r = 0; r < R; r++) {
for (let c = 0; c < C; c++) {
if (grid[r][c] !== '1') continue
islands++
grid[r][c] = '0' // mark when you enqueue, not when you dequeue
const queue = [[r, c]]
for (let head = 0; head < queue.length; head++) { // a pointer, not queue.shift()
const [cr, cc] = queue[head]
for (const [dr, dc] of dirs) {
const nr = cr + dr, nc = cc + dc
if (nr >= 0 && nr < R && nc >= 0 && nc < C && grid[nr][nc] === '1') {
grid[nr][nc] = '0'
queue.push([nr, nc])
}
}
}
}
}
return islands
}Using DFS for the follow-up "now find the shortest path". DFS finds a path, not the shortest; BFS explores in rings of increasing distance, which is why the first time it reaches the target is the shortest route. Mixing the two up is the most common wrong answer at this point in the round.
Three details that score. Mark on enqueue: marking on dequeue lets the same cell enter the queue several times from different neighbours. A head pointer instead of shift(), because shift is O(n) and makes the BFS quadratic on a big grid. And ask before mutating the input — overwriting the grid is the no-extra-space answer, but if the caller needs it back, keep a separate visited array and say what it costs.
The same skeleton does Rotting Oranges (start the queue with every rotten cell at once — multi-source BFS), the shortest path through a binary maze (BFS finds shortest paths in unweighted grids; DFS does not), and Surrounded Regions (flood from the border first).
- Now return the size of the largest island.
- Find the shortest path from the top-left to the bottom-right.
- Land is added one cell at a time. Keep the island count after each addition.
- What if the grid is too large to fit in memory?
This round is ten questions: how to behave in the room, and the handful of patterns worth having cold. The patterns themselves are a whole track — 48 chapters and 694 exercises — and this is the order to walk it.
- Interview strategy and Complexity analysis — start here; they change how you answer everything else
- The patterns that carry most interviews: Hashing, Two pointers, Sliding window, Binary search
- Then the structures: Trees, Graphs, Heaps, Stacks & queues
- Then the two that decide mid-size product loops: DP: 1D and Backtracking
Finished this round?
0 of 27 rounds read in this book