interviewDeck

Your one-stop platform to prepare, practice and ace your interviews.

Loading your questions…

All Questions

Filters & tools

JS Coding Interview Questions and Answers

83 hand-picked JS Coding interview questions with detailed answers. Open the interactive version above to search, filter by difficulty, run code, bookmark questions and track your progress.

Reverse a string without using .reverse().

Walk the string from the end to the start and build a new one, or convert to an array and back. Time O(n), space O(n).

function reverse(str) {
  let out = '';
  for (let i = str.length - 1; i >= 0; i--) out += str[i];
  return out;
}
reverse('hello'); // 'olleh'

Check whether a string is a palindrome.

A palindrome reads the same forward and backward (e.g. madam, racecar). The optimal solution uses the Two Pointer approach. Keep one pointer at the beginning and another at the end. Compare both characters. If they match, move both pointers toward the center. If any pair doesn't match, return false.

function isPalindrome(str) {
  let left = 0;
  let right = str.length - 1;

  while (left < right) {
    if (str[left] ! == str[right]) {
      return false;
    }

    left++;
    right--;
  }

  return true;
}

console.log(isPalindrome('madam'));    // true
console.log(isPalindrome('racecar'));  // true
console.log(isPalindrome('hello'));    // false

Implement FizzBuzz for numbers from 1 to n.

Loop from 1 to n. For each number:

  • If divisible by both 3 and 5, print FizzBuzz.
  • Else if divisible by 3, print Fizz.
  • Else if divisible by 5, print Buzz.
  • Otherwise, print the number itself.
function fizzBuzz(n) {
  for (let i = 1; i <= n; i++) {
    if (i % 3 === 0 && i % 5 === 0) {
      console.log('FizzBuzz');
    } else if (i % 3 === 0) {
      console.log('Fizz');
    } else if (i % 5 === 0) {
      console.log('Buzz');
    } else {
      console.log(i);
    }
  }
}

fizzBuzz(15);

Two Sum - Given an array of integers and a target, return the indices of the two numbers whose sum equals the target.

The brute-force solution uses two nested loops and checks every pair, resulting in O(n²) time complexity.

A better approach is to use a HashMap (Map). As we iterate through the array, we calculate the number we need (target - currentNumber). If that number already exists in the Map, we've found the answer. Otherwise, store the current number and its index in the Map.

function twoSum(nums, target) {
  const seen = new Map();

  for (let i = 0; i < nums.length; i++) {
    const current = nums[i];
    const need = target - current;

    // Have we already seen the required number?
    if (seen.has(need)) {
      return [seen.get(need), i];
    }

    // Store current number and its index
    seen.set(current, i);
  }
}

console.log(twoSum([2, 7, 11, 15], 9)); // [0, 1]

Group Anagrams — group strings that are anagrams together.

Sort each word alphabetically and use the sorted string as the key inside a Map. Words with the same sorted characters belong to the same group.

function groupAnagrams(strs) {
  const map = new Map();

  for (const word of strs) {
    const key = word.split('').sort().join('');

    if (!map.has(key)) {
      map.set(key, []);
    }

    map.get(key).push(word);
  }

  return [...map.values()];
}

console.log(groupAnagrams(['eat','tea','tan','ate','nat','bat']));

Top K Frequent Elements — return the K most frequent elements.

Count the frequency of every number using a Map, sort by frequency in descending order, then return the first K elements.

function topKFrequent(nums, k) {
  const freq = new Map();

  for (const num of nums) {
    freq.set(num, (freq.get(num) || 0) + 1);
  }

  return [...freq.entries()]
    .sort((a, b) => b[1] - a[1])
    .slice(0, k)
    .map(item => item[0]);
}

console.log(topKFrequent([1,1,1,2,2,3],2));

Product of Array Except Self — return the product of all elements except itself.

Build prefix products from the left and suffix products from the right. Multiply both values to get the answer for each index without using division.

function productExceptSelf(nums) {
  const result = Array(nums.length).fill(1);

  let prefix = 1;
  for (let i = 0; i < nums.length; i++) {
    result[i] = prefix;
    prefix *= nums[i];
  }

  let suffix = 1;
  for (let i = nums.length - 1; i >= 0; i--) {
    result[i] *= suffix;
    suffix *= nums[i];
  }

  return result;
}

console.log(productExceptSelf([1,2,3,4]));

Longest Consecutive Sequence — find the length of the longest consecutive sequence.

