29 Google JavaScript Frontend Developer Interview Questions & Answers

google icon

Questions with Detailed ExplanationsWith Detailed Explanations

(Last Updated: August 28, 2026)

1. Find all valid subsets of a deck under a supplied validity rule.CodingHardGoogle

Question Details

Implement validCardSubsets(cards, isValid). cards is an array of 0 through 25 unique immutable records with string id; isValid(subset) is a pure Boolean predicate supplied by the interviewer and may be called only with subsets in original card order. Return every non-empty subset for which the predicate is true, ordered first by subset size and then lexicographically by original index sequence. Do not mutate cards, expose partial arrays to later mutation, or return duplicate subsets. Reject invalid input. Example: for cards A, B, C and a predicate accepting subsets of size 2, return [A,B], [A,C], [B,C]. Explain the unavoidable O(2^n) search bound and keep auxiliary state O(n) beyond output.

Short Interview Answer (30-60 seconds)

I would generate the subsets by target size, from 1 through n. For each size, I use backtracking and choose card indices only in increasing order. That keeps every subset in original card order and naturally gives size-first, lexicographic index order without sorting. When a complete subset is built, I call isValid and copy it into the result if it passes. The search requires Θ(2^n) predicate calls, with O(n) auxiliary space beyond the output.

Detailed Explanation

See the Code while reading this explanation.

We have up to 25 cards. We must return every non-empty group of cards that passes a rule supplied by the interviewer. Cards inside each group must stay in their original order. The returned groups must be ordered by group size first, then by their original index sequence. We must not change the input cards or keep returned arrays connected to a working array that will later change. The solution generates combinations one size at a time and always chooses later indices.

Useful Questions to Ask the Interviewer
  1. Should invalid inputs such as duplicate card ids, a non-array cards value, or a non-function isValid value throw an error?
  2. Should each returned subset contain the original card object references while the subset array itself is a new array?
Find all valid subsets of a deck under a supplied validity rule. diagram
How to Explain It in an Interview
1. Understand the input and required output

The function receives cards and isValid. cards contains from 0 through 25 records. Each card has a string id. isValid is a pure Boolean predicate. We may call it only with subsets whose cards remain in original card order.

We return every non-empty subset for which isValid returns true. Smaller subsets come first. Subsets of the same size are ordered lexicographically by their original index sequence.

For the diagram example, A, B, and C are at indices 0, 1, and 2. The predicate accepts subsets of exactly two cards. The returned result is [A,B], [A,C], [B,C].

2. Choose the backtracking order

The solution generates combinations by target size. It first generates every size-1 subset, then every size-2 subset, and continues through size n.

For one target size, the recursion only chooses indices greater than the previously chosen index. This keeps cards in original order and prevents duplicate combinations.

Because target sizes increase from 1 through n, size ordering is automatic. Because candidate indices are tried from left to right, combinations of the same size are generated in lexicographic index order. No final sorting step is needed.

3. Initialize and validate the state

First, the code checks that cards is an array and contains at most 25 entries. It checks that isValid is a function. Each card must be a non-null object with a string id. A Set stores ids that have already appeared so duplicate ids can be rejected.

results stores accepted subsets. path stores the current combination. path is private working state. It grows when a card is chosen and shrinks after the recursive call returns.

4. Walk through the example

For target size 1, the search generates [A], [B], and [C]. The predicate accepts only size 2, so all three return false.

For target size 2, increasing indices produce [A,B] from [0,1], then [A,C] from [0,2], then [B,C] from [1,2]. isValid returns true for each one. Each accepted subset is copied into results.

For target size 3, the only candidate is [A,B,C]. Its size is 3, so the predicate returns false.

The exact evaluation order is [A], [B], [C], [A,B], [A,C], [B,C], [A,B,C]. The retained result is [A,B], [A,C], [B,C].

5. Explain why the result is correct

For each target size k, the recursion chooses strictly increasing original indices. Therefore every candidate keeps original card order. Every k-card combination appears exactly once because each subset has one unique increasing index sequence.

Processing k from 1 through n gives size-first order. Trying indices in increasing order gives lexicographic index order within a fixed size. isValid is called only after a complete non-empty candidate has been built. The candidate and stored result are copied, so later backtracking cannot change an earlier returned subset.

6. Explain the JavaScript implementation

The outer loop chooses the target subset size. dfs(start, remaining) builds combinations of that exact size. start is the first original index that may be chosen next. remaining tells us how many more cards are needed.

When remaining becomes zero, path is a complete candidate. The code copies path and passes that complete array to isValid. If the predicate returns true, it stores a separate copy in results.

For each recursive choice, the code pushes one card, calls dfs with i + 1 and one fewer remaining card, then pops the card. The pop restores path before trying the next possible index.

7. Explain complexity and edge cases

There are 2^n subsets in total, including the empty subset. With an arbitrary supplied predicate, the algorithm may need to examine every non-empty subset, so predicate calls are Θ(2^n).

Creating candidate arrays and copying accepted subsets adds Θ(n * 2^n) work in the worst case, excluding the cost inside isValid. The recursion stack, current path, temporary candidate, and validation Set use O(n) auxiliary space beyond output.

If n is 0, the result is []. If isValid always returns false, the result is []. If it always returns true, all 2^n - 1 non-empty subsets are returned in the required order. Invalid input, duplicate ids, and more than 25 cards are rejected.

Key Insight / Why This Solution Works

The key idea is to generate combinations in the same order required by the output. The outer loop chooses target size k from 1 through n. For each k, backtracking chooses strictly increasing original indices. The central invariant is that path always contains cards whose original indices are strictly increasing. This guarantees that every predicate call receives a subset in original card order, each combination is generated exactly once, and combinations of one size appear in lexicographic index order. Processing sizes in increasing order gives size-first ordering automatically, so no separate sorting step is needed.

Code
function validCardSubsets(cards, isValid) {
  // Validate the top-level inputs before starting the exponential search.
  if (!Array.isArray(cards)) {
    throw new TypeError('cards must be an array');
  }

  const n = cards.length;

  // The problem allows between 0 and 25 cards.
  if (n > 25) {
    throw new RangeError('cards length must be between 0 and 25');
  }

  if (typeof isValid !== 'function') {
    throw new TypeError('isValid must be a function');
  }

  // Validate each card and reject duplicate ids.
  // This Set is only for input validation.
  const seenIds = new Set();
  for (let i = 0; i < n; i++) {
    const card = cards[i];

    if (card === null || typeof card !== 'object') {
      throw new TypeError('each card must be an object');
    }

    if (typeof card.id !== 'string') {
      throw new TypeError('card id must be a string');
    }

    if (seenIds.has(card.id)) {
      throw new Error('duplicate card id');
    }

    seenIds.add(card.id);
  }

  const results = [];

  // path is the current combination of original card references.
  // Only this private working array is mutated during backtracking.
  const path = [];

  function dfs(start, remaining) {
    // remaining === 0 means the candidate has the exact target size.
    if (remaining === 0) {
      // Give the predicate a complete copy, never the mutable working path.
      const candidate = path.slice();

      // The candidate is already in original card order because its
      // indices were chosen strictly from left to right.
      if (isValid(candidate)) {
        // Store another array copy so later backtracking cannot mutate output.
        results.push(candidate.slice());
      }
      return;
    }

    // Try possible next indices in increasing order.
    // n - remaining is the last index that still leaves enough cards.
    for (let i = start; i <= n - remaining; i++) {
      // Choose this card for the current combination.
      path.push(cards[i]);

      // Continue only with later original indices.
      dfs(i + 1, remaining - 1);

      // Undo the choice before trying the next index.
      path.pop();
    }
  }

  // Generate smaller subsets before larger subsets.
  // Within one size, dfs produces lexicographic original-index order.
  for (let size = 1; size <= n; size++) {
    dfs(0, size);
  }

  return results;
}

// Diagram example: cards A, B, C and a rule that accepts size 2.
const cards = [Object.freeze({ id: 'A' }), Object.freeze({ id: 'B' }), Object.freeze({ id: 'C' })];

const isValid = (subset) => subset.length === 2;
const output = validCardSubsets(cards, isValid);

// Print ids to verify the required result.
console.log(output.map((subset) => subset.map((card) => card.id)));
// [ [ 'A', 'B' ], [ 'A', 'C' ], [ 'B', 'C' ] ]
Time & Space Complexity

Let n be the number of cards. There are 2^n - 1 non-empty subsets. With an arbitrary black-box predicate, the algorithm may need to evaluate every one, so it makes Θ(2^n) predicate calls. Creating candidate arrays and copying subsets can require Θ(n * 2^n) work in the worst case, not including the time spent inside isValid. The recursion stack, current path, temporary candidate array, and Set used for id validation each use at most O(n) memory, so auxiliary space is O(n) beyond the returned output.

Where it is used

This backtracking pattern is useful when software must enumerate combinations while keeping original order. Examples include rule-based bundles, feature combinations, permission sets, test configurations, and other small search spaces where a separate rule decides whether each complete combination is valid.

Why Interviewers Ask This

This problem tests more than basic subset generation. The interviewer can check whether you preserve original order, generate each combination exactly once, satisfy a precise output ordering, isolate mutable backtracking state, validate JavaScript input, and reason correctly about an unavoidable exponential search. It also tests whether you can distinguish predicate-call complexity from subset-copying work and separate auxiliary memory from the potentially exponential output.

Common interview mistakes

Common mistakes include generating cards in an order that lets isValid receive a reordered subset, using an arbitrary powerset order and forgetting the required size-first ordering, storing path directly instead of copying it, forgetting path.pop() after recursion, calling isValid on partial combinations, generating the same subset more than once, failing to reject duplicate ids, and claiming only O(2^n) total work without accounting for the cost of creating and copying subset arrays.

Interview tip

Explain the output ordering before writing the recursion. Say that the outer loop controls subset size and strictly increasing indices control lexicographic order. This makes it easy to show why no final sorting step is needed.

Interviewer may ask next
Can we reduce the Θ(2^n) search if isValid remains an arbitrary black-box predicate?

Not in the general case. Without extra information about isValid, the result for one subset tells us nothing reliable about another subset. A predicate could return false for every checked subset and true for one unchecked subset. Therefore a correct algorithm may need to evaluate all 2^n - 1 non-empty subsets. Auxiliary search space can still stay O(n) beyond output. The tradeoff is that stronger assumptions about the predicate could allow pruning, but the original black-box contract does not.

What changes if the interviewer provides a safe pruning rule for partial subsets?

The same increasing-index backtracking structure can remain, but before exploring a branch we can ask whether the current partial subset can still lead to any valid complete subset. If the pruning rule says no, that branch can stop. Correctness is preserved only if the pruning rule never removes a branch containing a valid answer. Worst-case time is still exponential because pruning may never occur. Auxiliary space remains O(n) beyond output. The tradeoff is extra rule complexity in exchange for potentially much less practical work.

2. Traverse HTML element nodes in depth-first order.CodingEasyGoogle

Question Details

Implement depthFirstElements(root). root is an Element; return a static array containing root and every light-DOM descendant element in pre-order depth-first traversal. Count element children only, preserve DOM sibling order, do not enter shadow roots or iframes, and do not mutate the document. Reject non-elements with TypeError. Avoid recursive call-stack overflow for a depth of 50,000 by using an explicit stack. Example: for <main><section><b></b></section><p></p></main>, the returned tag-name sequence must be MAIN, SECTION, B, P. Time is O(n) and auxiliary space is O(depth).

Short Interview Answer (30-60 seconds)

I would use iterative pre-order depth-first traversal with an explicit stack of traversal frames. I start the result with the root. Each frame stores a parent element and the index of its next child. I process children from left to right, append each child before visiting its descendants, and pop a frame when all of its children are finished. This preserves DOM sibling order without recursion. The time complexity is O(n), and the auxiliary stack space is O(depth).

Detailed Explanation

See the Code while reading this explanation.

The function receives one HTML element. It must return a normal array containing that element and every light-DOM descendant element below it. The order is parent first, then children from left to right. Text and comment nodes do not count. Shadow-root content and iframe documents are not entered. The page must not be changed. A very deeply nested DOM must also work safely, so recursion is avoided. An explicit stack lets us remember where we are while using extra memory based on nesting depth.

Useful Questions to Ask the Interviewer
  1. Should the returned array include the root element itself? The stated contract says yes.
  2. Should only light-DOM element children be visited, while shadow roots and iframe documents stay excluded? The stated contract says yes.
  3. Is O(depth) auxiliary stack space required even when an element has many siblings? The stated contract says yes.
