Why You Need to Know Sorting Algorithms

Google’s official guidance is explicit: "Know at least one or two O(n log n) sorting algorithms. Avoid Bubble Sort."

Key misconceptions: - You probably won’t implement a sort from scratch in an interview. - But you MUST understand: * How they work conceptually * Time and space complexity * When to use which one * How to optimize them

Critical insight: Sorting is often a preprocessing step that unlocks simpler solutions to hard problems (Two Sum, 3Sum, Merge Intervals, etc.).

The most common JS mistake in interviews:

[10, 2, 1].sort()  // Returns [1, 10, 2] — NOT what you want!

Without a comparator, sort() converts to strings. Always use:

[10, 2, 1].sort((a, b) => a - b)  // [1, 2, 10]

Merge Sort: Divide and Conquer

Concept

Merge Sort is a stable, divide-and-conquer algorithm:

  1. Divide: Split array in half recursively until single elements

  2. Conquer: Two sorted halves are already sorted

  3. Combine: Merge two sorted halves into one sorted array

Complexity Analysis

  • Time: O(n log n) — guaranteed (best, average, worst)

    • Why? Recursion depth = log n levels

    • Each level does O(n) work (merging all elements)

    • Total: log n × O(n) = O(n log n)

  • Space: O(n) auxiliary (the temp array during merge)

    • This is a big trade-off vs QuickSort

Why Stable Sort Matters

A stable sort maintains the relative order of equal elements.

// Suppose we're sorting students by name:
// Input: [{name: 'Alice', id: 2}, {name: 'Alice', id: 1}, {name: 'Bob', id: 3}]
// Merge Sort (stable): Alice(2), Alice(1), Bob  — id 2 comes before id 1
// QuickSort (unstable): Alice(1), Alice(2), Bob — might reorder the Alices

Stability matters when: - Sorting by one field while preserving order of another - Sorting already partially-sorted data

When to Use Merge Sort

  • Stability required

  • Sorting linked lists (no random access, so QuickSort is bad)

  • External sorting (data too big for memory — merge external chunks)

  • Guaranteed O(n log n) needed (not average-case)

Visual: Divide and Merge

Diagram

JavaScript Implementation

function mergeSort(arr) {
  if (arr.length <= 1) return arr;

  const mid = Math.floor(arr.length / 2);
  const left = mergeSort(arr.slice(0, mid));
  const right = mergeSort(arr.slice(mid));

  return merge(left, right);
}

function merge(left, right) {
  const result = [];
  let i = 0, j = 0;

  // Merge while both arrays have elements
  while (i < left.length && j < right.length) {
    if (left[i] <= right[j]) {  // <= ensures stability
      result.push(left[i++]);
    } else {
      result.push(right[j++]);
    }
  }

  // Add remaining elements
  result.push(...left.slice(i), ...right.slice(j));
  return result;
}

QuickSort: Fast and In-Place

Concept

QuickSort uses partition-based divide and conquer:

  1. Pick a pivot element

  2. Partition: Elements < pivot go left, > pivot go right

  3. Recursively sort left and right partitions

Complexity Analysis

  • Time: O(n log n) average, O(n²) worst case

    • Average: good pivot choices → balanced recursion tree

    • Worst: bad pivot (e.g., first element on sorted array) → O(n) per level × n levels

  • Space: O(log n) for recursion stack (in-place!)

    • Much better than Merge Sort’s O(n) auxiliary space

  • Not stable: equal elements may be reordered

The Pivot Choice Problem

Choosing the wrong pivot is dangerous:

// Worst case: QuickSort on already sorted array with first element as pivot
// [1, 2, 3, 4, 5]
// Pivot = 1: left = [], right = [2, 3, 4, 5]  -> recurse on [2, 3, 4, 5]
// Pivot = 2: left = [], right = [3, 4, 5]      -> recurse on [3, 4, 5]
// Each partition removes only ONE element → O(n²) behavior!

Solutions: 1. Random pivot: Pick a random element (almost always avoids worst case) 2. Median-of-three: Pick median of first, middle, last (more complex, rarely needed in interviews)