Store all numbers inside a Set. Only start counting when the current number is the beginning of a sequence (its previous number doesn't exist). This guarantees O(n) time.

function longestConsecutive(nums) {
  const set = new Set(nums);
  let longest = 0;

  for (const num of set) {
    if (!set.has(num - 1)) {
      let current = num;
      let length = 1;

      while (set.has(current + 1)) {
        current++;
        length++;
      }

      longest = Math.max(longest, length);
    }
  }

  return longest;
}

console.log(longestConsecutive([100,4,200,1,3,2])); // 4

3Sum — find all unique triplets whose sum is zero.

Sort the array first. Fix one number and use two pointers (left and right) to find the remaining two numbers. Skip duplicate values to avoid duplicate triplets.

function threeSum(nums) {
  nums.sort((a,b)=>a-b);
  const result = [];

  for(let i=0;i<nums.length-2;i++){
    if(i>0 && nums[i]===nums[i-1]) continue;

    let left=i+1;
    let right=nums.length-1;

    while(left<right){
      const sum=nums[i]+nums[left]+nums[right];

      if(sum===0){
        result.push([nums[i],nums[left],nums[right]]);
        while(left<right && nums[left]===nums[left+1]) left++;
        while(left<right && nums[right]===nums[right-1]) right--;
        left++;
        right--;
      }else if(sum<0){
        left++;
      }else{
        right--;
      }
    }
  }

  return result;
}

console.log(threeSum([-1,0,1,2,-1,-4]));

Container With Most Water — find the maximum amount of water a container can store.

Use two pointers at both ends of the array. Calculate the area and move the pointer with the smaller height inward because moving the taller one cannot increase the area.

function maxArea(height) {
  let left=0;
  let right=height.length-1;
  let max=0;

  while(left<right){
    const area=Math.min(height[left],height[right])*(right-left);
    max=Math.max(max,area);

    if(height[left]<height[right]) left++;
    else right--;
  }

  return max;
}

console.log(maxArea([1,8,6,2,5,4,8,3,7]));

Longest Substring Without Repeating Characters.

Maintain a sliding window using two pointers. Store the latest index of every character inside a Map. Whenever a duplicate appears, move the left pointer accordingly.

function lengthOfLongestSubstring(s){
  const map=new Map();
  let left=0;
  let max=0;

  for(let right=0;right<s.length;right++){
    if(map.has(s[right])){
      left=Math.max(left,map.get(s[right])+1);
    }

    map.set(s[right],right);
    max=Math.max(max,right-left+1);
  }

  return max;
}

console.log(lengthOfLongestSubstring('abcabcbb'));

Longest Palindromic Substring.

Expand around every character (and every gap between characters) as the center of a palindrome. Keep track of the longest palindrome found.

function longestPalindrome(s){
  let start=0,end=0;

  function expand(left,right){
    while(left>=0 && right<s.length && s[left]===s[right]){
      left--;
      right++;
    }
    return [left+1,right-1];
  }

  for(let i=0;i<s.length;i++){
    const odd=expand(i,i);
    const even=expand(i,i+1);

    const longer=(odd[1]-odd[0])>(even[1]-even[0])?odd:even;

    if(longer[1]-longer[0]>end-start){
      start=longer[0];
      end=longer[1];
    }
  }

  return s.substring(start,end+1);
}

console.log(longestPalindrome('babad'));

Valid Parentheses — determine whether the parentheses are valid.

Use a Stack. Push every opening bracket. Whenever a closing bracket appears, verify it matches the top of the stack. At the end, the stack must be empty.

function isValid(s){
  const stack=[];
  const map={')':'(',']':'[','}':'{'};

  for(const ch of s){
    if(ch==='('||ch==='['||ch==='{'){
      stack.push(ch);
    }else{
      if(stack.pop()!==map[ch]){
        return false;
      }
    }
  }

  return stack.length===0;
}

console.log(isValid('()[]{}'));

Generate Parentheses — generate all combinations of well-formed parentheses.

Use backtracking. Keep adding '(' until the limit is reached, then add ')' only when it won't make the string invalid. Backtrack after each recursive call.

function generateParenthesis(n) {
  const result = [];

  function backtrack(str, open, close) {
    if (str.length === n * 2) {
      result.push(str);
      return;
    }

    if (open < n) {
      backtrack(str + '(', open + 1, close);
    }

    if (close < open) {
      backtrack(str + ')', open, close + 1);
    }
  }

  backtrack('', 0, 0);
  return result;
}

console.log(generateParenthesis(3));

Daily Temperatures — return how many days until a warmer temperature.

Maintain a monotonic decreasing stack of indices. Whenever the current temperature is higher than the top of the stack, calculate the waiting days and pop it.

function dailyTemperatures(temperatures) {
  const result = Array(temperatures.length).fill(0);
  const stack = [];

  for (let i = 0; i < temperatures.length; i++) {
    while (stack.length && temperatures[i] > temperatures[stack[stack.length - 1]]) {
      const index = stack.pop();
      result[index] = i - index;
    }
    stack.push(i);
  }

  return result;
}

console.log(dailyTemperatures([73,74,75,71,69,72,76,73]));

Min Stack — design a stack that supports retrieving the minimum element in O(1).

Maintain two stacks. One stores all values, while the other stores the current minimum after each push. The minimum is always at the top of the second stack.

class MinStack {
  constructor() {
    this.stack = [];
    this.minStack = [];
  }

  push(val) {
    this.stack.push(val);

    if (!this.minStack.length || val <= this.getMin()) {
      this.minStack.push(val);
    }
  }

  pop() {
    if (this.stack.pop() === this.getMin()) {
      this.minStack.pop();
    }
  }

  top() {
    return this.stack[this.stack.length - 1];
  }

  getMin() {
    return this.minStack[this.minStack.length - 1];
  }
}

Evaluate Reverse Polish Notation.

Traverse the tokens. Push numbers onto the stack. When an operator is found, pop the last two numbers, evaluate them, and push the result back.

function evalRPN(tokens) {
  const stack = [];

  for (const token of tokens) {
    if (!'+-*/'.includes(token)) {
      stack.push(Number(token));
    } else {
      const b = stack.pop();
      const a = stack.pop();

      switch (token) {
        case '+': stack.push(a + b); break;
        case '-': stack.push(a - b); break;
        case '*': stack.push(a * b); break;
        case '/': stack.push(Math.trunc(a / b)); break;
      }
    }
  }

  return stack.pop();
}

console.log(evalRPN(['2','1','+','3','*']));

Search in Rotated Sorted Array.

Apply Binary Search while determining which half of the array is sorted. Decide whether the target lies in the sorted half, then continue searching only that half.

function search(nums, target) {
  let left = 0;
  let right = nums.length - 1;

  while (left <= right) {
    const mid = Math.floor((left + right) / 2);

    if (nums[mid] === target) return mid;

    if (nums[left] <= nums[mid]) {
      if (target >= nums[left] && target < nums[mid]) {
        right = mid - 1;
      } else {
        left = mid + 1;
      }
    } else {
      if (target > nums[mid] && target <= nums[right]) {
        left = mid + 1;
      } else {
        right = mid - 1;
      }
    }
  }

  return -1;
}

console.log(search([4,5,6,7,0,1,2],0));

Find First and Last Position of Element in Sorted Array.

Use Binary Search twice—once to find the leftmost occurrence and once to find the rightmost occurrence. This gives O(log n) time complexity.

function searchRange(nums, target) {
  function find(first) {
    let left = 0, right = nums.length - 1;
    let ans = -1;

    while (left <= right) {
      const mid = Math.floor((left + right) / 2);

      if (nums[mid] === target) {
        ans = mid;
        if (first) right = mid - 1;
        else left = mid + 1;
      } else if (nums[mid] < target) {
        left = mid + 1;
      } else {
        right = mid - 1;
      }
    }

    return ans;
  }

  return [find(true), find(false)];
}

console.log(searchRange([5,7,7,8,8,10],8));

Merge Intervals — merge all overlapping intervals.

Sort intervals by start time. Compare each interval with the previous one. If they overlap, merge them; otherwise, add a new interval.

function merge(intervals) {
  intervals.sort((a,b)=>a[0]-b[0]);

  const result=[];

  for(const interval of intervals){
    if(!result.length || result[result.length-1][1] < interval[0]){
      result.push(interval);
    }else{
      result[result.length-1][1]=Math.max(result[result.length-1][1],interval[1]);
    }
  }

  return result;
}

console.log(merge([[1,3],[2,6],[8,10],[15,18]]));

Insert Interval — insert a new interval into sorted intervals.

Add all intervals before the new interval, merge overlapping intervals, then append the remaining intervals.

function insert(intervals,newInterval){
  const result=[];
  let i=0;

  while(i<intervals.length && intervals[i][1]<newInterval[0]){
    result.push(intervals[i++]);
  }

  while(i<intervals.length && intervals[i][0]<=newInterval[1]){
    newInterval=[
      Math.min(newInterval[0],intervals[i][0]),
      Math.max(newInterval[1],intervals[i][1])
    ];
    i++;
  }

  result.push(newInterval);

  while(i<intervals.length){
    result.push(intervals[i++]);
  }

  return result;
}

console.log(insert([[1,3],[6,9]],[2,5]));

Spiral Matrix — return all elements in spiral order.

Maintain four boundaries (top, bottom, left, right). Traverse each boundary and shrink it until every element is visited.

function spiralOrder(matrix){
  const result=[];

  let top=0;
  let bottom=matrix.length-1;
  let left=0;
  let right=matrix[0].length-1;

  while(top<=bottom && left<=right){
    for(let i=left;i<=right;i++) result.push(matrix[top][i]);
    top++;

    for(let i=top;i<=bottom;i++) result.push(matrix[i][right]);
    right--;

    if(top<=bottom){
      for(let i=right;i>=left;i--) result.push(matrix[bottom][i]);
      bottom--;
    }

    if(left<=right){
      for(let i=bottom;i>=top;i--) result.push(matrix[i][left]);
      left++;
    }
  }

  return result;
}

console.log(spiralOrder([[1,2,3],[4,5,6],[7,8,9]]));

Number of Islands — count the number of islands in a grid.

Traverse every cell. Whenever you find land ('1'), increment the count and perform DFS/BFS to mark the entire island as visited.

function numIslands(grid){
  let count=0;

  function dfs(r,c){
    if(r<0||c<0||r>=grid.length||c>=grid[0].length||grid[r][c]==='0') return;

    grid[r][c]='0';

    dfs(r+1,c);
    dfs(r-1,c);
    dfs(r,c+1);
    dfs(r,c-1);
  }

  for(let r=0;r<grid.length;r++){
    for(let c=0;c<grid[0].length;c++){
      if(grid[r][c]==='1'){
        count++;
        dfs(r,c);
      }
    }
  }

  return count;
}

Clone Graph — create a deep copy of an undirected graph.

Use DFS (or BFS) with a Map to store already cloned nodes. If a node is visited again, return its clone instead of creating a new one.

function cloneGraph(node) {
  if (!node) return null;

  const map = new Map();

  function dfs(node) {
    if (map.has(node)) return map.get(node);

    const clone = { val: node.val, neighbors: [] };
    map.set(node, clone);

    for (const nei of node.neighbors) {
      clone.neighbors.push(dfs(nei));
    }

    return clone;
  }

  return dfs(node);
}

Course Schedule — determine whether all courses can be completed.

Represent courses as a graph and detect cycles using DFS. If a cycle exists, all courses cannot be completed. Otherwise, return true.

function canFinish(numCourses, prerequisites) {
  const graph = Array.from({ length: numCourses }, () => []);

  for (const [a, b] of prerequisites) {
    graph[a].push(b);
  }

  const visiting = new Set();
  const visited = new Set();

  function dfs(course) {
    if (visiting.has(course)) return false;
    if (visited.has(course)) return true;

    visiting.add(course);

    for (const next of graph[course]) {
      if (!dfs(next)) return false;
    }

    visiting.delete(course);
    visited.add(course);

    return true;
  }

  for (let i = 0; i < numCourses; i++) {
    if (!dfs(i)) return false;
  }

  return true;
}

Lowest Common Ancestor of a Binary Tree.

Traverse recursively. If either node is found, return it. If both left and right recursive calls return non-null values, the current node is the Lowest Common Ancestor.

function lowestCommonAncestor(root, p, q) {
  if (!root || root === p || root === q) return root;

  const left = lowestCommonAncestor(root.left, p, q);
  const right = lowestCommonAncestor(root.right, p, q);

  if (left && right) return root;

  return left || right;
}

Kth Largest Element in an Array.

Sort the array in descending order and return the Kth element. Interviewers may later ask for an O(n log k) Heap solution or O(n) Quick Select solution.

function findKthLargest(nums, k) {
  nums.sort((a, b) => b - a);
  return nums[k - 1];
}

console.log(findKthLargest([3,2,1,5,6,4],2));

Letter Combinations of a Phone Number.

Use Backtracking to generate all possible combinations. For every digit, iterate through its mapped letters and recursively build the current string.

function letterCombinations(digits) {
  if (!digits.length) return [];

  const map = {
    '2':'abc','3':'def','4':'ghi','5':'jkl',
    '6':'mno','7':'pqrs','8':'tuv','9':'wxyz'
  };

  const result = [];

  function backtrack(index, current) {
    if (index === digits.length) {
      result.push(current);
      return;
    }

    for (const ch of map[digits[index]]) {
      backtrack(index + 1, current + ch);
    }
  }

  backtrack(0, '');
  return result;
}

console.log(letterCombinations('23'));

Remove duplicates from an array.

Wrap it in a Set (which keeps unique values) and spread back to an array. For objects by a key, use a Map keyed on that field.

const unique = arr => [...new Set(arr)];
unique([1, 1, 2, 3, 3]); // [1, 2, 3]

// by key:
const byId = arr => [...new Map(arr.map(o => [o.id, o])).values()];

Flatten a deeply nested array (without .flat(Infinity)).

Reduce over the array; if an element is itself an array, recurse and concat the result, otherwise include it. Or use an explicit stack to avoid recursion.

function flatten(arr) {
  return arr.reduce((acc, v) =>
    acc.concat(Array.isArray(v) ? flatten(v) : v), []);
}
flatten([1, [2, [3, [4]]]]); // [1, 2, 3, 4]

Implement a debounce function.

Return a function that clears a pending timer and sets a new one on each call, so the wrapped function only runs after calls stop for ms. The timer id lives in a closure.

function debounce(fn, ms) {
  let t;
  return function (...args) {
    clearTimeout(t);
    t = setTimeout(() => fn.apply(this, args), ms);
  };
}

Implement a throttle function.

Track the last run time; only invoke the wrapped function if at least ms has elapsed since the previous call. Guarantees a maximum call rate.

function throttle(fn, ms) {
  let last = 0;
  return function (...args) {
    const now = Date.now();
    if (now - last >= ms) { last = now; fn.apply(this, args); }
  };
}

Deep clone an object (without structuredClone).

Recurse: return primitives as-is, map arrays, and rebuild objects key by key cloning each value. Handle Date/arrays explicitly. (In real code prefer structuredClone.)

function deepClone(v) {
  if (v === null || typeof v !== 'object') return v;
  if (v instanceof Date) return new Date(v);
  if (Array.isArray(v)) return v.map(deepClone);
  return Object.fromEntries(
    Object.entries(v).map(([k, val]) => [k, deepClone(val)])
  );
}

Write a generic memoize higher-order function.

Cache results in a Map keyed by the serialized arguments; return the cached value on repeat calls. Great for expensive pure functions.

function memoize(fn) {
  const cache = new Map();
  return function (...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

Implement curry — curry(fn)(a)(b)(c) === fn(a,b,c).

Collect arguments across calls; once you have at least fn.length of them, invoke. Otherwise return a function that keeps gathering. Uses closures + recursion.

function curry(fn) {
  return function curried(...args) {
    return args.length >= fn.length
      ? fn.apply(this, args)
      : (...next) => curried.apply(this, args.concat(next));
  };
}
const add = (a, b, c) => a + b + c;
curry(add)(1)(2)(3); // 6

Implement Promise.all.

Return a new Promise. Track a results array and a completion counter; resolve when every input settles, preserving order by index; reject on the first failure.

function promiseAll(promises) {
  return new Promise((resolve, reject) => {
    const results = []; let done = 0;
    if (!promises.length) return resolve(results);
    promises.forEach((p, i) => {
      Promise.resolve(p).then(v => {
        results[i] = v;
        if (++done === promises.length) resolve(results);
      }, reject);
    });
  });
}

Build a simple EventEmitter (on / off / emit).

Keep a map of event name → array of listeners. on pushes, off filters out, emit calls each listener with the payload. This is the pub/sub pattern behind RxJS Subjects and Node's EventEmitter.

class EventEmitter {
  constructor() { this.map = {}; }
  on(e, fn) { (this.map[e] ||= []).push(fn); return () => this.off(e, fn); }
  off(e, fn) { this.map[e] = (this.map[e] || []).filter(f => f !== fn); }
  emit(e, ...args) { (this.map[e] || []).forEach(fn => fn(...args)); }
}

Group an array of objects by a property.

Reduce into an object whose keys are the group values and whose values are arrays. Modern JS also has Object.groupBy.

const groupBy = (arr, key) =>
  arr.reduce((acc, item) => {
    (acc[item[key]] ||= []).push(item);
    return acc;
  }, {});
groupBy([{ t: 'a', v: 1 }, { t: 'b', v: 2 }, { t: 'a', v: 3 }], 't');

Flatten a nested object to dot-notation keys.

Recurse over entries; for nested objects, prefix the parent key with a dot and keep descending; for leaves, assign the flattened key.

function flatten(obj, prefix = '', out = {}) {
  for (const [k, v] of Object.entries(obj)) {
    const key = prefix ? `${prefix}.${k}` : k;
    if (v && typeof v === 'object' && !Array.isArray(v)) flatten(v, key, out);
    else out[key] = v;
  }
  return out;
}
flatten({ a: { b: { c: 1 } } }); // { 'a.b.c': 1 }

Count word frequency in a sentence.

Lowercase, split into words, and tally each in a Map/object. Sort by count if you need the most frequent.

function wordFreq(str) {
  const map = {};
  for (const w of str.toLowerCase().match(/\w+/g) || [])
    map[w] = (map[w] || 0) + 1;
  return map;
}

Write an async sleep(ms) helper.

Return a Promise that resolves via setTimeout. You can then await sleep(1000) to pause inside an async function.

const sleep = ms => new Promise(res => setTimeout(res, ms));

async function run() {
  console.log('start');
  await sleep(1000);
  console.log('1s later');
}

Polyfill Array.prototype.map.

Add a method to the prototype that loops the array, calls the callback with (value, index, array), and pushes each result into a new array — never mutating the original.

Array.prototype.myMap = function (cb) {
  const out = [];
  for (let i = 0; i < this.length; i++) out.push(cb(this[i], i, this));
  return out;
};
[1, 2, 3].myMap(x => x * 2); // [2, 4, 6]

Check whether two strings are anagrams.

Normalise (lowercase, strip non-word chars), then compare sorted characters — or compare character-frequency maps for O(n) instead of O(n log n).

const norm = s => s.toLowerCase().replace(/\W/g, '').split('').sort().join('');
const isAnagram = (a, b) => norm(a) === norm(b);
isAnagram('listen', 'silent'); // true

Find the maximum sum of a contiguous subarray (Kadane's).

Track the best sum ending at the current index (cur) and the global best. Extend or restart at each element. O(n) time, O(1) space.

function maxSubArray(nums) {
  let cur = nums[0], best = nums[0];
  for (let i = 1; i < nums.length; i++) {
    cur = Math.max(nums[i], cur + nums[i]);
    best = Math.max(best, cur);
  }
  return best;
}
maxSubArray([-2, 1, -3, 4, -1, 2, 1, -5, 4]); // 6

Predict the output: sync vs setTimeout vs Promise.

Output: 1, 4, 3, 2.

Synchronous code runs first (1, then 4). When the stack is empty, the engine drains the microtask queue (the Promise .then3) before any macrotask. The setTimeout callback is a macrotask, so 2 runs last — even with a 0ms delay.

console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(3));
console.log(4);

// Output:
// 1
// 4
// 3
// 2

Predict the output: async/await ordering.

Output: start, async1 start, async2, end, async1 end, promise, timeout.

Everything up to the first await runs synchronously. await async2() runs async2's body now, then schedules the rest of async1 as a microtask. The synchronous script finishes (end) before any microtask. Microtasks (the await continuation and the .then) drain before the setTimeout macrotask.

async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}
async function async2() {
  console.log('async2');
}

console.log('start');
setTimeout(() => console.log('timeout'), 0);
async1();
Promise.resolve().then(() => console.log('promise'));
console.log('end');

// Output:
// start
// async1 start
// async2
// end
// async1 end
// promise
// timeout

Predict the output: setTimeout inside a for-loop (var vs let).

With var3, 3, 3. With let0, 1, 2.

var is function-scoped, so all three callbacks close over the same i. The loop finishes (i = 3) before any timeout fires, so all log 3. let is block-scoped and creates a fresh binding each iteration, so each callback captures its own value.

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 3, 3, 3

for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j), 0);
}
// 0, 1, 2