Traverse HTML element nodes in depth-first order. diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is root, and it must be an Element. If it is not an Element, the function throws TypeError.

The output is a static JavaScript array of Element references. It starts with root. After that, it contains every light-DOM descendant element in pre-order depth-first order.

For the example <main><section><b></b></section><p></p></main>, the tag-name order is MAIN, SECTION, B, P.

We use children, so text nodes and comment nodes are ignored. We do not enter shadow roots or iframe documents. We also do not mutate the document.

2. Choose the algorithm and data structure

I use iterative depth-first search with an explicit stack of traversal frames.

Each frame stores two values. parent is the element whose children we are scanning. nextIndex tells us which element child should be processed next.

The key invariant is simple. Every frame points to one active parent and the next unvisited child of that parent. Because nextIndex moves from left to right through parent.children, DOM sibling order is preserved.

This frame design also keeps the auxiliary stack bounded by the active nesting depth.

3. Initialize the state

First, I validate the input.

Then I create result = [root]. The root is already visited because pre-order traversal visits a node before its descendants.

I also create stack = [{ parent: root, nextIndex: 0 }]. This means the root is the active parent, and its first element child is the next child to inspect.

4. Walk through the example

The example tree is MAIN with children SECTION and P. SECTION has child B.

Initial state: result is [MAIN]. The stack is [MAIN:0].

The top frame is MAIN:0. Its first child is SECTION. We advance MAIN's index to 1, append SECTION, and push SECTION:0. Result becomes [MAIN, SECTION].

The top frame is now SECTION:0. Its first child is B. We advance SECTION's index to 1, append B, and push B:0. Result becomes [MAIN, SECTION, B].

B has no element children, so its frame is exhausted and popped. SECTION:1 is also exhausted, so it is popped.

We return to MAIN:1. Its next child is P. We advance MAIN's index to 2, append P, and push P:0. Result becomes [MAIN, SECTION, B, P].

P has no element children, so it is popped. MAIN:2 is now exhausted and is also popped. The stack becomes empty, so traversal stops.

5. Explain why the result is correct

Each frame scans parent.children in increasing index order. Therefore siblings are visited from left to right.

A child is appended to the result before a frame for that child is processed. Therefore every element appears before its descendants. That is exactly pre-order depth-first traversal.

When a frame has no unvisited children left, it is popped. This returns processing to the parent frame. Each element is therefore visited exactly once in the required light-DOM order.

6. Explain the JavaScript implementation

The code first checks root instanceof Element. It then puts root in the result and creates one frame for it.

Inside the loop, the code reads the top frame and its parent.children collection. If nextIndex has reached the number of children, that frame is finished and is popped.

Otherwise, the code reads the next child and increments nextIndex. It appends that child to the result and pushes a new frame for the child. The loop ends when no frames remain.

7. Explain complexity and edge cases

If there are n element nodes in the light-DOM subtree, each one is visited once. The time complexity is O(n).

The explicit frame stack contains at most one frame for each active nesting level. Its auxiliary space is O(depth). The returned array itself contains n element references, but that is required output storage rather than auxiliary stack storage.

A root with no element children returns [root]. Very deep nesting, including a depth of 50,000, does not use the JavaScript recursive call stack. A non-Element input throws TypeError. Shadow roots and iframe documents are not entered.

Key Insight / Why This Solution Works

The key idea is to simulate recursive pre-order DFS with explicit traversal frames. Each frame stores a parent element and nextIndex, the index of the next element child to visit. The top frame represents the current active nesting level. We read parent.children in normal DOM order. Before descending into a child, we advance the parent's index, append that child to the result, and push a new frame for it. When a frame has no children left, we pop it. The central invariant is that every frame points to an active parent and its next unvisited child. This preserves pre-order and sibling order while keeping the stack proportional to DOM depth.

Code
function depthFirstElements(root) {
  // Reject invalid input before creating any traversal state.
  if (!(root instanceof Element)) {
    throw new TypeError('root must be an Element');
  }

  // Pre-order starts with the root itself, so add it immediately.
  const result = [root];

  // Each frame remembers one active parent and the index of its next child.
  // This keeps auxiliary stack usage proportional to nesting depth.
  const stack = [{ parent: root, nextIndex: 0 }];

  while (stack.length > 0) {
    // Read the active frame without removing it yet.
    const frame = stack[stack.length - 1];

    // children contains element children only in normal DOM sibling order.
    const children = frame.parent.children;

    // If every child of this parent has been processed, return to its parent.
    if (frame.nextIndex >= children.length) {
      stack.pop();
      continue;
    }

    // Take the next child from left to right and advance this frame's position.
    const child = children[frame.nextIndex++];

    // Visit the child before its descendants to produce pre-order traversal.
    result.push(child);

    // Descend into the child with a fresh frame starting at its first child.
    stack.push({ parent: child, nextIndex: 0 });
  }

  // Return a static array of Element references in traversal order.
  return result;
}

// Build the exact example as a detached DOM subtree.
const root = document.createElement('main');
root.innerHTML = '<section><b></b></section><p></p>';

// Run the traversal and print the exact tag-name sequence from the diagram.
const elements = depthFirstElements(root);
console.log(elements.map((element) => element.tagName).join(', '));
// MAIN, SECTION, B, P
Time & Space Complexity

Let n be the number of element nodes in the light-DOM subtree. Each element is visited once, so the total time is O(n). Let depth be the maximum element nesting depth. The explicit stack keeps at most one active frame for each nesting level, so auxiliary space is O(depth). The returned array contains all n elements and therefore uses O(n) output space, but that required output array is not counted as auxiliary stack space.

Where it is used

This pattern is useful when browser code must walk a DOM subtree in a predictable parent-before-child order without using recursion. It can be used for DOM inspection, accessibility tooling, testing utilities, static analysis of page structure, and building ordered lists of elements. The traversal-frame approach is especially useful when the DOM can be extremely deep and recursive JavaScript could overflow the call stack.

Why Interviewers Ask This

This question tests whether you understand DOM node types, traversal order, and JavaScript execution limits. The interviewer can see whether you distinguish element children from all child nodes, preserve sibling order, and avoid entering separate trees such as shadow roots or iframe documents. It also tests whether you can replace recursion with an explicit stack, maintain a clear traversal invariant, reason about very deep inputs, and explain why the chosen frame representation gives O(n) time with O(depth) auxiliary space.

Common interview mistakes
  1. Using recursion even though a depth of 50,000 can overflow the JavaScript call stack.
  2. Using childNodes instead of children, which would include text and comment nodes that the problem says not to count.
  3. Appending the root again inside the loop even though result already starts with [root].
  4. Forgetting to advance nextIndex before descending, which can cause the same child to be visited repeatedly.
  5. Traversing into shadowRoot or an iframe's document even though the required traversal is light DOM only.
  6. Claiming O(depth) auxiliary space while using a different pending-node stack that can hold many siblings at the same time.
Interview tip

Explain what one stack frame means before writing the loop: parent is the active element, and nextIndex is its next unvisited element child. Then trace MAIN:0, SECTION:0, and B:0 on the example. This makes the O(depth) space bound and the left-to-right pre-order behavior easy to justify.

Interviewer may ask next
Why use traversal frames instead of pushing all child elements onto a normal node stack?

A normal DFS node stack can preserve pre-order if children are pushed in reverse sibling order, but it may hold many siblings at the same time. Its extra memory can therefore depend on tree width as well as depth. The frame stack stores one active frame per nesting level. Each frame remembers only the parent and the index of its next child. This preserves correctness because children are still read from left to right and each child is visited before its descendants. Time remains O(n), while auxiliary stack space remains O(depth).

What would change if shadow-root descendants also had to be traversed?

The traversal contract would first need to define where a host's shadow-root descendants appear relative to its light-DOM children. The current code intentionally uses only parent.children, so it stays inside light DOM. To include shadow DOM, the frame would need to track the next allowed child source according to that new ordering rule and enter shadowRoot when required. The explicit-frame idea can still be used. If n means all visited elements across those trees, time remains O(n), and auxiliary space remains O(depth) when the frame representation still stores only active nesting levels. The tradeoff is more traversal state and a more complex ordering rule.

3. Select every timeline node that intersects a time-range selection.CodingEasyGoogle

Question Details

Implement selectTimelineNodes(nodes, selectionStart, selectionEnd). nodes is an array of unique records {id:string,start:number,end:number} with finite values and start <= end; it may be unsorted. The selection is a finite half-open interval [selectionStart, selectionEnd) with selectionStart <= selectionEnd. Return the IDs of nodes whose half-open intervals overlap the selection, ordered by increasing start and then original input order. Touching only at an endpoint is not overlap. Do not mutate the array. Reject malformed records or invalid ranges. Example: nodes A:[0,4), B:[4,7), C:[2,5) with selection [4,6) must return ['C','B']. Target O(n log n) time or better.

Short Interview Answer (30-60 seconds)

I would first validate the selection and every node. Then I would keep only non-empty node intervals that truly overlap the selection using the strict half-open interval rule. I store each matching node with its original index, sort the matches by increasing start time and then original input order, and return their IDs. For the example, the result is ['C','B']. The worst-case time is O(n log n), and the auxiliary space is O(n).

Detailed Explanation

See the Code while reading this explanation.

We have a list of timeline items. Each item has an ID, a start time, and an end time. We also have a selected time range. We need to return the IDs of items that share some real time with that range. Simply touching the range at one endpoint does not count. An empty item or empty selection also has no overlap. The input may be in any order. After finding the matching items, we order them by start time. Equal start times keep their original input order.

Useful Questions to Ask the Interviewer
  1. Should an empty selection such as [2,2) return an empty result after all input records are validated?
  2. If two matching nodes have the same start time, should their original input order be preserved?
  3. Should malformed node records cause an error instead of being skipped?
Select every timeline node that intersects a time-range selection. diagram
How to Explain It in an Interview
1. Validate the input first

I first check that nodes is an array. I also check that selectionStart and selectionEnd are finite numbers and that selectionStart <= selectionEnd.

Then I validate every node before returning any result. Each node must be an object with a string id, finite numeric start and end values, and start <= end.

This order matters. Even when the selection is empty, malformed node records must still be rejected.

2. Handle an empty selection

After every node is valid, I check whether selectionStart === selectionEnd.

A half-open range such as [2,2) contains no time at all, so no node can overlap it. I return [] immediately.

3. Use the strict half-open overlap rule

A node can overlap the selection only when the node itself is non-empty and both strict comparisons are true:

node.start < node.end

node.start < selectionEnd

node.end > selectionStart

The strict comparisons are important. A node ending exactly where the selection starts does not overlap. A node starting exactly where the selection ends also does not overlap.

4. Walk through the diagram example

The input order is B:[4,7), A:[0,4), C:[2,5). The selection is [4,6).

For B, 4 < 7, 4 < 6, and 7 > 4, so B overlaps.

For A, its end is exactly 4. The test 4 > 4 is false, so A only touches the selection and is not kept.

For C, 2 < 5, 2 < 6, and 5 > 4, so C overlaps.

The matching nodes are B and C. We then sort them by start time. C starts at 2 and B starts at 4, so the final order is C, B. The returned result is ['C','B'].

5. Preserve the required ordering

For every matching node, I store its ID, start time, and original input index.

I sort first by increasing start. If two starts are equal, I compare the saved original indices. This gives the required tie behavior without changing the original nodes array.

6. Explain why the result is correct

The main invariant is that the match array contains only valid, non-empty nodes that have a real intersection with the selection.

The strict overlap test removes endpoint-only contact. Saving the original index gives the required tie-breaker. Sorting the matches by (start, original index) therefore produces the required output order.

7. Explain complexity and edge cases

Validation takes O(n) time. Filtering also takes O(n) time. If m nodes overlap, sorting the matches takes O(m log m). Since m can equal n, the worst-case total time is O(n log n).

The algorithm stores up to O(n) matching records, so auxiliary space is O(n).

Important edge cases are an empty selection, a zero-length node, endpoint-only contact, negative times, unsorted input, no matching nodes, and matching nodes with the same start time.

Key Insight / Why This Solution Works

The key idea is to separate validation, overlap testing, and ordering. First validate the selection and every node. After validation, an empty selection can immediately return an empty array. For a non-empty selection, keep a node only when the node is non-empty and satisfies node.start < selectionEnd && node.end > selectionStart. This is the strict overlap rule for half-open intervals. Each match keeps its original input index. The invariant is that every stored match is valid, non-empty, and truly overlaps the selection. Sorting by start and then original index gives the required order.