When to Use QuickSort

  • In-place sorting required (space is critical)

  • General-purpose sorting (fastest in practice despite O(n²) worst case)

  • JavaScript’s Array.sort() uses TimSort (hybrid of Merge Sort + Insertion Sort) — it’s optimized, not vanilla QuickSort

JavaScript Implementation with Random Pivot

function quickSort(arr, low = 0, high = arr.length - 1) {
  if (low < high) {
    const pi = partition(arr, low, high);
    quickSort(arr, low, pi - 1);
    quickSort(arr, pi + 1, high);
  }
  return arr;
}

function partition(arr, low, high) {
  // Random pivot to avoid worst case
  const randomIndex = low + Math.floor(Math.random() * (high - low + 1));
  [arr[randomIndex], arr[high]] = [arr[high], arr[randomIndex]];

  const pivot = arr[high];
  let i = low - 1;

  for (let j = low; j < high; j++) {
    if (arr[j] < pivot) {
      i++;
      [arr[i], arr[j]] = [arr[j], arr[i]];
    }
  }

  [arr[i + 1], arr[high]] = [arr[high], arr[i + 1]];
  return i + 1;
}

Linear-Time Sorting: Counting Sort & Radix Sort

When to Use

If the range of values is bounded and small (e.g., 0 to k), you can sort in O(n) time:

  • Counting Sort: O(n + k) where k = range of values

    • Only for integers or values mappable to small integers

  • Radix Sort: Sort by digits/bits — O(n × d) where d = number of digits

    • E.g., sorting 32-bit integers: O(n × 32) = O(n)

These are rarely asked in interviews, but knowing they exist is good. In an interview, if you hear "values are in range 0 to 100," mention Counting Sort.


When to Use Built-In Sort()

In real interviews, using array.sort((a, b) ⇒ a - b) is perfectly fine and expected for most problems.

// DO THIS in interviews:
array.sort((a, b) => a - b);  // Ascending

// DON'T DO THIS (sorts as strings):
array.sort();  // [1, 10, 2] → ["1", "10", "2"] (lexicographic)

// For objects:
people.sort((a, b) => a.age - b.age);

Complexity: O(n log n) guaranteed in modern JS engines (V8 uses TimSort).


Sorting as Preprocessing: Unlock Easier Solutions

Many hard problems become trivial after sorting:

Two Sum with Sorting

// Problem: Find two numbers that sum to target
// Naive: O(n²) nested loops
// Better: O(n log n) sort + two pointers

function twoSum(arr, target) {
  arr.sort((a, b) => a - b);  // O(n log n)
  let left = 0, right = arr.length - 1;

  while (left < right) {
    const sum = arr[left] + arr[right];
    if (sum === target) return [arr[left], arr[right]];
    if (sum < target) left++;
    else right--;
  }
  return null;  // No pair found
}

Merge Intervals (Example Problem)

Sort by start time, then merge overlapping intervals. See "Problems" section below.

Meeting Rooms II (Example Problem)

Sort events, use min-heap or sweep line. See "Problems" section below.


Stacks and Queues: Foundation Patterns

Stack (LIFO: Last-In-First-Out)

const stack = [];
stack.push(1);      // Add to top: O(1)
stack.pop();        // Remove from top: O(1)
stack[stack.length - 1];  // Peek top: O(1)

When to use: - Parentheses matching / bracket validation - Undo/redo functionality - Expression evaluation - DFS-style traversal

Queue (FIFO: First-In-First-Out)

// WRONG: Don't use array.shift() — it's O(n)!
const queue = [];
queue.push(1);      // O(1)
queue.shift();      // O(n) ← BAD! All elements shift down

// RIGHT: Use index tracking or a proper queue
class Queue {
  constructor() { this.items = {}; this.head = 0; this.tail = 0; }
  enqueue(val) { this.items[this.tail++] = val; }
  dequeue() {
    if (this.head === this.tail) return undefined;
    const val = this.items[this.head];
    delete this.items[this.head++];
    return val;
  }
  isEmpty() { return this.head === this.tail; }
}

When to use: - BFS traversal - Task scheduling - Rate limiting

Monotonic Stack Pattern