Predict the output: interleaved Promise chain and setTimeout.

Output: script start, script end, promise1, promise2, setTimeout.

Both console.logs in the main body are synchronous. The Promise chain runs as microtasks (promise1 then promise2), and the whole microtask queue drains before the single setTimeout macrotask.

console.log('script start');

setTimeout(() => console.log('setTimeout'), 0);

Promise.resolve()
  .then(() => console.log('promise1'))
  .then(() => console.log('promise2'));

console.log('script end');

// Output:
// script start
// script end
// promise1
// promise2
// setTimeout

Design an LRU Cache supporting get() and put() in O(1).

The optimal solution combines a HashMap with a doubly linked list. The HashMap provides O(1) lookup, while the linked list maintains usage order. Recently used items move to the front, and the least recently used item is removed from the tail.

class LRUCache {
  constructor(capacity) {
    this.capacity = capacity;
    this.map = new Map();
  }

  get(key) {
    if (!this.map.has(key)) return -1;

    const value = this.map.get(key);
    this.map.delete(key);
    this.map.set(key, value);

    return value;
  }

  put(key, value) {
    if (this.map.has(key)) {
      this.map.delete(key);
    }

    this.map.set(key, value);

    if (this.map.size > this.capacity) {
      const oldest = this.map.keys().next().value;
      this.map.delete(oldest);
    }
  }
}