Code
function selectTimelineNodes(nodes, selectionStart, selectionEnd) {
  // Validate the top-level collection before reading or iterating it.
  if (!Array.isArray(nodes)) {
    throw new Error('nodes must be an array');
  }

  // The selection endpoints must both be finite numbers.
  if (
    typeof selectionStart !== 'number' ||
    typeof selectionEnd !== 'number' ||
    !Number.isFinite(selectionStart) ||
    !Number.isFinite(selectionEnd)
  ) {
    throw new Error('invalid selection range');
  }

  // A valid half-open selection cannot run backward.
  if (selectionStart > selectionEnd) {
    throw new Error('invalid selection range');
  }

  // Validate every node before any early return.
  // This means malformed records are still rejected for an empty selection.
  for (const node of nodes) {
    if (
      node === null ||
      typeof node !== 'object' ||
      typeof node.id !== 'string' ||
      typeof node.start !== 'number' ||
      typeof node.end !== 'number' ||
      !Number.isFinite(node.start) ||
      !Number.isFinite(node.end) ||
      node.start > node.end
    ) {
      throw new Error('malformed node');
    }
  }

  // A half-open interval [x, x) is empty, so it overlaps nothing.
  if (selectionStart === selectionEnd) {
    return [];
  }

  // Store matches separately so the original nodes array is not mutated.
  // Save the original index for the required tie-breaker.
  const withIndex = [];

  for (let index = 0; index < nodes.length; index += 1) {
    const node = nodes[index];

    // A node must be non-empty and must cross both selection boundaries.
    // Strict comparisons exclude endpoint-only contact.
    if (node.start < node.end && node.start < selectionEnd && node.end > selectionStart) {
      withIndex.push({ id: node.id, start: node.start, index });
    }
  }

  // Sort by start time. Equal starts keep original input order by index.
  withIndex.sort((a, b) => a.start - b.start || a.index - b.index);

  // Return only the IDs, as required by the problem.
  return withIndex.map((node) => node.id);
}

// Run the same unsorted example shown in the diagram.
const nodes = [
  { id: 'B', start: 4, end: 7 },
  { id: 'A', start: 0, end: 4 },
  { id: 'C', start: 2, end: 5 },
];

console.log(selectTimelineNodes(nodes, 4, 6)); // ['C', 'B']
Time & Space Complexity

Let n be the number of input nodes and m be the number of nodes that overlap the selection. Validating all nodes takes O(n) time. Finding the overlaps also takes O(n) time. Sorting the m matching records takes O(m log m) time. In the worst case, every node matches, so m = n and the total time is O(n log n). The matching-record array can grow to size n, so the auxiliary space is O(n).

Where it is used

This pattern is useful in timeline editors, calendar interfaces, media editors, scheduling tools, chart selections, and other user interfaces where a selected time range must find every item that really overlaps it. Using half-open intervals also gives clear boundary behavior when one item ends exactly where another range begins.

Why Interviewers Ask This

This problem checks whether you understand interval boundaries, especially half-open ranges and endpoint-only contact. It also tests careful validation, preserving original ordering information, avoiding unwanted array mutation, and connecting a sorting rule to an output contract. The interviewer can see whether your JavaScript handles empty intervals correctly and whether your O(n log n) time and O(n) auxiliary-space analysis matches the implementation.

Common interview mistakes

A common mistake is using non-strict comparisons, which incorrectly counts endpoint-only contact as overlap. Another is forgetting that a zero-length node such as [3,3) is empty. Returning [] for an empty selection before validating all node records is also wrong. Sorting the original nodes array violates the no-mutation requirement. Finally, sorting only by start time without keeping the original index can break the required order when two matching nodes have the same start.

Interview tip

State the overlap rule before writing the filtering loop: a node must be non-empty, its start must be before the selection end, and its end must be after the selection start. Then explain that the saved original index is used only as the tie-breaker for equal start times.

Interviewer may ask next
What changes if the matching IDs must be returned in original input order instead of start-time order?

The validation and overlap test stay the same. I would remove the sorting step. Because the filtering pass already visits nodes in original input order, I could append each matching ID directly to the result. The time becomes O(n). Auxiliary space is O(1) excluding the returned array, and the output itself uses O(k) space for k matching IDs. The tradeoff is that the result is no longer ordered by start time.

How would you handle a very large input when only a small number of nodes usually overlap the selection?

With unsorted input, every node still has to be validated and checked, so that part remains O(n). I would continue collecting only the m overlapping nodes and sort those matches. The time is O(n + m log m), which is O(n log n) in the worst case, and auxiliary space is O(m). When m is much smaller than n, this avoids sorting all input nodes. The tradeoff is that unsorted input still requires a full pass.

4. Add an accessible toolbar button to an existing browser document editor.CodingEasyGoogle

Question Details

Implement createEditorButton(toolbar, config) in framework-neutral browser JavaScript. toolbar is an existing Element; config is {label:string, pressed?:boolean, onActivate(nextPressed):void}. Append one native button whose visible text and accessible name are label, expose toggle state with aria-pressed, and toggle that state on click or keyboard activation before calling onActivate. Return {setPressed(value), destroy()}. Multiple instances must be independent. Render labels as text, reject invalid arguments before mutating the DOM, and remove the button and listeners on destroy(). Example: with initial pressed:false, one activation must expose aria-pressed='true' and call onActivate(true) exactly once.

Short Interview Answer (30-60 seconds)

I would validate the toolbar, label, and callback before changing the document. Then I create one native button, render the label with textContent, and keep its toggle state in a private boolean mirrored by aria-pressed. Click, Enter, or Space goes through one activation function that flips the state before calling onActivate. Each instance keeps separate state and listeners. setPressed updates the state without firing the callback, and destroy removes the listeners and button. Each operation is O(1) time and O(1) auxiliary space per instance.

Detailed Explanation

See the Code while reading this explanation.

The function adds one accessible toggle button to an editor toolbar that already exists. The button displays the supplied label and exposes whether it is pressed. Each created button keeps its own state. A click, Enter, or Space activation changes that state first and then calls the supplied callback with the new value. The function also returns one method for changing the state from code and one method for removing the button. Invalid required arguments are rejected before the existing toolbar is changed.

Useful Questions to Ask the Interviewer
  1. Should setPressed call onActivate? The shown design does not. It only changes the stored state and aria-pressed.
  2. Should destroy be safe when called more than once? The shown implementation makes repeated calls harmless.
  3. Should values passed to setPressed be converted to booleans? The shown implementation uses Boolean conversion.
Add an accessible toolbar button to an existing browser document editor. diagram
How to Explain It in an Interview
1. Validate the required inputs

First, check that toolbar is an Element. Then check that config contains a non-empty string label and that onActivate is a function. These checks happen before the button is appended, so an invalid call does not leave a partial control in the existing toolbar.

2. Create the button and initialize its state

Create a native button and set type to button. Set textContent to label, so the label is rendered as text rather than HTML. The visible text also gives the button its accessible name. Store the current pressed state in a private isPressed boolean. Set aria-pressed to "true" or "false" so assistive technology can read the toggle state.

3. Use one activation function

The activate function flips isPressed. It then updates aria-pressed. Only after the state is updated does it call onActivate(isPressed). This order is important because the callback receives the new state and the DOM already exposes that same state.

4. Handle click, Enter, and Space

A click listener calls activate. The keydown listener handles Enter and Space. It calls preventDefault and then calls the same activate function. Using the same activation function keeps the state-change logic in one place. In the diagram example, pressed starts as false. One activation changes aria-pressed to "true" and calls onActivate(true) exactly once.

5. Support programmatic updates and cleanup

setPressed(value) converts value to a boolean, stores it in isPressed, and updates aria-pressed. It does not call onActivate because it is a programmatic state change. destroy removes both listeners and removes the button when it is still attached. The parent check makes repeated destroy calls harmless.

6. Explain independence, correctness, and complexity

Every createEditorButton call creates a new isPressed variable, button element, and pair of listeners. Therefore, multiple instances do not share state. The main invariant is that isPressed and aria-pressed always represent the same boolean state. Creation, activation, setPressed, and destroy each perform a fixed amount of work. They are O(1) time, with O(1) auxiliary space per button instance.

Key Insight / Why This Solution Works

The solution uses one private boolean for each button instance. aria-pressed always mirrors that boolean. After validation, the function creates a native button, writes the label with textContent, initializes the toggle state, and appends the button to the supplied toolbar. Click, Enter, and Space all reach the same activate function. activate flips the private state, updates aria-pressed, and then calls onActivate with the new value. setPressed changes the same state without invoking the callback. destroy removes both event listeners and the element. The central invariant is that the internal state and aria-pressed always stay synchronized.

Code
function createEditorButton(toolbar, config) {
  // Reject an invalid toolbar before changing the existing document editor.
  if (!(toolbar instanceof Element)) {
    throw new TypeError('toolbar must be an Element');
  }

  // The diagram requires a config object with a non-empty text label.
  if (!config || typeof config.label !== 'string' || config.label.trim() === '') {
    throw new TypeError('config.label must be a non-empty string');
  }

  const { label, pressed = false, onActivate } = config;

  // Activation cannot work without the required callback.
  if (typeof onActivate !== 'function') {
    throw new TypeError('onActivate must be a function');
  }

  // Keep one private boolean for this button instance only.
  let isPressed = !!pressed;

  // Create one native button so it has normal button semantics.
  const button = document.createElement('button');
  button.type = 'button';

  // Render the supplied label as text, not as HTML.
  button.textContent = label;

  // Expose the initial toggle state to assistive technology.
  button.setAttribute('aria-pressed', isPressed ? 'true' : 'false');

  // Keep the DOM attribute synchronized with the private boolean state.
  function updateAria() {
    button.setAttribute('aria-pressed', isPressed ? 'true' : 'false');
  }

  // All user activation paths use the same state-change order.
  function activate() {
    isPressed = !isPressed;
    updateAria();

    // The callback runs only after the new state is visible in the DOM.
    onActivate(isPressed);
  }

  // Pointer activation uses the shared activation function.
  function onClick() {
    activate();
  }

  // Handle the two standard keyboard activation keys shown in the diagram.
  function onKeyDown(event) {
    if (event.key === ' ' || event.key === 'Enter') {
      // Suppress the native default path because this handler activates directly.
      event.preventDefault();
      activate();
    }
  }

  // Install both listeners before exposing the button in the toolbar.
  button.addEventListener('click', onClick);
  button.addEventListener('keydown', onKeyDown);

  // Mutate the supplied toolbar only after validation and setup are complete.
  toolbar.appendChild(button);

  function setPressed(value) {
    // Programmatic updates coerce the value and do not call onActivate.
    isPressed = !!value;
    updateAria();
  }

  function destroy() {
    // Remove every listener owned by this instance.
    button.removeEventListener('click', onClick);
    button.removeEventListener('keydown', onKeyDown);

    // Remove the element when it is still attached; repeated calls stay harmless.
    if (button.parentNode) {
      button.parentNode.removeChild(button);
    }
  }

  return { setPressed, destroy };
}

// Verified diagram example: pressed starts false and activation happens once.
const toolbar = document.createElement('div');
document.body.appendChild(toolbar);

const calls = [];
const api = createEditorButton(toolbar, {
  label: 'Bold',
  pressed: false,
  onActivate(nextPressed) {
    // Record the callback value so the example can verify exactly one call.
    calls.push(nextPressed);
  },
});

const button = toolbar.querySelector('button');

// One click changes false to true and invokes onActivate(true) once.
button.click();

console.log(button.textContent); // "Bold"
console.log(button.getAttribute('aria-pressed')); // "true"
console.log(calls); // [true]

// Remove this example instance and its listeners.
api.destroy();
toolbar.remove();
Time & Space Complexity

Button creation takes O(1) time because it performs a fixed number of checks, assignments, and DOM operations. Each activation is also O(1) because it flips one boolean, updates one attribute, and calls one callback. setPressed and destroy are O(1) as well. Auxiliary space is O(1) per button instance because each instance stores only a fixed number of variables, functions, and one button reference. If many independent buttons are created, total memory grows with the number of instances.

Where it is used