A monotonic stack maintains elements in increasing or decreasing order. When you encounter a smaller (for increasing) or larger (for decreasing) element, you process the stack elements.

Use case: Find next/previous greater/smaller element in O(n) time

// Problem: For each element, find the next greater element
// Input: [1, 3, 2, 4]
// Output: [3, 4, -1, -1]  (next greater or -1)

function nextGreaterElement(arr) {
  const result = new Array(arr.length).fill(-1);
  const stack = [];

  for (let i = 0; i < arr.length; i++) {
    // While stack is not empty and current > top of stack
    while (stack.length && arr[i] > arr[stack[stack.length - 1]]) {
      const prevIdx = stack.pop();
      result[prevIdx] = arr[i];  // Found next greater!
    }
    stack.push(i);  // Push current index
  }

  return result;
}
// Time: O(n), Space: O(n) — each element pushed/popped once

Why is this O(n) and not O(n²)? - Each element is pushed once and popped once - Total operations = O(n), not O(n log n) or O(n²)


Linked Lists: When and Why

Why Linked Lists Exist

Operation

Array

Linked List

Access arr[i]

O(1)

O(n)

Insert at front

O(n)

O(1)

Insert at known position

O(n)

O(1)

Delete at front

O(n)

O(1)

Sorted traversal

O(1) per step

O(1) per step

In interviews: Linked lists are asked less frequently now (maybe 1 out of 5-6 interviews), but when they appear, they test: - Pointer manipulation - Two-pointer patterns - Recursion understanding

Basic Structure

class ListNode {
  constructor(val = 0, next = null) {
    this.val = val;
    this.next = next;
  }
}

// Create a linked list: 1 -> 2 -> 3
const head = new ListNode(1, new ListNode(2, new ListNode(3)));

Key Patterns

Two Pointers (Fast & Slow)

Detect cycles or find middle:

// Floyd's Cycle Detection
function hasCycle(head) {
  let slow = head, fast = head;
  while (fast && fast.next) {
    slow = slow.next;
    fast = fast.next.next;
    if (slow === fast) return true;  // Found cycle
  }
  return false;
}
// Why it works: If there's a cycle, fast eventually laps slow

Dummy Head Node

Simplifies code when the head might be deleted:

// Without dummy, deleting head is messy:
if (head.val === val) return head.next;  // Special case
// ... handle rest

// With dummy:
const dummy = new ListNode(0, head);
let curr = dummy;
while (curr.next && curr.next.val === val) {
  curr.next = curr.next.next;  // Skip
}
return dummy.next;  // Always works

Why They’re Less Common Now

  • Most interview questions avoid linked lists (too fiddly, tests pointers not algorithms)

  • When they do appear, they’re usually "implement reverse linked list" or "detect cycle"

  • Exception: If interviewer specializes in systems/infrastructure, linked lists matter more


Problems: Sorting & Preprocessing

1. Merge Intervals

Problem: Given intervals like [[1,3],[2,6],[8,10],[15,18]], merge overlapping ones.

Key insight: Sort by start time, then merge greedily.

Approach: 1. Sort by start time: O(n log n) 2. Iterate: If current start ≤ prev end, merge. Else, add prev and start new.

Code:

function mergeIntervals(intervals) {
  if (!intervals.length) return [];

  intervals.sort((a, b) => a[0] - b[0]);  // Sort by start
  const result = [intervals[0]];

  for (let i = 1; i < intervals.length; i++) {
    const last = result[result.length - 1];
    const current = intervals[i];

    if (current[0] <= last[1]) {
      // Overlapping: merge
      last[1] = Math.max(last[1], current[1]);
    } else {
      // No overlap: add as new interval
      result.push(current);
    }
  }

  return result;
}

Time: O(n log n) sorting + O(n) merge = O(n log n) Space: O(n) for result (or O(1) if modifying in place)


2. Meeting Rooms II

Problem: Given meeting times [[0,30],[5,10],[15,20]], how many conference rooms are needed?

Key insight: At any moment, count how many meetings are ongoing. Use a priority queue (min-heap) or sweep line.