const cache = new LRUCache(2);
cache.put(1,1);
cache.put(2,2);
console.log(cache.get(1)); // 1

Next Greater Element — find the next greater element for each number.

Use a monotonic decreasing stack. While the current element is greater than the stack top, pop it and map its next greater element. Remaining elements have no greater value and map to -1.

function nextGreaterElement(nums1, nums2) {
  const stack = [];
  const map = new Map();

  for (const num of nums2) {
    while (stack.length && num > stack[stack.length - 1]) {
      map.set(stack.pop(), num);
    }
    stack.push(num);
  }

  while (stack.length) {
    map.set(stack.pop(), -1);
  }

  return nums1.map(n => map.get(n));
}

console.log(nextGreaterElement([4,1,2],[1,3,4,2]));

Permutations — generate all possible permutations of an array.

Use Backtracking. Choose one unused element at each recursive step, mark it as visited, recurse, then backtrack by removing it and marking it unused again.

function permute(nums) {
  const result = [];
  const used = Array(nums.length).fill(false);

  function backtrack(path) {
    if (path.length === nums.length) {
      result.push([...path]);
      return;
    }

    for (let i = 0; i < nums.length; i++) {
      if (used[i]) continue;

      used[i] = true;
      path.push(nums[i]);

      backtrack(path);

      path.pop();
      used[i] = false;
    }
  }

  backtrack([]);
  return result;
}