This pattern is useful for editor toolbar controls such as Bold, Italic, Underline, Track Changes, alignment modes, and other on-or-off commands. It is also useful for small framework-neutral browser components that need accessible toggle state, keyboard activation, independent state, programmatic updates, and explicit cleanup.

Why Interviewers Ask This

This problem checks practical frontend skills rather than a complex data structure. The interviewer can see whether you understand DOM creation, native button semantics, ARIA toggle state, keyboard events, event ordering, and cleanup. It also tests whether you render untrusted labels as text, isolate state between multiple component instances, and design a small public API whose programmatic update and destroy behavior are predictable.

Common interview mistakes

One mistake is calling onActivate before aria-pressed is updated. The required order is state change first and callback second. Another mistake is using innerHTML instead of textContent, which treats the supplied label as markup. Candidates may also forget preventDefault in the custom Enter and Space key handler, which can allow the browser's native activation behavior to run as well. Other common mistakes are sharing state between instances, calling onActivate inside setPressed, changing the toolbar before validating required inputs, or removing the button without removing its event listeners.

Interview tip

Explain the activation order clearly: change the private boolean, update aria-pressed, and only then call onActivate. Also point out that click, Enter, and Space all reuse the same activate function, so the behavior stays consistent.

Interviewer may ask next
What would change if setPressed also had to call onActivate?

setPressed would first convert the supplied value to a boolean and update aria-pressed, just as it does now. After the DOM state is current, it would call onActivate(isPressed). I would also clarify whether setting the same value should still notify the callback. The operation would remain O(1) time and O(1) auxiliary space. The main tradeoff is that a programmatic state update would now have an application side effect, which could create feedback loops if the caller also responds by calling setPressed again.

How would you add a disabled state to this button?

I would add a disabled option and update the native button.disabled property. A native disabled button already blocks normal user activation, so the activation logic does not need a separate custom disabled-event system. I would decide with the interviewer whether setPressed is still allowed while disabled. Enabling or disabling the button would take O(1) time and O(1) auxiliary space. The advantage is that native disabled semantics remain available to the browser and assistive technology.

5. Implement a delayed-callback wrapper in browser JavaScript.CodingEasyGoogle

Question Details

Implement delayCall(fn, delayMs). fn must be callable and delayMs must be a finite number from 0 through 60,000. Return a normal function that accepts arbitrary arguments and, each time it is called, schedules one browser timeout that later invokes fn with the call-time this value and arguments. Calls are independent; do not debounce or cancel earlier calls. Return the timeout identifier from each wrapper call. Throw TypeError for an invalid function or non-number delay and RangeError for an out-of-range delay. Use only setTimeout. Example: const later = delayCall((x) => log(x), 10); later('A'); must schedule exactly one later call equivalent to log('A'). Each wrapper call is O(1).

Short Interview Answer (30-60 seconds)

I would validate the function and delay first. Then I return a normal wrapper function. Every wrapper call captures its own call-time this value and arguments, schedules exactly one setTimeout, and returns that timeout identifier. When the timer fires, it calls the original function with fn.apply(context, args). Calls stay independent, so nothing is debounced or cancelled. Under the diagram's per-call model, each wrapper call takes O(1) time and O(1) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

This problem asks us to create a function that delays another function call. The caller gives us a function and a delay from 0 through 60,000 milliseconds. We return a new normal function. Every time that returned function is called, it creates one timer. Later, the original function runs with the same this value and arguments from that call. Different calls must not replace or cancel each other. Each wrapper call must also return its timer identifier.

Useful Questions to Ask the Interviewer
  1. Should every wrapper call create a separate timeout even when another timeout is already waiting? Yes. The stated contract requires independent calls.
  2. Should the wrapper preserve the this value from the moment the wrapper is called? Yes. That call-time value must be forwarded to fn.
Implement a delayed-callback wrapper in browser JavaScript. diagram
How to Explain It in an Interview
1. Validate the inputs

First, check fn. If it is not a function, throw TypeError. Next, check delayMs. It must be a number and it must be finite. Values such as NaN, Infinity, and -Infinity are invalid and cause TypeError. After that, check the range. A delay below 0 or above 60,000 causes RangeError.

2. Return a normal wrapper function

delayCall does not run fn immediately. It returns wrapper(...args). It must be a normal function instead of an arrow function because a normal function receives its own call-time this value. The wrapper can therefore capture both this and the arguments from each individual call.

3. Capture the call-time state

Inside each wrapper call, save this in context. The rest parameter ...args collects the arguments for that specific call. This state belongs to that invocation. A later wrapper call gets its own context and arguments, so the calls do not overwrite one another.

4. Schedule exactly one timeout per call

The wrapper calls setTimeout exactly once. The timeout callback later runs fn.apply(context, args). apply is important because it calls fn with the captured this value and passes the captured arguments in the same order. The wrapper immediately returns the timeout identifier returned by setTimeout.

5. Walk through the verified example

The diagram uses log(x) to push values into logs, then creates later = delayCall(log, 10). Calling later('A'), later('B'), and later('C') schedules three independent timers and returns three timeout identifiers. After about 10 milliseconds, each timer callback invokes log with the argument captured by that wrapper call. The diagram shows the resulting log values as A, B, and C, while noting that relative timer execution order should not be treated as guaranteed.

6. Explain why the result is correct

The central invariant is that every wrapper invocation owns its own captured this value, captured arguments, and timeout. The implementation never stores one shared pending timer and never calls a cancellation API. Therefore each wrapper call independently schedules one later invocation of fn with the correct call-time state and returns the required timeout identifier.

7. Explain complexity and edge cases

Each wrapper call performs a fixed sequence of operations in the diagram: capture the call state, call setTimeout once, and return the timeout identifier. Under that per-call model, this is O(1) time and O(1) auxiliary space. A delay of 0 is valid and still runs asynchronously. A delay of 60,000 is also valid. A non-function fn or non-finite/non-number delay causes TypeError. A delay outside [0, 60000] causes RangeError.

Key Insight / Why This Solution Works

The key idea is one independent timer per wrapper invocation. delayCall validates its inputs once and returns a normal function. Each call to that wrapper captures its own call-time this value and argument list, then schedules one setTimeout. When the timeout callback runs, fn.apply(context, args) restores the captured receiver and arguments. The central invariant is: one wrapper call owns one captured call state and one timeout. Because there is no shared pending timer and no cancellation, calls remain independent.

Code
function delayCall(fn, delayMs) {
  // Validate the callback because the wrapper must invoke a callable value.
  if (typeof fn !== 'function') {
    throw new TypeError('fn must be a function');
  }

  // The delay must be a finite JavaScript number.
  // This rejects strings, NaN, Infinity, and -Infinity.
  if (typeof delayMs !== 'number' || !Number.isFinite(delayMs)) {
    throw new TypeError('delayMs must be a finite number');
  }

  // The allowed delay range is inclusive: 0 through 60,000 ms.
  if (delayMs < 0 || delayMs > 60000) {
    throw new RangeError('delayMs must be between 0 and 60000 (inclusive)');
  }

  // Use a normal function so each call has its own call-time `this` value.
  return function wrapper(...args) {
    // Capture the receiver for this specific wrapper invocation.
    const context = this;

    // Schedule exactly one independent browser timeout for this call.
    const id = setTimeout(function callback() {
      // Restore the captured `this` value and pass this call's arguments.
      fn.apply(context, args);
    }, delayMs);

    // Return the timeout identifier required by the contract.
    return id;
  };
}

// Verified example from the diagram.
const logs = [];
function log(x) {
  // Record the value received when this timer callback runs.
  logs.push(x);
}

const later = delayCall(log, 10);

// Each call schedules one independent timeout and returns its identifier.
const id1 = later('A');
const id2 = later('B');
const id3 = later('C');

// id1, id2, and id3 are the three timeout identifiers.
// After the scheduled callbacks run, logs contains the three values
// produced by these independent calls: 'A', 'B', and 'C'.
Time & Space Complexity

The diagram treats each wrapper call as constant work. It captures the call-time state, calls setTimeout once, and returns the timeout identifier. That is O(1) time per wrapper call. The diagram also gives O(1) auxiliary space per call under its per-call model because the wrapper keeps only a fixed set of references for that scheduled callback. The browser manages the timer itself.

Where it is used

This pattern is useful when browser work must be delayed without combining separate requests. Examples include scheduling a delayed UI action, postponing a callback after an event, or building a small timing helper that must keep the caller's this value and arguments. It is different from debounce because every call remains active and gets its own timeout.

Why Interviewers Ask This

This question checks whether you understand JavaScript function calls, this, closures, rest arguments, and browser timers. It also tests whether you follow an exact API contract instead of changing it into debounce behavior. The interviewer can see whether you validate finite numbers correctly, distinguish TypeError from RangeError, preserve call-time state, return the timeout identifier, and explain the per-call complexity accurately.

Common interview mistakes

A common mistake is returning an arrow function as the wrapper. An arrow function would not receive its own call-time this value. Another mistake is calling fn(...args) and losing the required receiver instead of using fn.apply(context, args). Some candidates accidentally implement debounce by storing one timer and cancelling the previous one, but this problem requires all calls to stay independent. It is also easy to accept NaN or Infinity by checking only typeof delayMs === 'number'. Finally, the wrapper must return the timeout identifier from every call.

Interview tip

State the key invariant before coding: "Every wrapper call keeps its own this, arguments, and timeout." That makes the normal wrapper function, fn.apply(context, args), and the no-debounce requirement easy to justify.

Interviewer may ask next
How would the implementation change if repeated calls should debounce instead of staying independent?

The returned wrapper would keep one shared timeout identifier in its closure. On each call, it would cancel the previous timer and schedule a new one using the newest this value and arguments. Correctness changes because only the most recent pending call is allowed to run. Each call would still do O(1) scheduling work and use O(1) auxiliary state under the same per-call model. The tradeoff is that earlier pending calls are intentionally discarded.

What happens when delayMs is 0?

Zero is valid because the allowed range includes 0. The wrapper still calls setTimeout, so fn does not run synchronously inside the wrapper call. The callback becomes eligible to run later when the browser event loop can process it. The wrapper still returns the timeout identifier immediately. The per-call complexity remains O(1) time and O(1) auxiliary space under the diagram's model.

6. Implement an outline view for a document from its heading elements.CodingEasyGoogle

Question Details

Implement buildDocumentOutline(root), where root is a Document or Element. Read descendant h1 through h6 elements in document order and return an array of records {level, text, element}. level is the numeric heading level, text is trimmed textContent, and element is the original heading. Ignore headings whose trimmed text is empty. Do not mutate the DOM, enter shadow roots, or infer missing headings. Reject an invalid root with TypeError. Example: <h2>Intro</h2><h3>Setup</h3><h2>API</h2> must return levels and text [{2,'Intro'}, {3,'Setup'}, {2,'API'}] while preserving the corresponding element references. Run in O(n) time over visited descendants.

Short Interview Answer (30-60 seconds)

I would first validate that the root is a Document or Element. Then I would use querySelectorAll to collect descendant h1 through h6 elements in document order. For each heading, I trim its text, skip it if the text is empty, get the numeric level from the tag name, and keep the original element reference. I never change the DOM or enter shadow roots. The time is O(n) over visited descendants, and the additional storage, including matched references and the result, is O(h).

Detailed Explanation

See the Code while reading this explanation.

The goal is to create a simple outline from the headings inside a document or one element. We read headings from top to bottom in the same order they appear. For every heading, we keep its heading number, its cleaned text, and the original heading itself. A heading containing only spaces is ignored. We do not create missing heading levels or change anything on the page. The browser can collect the needed headings in their existing order, so one DOM query followed by one simple loop fits the problem well.

Useful Questions to Ask the Interviewer
  1. Should a heading with only whitespace be ignored after trimming its text?
  2. Should the element field keep the exact original DOM element reference?
  3. Should any root that is not a Document or Element throw TypeError?
Implement an outline view for a document from its heading elements. diagram
How to Explain It in an Interview
1. Validate the input

The function accepts a Document or an Element. If the root is neither one, I throw TypeError. This stops invalid values before the code tries to call DOM methods on them.

2. Collect only heading elements in document order

I call root.querySelectorAll('h1, h2, h3, h4, h5, h6'). This returns a static NodeList containing matching descendant headings in document order. It does not include normal non-heading elements, and this query does not cross shadow-root boundaries.

3. Convert each non-empty heading into an outline record

I iterate through the heading-only NodeList. For each heading, I read textContent and trim it. If the trimmed text is empty, I skip that heading. Otherwise, I read the heading level from its tag name. For example, H3 gives level 3. I then push { level, text, element }, where element is the original heading object.

