Skip to the notes
JSGroundwork
JSGroundwork handwritten · web dev
✎Playground→⌘Problems↻Review🔥Progress

Chapters

34 chapters
⌕
Beginner10›
B1Complexity analysisB2Arrays & stringsB3HashingB4Two pointersB5Sliding windowB6Binary searchB7Sorting algorithmsB8Stacks & queuesB9Linked listsB10Basic recursion
Intermediate12›
I1TreesI2Tree problems in depthI3Heaps & priority queuesI4Graphs: representationI5Graph problemsI6BacktrackingI7DP: 1DI8DP: 2DI9Greedy algorithmsI10IntervalsI11Bit manipulationI12Matrix problems
Advanced12›
A1Advanced DPA2Union-FindA3Advanced graph algorithmsA4Minimum Spanning TreeA5TriesA6Segment & Fenwick treesA7String algorithmsA8Monotonic stack & queueA9Design problemsA10Advanced backtrackingA11Topological patternsA12Interview strategy
/ search[ ] chaptert top

DSA in JS levels

1Beginner2Intermediate3Advanced

Ready to read

JSJavaScript⑂Git◎Interview prepΣDSA in JSSDSystem Design
More topics15›
</>HTML{ }CSS⚛ReactNNext.jsNeNest.jsTSTypeScriptNoNode.js🐳DockerDBSQL & Databases✓Testing🔒Web Security☁Cloud & DevOps◈GraphQL◆Redis☸Kubernetes
100%
B7

Sorting algorithms

You'll rarely hand-write one, but you'll constantly need to reason about them.

The cheat sheet interviewers expect you to know cold

AlgorithmTime (avg)Time (worst)SpaceStable?
Bubble/Insertion sortO(n²)O(n²)O(1)yes
Merge sortO(n log n)O(n log n)O(n)yes
QuicksortO(n log n)O(n²)O(log n)no
HeapsortO(n log n)O(n log n)O(1)no
Counting sortO(n + k)O(n + k)O(k)yes

"Stable" means equal elements keep their original relative order — matters when you're sorting objects by one field but want ties to preserve a previous sort order.

Bubble sort — repeatedly swap neighbors into order

The simplest possible sort: walk the array, and whenever two neighbors are out of order, swap them. Repeat full passes until a pass makes zero swaps — that's your signal the array is sorted.

5 8 2 9 8 > 2 → swap 5 2 8 9 one pass "bubbles" the largest seen so far rightward
n passes, each an O(n) scan → O(n²), but it's the easiest to reason about by hand.
function bubbleSort(arr) {
  for (let i = 0; i < arr.length - 1; i++) {
    let swapped = false;
    for (let j = 0; j < arr.length - 1 - i; j++) {
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
        swapped = true;
      }
    }
    if (!swapped) break; // already sorted — stop early
  }
  return arr;
}

Notice arr.length - 1 - i: after pass i, the i largest elements are already bubbled to their final spot at the end, so each pass has one less element left to check. That's why it's O(n²) and not O(n³) despite "a pass, repeated n times" sounding like it could be worse.

Selection sort — repeatedly pick the minimum

The mirror image of bubble sort: instead of bubbling large values right via many small swaps, scan the unsorted remainder for its minimum and swap it directly into place — one swap per pass, not many.