Approach (Min-Heap): 1. Sort by start time 2. Use min-heap to track end times of ongoing meetings 3. For each meeting: - If earliest end ≤ current start, free up that room (pop heap) - Schedule this meeting (push end time) 4. Heap size = max rooms needed

Code:

function meetingRooms2(intervals) {
  if (!intervals.length) return 0;

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

  // Min-heap of end times (use simple array + manual heap or use library)
  const endTimes = [intervals[0][1]];

  for (let i = 1; i < intervals.length; i++) {
    const [start, end] = intervals[i];

    // If earliest meeting ends before or at current start, reuse room
    if (endTimes[0] <= start) {
      // Remove min
      endTimes[0] = endTimes[endTimes.length - 1];
      endTimes.pop();
      minHeapifyDown(endTimes, 0);
    }

    // Add this meeting's end time
    endTimes.push(end);
    minHeapifyUp(endTimes, endTimes.length - 1);
  }

  return endTimes.length;  // Rooms needed = size of heap
}

// Simple min-heap helpers
function minHeapifyUp(heap, idx) {
  while (idx > 0) {
    const parentIdx = Math.floor((idx - 1) / 2);
    if (heap[parentIdx] <= heap[idx]) break;
    [heap[parentIdx], heap[idx]] = [heap[idx], heap[parentIdx]];
    idx = parentIdx;
  }
}

function minHeapifyDown(heap, idx) {
  while (2 * idx + 1 < heap.length) {
    let smallest = idx;
    const left = 2 * idx + 1, right = 2 * idx + 2;
    if (heap[left] < heap[smallest]) smallest = left;
    if (right < heap.length && heap[right] < heap[smallest]) smallest = right;
    if (smallest === idx) break;
    [heap[idx], heap[smallest]] = [heap[smallest], heap[idx]];
    idx = smallest;
  }
}

Time: O(n log n) sorting + O(n log n) heap ops = O(n log n) Space: O(n) for heap


3. 3Sum

Problem: Find all unique triplets that sum to zero. Input: [-1, 0, 1, 2, -1, -4] Output: [[-1, -1, 2], [-1, 0, 1]]

Key insight: Sort first, then use two pointers to avoid duplicates.

Approach: 1. Sort the array: O(n log n) 2. For each element, fix it and run two-sum with two pointers on remaining elements 3. Skip duplicates carefully

Code:

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

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

    // Optimization: if first num > 0, no triplet sums to 0
    if (nums[i] > 0) break;

    // Two-sum with two pointers
    let left = i + 1, 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]]);

        // Skip duplicates
        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;
}

Time: O(n log n) sort + O(n²) two-pointers (for each i, two-pointers is O(n)) = O(n²) Space: O(1) if not counting output


4. Kth Largest Element — QuickSelect

Problem: Find the kth largest element without fully sorting. Input: [3, 2, 1, 5, 6, 4], k = 2 Output: 5 (second largest)

Key insight: Use QuickSelect — O(n) average time! Why? - QuickSort recurses into both halves → O(n log n) - QuickSelect recurses into only one half → O(n) average

Approach: 1. Partition the array (like QuickSort) 2. If pivot index == target index, found it 3. Else, recurse into left or right half

Code:

function findKthLargest(nums, k) {
  // Convert "kth largest" to kth smallest from end
  const target = nums.length - k;
  return quickSelect(nums, 0, nums.length - 1, target);
}

function quickSelect(arr, low, high, targetIdx) {
  if (low === high) return arr[low];

  // Random pivot to avoid worst case
  const randomIdx = low + Math.floor(Math.random() * (high - low + 1));
  [arr[randomIdx], arr[high]] = [arr[high], arr[randomIdx]];

  const pi = partition(arr, low, high);

  if (targetIdx === pi) return arr[pi];
  if (targetIdx < pi) return quickSelect(arr, low, pi - 1, targetIdx);
  return quickSelect(arr, pi + 1, high, targetIdx);
}

function partition(arr, low, high) {
  const pivot = arr[high];
  let i = low - 1;

  for (let j = low; j < high; j++) {
    if (arr[j] < pivot) {
      i++;
      [arr[i], arr[j]] = [arr[j], arr[i]];
    }
  }

  [arr[i + 1], arr[high]] = [arr[high], arr[i + 1]];
  return i + 1;
}