4. Walk through the exact example

The example is <h2>Intro</h2><h3>Setup</h3><h2>API</h2>. First, Intro is an H2, so its level is 2. Next, Setup is an H3, so its level is 3. Finally, API is an H2, so its level is 2. The result stays in that same order: Intro, Setup, API. Each record also keeps the corresponding original heading reference.

5. Explain why the result is correct

The main invariant is that after each loop iteration, the result contains exactly the earlier non-empty matching headings in their original document order. Every stored level comes from that heading's tag name. Every stored text value comes from that heading's trimmed textContent. Every stored element is the same DOM element that was visited. We never infer missing levels, mutate the DOM, or enter shadow roots.

6. Explain the implementation, complexity, and edge cases

The code validates the root, creates the heading NodeList, creates an empty result array, processes every matched heading, and returns the result. The time is O(n) over visited descendants. If h headings match, the static NodeList can hold up to h element references and the result can contain up to h records, so the additional/result storage is O(h). Invalid roots throw TypeError. Empty headings are skipped. Deeply nested headings still follow document order.

Key Insight / Why This Solution Works

The key idea is to ask the browser for exactly the descendant heading elements that matter. querySelectorAll('h1, h2, h3, h4, h5, h6') already returns those headings in document order, so no sorting or outline-tree reconstruction is needed. The central invariant is: after processing any heading, the result contains exactly the non-empty headings processed so far, in the same order, with the correct numeric level, trimmed text, and original element reference. This directly satisfies the required output contract.

Code
function buildDocumentOutline(root) {
  // Reject values that do not provide the required Document or Element DOM contract.
  if (!(root instanceof Document) && !(root instanceof Element)) {
    throw new TypeError('root must be a Document or Element');
  }

  // Collect only descendant h1-h6 elements in document order.
  // querySelectorAll returns a static NodeList and does not cross shadow-root boundaries.
  const headings = root.querySelectorAll('h1, h2, h3, h4, h5, h6');

  // Store outline records in the same order as the matching headings.
  const result = [];

  for (const el of headings) {
    // Trim text so headings containing only whitespace are ignored.
    const text = (el.textContent || '').trim();
    if (text === '') continue;

    // tagName is H1 through H6, so its second character is the numeric level.
    const level = Number(el.tagName[1]);

    // Keep the original DOM element reference. Do not clone or mutate the heading.
    result.push({ level, text, element: el });
  }

  // Return the collected headings in their original document order.
  return result;
}

// Run the exact example from the diagram.
const root = document.createElement('div');
root.innerHTML = '<h2>Intro</h2><h3>Setup</h3><h2>API</h2>';

const outline = buildDocumentOutline(root);
console.log(outline);

// Show the level and text values from the required example result.
console.log(outline.map(({ level, text }) => ({ level, text })));
// [
//   { level: 2, text: 'Intro' },
//   { level: 3, text: 'Setup' },
//   { level: 2, text: 'API' }
// ]

// Each outline[i].element is the corresponding original heading element.
console.log(outline[0].element === root.children[0]); // true
console.log(outline[1].element === root.children[1]); // true
console.log(outline[2].element === root.children[2]); // true
Time & Space Complexity

Let n be the number of descendants the selector may need to visit, and let h be the number of matching h1 through h6 elements. The time is O(n) over visited descendants. querySelectorAll creates a static NodeList with up to h heading references, and the returned outline can contain up to h records. Therefore the additional/result storage is O(h). No copied DOM tree, sorting structure, recursion stack, map, or set is needed.

Where it is used

This pattern is useful for table-of-contents features, document outline sidebars, rich-text editors, page navigation tools, and accessibility helpers. It works well when headings already exist in the DOM and the application needs an ordered outline without changing the page.

Why Interviewers Ask This

This problem checks practical DOM knowledge rather than a complicated data structure. The interviewer can see whether you understand CSS selectors, document order, textContent, DOM element identity, and shadow-root boundaries. It also tests whether you follow an exact API contract, reject invalid input correctly, avoid unnecessary DOM mutation, and explain complexity carefully. A strong answer shows that you can use browser APIs directly without adding extra hierarchy-building logic that the question never requested.

Common interview mistakes

A common mistake is iterating every descendant even though the selected querySelectorAll already returns only headings. Another is forgetting to trim textContent, which incorrectly keeps whitespace-only headings. Candidates may return only level and text and lose the required original element reference. They may also try to infer missing heading levels, even though the problem forbids that. Manually entering shadow roots or mutating the DOM also breaks the contract. Finally, the memory explanation should account for the static NodeList and returned records.

Interview tip

Explain that querySelectorAll already gives exactly the matching h1 through h6 descendants in document order. Then show that the loop only trims text, skips empty headings, reads the level, and stores the original element reference. That makes both the implementation and correctness argument easy to follow.

Interviewer may ask next
What changes if the caller wants an outline only for headings inside one section of the page?

The algorithm does not need to change. Pass that section Element as root. querySelectorAll then searches only that element's descendants. The same document-order traversal, text trimming, level extraction, empty-heading filtering, and original-reference behavior still apply. The time is O(n) over descendants visited inside that section, and the additional/result storage is O(h) for its matching headings.

What changes if headings inside open shadow roots must also be included?

The current single querySelectorAll call is no longer enough because it does not cross shadow-root boundaries. I would explicitly traverse normal descendants and recursively visit an element's accessible shadowRoot when one exists. I would still apply the same h1-through-h6 test, trimming rule, level extraction, and original-reference rule. The time remains O(n) over all visited nodes. The output uses O(h) space, and the explicit traversal also needs stack or queue space depending on its implementation.

7. Determine whether four ordered numbers can evaluate to 24.CodingMediumGoogle

Question Details

Implement canMake24InOrder(numbers). numbers is an array of exactly four finite integers from 1 through 9. Insert the binary operators +, -, *, and / between values and choose any valid parenthesization, but do not reorder or concatenate numbers. Intermediate values are rational; avoid floating-point equality errors by representing exact fractions. Division by zero is invalid. Return a Boolean and do not mutate the input. Example: [6,1,3,4] can return true because an order-preserving expression exists, while a case with no exact expression returns false. Enumerate all operator choices and binary parenthesizations; the input size is fixed, so space is O(1).

Short Interview Answer (30-60 seconds)

I would try every valid expression while keeping the four numbers in their original order. There are five binary parenthesizations and 64 operator triples, so at most 320 expression forms are possible. I represent every intermediate value as a reduced fraction, which avoids floating-point equality errors and lets me reject division by zero safely. I return true as soon as an exact result is 24. Because the input always contains exactly four numbers, both time and auxiliary space are O(1).

Detailed Explanation

See the Code while reading this explanation.

We have exactly four numbers. We must keep them in the given order. We can put +, -, *, or / between them and add parentheses in any valid way. We need to decide whether one expression gives exactly 24. Division can create values that are difficult to compare safely with normal decimal arithmetic, so we keep every value as an exact fraction. There are only five parenthesizations and 64 operator choices for each one, so checking every possibility is small and complete.

Useful Questions to Ask the Interviewer
  1. Must the four numbers stay in exactly their original order? Yes. The problem does not allow reordering.
  2. Should intermediate division use exact arithmetic instead of floating-point comparison? Yes. The problem requires exact rational values.
Determine whether four ordered numbers can evaluate to 24. diagram
How to Explain It in an Interview
1. Understand the input and output

The input is an array of exactly four integers from 1 through 9. We do not mutate this array. We place one binary operator between each neighboring value and may choose any valid binary parenthesization. The output is a Boolean. We return true when at least one valid order-preserving expression equals exactly 24. Otherwise, we return false.

2. Enumerate every valid expression

Four ordered values have exactly five binary parenthesizations. There are three operator positions, and each position has four choices: +, -, *, or /. That gives 4^3 = 64 operator triples for each parenthesization. Therefore, there are at most 5 × 64 = 320 expression forms to test.

3. Keep every intermediate result exact

Each value is represented as a numerator and denominator. For example, 6 is stored as 6/1. After an operation, the fraction is reduced with the greatest common divisor, and the denominator is kept positive. If a division would use a zero-valued right operand, that expression is invalid and is skipped. This avoids floating-point equality errors.

4. Walk through the diagram example

The input is [6, 1, 3, 4]. One successful parenthesization is a / (b - (c / d)), with operators /, -, /. First, 3 / 4 = 3/4. Next, 1 - 3/4 = 1/4. Then, 6 / (1/4) = 24/1. Since 24/1 is exactly the target 24/1, the function returns true immediately. No later expression forms need to be checked after this match.

5. Explain why the algorithm is correct

Every valid expression that preserves the order of the four values uses one of the five binary parenthesizations. Every choice of the three operators is one of the 64 operator triples. The algorithm covers both complete sets. Exact fraction arithmetic preserves the true mathematical value of every intermediate expression. Therefore, the search returns true exactly when a valid order-preserving expression evaluates to 24.

6. Explain the JavaScript implementation

The code defines helpers for greatest common divisor, normalized fractions, and applying one operator to two fractions. It converts the four inputs to exact BigInt fractions without changing the original array. Three nested loops enumerate all operator triples. For each triple, the five valid parenthesizations are evaluated in order. After each one, the code checks for exactly 24 and returns true immediately when a match is found. If every valid combination fails, it returns false.

7. Explain complexity and edge cases

At most 320 expression forms are possible. Each one needs only a fixed number of arithmetic operations, so this problem takes O(1) time because the input always has exactly four numbers. The algorithm also uses only a fixed number of fraction objects and temporary values, so auxiliary space is O(1). Important cases are division by zero, negative or fractional intermediate values, exact fraction comparison, and the case where no expression reaches 24.

Key Insight / Why This Solution Works

The key idea is exhaustive search over a very small fixed search space. The four input values always stay in their original order. We enumerate all five binary parenthesizations and all 64 operator triples. Every intermediate result is stored as a normalized exact fraction. The central invariant is that each stored fraction equals the exact mathematical value of the expression subtree that produced it. Because every valid ordered expression belongs to one of the five parenthesizations and one of the 64 operator triples, checking all of them is complete. The search stops as soon as one exact result is 24.

Code
function canMake24InOrder(numbers) {
  // Find the greatest common divisor so every fraction can be reduced.
  const gcd = (a, b) => {
    a = a < 0n ? -a : a;
    b = b < 0n ? -b : b;

    // Euclid's algorithm repeatedly reduces the pair until the remainder is zero.
    while (b !== 0n) {
      [a, b] = [b, a % b];
    }
    return a;
  };

  // Create one exact normalized fraction.
  // null means the denominator was zero, so that expression is invalid.
  const frac = (numerator, denominator = 1n) => {
    if (denominator === 0n) return null;

    // Keep the denominator positive so equivalent fractions have one form.
    if (denominator < 0n) {
      numerator = -numerator;
      denominator = -denominator;
    }

    const divisor = gcd(numerator, denominator);
    return {
      n: numerator / divisor,
      d: denominator / divisor,
    };
  };

  // Apply one binary operator to two exact fractions.
  // An invalid earlier calculation is carried forward as null.
  const apply = (left, operator, right) => {
    if (left === null || right === null) return null;

    if (operator === '+') {
      return frac(left.n * right.d + right.n * left.d, left.d * right.d);
    }

    if (operator === '-') {
      return frac(left.n * right.d - right.n * left.d, left.d * right.d);
    }

    if (operator === '*') {
      return frac(left.n * right.n, left.d * right.d);
    }

    // Division by a zero-valued fraction is invalid.
    if (right.n === 0n) return null;
    return frac(left.n * right.d, left.d * right.n);
  };

  // Convert the four inputs to fractions without mutating the input array.
  const [a, b, c, d] = numbers.map((value) => frac(BigInt(value)));
  const operators = ['+', '-', '*', '/'];

  // Because fractions are exact, this checks mathematical equality with 24.
  const is24 = (value) => value !== null && value.n === 24n * value.d;

  // Enumerate all 4^3 = 64 operator triples.
  for (const op1 of operators) {
    for (const op2 of operators) {
      for (const op3 of operators) {
        // Shape 1: (((a op1 b) op2 c) op3 d)
        const shape1 = apply(apply(apply(a, op1, b), op2, c), op3, d);
        if (is24(shape1)) return true;

        // Shape 2: ((a op1 (b op2 c)) op3 d)
        const shape2 = apply(apply(a, op1, apply(b, op2, c)), op3, d);
        if (is24(shape2)) return true;

        // Shape 3: (a op1 ((b op2 c) op3 d))
        const shape3 = apply(a, op1, apply(apply(b, op2, c), op3, d));
        if (is24(shape3)) return true;

        // Shape 4: (a op1 (b op2 (c op3 d)))
        // For [6, 1, 3, 4] with /, -, /, this becomes 6 / (1 - (3 / 4)).
        const shape4 = apply(a, op1, apply(b, op2, apply(c, op3, d)));
        if (is24(shape4)) return true;

        // Shape 5: ((a op1 b) op2 (c op3 d))
        const shape5 = apply(apply(a, op1, b), op2, apply(c, op3, d));
        if (is24(shape5)) return true;
      }
    }
  }

  // Every valid ordered expression was tested and none equaled 24.
  return false;
}