function selectionSort(arr) {
  for (let i = 0; i < arr.length - 1; i++) {
    let minIndex = i;
    for (let j = i + 1; j < arr.length; j++) {
      if (arr[j] < arr[minIndex]) minIndex = j;
    }
    if (minIndex !== i) [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
  }
  return arr;
}

Still O(n²) — finding the minimum is O(n), done n times — but it makes at most n swaps total, versus bubble sort's up to O(n²) swaps. Worth knowing as the answer to "which of these two simple sorts writes to memory less."

Insertion sort — build up a sorted prefix, one element at a time

2 5 8 4 9 sorted prefix [2,5,8] — take 4, shift 8 and 5 right, insert 4 between 2 and 5 2 4 5 8 9
Green = sorted so far. This is exactly how most people sort a hand of playing cards.
function insertionSort(arr) {
  for (let i = 1; i < arr.length; i++) {
    const current = arr[i];
    let j = i - 1;
    while (j >= 0 && arr[j] > current) {
      arr[j + 1] = arr[j]; // shift bigger elements right
      j--;
    }
    arr[j + 1] = current; // drop it into the gap
  }
  return arr;
}
Why insertion sort still matters It's O(n²) worst case, but O(n) on nearly-sorted data — each element only shifts a few positions. That's exactly why TimSort (JS's real .sort()) switches to insertion sort for small or nearly-sorted runs instead of using merge sort the whole way down.

Merge sort — divide, conquer, then combine

5 3 8 1 9 2 7 4 5 3 8 1 9 2 7 4 5 3 8 1 9 2 7 4 split down to size 1 (free) → merge pairs back up in order (does the real work)
log n split levels × O(n) work to merge each level = O(n log n) total.
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;
  while (i < left.length && j < right.length) {
    result.push(left[i] <= right[j] ? left[i++] : right[j++]);
  }
  return result.concat(left.slice(i), right.slice(j));
}

Quicksort — partition, then recurse

function quickSort(arr, lo = 0, hi = arr.length - 1) {
  if (lo >= hi) return arr;

  const pivotIndex = partition(arr, lo, hi);
  quickSort(arr, lo, pivotIndex - 1);
  quickSort(arr, pivotIndex + 1, hi);
  return arr;
}

function partition(arr, lo, hi) {
  const pivot = arr[hi];
  let i = lo;
  for (let j = lo; j < hi; j++) {
    if (arr[j] < pivot) {
      [arr[i], arr[j]] = [arr[j], arr[i]];
      i++;
    }
  }
  [arr[i], arr[hi]] = [arr[hi], arr[i]];
  return i; // pivot's final sorted position
}

Watch one partition pass, step by step

arr = [8, 2, 9, 1, 5], pivot = last element = 5:

jarr[j]< pivot (5)?actionarray after
08nonothing[8, 2, 9, 1, 5] (i=0)
12yesswap arr[0], arr[1]; i++[2, 8, 9, 1, 5] (i=1)
29nonothing[2, 8, 9, 1, 5] (i=1)
31yesswap arr[1], arr[3]; i++[2, 1, 9, 8, 5] (i=2)
———swap arr[2], arr[4] (pivot into place)[2, 1, 5, 8, 9]

After one pass, 5 sits at its final sorted position (index 2), everything smaller is to its left, everything bigger is to its right — and neither side is sorted yet. That's the whole trick: quicksort now recurses on [2, 1] and [8, 9] independently, and the pivot never needs to move again.

⚠ Why quicksort's worst case is O(n²) If the pivot is always the smallest or largest remaining element (e.g. an already-sorted array with a naive "last element" pivot), each partition only shrinks the problem by 1, not by half — n levels of O(n) work each. Randomizing the pivot choice makes this worst case astronomically unlikely in practice, which is why real quicksorts do it.

Seeing the full recursion tree, not just one level

Both merge sort and quicksort are divide-and-conquer — the diagram earlier only showed one split/merge. Here's why the total work across every level is O(n log n): each level does O(n) work combined (merging, or partitioning), and there are O(log n) levels because the problem size halves each time.

n = 8 n = 4 n = 4 n=2 n=2 n=2 n=2 level 0: 1×8=8 work · level 1: 2×4=8 work · level 2: 4×2=8 work each level does O(n) total work, and there are log₂(n) levels → O(n log n)
The "n" per level never changes — only how many pieces it's split into. That's the source of the log n factor.

Heap sort — sort using a heap as scratch space

Covered fully in the heaps chapter next, but the shape belongs here too: build a max-heap out of the array in O(n), then repeatedly pull the maximum off the top and place it at the end — O(log n) per extraction, n extractions, O(n log n) total. Unlike merge sort, it sorts in place (O(1) extra space); unlike quicksort, its worst case is guaranteed O(n log n), never O(n²). The tradeoff: it's not stable, and in practice it's usually a bit slower than a well-tuned quicksort due to cache behavior.

Why JS's built-in .sort() usually wins anyway

Array.prototype.sort() defaults to comparing elements as strings — [10, 2, 1].sort() gives [1, 10, 2], not [1, 2, 10], unless you pass a comparator. Always sort numbers with an explicit comparator:

nums.sort((a, b) => a - b);       // ascending
nums.sort((a, b) => b - a);       // descending
people.sort((a, b) => a.age - b.age); // by a field

V8's engine uses TimSort (a hybrid of merge sort and insertion sort) — O(n log n) worst case, and stable. In an interview, you almost never hand-roll a sort; you use it as a fast O(n log n) black box and put your effort into everything around it.

Counting sort — when the range is small

If values are bounded integers in a small known range (say, 0–100), you can sort in O(n + k) instead of O(n log n) by counting occurrences directly instead of comparing elements at all.

function countingSort(arr, maxVal) {
  const counts = new Array(maxVal + 1).fill(0);
  for (const x of arr) counts[x]++;

  const result = [];
  for (let val = 0; val <= maxVal; val++) {
    for (let i = 0; i < counts[val]; i++) result.push(val);
  }
  return result;
}
Say it like this → "Comparison-based sorting is bounded at O(n log n) — you can't beat that by comparing elements. But if the values are bounded integers, counting sort sorts in O(n + k) by never comparing elements at all, just counting them."

Bucket sort — counting sort's cousin for spread-out values

When values aren't small integers but are uniformly spread across a known range (e.g. floats between 0 and 1), distribute elements into k buckets by value, sort each small bucket (often with insertion sort, since buckets are tiny), then concatenate. Average case O(n + k); worst case (everything lands in one bucket) degrades to whatever the per-bucket sort costs.

function bucketSort(arr, bucketCount = 10) {
  const buckets = Array.from({ length: bucketCount }, () => []);
  for (const x of arr) {
    const idx = Math.min(bucketCount - 1, Math.floor(x * bucketCount));
    buckets[idx].push(x);
  }
  return buckets.flatMap(bucket => bucket.sort((a, b) => a - b));
}

The decision framework

SituationReach for
Just sort it, no special constraintsarr.sort((a,b) => a-b) — O(n log n), stable, done
Values are small bounded integersCounting sort — O(n + k)
Values are floats spread evenly over a rangeBucket sort — O(n + k) average
Need worst-case O(n log n) guarantee, O(1) spaceHeap sort
Data is nearly sorted alreadyInsertion sort — O(n) on nearly-sorted input
Explaining/hand-tracing on a whiteboardBubble or selection sort — simplest to reason about, even though you'd never ship them

Recognizing when sorting is the actual pattern

  • The problem gets easier once order exists — enables two pointers or binary search
  • You need the k-th smallest/largest, or a top-K — sorting is O(n log n), often beaten by a heap (see the heaps chapter)
  • Grouping by "same after sorting" (anagrams) — sort each item as a normalizing key
  • Interval problems almost always start with "sort by start time"
Practice this layer

Opens in the editor — write it, run it, and check it against real tests.

Merge Sort5 tests · intermediateKth Largest Element — Quickselect5 tests · advancedSort an Array5 tests · intermediateLargest Number5 tests · intermediate
←previousBinary search↑ CovernextStacks & queues→