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:
Without a comparator,
|
Merge Sort: Divide and Conquer
Concept
Merge Sort is a stable, divide-and-conquer algorithm:
-
Divide: Split array in half recursively until single elements
-
Conquer: Two sorted halves are already sorted
-
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
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:
-
Pick a pivot element
-
Partition: Elements < pivot go left, > pivot go right
-
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.