console.log(permute([1,2,3]));

Predict the output: microtask queued inside a macrotask.

Output: 1, 5, 3, 2, 4.

Sync first: 1, 5. The stack empties, so the microtask 3 runs. Then the timeout macrotask runs 2 and, inside it, queues a new microtask. Because the microtask queue is drained again right after that macrotask finishes, 4 runs next — before any further macrotask.

console.log(1);

setTimeout(() => {
  console.log(2);
  Promise.resolve().then(() => console.log(4));
}, 0);

Promise.resolve().then(() => console.log(3));

console.log(5);

// Output:
// 1
// 5
// 3
// 2
// 4

Predict the output: what does an async function return, and when?

Output: A, C, B.

An async function returns a Promise immediately. Calling f() runs its body synchronously up to the return (A), but the .then attached to the returned Promise (B) is a microtask. So the synchronous C after the call runs before B.

async function f() {
  console.log('A');
  return 'done';
}

f().then((v) => console.log('B', v));
console.log('C');

// Output:
// A
// C
// B done

Find the first non-repeating character in a string.

Count frequencies in one pass (LinkedHashMap keeps encounter order), then return the first key with count 1. O(n) time, O(1) space (bounded alphabet).

Very often asked as a Java 8 streams one-liner — practice both forms.

// classic
char firstUnique(String s) {
  Map<Character, Integer> freq = new LinkedHashMap<>();
  for (char c : s.toCharArray()) freq.merge(c, 1, Integer::sum);
  for (var e : freq.entrySet()) if (e.getValue() == 1) return e.getKey();
  return 0;
}

// streams
Character first = s.chars().mapToObj(c -> (char) c)
  .collect(Collectors.groupingBy(c -> c, LinkedHashMap::new, Collectors.counting()))
  .entrySet().stream()
  .filter(e -> e.getValue() == 1)
  .map(Map.Entry::getKey)
  .findFirst().orElse(null);   // "swiss" -> 'w'