// Same successful example shown in the diagram.
console.log(canMake24InOrder([6, 1, 3, 4])); // true

// Additional checks shown in the final diagram.
console.log(canMake24InOrder([1, 2, 1, 1])); // false
console.log(canMake24InOrder([9, 9, 9, 9])); // false
Time & Space Complexity

There are exactly five binary parenthesizations and 64 operator triples. That means at most 5 × 64 = 320 expression forms are checked. Every expression needs only a constant number of exact-fraction operations. Since the problem always gives exactly four numbers, this fixed amount of work is O(1) time. The code also keeps only a fixed number of fractions and temporary values, so auxiliary space is O(1).

Where it is used

This exhaustive-search pattern is useful when the number of possibilities is small and fixed. It is often simpler and safer to test every valid combination than to build a more complicated optimization. Exact fraction arithmetic is useful in calculators, rule engines, puzzle solvers, and other software where rational values must be compared exactly.

Why Interviewers Ask This

This problem tests whether you can systematically cover a small combinatorial search space without missing cases. It also checks whether you preserve the required input order, reason about all binary parenthesizations, handle invalid division, and avoid floating-point precision bugs. The interviewer can also evaluate your JavaScript BigInt usage, early-return reasoning, correctness argument, and ability to explain why a fixed input size gives O(1) time and space.

Common interview mistakes

One common mistake is using normal floating-point arithmetic and comparing a result directly with 24. Rational intermediate values can have rounding errors. Another mistake is forgetting one of the five binary parenthesizations. Reordering the four input numbers also breaks the problem rules. Candidates may forget to reject division by a zero-valued intermediate result. Another mistake is continuing to process expressions after a valid result has already been found. Finally, the complexity should be O(1) for this exact fixed-size problem.

Interview tip

Before writing code, explain that four ordered values have only five binary parenthesizations and 64 operator triples. Then show why exact fractions are needed. This makes both the completeness argument and the code structure easy for the interviewer to follow.

Interviewer may ask next
How would you return one actual expression that evaluates to 24 instead of only true or false?

Keep the same five parenthesizations and 64 operator triples, but carry an expression string together with each exact fraction. When a result equals 24, return that expression instead of true. Correctness stays the same because the search still covers every valid ordered expression. With exactly four inputs, time remains O(1) and auxiliary space remains O(1). The tradeoff is extra string construction and slightly more temporary storage.

What changes if the input contains a variable number n of ordered values instead of exactly four?

The same exhaustive idea can enumerate every order-preserving binary expression tree and every operator assignment, but the search is no longer constant. There are Catalan(n - 1) binary parenthesizations and 4^(n - 1) operator assignments. If each expression is evaluated from scratch in O(n) work, time is O(Catalan(n - 1) × 4^(n - 1) × n). A recursive evaluator uses O(n) stack space for one expression path. Correctness is preserved because every valid ordered binary expression is still covered. The tradeoff is exponential growth as n increases.

8. Implement the reveal operation for a Minesweeper board.NEWCodingMediumGoogle

Question Details

Implement revealMinesweeper(board, row, column). board is a rectangular array of 1 through 500 rows and columns containing '*' for a bomb or an integer 0 through 8 giving adjacent-bomb count. The coordinates are valid zero-based indexes. Return {gameOver:boolean, revealed:Array<[row,column]>}. If the chosen cell is a bomb, return only that coordinate with gameOver:true. Otherwise reveal it; whenever a revealed value is 0, reveal all eight-direction neighbors and continue through connected zeroes, including their numbered boundary cells. Return coordinates once each in breadth-first discovery order, do not mutate board, and avoid recursive overflow. Example: clicking an isolated numbered cell reveals one tile; clicking a zero expands its zero region and boundary.

Short Interview Answer (30-60 seconds)

I would use iterative breadth-first search. If the clicked cell is a bomb, I return that coordinate immediately with gameOver true. Otherwise, I put the starting cell in a queue and mark it visited. When I remove a cell, I reveal it. Only a 0 cell adds its eight unvisited neighbors. Marking cells when they enter the queue prevents duplicates. This preserves breadth-first discovery order. The worst-case time is O(R × C), and the auxiliary space is O(R × C).

Detailed Explanation

See the Code while reading this explanation.

The board contains bombs and numbers. A click can end the game or reveal part of the board. If the clicked cell is a bomb, we return only that cell and say the game is over. Otherwise, we reveal the clicked cell. A 0 means the safe empty area continues, so we also reveal its eight-direction neighbors. Connected 0 cells continue this process. Numbered cells around that region are revealed but do not expand. We must return each revealed coordinate once, in breadth-first discovery order, without changing the board.

Useful Questions to Ask the Interviewer
  1. Should diagonal cells count as neighbors? Yes. The problem says to use all eight directions.
  2. Does the returned order matter? Yes. Coordinates must be returned in breadth-first discovery order.
  3. May I modify the board to mark visited cells? No. The board must remain unchanged.
Implement the reveal operation for a Minesweeper board. diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is a rectangular board plus a valid zero-based row and column. Each board cell is either "*" or an integer from 0 through 8. The output is {gameOver:boolean, revealed:Array<[row,column]>}. If the clicked cell is a bomb, we return only that coordinate with gameOver:true. Otherwise, gameOver is false and revealed contains every discovered coordinate exactly once.

2. Choose BFS and a visited matrix

I use breadth-first search with a FIFO queue. FIFO means first in, first out. This gives the required breadth-first discovery order. I also use a visited matrix with the same dimensions as the board. A coordinate is marked visited when it enters the queue. The main invariant is that every coordinate in the queue has already been marked visited, so the same coordinate cannot be enqueued twice.

3. Initialize the state

For the safe click shown in the diagram, the starting coordinate is (4,0). The queue begins as [(4,0)]. The revealed array begins empty, and visited[4][0] is set to true. The eight directions are checked in this exact order: (-1,-1), (-1,0), (-1,1), (0,-1), (0,1), (1,-1), (1,0), (1,1).

4. Walk through the verified example

The board is [[0,0,1,"*",1],[0,0,2,2,2],[0,0,1,"*",1],[0,0,1,1,1],[0,0,0,0,0]], and we click (4,0). The clicked value is 0. We dequeue (4,0), append it to revealed, and examine its eight neighbors. Each valid unseen neighbor is marked visited immediately and added to the queue. BFS continues in FIFO order. When a dequeued cell is a number from 1 through 8, it is revealed but does not add neighbors. When a dequeued cell is 0, it expands. The exact discovery order is [[4,0],[3,0],[3,1],[4,1],[2,0],[2,1],[2,2],[3,2],[4,2],[1,0],[1,1],[1,2],[3,3],[4,3],[0,0],[0,1],[0,2],[3,4],[4,4]]. The queue eventually becomes empty, so 19 coordinates have been revealed.

5. Explain why the result is correct

Only revealed 0 cells expand. Every valid neighbor discovered from a 0 is marked visited before it is enqueued. Therefore, no coordinate can be added to the queue more than once. Numbered boundary cells are still revealed because they are discovered as neighbors, but they never expand. Since the queue is FIFO, coordinates are appended to revealed in breadth-first discovery order.

6. Explain the JavaScript implementation

The code first handles the bomb case with an immediate return. For a safe cell, it creates the queue, a head index, the visited matrix, the direction array, and the revealed array. The head index gives FIFO queue behavior without repeatedly calling Array.shift(). Each dequeued coordinate is appended to revealed. A nonzero value does not expand. A 0 checks all eight neighbors. Out-of-bounds or already visited neighbors are skipped. Every new valid neighbor is marked visited before being enqueued.

7. Explain complexity and edge cases

Let R be the number of rows and C be the number of columns. Each coordinate can enter the queue at most once. Each 0 cell checks at most eight neighbors. Therefore, the worst-case time is O(R × C). The visited matrix and queue require O(R × C) auxiliary space. Important cases are a bomb click, a nonzero numbered click, a zero on an edge or corner, a connected zero region with numbered boundary cells, and a board as large as 500 × 500 where iterative BFS avoids recursive stack overflow.

Key Insight / Why This Solution Works

Use iterative breadth-first search starting from the clicked cell. The important rule is that only cells whose value is 0 expand to neighbors. Numbered cells can be discovered and revealed, but they stop expansion. Mark each coordinate visited when it is enqueued. This maintains the invariant that every queued coordinate is unique. A FIFO queue then preserves the required breadth-first discovery order. This approach also avoids recursion, which is important because a connected region on a board as large as 500 × 500 could overflow the JavaScript call stack.

Code
function revealMinesweeper(board, row, column) {
  // A bomb click ends the game immediately.
  // Only the clicked bomb coordinate is returned.
  if (board[row][column] === '*') {
    return { gameOver: true, revealed: [[row, column]] };
  }

  // Read the dimensions without changing the board.
  const rows = board.length;
  const cols = board[0].length;

  // Check all eight neighbors in the exact order used by the diagram.
  // This order determines the discovery order among neighbors.
  const directions = [
    [-1, -1],
    [-1, 0],
    [-1, 1],
    [0, -1],
    [0, 1],
    [1, -1],
    [1, 0],
    [1, 1],
  ];

  // Start iterative BFS from the clicked safe cell.
  // A head index provides FIFO behavior without Array.shift().
  const queue = [[row, column]];
  let head = 0;

  // Keep visited state separately so board remains read-only.
  const visited = Array.from({ length: rows }, () => Array(cols).fill(false));

  // Coordinates are appended here in BFS dequeue order.
  const revealed = [];

  // Mark the start when it is enqueued so it cannot be added again.
  visited[row][column] = true;

  // Continue until every queued coordinate has been processed.
  while (head < queue.length) {
    const [r, c] = queue[head++];

    // Every dequeued coordinate is revealed exactly once.
    revealed.push([r, c]);

    // A numbered cell is a boundary cell.
    // It is revealed, but only a 0 cell expands to neighbors.
    if (board[r][c] !== 0) {
      continue;
    }

    // Expand a 0 cell to each valid unseen neighbor.
    for (const [dr, dc] of directions) {
      const nr = r + dr;
      const nc = c + dc;

      // Skip positions outside the board or already discovered cells.
      if (nr < 0 || nr >= rows || nc < 0 || nc >= cols || visited[nr][nc]) {
        continue;
      }

      // Mark before enqueueing to prevent duplicate queue entries.
      visited[nr][nc] = true;
      queue.push([nr, nc]);
    }
  }

  // The queue is exhausted, so the full reveal region is complete.
  return { gameOver: false, revealed };
}

// Exact 5 x 5 example from the approved diagram.
const board = [
  [0, 0, 1, '*', 1],
  [0, 0, 2, 2, 2],
  [0, 0, 1, '*', 1],
  [0, 0, 1, 1, 1],
  [0, 0, 0, 0, 0],
];

console.log(revealMinesweeper(board, 4, 0));
Time & Space Complexity

Let R be the number of rows and C be the number of columns. A coordinate is marked visited before it enters the queue, so it can be enqueued at most once. A 0 cell checks at most eight neighbors, which is a constant amount of work. Therefore, the worst-case time is O(R × C). The visited matrix and queue can both grow with the size of the board, so auxiliary space is O(R × C). The returned revealed array is output space.

Where it is used

This BFS pattern is useful for expanding connected regions in grids. Examples include Minesweeper, flood-fill tools, map-area exploration, and board games that reveal connected open cells. A FIFO queue is especially useful when the order in which states are discovered must be breadth first.

Why Interviewers Ask This

