CareerKit

DSA Interview Questions in JavaScript

Coding round problems solved in JavaScript, from arrays and strings to linked lists, trees and dynamic programming, with brute-force and optimised approaches and how to explain them to the interviewer.

12 free answers from our DSA for Interviews in JavaScript ebook, which has all 80 problems.

Big O in plain words

What is Big O notation?

Big O describes how an algorithm's work grows as the input grows, ignoring constants and small terms. It answers: "If the input becomes 10 times bigger, roughly how much slower does this get?"

The examples below count the basic operations each function does for different input sizes:

Solution
function constant(arr) {
  return arr[0];
}

function linear(arr) {
  let ops = 0;
  for (const x of arr) ops++;
  return ops;
}

function quadratic(arr) {
  let ops = 0;
  for (const a of arr) for (const b of arr) ops++;
  return ops;
}

for (const n of [10, 100, 1000]) {
  const arr = Array.from({ length: n }, (_, i) => i);
  console.log(`n=${n}: O(1) -> 1 op, O(n) -> ${linear(arr)} ops, O(n^2) -> ${quadratic(arr)} ops`);
}
Output
n=10: O(1) -> 1 op, O(n) -> 10 ops, O(n^2) -> 100 ops
n=100: O(1) -> 1 op, O(n) -> 100 ops, O(n^2) -> 10000 ops
n=1000: O(1) -> 1 op, O(n) -> 1000 ops, O(n^2) -> 1000000 ops

When n grows 10 times, O(n) does 10 times more work, but O(n²) does 100 times more. That's why an O(n²) solution that passes small test cases can time out on large ones.

The common classes, fastest to slowest: O(1) constant, O(log n) logarithmic, O(n) linear, O(n log n), O(n²) quadratic, O(2ⁿ) exponential.

Explain it to the interviewer

Big O describes how the running time or memory grows with input size, keeping only the dominant term. O(n) grows in proportion to the input, O(n squared) grows with its square, so for large inputs the growth rate matters far more than constant factors.

How do you work out the time complexity of a piece of code?

A practical method:

  • A simple statement is O(1).
  • A loop over n items is O(n) times the cost of its body.
  • Nested loops multiply: a loop inside a loop over the same data is O(n²).
  • Loops one after another add, and you keep the biggest: O(n) + O(n²) is O(n²).
  • Halving the problem each step gives O(log n).
  • Remember the hidden cost of built-ins: includes, indexOf, slice and spread are O(n) each.
Solution
function hasDuplicateSlow(arr) {
  for (let i = 0; i < arr.length; i++) {
    if (arr.slice(i + 1).includes(arr[i])) return true;
  }
  return false;
}

function hasDuplicateFast(arr) {
  const seen = new Set();
  for (const x of arr) {
    if (seen.has(x)) return true;
    seen.add(x);
  }
  return false;
}

const data = Array.from({ length: 5000 }, (_, i) => i);
console.log(hasDuplicateSlow(data), hasDuplicateFast(data));
console.log(hasDuplicateFast([3, 1, 3]));
Output
false false
true

hasDuplicateSlow looks like one loop, but slice and includes inside it are each O(n), so it's really O(n²). hasDuplicateFast is O(n) because Set lookups are O(1) on average.

Explain it to the interviewer

I count how often the innermost work runs: nested loops multiply, sequential steps add and I keep the largest term, and halving gives log n. I also watch for hidden loops in built-ins like includes, slice and spread.

Arrays and strings