String compression: aabbbc → a2b3c1.

Walk the string counting runs of the same character; append char + count when the run breaks. Use StringBuilder — string concatenation in a loop is O(n²). O(n) time.

String compress(String s) {
  StringBuilder sb = new StringBuilder();
  int count = 1;
  for (int i = 1; i <= s.length(); i++) {
    if (i < s.length() && s.charAt(i) == s.charAt(i - 1)) count++;
    else { sb.append(s.charAt(i - 1)).append(count); count = 1; }
  }
  return sb.toString();
}
compress("aabbbc"); // "a2b3c1"

Longest common prefix of an array of strings.

Take the first string as candidate; for each other string, shrink the candidate from the end while it isn't a prefix. O(S) where S = total characters.

Alternative: sort and compare only first vs last string — they're the most different.

String longestCommonPrefix(String[] strs) {
  String prefix = strs[0];
  for (int i = 1; i < strs.length; i++) {
    while (!strs[i].startsWith(prefix))
      prefix = prefix.substring(0, prefix.length() - 1);
    if (prefix.isEmpty()) return "";
  }
  return prefix;
}
// ["flower","flow","flight"] -> "fl"

Check if one string is a rotation of another (and rotate a string by k).

The elegant trick: b is a rotation of a iff they're the same length and b is a substring of a + a. One line after the length check.

Rotating by k: s.substring(k) + s.substring(0, k) (normalize k with modulo first).

boolean isRotation(String a, String b) {
  return a.length() == b.length() && (a + a).contains(b);
}

String rotateLeft(String s, int k) {
  k %= s.length();
  return s.substring(k) + s.substring(0, k);
}

Count vowels and consonants in a string (loop and streams).

Normalize to lowercase, check membership in the vowel set, count letters only. Trivial logic — what's evaluated is clean code and the streams variant.

int[] countVowelsConsonants(String s) {
  int v = 0, c = 0;
  for (char ch : s.toLowerCase().toCharArray()) {
    if (ch < 'a' || ch > 'z') continue;
    if ("aeiou".indexOf(ch) >= 0) v++; else c++;
  }
  return new int[]{v, c};
}

// streams
long vowels = s.toLowerCase().chars()
    .filter(ch -> "aeiou".indexOf(ch) >= 0).count();

Find max, min, and the second largest in an array — without sorting.

Single pass with two trackers: when a new max appears, the old max becomes second; else update second when the value fits between. O(n) time, O(1) space — sorting (O(n log n)) is the answer they don't want.

int secondLargest(int[] a) {
  int max = Integer.MIN_VALUE, second = Integer.MIN_VALUE;
  for (int x : a) {
    if (x > max) { second = max; max = x; }
    else if (x > second && x < max) second = x;
  }
  return second; // MIN_VALUE => not found
}
// [12, 35, 1, 10, 34, 1] -> 34

Move all zeros to the end, keeping the order of non-zeros (in place).

Two pointers: insertPos tracks where the next non-zero goes; sweep once copying non-zeros forward, then fill the tail with zeros. O(n) time, O(1) space, stable.

void moveZeroes(int[] nums) {
  int insertPos = 0;
  for (int x : nums) if (x != 0) nums[insertPos++] = x;
  while (insertPos < nums.length) nums[insertPos++] = 0;
}
// [0,1,0,3,12] -> [1,3,12,0,0]

Remove duplicates from a sorted array in place; return the new length.

Because it's sorted, duplicates are adjacent. Slow pointer marks the end of the unique prefix; fast pointer scans — copy forward only when the value differs from the last kept one. O(n) time, O(1) space.

int removeDuplicates(int[] nums) {
  if (nums.length == 0) return 0;
  int k = 1;                       // nums[0..k-1] = unique prefix
  for (int i = 1; i < nums.length; i++)
    if (nums[i] != nums[k - 1]) nums[k++] = nums[i];
  return k;
}
// [1,1,2,2,3] -> 3, array starts [1,2,3,...]

Find the missing number in 1..n — and the duplicate number in an array.

Missing: expected sum n(n+1)/2 minus actual sum; or XOR all indices and values (overflow-proof). O(n)/O(1).

Duplicate: HashSet catches the first repeat in O(n)/O(n); the O(1)-space follow-up is Floyd's cycle detection treating values as pointers.

int missing(int[] a, int n) {          // values 1..n, one absent
  long sum = (long) n * (n + 1) / 2;
  for (int x : a) sum -= x;
  return (int) sum;
}

int firstDuplicate(int[] a) {
  Set<Integer> seen = new HashSet<>();
  for (int x : a) if (!seen.add(x)) return x;
  return -1;
}

Reverse an array in place, and rotate an array by k (reversal algorithm).

Reverse: two pointers swapping inward. Rotate right by k without extra space — the reversal trick: reverse the whole array, then reverse the first k, then the rest. O(n) time, O(1) space.

void rotate(int[] a, int k) {
  k %= a.length;
  reverse(a, 0, a.length - 1);   // [1,2,3,4,5,6,7] k=3
  reverse(a, 0, k - 1);          // -> [5,6,7,1,2,3,4]
  reverse(a, k, a.length - 1);
}
void reverse(int[] a, int i, int j) {
  while (i < j) { int t = a[i]; a[i++] = a[j]; a[j--] = t; }
}

Sort an array of 0s, 1s and 2s in one pass (Dutch National Flag).

Three pointers: low (next slot for 0), mid (scanner), high (next slot for 2). Swap 0s down, 2s up, walk past 1s. One pass, O(n) time, O(1) space — no counting, no sorting.

void sortColors(int[] a) {
  int low = 0, mid = 0, high = a.length - 1;
  while (mid <= high) {
    switch (a[mid]) {
      case 0 -> { swap(a, low++, mid++); }
      case 1 -> mid++;
      case 2 -> { swap(a, mid, high--); }  // mid stays!
    }
  }
}
// [2,0,2,1,1,0] -> [0,0,1,1,2,2]

Intersection and union of two arrays in O(n).

Sets do the work: union = add everything to one set; intersection = build a set from the first array, keep elements of the second found in it (retainAll or a manual contains loop). O(n + m) time — the point is avoiding the O(n·m) nested loop.