This problem checks whether you can recognize a grid traversal problem and choose BFS when discovery order matters. It tests careful state management because visited cells must be marked at the correct time. It also tests eight-direction boundary handling, duplicate prevention, input immutability, and the difference between a zero cell that expands and a numbered boundary cell that does not. The interviewer can also evaluate your JavaScript queue implementation and your O(R × C) complexity reasoning.

Common interview mistakes

A common mistake is marking a coordinate visited only when it is dequeued. Two different 0 cells could then enqueue the same neighbor before it is processed. Another mistake is expanding from numbered cells. Only 0 cells expand. Candidates may also forget diagonal neighbors even though this problem requires all eight directions. Mutating the board to record visited state breaks the contract. Recursive flood fill can overflow on a large board. Repeatedly using Array.shift() is also unnecessary because a head index gives FIFO behavior without removing elements from the front.

Interview tip

State the two key rules before coding: mark a coordinate visited when it enters the queue, and expand neighbors only when the current cell is 0. Those rules explain the duplicate prevention, the boundary behavior, and most of the correctness of the BFS.

Interviewer may ask next
Why should a coordinate be marked visited when it is enqueued instead of when it is dequeued?

Two different 0 cells can discover the same neighbor before that neighbor reaches the front of the queue. If we wait until dequeue time, the same coordinate could be enqueued more than once. Marking it visited immediately when it is enqueued prevents duplicate queue entries and keeps every returned coordinate unique. The worst-case time remains O(R × C), and the auxiliary space remains O(R × C).

What happens if the clicked cell is a nonzero number instead of a zero?

The safe starting coordinate is still enqueued, marked visited, dequeued, and added to revealed. Because its value is not 0, the code does not inspect or enqueue any neighbors. The queue then becomes empty. The result is gameOver:false with only the clicked coordinate. The same BFS implementation handles this case without a separate special branch.

9. How would you prevent login cross-site request forgery and session fixation?SecurityMediumGoogle

Question Details

A sign-in endpoint accepts credentials and sets a session cookie. An attacker tries to make a victim's browser sign in to the attacker's account, after which the victim may enter private data into that account. Map the attacker site, sign-in request, session cookie, user identity, and protected data. Define anti-forgery state, origin checks, session rotation, credential handling, SameSite behavior, and failure responses for the login flow. Explain how the threat differs from forcing an already signed-in victim to perform a transaction. Include tests for cross-site form submission, a reused login token, a preexisting session ID, a top-level redirect from an identity provider, and a victim who opens two sign-in tabs.

Short Interview Answer (30-60 seconds)

I would protect login with a short-lived, single-use server-bound anti-forgery token and origin checks. After verifying credentials, I would invalidate the pre-login session and issue a fresh session ID. Secure, HttpOnly, appropriate SameSite cookies, validated identity-provider state, server-side authorization, and replay tests provide defense in depth.

Detailed Explanation

This question asks how to stop another website from making a person's browser sign in to an account chosen by an attacker. The sign-in can appear successful, but the browser may now be connected to the attacker's account. The person could then unknowingly place private information into that account. It also asks how to stop an old browser identifier from becoming the identifier for a newly signed-in account. A safe design makes each sign-in belong to the browser flow that started it, gives the browser a fresh identity after success, accepts only expected sign-in returns, and rejects unexpected or repeated attempts safely.

Useful Questions to Ask the Interviewer
  1. Is sign-in performed directly with username and password, through an external identity provider, or both?
  2. Do we control the trusted server that creates and validates the session cookie?
  3. Must two or more sign-in tabs be able to remain active at the same time?
  4. Are legitimate cross-site top-level redirects from an identity provider part of the required flow?
How would you prevent login cross-site request forgery and session fixation? diagram
How to Explain It in an Interview

I would treat login as a security-sensitive state change even though the user is not authenticated yet.

The first threat is login CSRF. CSRF, or cross-site request forgery, means another site causes the victim's browser to send a request to our application. In login CSRF, the attacker can try to submit the attacker's own credentials through the victim's browser. If the application accepts that request without proving it belongs to a legitimate login flow, the victim may receive a session for the attacker's account. The victim can then unknowingly enter private data into an account the attacker can access.

This differs from ordinary authenticated CSRF. In ordinary CSRF, the victim is already signed in, and the attacker tries to make that existing identity perform a sensitive action, such as changing an email address. In login CSRF, the attacker's goal is instead to control which identity the victim becomes signed in as.

For a direct credential form, I would create a cryptographically unpredictable anti-forgery token on the trusted server when the login page or login attempt is created. I would bind it to that browser's pre-login context, give it a short lifetime, and require it on the credential submission. The server verifies the token before authenticating the request. A missing, mismatched, expired, or already-consumed token fails. The token becomes unusable after successful use so replaying an old login token does not work.

I would also validate request origin for the direct login endpoint. When the browser supplies an Origin header, the server compares the parsed origin exactly with the application's expected HTTPS origin. It should not use substring or suffix matching. If Origin can legitimately be absent for a supported client or browser path, a carefully defined HTTPS Referer check can be used as a fallback. If the origin cannot be established according to the application's policy, the login should fail safely rather than silently weakening the check.

These checks must be enforced by the trusted server. Frontend JavaScript cannot be the security boundary because the attacker controls the attacker's own page and can choose what requests that page attempts to send.

Credential verification also belongs on the trusted server. Passwords should travel only over HTTPS. They should never be placed in URLs, stored in localStorage or sessionStorage, embedded in frontend code, written to analytics, or logged. The frontend should contain no server secret or long-lived credential.

After credentials are successfully verified, I would rotate the session identifier. More precisely, I would invalidate the pre-login or anonymous session identifier and create a fresh, cryptographically unpredictable authenticated session identifier. The authenticated user is associated only with the new server-side session. This prevents session fixation, where an attacker tries to make the victim authenticate while continuing to use a session identifier that the attacker already knows or influenced.

The server must not accept an arbitrary session ID from a URL, form field, or other attacker-controlled input as the authenticated session identifier. If the browser arrives with a valid anonymous session cookie, that session can hold temporary state such as a CSRF token, but successful authentication must still replace its identifier. A test that begins with a known preexisting session ID should therefore observe a different session ID after login, and the old ID must no longer provide authenticated access.

For the session cookie, I would normally use Secure so the browser sends it only over HTTPS and HttpOnly so frontend JavaScript cannot read it. I would keep Domain and Path as narrow as practical. When deployment allows it, a host-only cookie such as one using the __Host- prefix is a useful additional hardening measure because it requires Secure, Path=/, and no Domain attribute.

I would choose SameSite based on the actual authentication architecture. SameSite=Lax is commonly appropriate for a first-party web session because it prevents the cookie from being sent with many cross-site requests while permitting cookies on ordinary top-level safe navigations. SameSite=Strict can be stronger but may break legitimate navigation and authentication flows. SameSite=None requires Secure and should be used only when cross-site cookie delivery is truly required. SameSite is defense in depth; it is not the sole proof that a login request is legitimate.

CORS and the same-origin policy are also not replacements for login-CSRF protection. The same-origin policy limits what an attacker's script can read from another origin, and CORS controls selected cross-origin script access. Neither means a cross-site HTML form is unable to send a request. The trusted server must therefore validate the login request even if the attacker cannot read its response.

For a redirect-based identity-provider login, I would not blindly apply the direct-form Origin rule to the callback because a legitimate top-level navigation returns from another origin. Instead, before redirecting the browser, the application creates an unpredictable correlation value, commonly the OAuth or OpenID Connect state value, and binds it to the login attempt. On callback, the trusted server validates state and all required protocol checks. Depending on the flow, that can also include PKCE and an OpenID Connect nonce. Only a callback tied to a login attempt that our application actually initiated is accepted.

The state value should not contain a raw secret merely because it travels through the browser. It should be random and associated with server-side state, or otherwise integrity-protected according to the chosen design. Authorization codes and protocol credentials must not be logged or exposed to unrelated frontend code.

For two sign-in tabs, I would not rely on one mutable global CSRF value if opening tab B would unexpectedly invalidate tab A. The server can maintain multiple outstanding, short-lived login attempts for the same browser, each with its own unpredictable, single-use identifier. Tab A must submit tab A's token, and tab B must submit tab B's token. A token used successfully cannot be replayed in the other tab.

If one tab authenticates while another login attempt is still open, the application needs an explicit policy. It may invalidate all outstanding login attempts, which is simpler and safer, or allow independent attempts if the product requires that behavior. If a later attempt is allowed to complete, it must verify its own credentials and anti-forgery state and rotate the session again. It must never inherit authentication merely because another tab completed login.

Authentication and authorization remain separate. Authentication establishes which user owns the session. Authorization decides whether that user may access a particular resource or perform an operation. Every protected request must be authorized by the trusted server. The frontend may hide or show controls for usability, but frontend state is not an authorization boundary.

Failure behavior should be safe and predictable. Invalid origin, bad anti-forgery state, token replay, invalid credentials, or invalid identity-provider correlation must not create or upgrade an authenticated session. Responses should avoid unnecessary account-enumeration detail. Security logs can record a failure category, time, request or correlation identifier, and useful diagnostic metadata, but should never record passwords, complete session cookies, anti-forgery tokens, OAuth authorization codes, or other credentials.

I would verify the design with the exact threat cases in the question. First, host a form on another origin and try to submit attacker credentials to the login endpoint; it must fail the anti-forgery or origin checks. Second, submit a valid login token once and replay it; the replay must fail. Third, start with a known pre-login session ID; successful authentication must produce a different session ID, and the old ID must not be authenticated. Fourth, perform a legitimate top-level redirect from the identity provider; a callback with valid correlation state must succeed while an uncorrelated callback must fail. Fifth, open two login tabs and verify that each has independent state, that tokens cannot be swapped or replayed, and that completion follows the application's documented concurrent-login policy.

Technical Approach
  1. Create a short-lived login attempt on the trusted server before processing credentials.
  2. Generate a cryptographically unpredictable, single-use anti-forgery value and bind it to the browser's pre-login context.
  3. Require the value on a direct credential submission and validate it before credential processing.
  4. Validate the direct login request's expected HTTPS Origin, using only a deliberately defined fallback when Origin can legitimately be absent.
  5. Reject missing, mismatched, expired, replayed, or wrong-origin requests without creating an authenticated session.
  6. Verify credentials only on the trusted server and never expose passwords or server secrets through URLs, storage, logs, or frontend code.
  7. On successful authentication, invalidate the pre-login session identifier and issue a fresh unpredictable authenticated session identifier.
  8. Set the session cookie with Secure, HttpOnly, narrow scope, and SameSite behavior chosen for the real login architecture.
  9. For identity-provider redirects, validate protocol correlation state and applicable nonce or PKCE checks rather than requiring the callback to look like a normal same-origin form submission.
  10. For multiple tabs, use independent short-lived single-use login attempts or deliberately invalidate outstanding attempts after one succeeds.
  11. Enforce authorization independently on every protected server request.
  12. Fail safely, log without secrets, and verify cross-site submission, token replay, preexisting-session rotation, identity-provider redirects, and two-tab behavior.
Practical Insights

The extra browser work is small: each login carries a small anti-forgery value and receives a new session cookie after successful authentication. Token and origin checks are normally constant-time operations compared with password verification, which is intentionally more expensive. The server needs a small amount of short-lived state for outstanding login attempts unless it uses an equivalent integrity-protected design. Supporting two tabs can require several temporary login-attempt records for one browser. The main operational and maintenance cost is getting expiration, replay prevention, cookie policy, concurrent-login behavior, and identity-provider exceptions correct and testing them whenever domains or authentication flows change.

Why Interviewers Ask This

This question tests whether the candidate understands that login itself is a security-sensitive state change. It evaluates whether they can distinguish login CSRF from ordinary authenticated CSRF, prevent an attacker from choosing or preserving a victim's session identifier, handle legitimate identity-provider redirects, reason about concurrent login tabs, define safe failure behavior, and verify that the controls actually work.

Common interview mistakes

Common mistakes include protecting only actions performed after login while leaving the login endpoint vulnerable; assuming SameSite alone prevents login CSRF; treating CORS or the same-origin policy as CSRF defenses; checking Origin with unsafe substring matching; accepting an attacker-supplied or pre-login session identifier after authentication instead of rotating it; failing to invalidate the old session ID; using reusable anti-forgery tokens; putting passwords or authentication secrets in URLs, browser storage, analytics, or logs; trusting frontend JavaScript as the enforcement point; rejecting every cross-site identity-provider callback instead of validating protocol correlation state; accepting an identity-provider callback without state validation; using one mutable login token that causes unsafe or unpredictable two-tab behavior; confusing authentication with authorization; revealing unnecessary account information in failures; and logging passwords, session cookies, anti-forgery tokens, or authorization codes.