Time: O(n) average, O(n²) worst (but rare with random pivot) Space: O(log n) recursion stack

Interview tip: If asked for kth largest and you don’t know QuickSelect, just say "I’d use a min-heap of size k" → O(n log k) and always works.


Problems: Stacks and Queues

5. Valid Parentheses

Problem: Check if parentheses are valid: "()[]{}"

Approach: Stack — push opening, pop and match closing.

Code:

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

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

  return stack.length === 0;
}

Time: O(n), Space: O(n)


6. Daily Temperatures (Monotonic Stack)

Problem: Given temperatures, for each day, find how many days until warmer. Input: [73, 74, 75, 71, 69, 72, 76, 73] Output: [1, 1, 4, 2, 1, 1, 0, 0]

Approach: Monotonic stack — maintain decreasing order of temps.

Code:

function dailyTemperatures(temps) {
  const result = new Array(temps.length).fill(0);
  const stack = [];  // Store indices

  for (let i = 0; i < temps.length; i++) {
    // While stack not empty and current temp > stack top temp
    while (stack.length && temps[i] > temps[stack[stack.length - 1]]) {
      const prevIdx = stack.pop();
      result[prevIdx] = i - prevIdx;  // Days until warmer
    }
    stack.push(i);
  }

  return result;
}

Time: O(n), Space: O(n)

Why O(n) not O(n²)? Each index pushed once, popped once → O(n) total operations.


Problems: Linked Lists

7. Reverse Linked List

Problem: Reverse a singly linked list. Input: 1 → 2 → 3 Output: 3 → 2 → 1

Approach: Iterative — maintain prev, curr, next pointers.

Code:

function reverseList(head) {
  let prev = null, curr = head;

  while (curr) {
    const next = curr.next;  // Save next
    curr.next = prev;         // Reverse the link
    prev = curr;              // Move prev forward
    curr = next;              // Move curr forward
  }

  return prev;  // New head
}

Time: O(n), Space: O(1)


8. Merge Two Sorted Lists

Problem: Merge two sorted linked lists. Input: 1 → 2 → 4 and 1 → 3 → 4 Output: 1 → 1 → 2 → 3 → 4 → 4

Approach: Two pointers, compare and link.

Code:

function mergeTwoLists(list1, list2) {
  const dummy = new ListNode(0);
  let curr = dummy;

  while (list1 && list2) {
    if (list1.val <= list2.val) {
      curr.next = list1;
      list1 = list1.next;
    } else {
      curr.next = list2;
      list2 = list2.next;
    }
    curr = curr.next;
  }

  // Attach remaining
  curr.next = list1 || list2;

  return dummy.next;
}

Time: O(n + m), Space: O(1)


9. Linked List Cycle Detection

Problem: Check if a linked list has a cycle.

Approach: Floyd’s cycle detection (tortoise and hare).

Code:

function hasCycle(head) {
  let slow = head, fast = head;

  while (fast && fast.next) {
    slow = slow.next;
    fast = fast.next.next;

    if (slow === fast) return true;  // Cycle detected
  }

  return false;  // No cycle
}

Time: O(n), Space: O(1)

Why it works: In a cycle, fast pointer (moving 2x speed) will eventually lap slow pointer.


Summary: What to Memorize

Sorting:
- Merge Sort: O(n log n), O(n) space, stable
- QuickSort: O(n log n) avg, O(log n) space, in-place, use random pivot
- Always use comparator: sort((a, b) ⇒ a - b)

Stacks & Queues: - Stack: push/pop O(1) - Queue: enqueue O(1), dequeue O(1) — don’t use shift() - Monotonic stack: find next/prev greater in O(n)

Linked Lists: - Two pointers for cycles and middle - Dummy node for simplifying head deletion - Less common now but know reverse + cycle detection

Preprocessing with Sorting: - Two Sum → O(n log n) with two pointers - 3Sum → O(n²) after sort - Merge Intervals → O(n log n) - Meeting Rooms II → O(n log n)

QuickSelect: O(n) average to find kth element — beats sorting.