Maximum subarray sum (Kadane's algorithm)

Given an array of integers (some negative), find the contiguous subarray with the largest sum.

Example: [-2, 1, -3, 4, -1, 2, 1, -5, 4] → 6, from [4, -1, 2, 1].

Idea: walk through the array keeping the best sum of a subarray ending here. At each number, either extend the previous subarray or start fresh from this number, whichever is bigger. Track the best seen overall.

Solution
function maxSubarraySum(nums) {
  let current = nums[0];
  let best = nums[0];
  for (let i = 1; i < nums.length; i++) {
    current = Math.max(nums[i], current + nums[i]);
    best = Math.max(best, current);
  }
  return best;
}

console.log(maxSubarraySum([-2, 1, -3, 4, -1, 2, 1, -5, 4]));
console.log(maxSubarraySum([5, 4, -1, 7, 8]));
console.log(maxSubarraySum([-3, -1, -2]));
Output
6
23
-1

Time: O(n) · Space: O(1). Starting from nums[0] (not 0) handles all-negative arrays correctly: the answer is the largest single number.

Explain it to the interviewer

I keep the best sum of a subarray ending at the current index: either the number alone or the number plus the previous best, whichever is larger, and track the overall maximum. It's one pass, O(n) time and O(1) space, and starting from the first element handles all-negative input.

Move all zeroes to the end

Move every 0 to the end of the array in place, keeping the order of the other elements.

Example: [0, 1, 0, 3, 12] → [1, 3, 12, 0, 0].

Idea: keep a pointer insert for where the next non-zero should go. Copy each non-zero forward, then fill the rest with zeroes.

Solution
function moveZeroes(nums) {
  let insert = 0;
  for (const n of nums) {
    if (n !== 0) nums[insert++] = n;
  }
  while (insert < nums.length) nums[insert++] = 0;
  return nums;
}

console.log(moveZeroes([0, 1, 0, 3, 12]).join(","));
console.log(moveZeroes([0, 0, 1]).join(","));
console.log(moveZeroes([1, 2, 3]).join(","));
Output
1,3,12,0,0
1,0,0
1,2,3

Time: O(n) · Space: O(1). Using filter plus padding would be simpler but creates a new array, which breaks the "in place" requirement.

Explain it to the interviewer

I use a write pointer: for every non-zero number I write it at the pointer and advance it, which keeps their order, then I fill the remaining positions with zeroes. It's O(n) time and O(1) extra space.

Hashing and two pointers

Two sum (unsorted array)

Return the indexes of the two numbers that add up to target. Exactly one answer exists.

Idea: for each number, check whether its complement (target - number) has already been seen, using a Map from value to index.

Solution
function twoSum(nums, target) {
  const seen = new Map();
  for (let i = 0; i < nums.length; i++) {
    const complement = target - nums[i];
    if (seen.has(complement)) return [seen.get(complement), i];
    seen.set(nums[i], i);
  }
  return [];
}

console.log(twoSum([2, 7, 11, 15], 9).join(","));
console.log(twoSum([3, 2, 4], 6).join(","));
console.log(twoSum([-1, -2, -3, -4, -5], -8).join(","));
Output
0,1
1,2
2,4

Time: O(n) · Space: O(n). The brute force checks every pair: O(n²).

Explain it to the interviewer

I store each number's index in a Map as I go, and before storing, check whether target minus the current number is already there. One pass gives O(n) time with O(n) space.

Two sum on a sorted array (two pointers)

The array is sorted. Find two numbers that add up to target, using O(1) extra space.

Idea: start one pointer at each end. If the sum is too small, move the left pointer right (bigger numbers); if too big, move the right pointer left.

Solution
function twoSumSorted(nums, target) {
  let left = 0;
  let right = nums.length - 1;
  while (left < right) {
    const sum = nums[left] + nums[right];
    if (sum === target) return [nums[left], nums[right]];
    if (sum < target) left++;
    else right--;
  }
  return null;
}

console.log(twoSumSorted([1, 3, 4, 6, 8, 11], 10).join("+"));
console.log(twoSumSorted([2, 7, 11, 15], 26).join("+"));
console.log(twoSumSorted([1, 2, 3], 100));
Output
4+6
11+15
null

Time: O(n) · Space: O(1). Each step safely discards one number, because sortedness tells us it can't be part of any answer.

Explain it to the interviewer

Because the array is sorted, I use two pointers from both ends: if the sum is too small I move left forward, if too big I move right back. Each move eliminates one candidate, so it's O(n) time with O(1) space.

Stacks, queues and linked lists

Implement a stack

A stack is last in, first out (LIFO), like a pile of plates: you add (push) and remove (pop) only at the top. Uses: undo history, the browser back button, function calls, matching brackets.

A JavaScript array already behaves like a stack with push and pop, both O(1). Wrapping it in a class gives a clear, limited interface.

Solution
class Stack {
  #items = [];
  push(item) {
    this.#items.push(item);
  }
  pop() {
    if (this.isEmpty()) throw new Error("Stack is empty");
    return this.#items.pop();
  }
  peek() {
    return this.#items.at(-1);
  }
  isEmpty() {
    return this.#items.length === 0;
  }
  get size() {
    return this.#items.length;
  }
}

const history = new Stack();
history.push("/home");
history.push("/ebooks");
history.push("/checkout");
console.log(history.pop(), history.peek(), history.size);

try {
  new Stack().pop();
} catch (error) {
  console.log(error.message);
}
Output
/checkout /ebooks 2
Stack is empty

All operations are O(1). Throwing on an empty pop (instead of returning undefined) makes bugs obvious.

Explain it to the interviewer

A stack is last in, first out. I implement it with an array using push and pop at the end, which are O(1), and expose push, pop, peek, isEmpty and size. Stacks suit undo, back navigation, recursion and bracket matching.

Min stack

Design a stack that also returns the minimum element in O(1).

Idea: keep a second stack of minimums. Each push records the minimum at that moment, so popping restores the previous minimum automatically.

Solution
class MinStack {
  #items = [];
  #mins = [];
  push(value) {
    this.#items.push(value);
    const currentMin = this.#mins.length ? this.#mins.at(-1) : Infinity;
    this.#mins.push(Math.min(value, currentMin));
  }
  pop() {
    this.#mins.pop();
    return this.#items.pop();
  }
  top() {
    return this.#items.at(-1);
  }
  getMin() {
    return this.#mins.at(-1);
  }
}

const stack = new MinStack();
stack.push(5);
stack.push(2);
stack.push(8);
stack.push(1);
console.log(stack.getMin());
stack.pop();
console.log(stack.getMin(), stack.top());
stack.pop();
stack.pop();
console.log(stack.getMin());
Output
1
2 8
5

Every operation is O(1); space is O(n) for the second stack.

Explain it to the interviewer

Alongside the main stack I keep a stack of minimums, pushing the smaller of the new value and the current minimum each time. Popping both together means getMin is always the top of the min stack, so every operation is O(1).

Trees and recursion

Preorder, inorder and postorder traversal

A binary tree node has a value and up to two children. Depth-first traversals differ only in when you visit the node itself:

  • Preorder: node, left, right (copying a tree, prefix expressions)
  • Inorder: left, node, right (gives a binary search tree in sorted order)
  • Postorder: left, right, node (deleting a tree, evaluating expressions)
Solution
const node = (val, left = null, right = null) => ({ val, left, right });
//        4
//      /   \
//     2     6
//    / \   / \
//   1   3 5   7
const root = node(4, node(2, node(1), node(3)), node(6, node(5), node(7)));

function preorder(n, out = []) {
  if (!n) return out;
  out.push(n.val);
  preorder(n.left, out);
  preorder(n.right, out);
  return out;
}
function inorder(n, out = []) {
  if (!n) return out;
  inorder(n.left, out);
  out.push(n.val);
  inorder(n.right, out);
  return out;
}
function postorder(n, out = []) {
  if (!n) return out;
  postorder(n.left, out);
  postorder(n.right, out);
  out.push(n.val);
  return out;
}

console.log("pre: ", preorder(root).join(" "));
console.log("in:  ", inorder(root).join(" "));
console.log("post:", postorder(root).join(" "));
Output
pre:  4 2 1 3 6 5 7
in:   1 2 3 4 5 6 7
post: 1 3 2 5 7 6 4

Each traversal is O(n) time and O(h) space for the recursion, where h is the tree's height.

Explain it to the interviewer

All three are depth-first and differ only in when the node is visited: before its children in preorder, between them in inorder, and after them in postorder. Inorder on a binary search tree gives sorted values. Each is O(n) time and O(height) space.

Sorting and searching

First and last position of a target

In a sorted array with duplicates, find the first and last index of target, in O(log n).

Idea: run binary search twice. When you find the target, don't stop: keep searching left for the first occurrence, or right for the last.

Solution
function findBound(nums, target, findFirst) {
  let low = 0;
  let high = nums.length - 1;
  let result = -1;
  while (low <= high) {
    const mid = (low + high) >> 1;
    if (nums[mid] === target) {
      result = mid;
      if (findFirst) high = mid - 1;
      else low = mid + 1;
    } else if (nums[mid] < target) {
      low = mid + 1;
    } else {
      high = mid - 1;
    }
  }
  return result;
}

const searchRange = (nums, target) => [findBound(nums, target, true), findBound(nums, target, false)];

console.log(searchRange([5, 7, 7, 8, 8, 8, 10], 8).join(","));
console.log(searchRange([5, 7, 7, 8, 8, 10], 6).join(","));
console.log(searchRange([2, 2, 2], 2).join(","));
Output
3,5
-1,-1
0,2

Last − first + 1 also gives the count of the target in O(log n). Time: O(log n) · Space: O(1).

Explain it to the interviewer

I run two binary searches. When the middle equals the target, I record it and keep searching to the left for the first occurrence, or to the right for the last. Both are O(log n), and their difference gives the count.

All 80 problems in the ebook

Linked questions are answered free on this page; the rest are in the ebook.

Big O in plain words

  1. What is Big O notation?
  2. How do you work out the time complexity of a piece of code?
  3. What does O(log n) mean?
  4. What is space complexity?
  5. What are best, average and worst case?
  6. What does amortised O(1) mean?
  7. What are the complexities of common JavaScript operations?
  8. What is the complexity of naive recursive Fibonacci?

Arrays and strings

  1. Maximum subarray sum (Kadane's algorithm)
  2. Move all zeroes to the end
  3. Best time to buy and sell a stock
  4. Product of array except self
  5. Spiral order of a matrix
  6. Merge overlapping intervals
  7. Longest common prefix
  8. Reverse the words in a sentence
  9. String compression
  10. Longest palindromic substring
  11. Pascal's triangle
  12. Majority element (Boyer-Moore voting)
  13. Plus one
  14. Leaders in an array
  15. Equilibrium index
  16. Find the missing and repeating numbers

Hashing and two pointers

  1. Two sum (unsorted array)
  2. Two sum on a sorted array (two pointers)
  3. Three sum
  4. Container with most water
  5. Longest substring without repeating characters
  6. Subarray sum equals k
  7. Group anagrams
  8. Top K frequent elements
  9. Longest consecutive sequence
  10. Remove duplicates from a sorted array in place
  11. Is one string a subsequence of another?
  12. Minimum size subarray with sum at least target
  13. Intersection of two arrays
  14. Contains a duplicate within k positions
  15. Trapping rain water
  16. Isomorphic strings

Stacks, queues and linked lists

  1. Implement a stack
  2. Min stack
  3. Next greater element
  4. Evaluate an expression in Reverse Polish Notation
  5. Implement a queue using two stacks
  6. Sliding window maximum
  7. Implement a singly linked list
  8. Reverse a linked list
  9. Detect a cycle in a linked list
  10. Find the middle of a linked list
  11. Merge two sorted linked lists
  12. Remove the nth node from the end
  13. Check whether a linked list is a palindrome
  14. Simplify a Unix file path

Trees and recursion

  1. Preorder, inorder and postorder traversal
  2. Level order traversal (breadth-first search)
  3. Maximum depth of a binary tree
  4. Invert a binary tree
  5. Check whether a tree is symmetric
  6. Validate a binary search tree
  7. Lowest common ancestor in a binary search tree
  8. Path sum
  9. Diameter of a binary tree
  10. Insert into and search a binary search tree
  11. Generate all subsets
  12. Generate all permutations of a string
  13. Tower of Hanoi
  14. Number of islands

Sorting and searching

  1. Binary search
  2. First and last position of a target
  3. Search in a rotated sorted array
  4. Integer square root with binary search
  5. Bubble sort
  6. Insertion sort
  7. Merge sort
  8. Quick sort
  9. Sort an array of 0s, 1s and 2s (Dutch national flag)
  10. Kth largest element (quickselect)
  11. Find a peak element
  12. Sort objects by several fields