Interview tip

Explain two independent protections first: prove that the login request belongs to a legitimate login flow, then replace the pre-login session identifier after successful authentication. Contrast login CSRF with transaction CSRF, explain the identity-provider exception, and finish by walking through the five required tests.

Interviewer may ask next
Why is SameSite=Lax not enough by itself to prevent login CSRF?

SameSite controls when an existing cookie accompanies a cross-site request, but a login endpoint may be useful to an attacker even before an authenticated cookie exists. It also does not prove that a credential submission was initiated by the victim through the application's intended login flow. I would therefore use SameSite as defense in depth together with a server-validated anti-forgery value and appropriate origin or identity-protocol correlation checks.

How would you support an OAuth or OpenID Connect login redirect without weakening these protections?

Before redirecting, I would create a distinct login attempt and bind an unpredictable state value to it. On callback, the trusted server verifies state and the provider response and, where applicable, verifies PKCE and an OpenID Connect nonce. A valid correlated top-level return is therefore accepted without treating arbitrary cross-site requests as trusted. After successful authentication, the local pre-login session identifier is still replaced with a fresh authenticated session identifier.

10. Why can loading sensitive data through JSONP become a script-execution risk?SecurityEasyGoogle

Question Details

A legacy frontend adds <script src='https://api.example/account?callback=show'> to bypass ordinary cross-origin reads. Analyze the trust boundary between the page, the remote response, and the callback name. Identify which origin executes the returned bytes, what an attacker can do if the endpoint or callback parameter is influenced, and which account data is protected. Explain how the response differs from inert JSON fetched under browser read controls, how an allowlist or fixed callback still leaves script-supply risk, and what a modern browser-facing contract should use instead. Include tests for a callback containing unexpected syntax, an authenticated response included from another site, and a compromised endpoint.

Short Interview Answer (30-60 seconds)

JSONP turns a remote response into JavaScript that runs in the including page. That creates code-supply and sensitive-data risks if the callback, endpoint, or response is attacker-influenced. Use normal JSON with fetch, server-side authorization, and narrowly configured CORS instead.

Detailed Explanation

This question asks why an old method for getting information from another website can be unsafe when that information is private. The key point is that the browser does not simply receive and read the returned information. It runs what comes back as instructions inside the requesting page. That changes who the page must trust. If the remote service, the requested function name, or the returned content can be changed, harmful actions may run. It also asks whether private account information can be exposed to another website and what safer design should replace this method.

Useful Questions to Ask the Interviewer
  1. Does the JSONP endpoint return user-specific or sensitive account data when the user is authenticated?
  2. Is the callback value fixed by the server, restricted to known values, or directly reflected from request input?
  3. Can the endpoint or its hosting infrastructure be controlled by a third party or become independently compromised?
Why can loading sensitive data through JSONP become a script-execution risk? diagram
How to Explain It in an Interview

The practical decision is to avoid JSONP for sensitive browser-facing data. Return ordinary JSON and use fetch. Keep authorization on the trusted server, and use explicit CORS only when a different browser origin genuinely needs permission to read the response.

A legacy JSONP request might use <script src='https://api.example/account?callback=show'>. The server could return show({"name":"Alice"});. Because the response was loaded through a script element, the browser treats those returned bytes as JavaScript source code. It does not treat them as inert JSON.

The returned JavaScript executes in the context of the page that included the script. That means the remote endpoint is being trusted as a supplier of executable code to that page. This is the central trust boundary: the page chooses the script URL, but the remote server controls the bytes that the browser executes.

If the endpoint is compromised, it does not have to return the expected show(...) call. It can return arbitrary JavaScript. That code can do what ordinary JavaScript in the including page is allowed to do, such as inspect page-readable state, change the DOM, make requests available to that page, or steal tokens that the application has incorrectly exposed to JavaScript. Browser boundaries still apply; JSONP does not magically grant access beyond the privileges of the including page.

The callback parameter creates a second risk. A JSONP service commonly inserts a requested callback name into executable source. If it accepts unexpected syntax and reflects that text unsafely, an attacker may be able to change the structure of the generated JavaScript and inject code. A test should therefore send a callback containing characters or syntax that are not valid for the intended callback format and verify that the endpoint rejects it safely instead of reflecting it into executable output.

A fixed callback or strict callback allowlist reduces callback-injection risk, but it does not solve the fundamental problem. Even with callback=show permanently fixed, the remote endpoint still supplies executable JavaScript. If that endpoint is compromised, arbitrary returned code still executes. Callback validation therefore protects only one input path; it does not remove the script-supply trust relationship.

Sensitive account data creates a confidentiality risk as well. The same-origin policy normally prevents a page from reading arbitrary cross-origin responses, but cross-origin script inclusion has historically been allowed because websites need to load scripts from other origins. JSONP deliberately uses that script-loading behavior to bypass normal cross-origin read restrictions.

If a JSONP account endpoint returns private data based on the user's authenticated session, an attacker-controlled site may try to include that endpoint as a script. Whether authentication credentials are actually attached depends on the cookie and credential design, including modern SameSite cookie behavior. The security requirement must not depend on hoping that cross-site inclusion fails. A sensitive endpoint must not expose private account data through executable JSONP, and the trusted server must enforce authorization for the requested resource.

Authentication and authorization are different. Authentication establishes which user is making a request. Authorization decides whether that user and request are permitted to access a particular account resource. Successful authentication alone must never be treated as proof that any requesting web page is authorized to receive the data. Authorization must be enforced by the trusted server.

This is different from ordinary JSON fetched with fetch. A response such as {"name":"Alice"} is data. Merely receiving it does not execute it as JavaScript. For a cross-origin fetch, the browser's same-origin policy normally prevents the calling JavaScript from reading the response unless the remote server explicitly permits that origin through CORS. CORS therefore controls browser-readable cross-origin access; it is not authentication or authorization.

The modern browser-facing contract should use HTTPS and return application/json. A same-origin frontend can use fetch without cross-origin permission. If a trusted frontend on another origin must read the API, the server should return narrowly scoped CORS headers for approved origins. For credentialed requests, it must not use a wildcard origin. The server should authenticate when necessary and independently authorize every sensitive resource or operation.

Session cookies should use appropriate Secure, HttpOnly, and SameSite settings for the application's architecture. HttpOnly reduces exposure of session cookies to JavaScript. SameSite can reduce cross-site cookie sending. These controls are useful defense in depth, but they do not make JSONP a suitable format for sensitive data. Cookie-authenticated state-changing endpoints also need appropriate CSRF defenses because CORS is not a CSRF protection mechanism.

The safe failure behavior is to reject unsupported or malformed requests without returning sensitive information. A modern JSON endpoint should not accept a JSONP callback parameter at all. Security logs may record request identifiers, authorization failures, origin decisions, and endpoint errors, but they should not record session tokens, credentials, secrets, or sensitive account payloads.

Three tests directly verify the important boundaries. First, send a callback containing unexpected syntax. A legacy endpoint must reject it instead of reflecting it into executable source; the preferred modern endpoint accepts no callback parameter. Second, from a different site, attempt to include an authenticated account response as a script and verify that sensitive account data cannot be obtained through that mechanism. Test with the real production cookie policy because SameSite behavior matters. Third, model a compromised endpoint that returns attacker-controlled JavaScript. If the application loads that endpoint through a script element, the attacker's code executes in the including page, demonstrating why callback validation or a callback allowlist cannot solve the underlying JSONP design risk.

Technical Approach
  1. Identify whether the remote response is loaded as executable script or fetched as inert data.
  2. Mark the trust boundaries among the including page, remote endpoint, callback input, authenticated session, and sensitive account resource.
  3. Determine who controls the returned bytes and whether request-controlled callback text is inserted into executable source.
  4. Check whether a different site can cause an authenticated JSONP response containing sensitive data to be returned, accounting for the actual cookie policy.
  5. Separate authentication from authorization and require the trusted server to authorize every sensitive resource.
  6. Replace JSONP with application/json over HTTPS and fetch.
  7. Use same-origin access by default or a narrow CORS policy for explicitly trusted cross-origin clients.
  8. Reject malformed requests safely and log failures without secrets or sensitive payloads.
  9. Verify callback-syntax rejection, cross-site authenticated inclusion, and a compromised-endpoint scenario.
Practical Insights

There is no meaningful algorithmic time or memory complexity in this security decision. The main cost is architectural and operational. Removing JSONP can require changes to the frontend request code, API response format, CORS policy, authentication behavior, authorization checks, and automated tests. Ordinary JSON is easier to maintain because data remains data instead of becoming executable source. Narrow CORS rules and server-side authorization add configuration and test work, but they create clearer trust boundaries and significantly reduce the security risk.

Why Interviewers Ask This

This question tests whether the candidate understands that JSONP crosses a security boundary by converting a remote response into executable JavaScript. It also checks whether they can distinguish cross-origin script loading from protected cross-origin data reads, recognize callback injection, authenticated-data exposure, and compromised-endpoint risks, and recommend a modern design using inert JSON, browser origin controls, trusted-server authorization, and deliberate CORS.

Common interview mistakes

A common mistake is saying JSONP becomes safe when the callback name is allowlisted. An allowlist can reduce callback-injection risk, but the endpoint is still supplying executable JavaScript. Another mistake is saying the returned script executes with the API server's origin. It executes in the context of the page that included it. Candidates also confuse CORS with authentication or authorization. CORS controls whether browser JavaScript may read a cross-origin response; the trusted server must still authenticate when needed and authorize sensitive resources. Another mistake is treating ordinary JSON and JSONP as equivalent because both may contain object-shaped information. Ordinary JSON fetched through browser APIs is data, while JSONP is executable JavaScript. It is also incorrect to promise that authenticated cookies are always sent to a cross-site JSONP request in modern browsers; SameSite policy can prevent that. The correct design must remain safe regardless of that defense-in-depth behavior.

Interview tip

Lead with the key distinction: JSONP converts a remote response into executable JavaScript, while fetch treats JSON as data under browser read controls. Then explain callback injection, compromised-endpoint code execution, and possible authenticated-data exposure separately. Finish with the modern contract: JSON, fetch, trusted-server authorization, and narrow CORS when cross-origin reads are required.

Interviewer may ask next
Would a strict allowlist of callback names make JSONP safe for sensitive account data?

No. A strict callback allowlist can block many callback-injection payloads, but it does not change the fundamental execution model. The endpoint still returns JavaScript that the including page executes. If that endpoint is compromised, it can return arbitrary code even when the callback name is perfectly valid. A sensitive endpoint should instead return inert JSON, enforce authorization on the trusted server, and use fetch with deliberate CORS if cross-origin browser access is required.

Why is ordinary cross-origin JSON with CORS safer than JSONP?

Ordinary JSON is data and is not automatically executed as JavaScript. The same-origin policy normally prevents browser JavaScript from reading a cross-origin response unless the server deliberately grants that origin access through CORS. That keeps cross-origin read permission separate from script execution. CORS is not authentication or authorization, so the trusted server must still verify the user's identity when required and authorize access to each sensitive resource. Credentialed cross-origin designs also need appropriate cookie and CSRF protections.

More questions load as you scroll

Disclaimer: This interview guide is for educational and informational purposes only. It is designed to help readers prepare, but it does not guarantee any interview result, hiring decision, offer, or outcome. Interview questions, hiring criteria, and preferred answers can vary by employer, interviewer, industry, location, and time. The examples and explanations reflect the authors' research and judgment, are provided without warranties of any kind, and should not be treated as the only correct approach. Diagrams are simplified illustrations intended to highlight the main components and their interactions; actual systems and implementations may be more complex. Alternative approaches may be equally valid or better suited to a particular question, context, or interviewer. To the fullest extent permitted by applicable law, the author, contributors, and publisher are not liable for decisions made, actions taken, or losses incurred based on this guide.

Company Notice: This guide is an independent educational resource and is not affiliated with, endorsed by, sponsored by, or approved by the company named in this guide. Company names are used only to identify interview experiences commonly reported by candidates. Interview practices can change without notice, and inclusion of company-specific content does not mean these questions are official, complete, or guaranteed to be asked. To the fullest extent permitted by law, the author, contributors, and publisher are not responsible for outcomes related to use of this material.