Set<Integer> intersection(int[] a, int[] b) {
  Set<Integer> setA = new HashSet<>();
  for (int x : a) setA.add(x);
  Set<Integer> result = new HashSet<>();
  for (int x : b) if (setA.contains(x)) result.add(x);
  return result;
}

// streams union
int[] union = IntStream.concat(Arrays.stream(a), Arrays.stream(b))
    .distinct().toArray();

Find the majority element (appears more than n/2 times).

Boyer-Moore voting: keep a candidate and a counter — same element increments, different decrements, zero picks a new candidate. Majority survives all cancellations. O(n) time, O(1) space.

The HashMap-count version is the obvious O(n)/O(n) warm-up; Boyer-Moore is the answer that impresses.

int majorityElement(int[] nums) {
  int count = 0, candidate = 0;
  for (int x : nums) {
    if (count == 0) candidate = x;
    count += (x == candidate) ? 1 : -1;
  }
  return candidate;
}
// [2,2,1,1,1,2,2] -> 2

Find all leaders in an array (elements greater than everything to their right).

Scan right to left tracking the running max: an element ≥ max-so-far is a leader. O(n) time, one pass — versus the naive O(n²) 'for each element scan its right side'.

List<Integer> leaders(int[] a) {
  List<Integer> res = new ArrayList<>();
  int maxRight = Integer.MIN_VALUE;
  for (int i = a.length - 1; i >= 0; i--) {
    if (a[i] >= maxRight) { res.add(a[i]); maxRight = a[i]; }
  }
  Collections.reverse(res);
  return res;
}
// [16,17,4,3,5,2] -> [17,5,2]

Find a contiguous subarray with a given sum.

Positive numbers: sliding window — grow the right edge, shrink from the left while the sum exceeds the target. O(n).

With negatives (window breaks): prefix sums + HashMap — at each index look up prefix − target; if seen before, the slice between is the answer. O(n)/O(n).

// positives: sliding window
int[] subarraySum(int[] a, int target) {
  int left = 0, sum = 0;
  for (int right = 0; right < a.length; right++) {
    sum += a[right];
    while (sum > target && left <= right) sum -= a[left++];
    if (sum == target) return new int[]{left, right};
  }
  return new int[]{-1, -1};
}
// negatives allowed: prefix + HashMap<prefixSum, index>

Transpose a matrix, and rotate it 90° clockwise in place.

Transpose: swap a[i][j] with a[j][i] across the diagonal (j starts at i+1 to avoid double-swapping).

Rotate 90° clockwise = transpose, then reverse each row. Two clean passes, O(n²) time, O(1) extra space. (Counter-clockwise = transpose + reverse columns.)

void rotate90(int[][] m) {
  int n = m.length;
  for (int i = 0; i < n; i++)                 // transpose
    for (int j = i + 1; j < n; j++) {
      int t = m[i][j]; m[i][j] = m[j][i]; m[j][i] = t;
    }
  for (int[] row : m) {                        // reverse each row
    for (int i = 0, j = n - 1; i < j; i++, j--) {
      int t = row[i]; row[i] = row[j]; row[j] = t;
    }
  }
}

Fibonacci — iterative, recursive, and memoized.

Three versions, three lessons: naive recursion is O(2ⁿ) (recomputes everything), memoization makes it O(n) by caching, iteration is O(n) time O(1) space and usually the best answer.

// iterative — O(n)/O(1), the answer to give
long fib(int n) {
  long a = 0, b = 1;
  for (int i = 0; i < n; i++) { long t = a + b; a = b; b = t; }
  return a;
}

// memoized — O(n)/O(n)
Map<Integer, Long> memo = new HashMap<>();
long fibMemo(int n) {
  if (n < 2) return n;
  return memo.computeIfAbsent(n, k -> fibMemo(k - 1) + fibMemo(k - 2));
}

Factorial — and count the trailing zeros of n! without computing it.

Factorial itself: iterative loop (use long/BigInteger — int overflows at 13!).

Trailing zeros: each zero comes from a factor 10 = 2×5, and 5s are rarer than 2s — so count factors of 5: n/5 + n/25 + n/125 + .... O(log n), no factorial computed.

long factorial(int n) {
  long f = 1;
  for (int i = 2; i <= n; i++) f *= i;
  return f;                       // 20! is the long limit
}

int trailingZeros(int n) {
  int count = 0;
  for (int p = 5; p <= n; p *= 5) count += n / p;
  return count;
}
trailingZeros(100); // 24

Check if a number is prime, and list primes up to N (Sieve of Eratosthenes).

Single check: trial division up to √n — a factor above √n implies one below it. O(√n).

All primes ≤ N: the sieve — mark multiples of each prime as composite, starting from p². O(N log log N), the expected answer for 'primes up to N'.

boolean isPrime(int n) {
  if (n < 2) return false;
  for (int i = 2; (long) i * i <= n; i++)
    if (n % i == 0) return false;
  return true;
}

boolean[] sieve(int n) {
  boolean[] composite = new boolean[n + 1];
  for (int p = 2; (long) p * p <= n; p++)
    if (!composite[p])
      for (int m = p * p; m <= n; m += p) composite[m] = true;
  return composite;   // !composite[i] && i >= 2 => prime
}

Armstrong number and perfect number checks.

Armstrong: sum of each digit raised to the power of the digit count equals the number (153 = 1³+5³+3³).

Perfect: sum of proper divisors equals the number (28 = 1+2+4+7+14). Collect divisors in pairs up to √n for O(√n).

boolean isArmstrong(int n) {
  int digits = String.valueOf(n).length(), sum = 0;
  for (int t = n; t > 0; t /= 10)
    sum += (int) Math.pow(t % 10, digits);
  return sum == n;
}

boolean isPerfect(int n) {
  if (n < 2) return false;
  int sum = 1;
  for (int i = 2; (long) i * i <= n; i++)
    if (n % i == 0) { sum += i; if (i != n / i) sum += n / i; }
  return sum == n;
}

Reverse an integer, and sum its digits.

Both are the same digit loop: peel with % 10, shrink with / 10. For reversal, build rev = rev * 10 + digit — and mention overflow: reversing 1999999999 exceeds int; check before multiplying or use long.

int reverse(int n) {
  long rev = 0;                       // long to detect overflow
  while (n != 0) {
    rev = rev * 10 + n % 10;
    n /= 10;
  }
  return (rev < Integer.MIN_VALUE || rev > Integer.MAX_VALUE) ? 0 : (int) rev;
}

int digitSum(int n) {
  int sum = 0;
  for (n = Math.abs(n); n > 0; n /= 10) sum += n % 10;
  return sum;
}

Compute GCD and LCM (Euclid's algorithm).

Euclid: gcd(a, b) = gcd(b, a % b) until b is 0 — O(log min(a,b)). Then lcm = a / gcd * b.

Divide before multiplying in LCM — a * b first overflows for large ints.

int gcd(int a, int b) {
  while (b != 0) { int t = b; b = a % b; a = t; }
  return a;
}
// recursive: gcd(a, b) = b == 0 ? a : gcd(b, a % b)

long lcm(int a, int b) {
  return (long) (a / gcd(a, b)) * b;   // divide first: no overflow
}

Bit tricks: power-of-two check, count set bits, swap without a temp variable.

  • Power of two: n > 0 && (n & (n − 1)) == 0 — a power of two has exactly one set bit, and n−1 flips it plus everything below.
  • Count set bits: Kernighan's loop — n &= n − 1 clears the lowest set bit; iterations = bit count.
  • Swap without temp: XOR three times (or arithmetic a = a + b...). Party trick — in real code use a temp.
boolean isPowerOfTwo(int n) { return n > 0 && (n & (n - 1)) == 0; }

int countSetBits(int n) {
  int c = 0;
  while (n != 0) { n &= n - 1; c++; }   // Kernighan
  return c;
}

// swap without temp
a ^= b; b ^= a; a ^= b;

Reverse a singly linked list.

Three pointers walking the list: prev, curr, next. Save next, point curr backwards, advance. O(n) time, O(1) space; prev is the new head.

class ListNode { int val; ListNode next; }

ListNode reverse(ListNode head) {
  ListNode prev = null, curr = head;
  while (curr != null) {
    ListNode next = curr.next;   // save
    curr.next = prev;            // flip
    prev = curr;                 // advance
    curr = next;
  }
  return prev;                   // new head
}

Detect a cycle in a linked list (Floyd's tortoise and hare).

Two pointers: slow moves 1, fast moves 2. If there's a cycle they must meet inside it (fast gains one step per iteration); if fast hits null, no cycle. O(n) time, O(1) space — the whole point versus the HashSet approach.

Follow-up: after meeting, reset one pointer to head; both stepping by 1 meet at the cycle start.

boolean hasCycle(ListNode head) {
  ListNode slow = head, fast = head;
  while (fast != null && fast.next != null) {
    slow = slow.next;
    fast = fast.next.next;
    if (slow == fast) return true;
  }
  return false;
}

Find the middle element of a linked list in one pass.

Slow/fast pointers again: fast moves 2, slow moves 1 — when fast reaches the end, slow is at the middle. One pass, no length count. For even lengths this yields the second middle; adjust the loop condition for the first.

ListNode middle(ListNode head) {
  ListNode slow = head, fast = head;
  while (fast != null && fast.next != null) {
    slow = slow.next;
    fast = fast.next.next;
  }
  return slow;   // 1->2->3->4->5 => 3
}

Merge two sorted linked lists.

A dummy head + tail pointer: repeatedly attach the smaller front node, advance that list; attach the leftover at the end. O(n+m) time, O(1) space — reusing existing nodes, no copies.

ListNode merge(ListNode a, ListNode b) {
  ListNode dummy = new ListNode(), tail = dummy;
  while (a != null && b != null) {
    if (a.val <= b.val) { tail.next = a; a = a.next; }
    else                { tail.next = b; b = b.next; }
    tail = tail.next;
  }
  tail.next = (a != null) ? a : b;   // leftover
  return dummy.next;
}

Implement a queue using two stacks (and a stack using queues).

Queue via two stacks: push into in; pop from out, refilling it by draining in only when empty. Each element moves at most twice → amortized O(1) per operation.

Stack via one queue: after enqueueing, rotate the queue (dequeue+enqueue n−1 times) so the newest sits at the front — push O(n), pop O(1).

class QueueViaStacks {
  Deque<Integer> in = new ArrayDeque<>(), out = new ArrayDeque<>();
  void enqueue(int x) { in.push(x); }
  int dequeue() {
    if (out.isEmpty())
      while (!in.isEmpty()) out.push(in.pop());   // reverse once
    return out.pop();
  }
}

Binary search — iterative and recursive.

On a sorted array, halve the search space each step: compare the middle, discard the wrong half. O(log n) time; iterative is O(1) space, recursive O(log n) stack.

The two classic bugs: mid = lo + (hi − lo) / 2 (not (lo+hi)/2 — overflow), and getting lo <= hi / boundary updates right.

int binarySearch(int[] a, int target) {
  int lo = 0, hi = a.length - 1;
  while (lo <= hi) {
    int mid = lo + (hi - lo) / 2;      // overflow-safe
    if (a[mid] == target) return mid;
    if (a[mid] < target) lo = mid + 1;
    else hi = mid - 1;
  }
  return -1;
}
// recursive: search(a, t, lo, mid-1) / (a, t, mid+1, hi)

Tower of Hanoi — explain the approach and complexity.

Move n disks A→C using B: recursively move n−1 disks A→B, move the largest disk A→C, then move n−1 disks B→C. The recursion IS the answer — three lines.

Moves: T(n) = 2T(n−1) + 1 = 2ⁿ − 1, so O(2ⁿ) — the standard example of exponential growth from an innocent recurrence.

void hanoi(int n, char from, char via, char to) {
  if (n == 0) return;
  hanoi(n - 1, from, to, via);            // n-1 out of the way
  System.out.println("disk " + n + ": " + from + " -> " + to);
  hanoi(n - 1, via, from, to);            // n-1 onto the big one
}
hanoi(3, 'A', 'B', 'C');   // 7 moves