Back
Data StructuresJavaScriptArticle · Jun 1, 2024 · 12 min

jonathanjuliani

Sorting Arrays with NodeJS and Javascript

Sorting algorithms in JavaScript: bubble, merge, quick, and when to pick each one. Commented code plus complexity in practice.

Merge sort divide-and-conquer on JavaScript arrays

[10, 2, 1].sort() without a comparator sorts as strings[1, 10, 2]. Knowing bubble, merge, and quick helps in interviews and when profiling sort-heavy code.

What you'll learn:

  • bubble, merge, quick in code
  • average vs worst case
  • stability
  • Array.sort comparators

Prerequisites: arrays (ep. 2).


Sorting algorithms: bubble, merge, and quick

Sorting is one of the most fundamental operations in computer science. You probably use Array.sort() every day without thinking about it—but do you know what it's actually doing? Do you know why sorting a million numbers is fast but sorting a billion is much, much slower? And do you know when the built-in sort will silently give you wrong results?

That's what this post is about. We'll implement a couple of classic sorting algorithms, understand the performance differences, and finish with tips on using JavaScript's native sort correctly.

Hands-on

First The Theory

Before we write any code, let's look at the Big-O picture:

AlgorithmBestAverageWorstStable?
Bubble SortO(n)O(n²)O(n²)Yes
Merge SortO(n log n)O(n log n)O(n log n)Yes
Quick SortO(n log n)O(n log n)O(n²)No
Native sortO(n log n)O(n log n)O(n log n)Yes (V8 ≥ Node 11)

A few key concepts:

  • Stable sort: if two elements are equal, they keep their original relative order. This matters when you sort objects by one property and want another property to stay in its previous order.
  • In-place: the algorithm sorts within the same array without allocating extra memory. Bubble sort and quick sort are in-place; merge sort needs extra space.

Here's how merge sort splits and merges:

Merge sort: split array to singles, merge sorted pairs into final sorted array

scene: en/data-structures-in-a-nutshell/merge-sort-split-merge

Interactive Excalidraw view is not available in this build.

Control or Command + scroll to zoom. Click and drag to pan.
graph TD
  A["[38, 27, 43, 3]"]
  B["[38, 27]"]
  C["[43, 3]"]
  D["[38]"]
  E["[27]"]
  F["[43]"]
  G["[3]"]
  H["[27, 38]"]
  I["[3, 43]"]
  J["[3, 27, 38, 43]"]

  A --> B
  A --> C
  B --> D
  B --> E
  C --> F
  C --> G
  D --> H
  E --> H
  F --> I
  G --> I
  H --> J
  I --> J

Split down to single elements (which are trivially sorted), then merge pairs back together—always in order.

Now The Practice

Create a file called sort.js and let's start with the simplest algorithm:

Bubble Sort

code
function bubbleSort(arr) {
  const a = [...arr] // don't mutate the original
  const n = a.length
  for (let i = 0; i < n - 1; i++) {
    let swapped = false
    for (let j = 0; j < n - i - 1; j++) {
      if (a[j] > a[j + 1]) {
        ;[a[j], a[j + 1]] = [a[j + 1], a[j]]
        swapped = true
      }
    }
    if (!swapped) break // already sorted — best case O(n)
  }
  return a
}

Bubble sort works by repeatedly swapping adjacent elements that are out of order. After each full pass, the largest unsorted element has "bubbled" to its correct position at the end. The swapped flag is an optimisation: if no swaps happened in a full pass, the array is already sorted and we can stop early.

Merge Sort

code
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 l = 0
  let r = 0
 
  while (l < left.length && r < right.length) {
    if (left[l] <= right[r]) {
      result.push(left[l++])
    } else {
      result.push(right[r++])
    }
  }
 
  return result.concat(left.slice(l)).concat(right.slice(r))
}

Merge sort is a classic divide-and-conquer algorithm:

  1. Divide: split the array in half recursively until each piece has one element.
  2. Conquer: merge pairs of sorted pieces back together, always picking the smaller front element.

It's stable and always O(n log n), but it needs extra memory proportional to the input size.

Native sort

code
// Lexicographic by default (DON'T use without a comparator for numbers!)
[10, 9, 2, 100].sort()           // [10, 100, 2, 9] — WRONG for numbers!
 
// Correct numeric ascending
[10, 9, 2, 100].sort((a, b) => a - b)   // [2, 9, 10, 100]
 
// Correct numeric descending
[10, 9, 2, 100].sort((a, b) => b - a)   // [100, 10, 9, 2]
 
// Sort objects by a property
const users = [
  { name: 'Charlie', age: 30 },
  { name: 'Alice', age: 25 },
  { name: 'Bob', age: 28 },
]
users.sort((a, b) => a.age - b.age)
// [Alice (25), Bob (28), Charlie (30)]

Now let's run a full test. Add this after the functions:

code
const nums = [38, 27, 43, 3, 9, 82, 10]
 
console.log('Original:', nums)
console.log('Bubble sort:', bubbleSort(nums))
console.log('Merge sort:', mergeSort(nums))
console.log('Native sort (wrong — lexicographic):', [...nums].sort())
console.log('Native sort (correct — numeric):', [...nums].sort((a, b) => a - b))

Run it:

code
node sort.js

Expected output:

code
Original: [
  38, 27, 43,  3,
   9, 82, 10
]
Bubble sort: [
   3,  9, 10, 27,
  38, 43, 82
]
Merge sort: [
   3,  9, 10, 27,
  38, 43, 82
]
Native sort (wrong  lexicographic): [
  10, 27,  3, 38,
  43,  9, 82
]
Native sort (correct  numeric): [
   3,  9, 10, 27,
  38, 43, 82
]

AWESOME!

All three correct implementations produce [3, 9, 10, 27, 38, 43, 82]. And see that "wrong" native sort? That's the classic gotcha—Array.sort converts elements to strings by default, so 10 comes before 3 because "1" < "3" lexicographically. Always pass a comparator when sorting numbers.


Edge cases: comparator and mutation

sort() mutates — clone first: [...arr].sort((a,b) => a - b).

Objects need a comparator returning negative / zero / positive — not boolean.

Numbers need (a, b) => a - b — never rely on default string sort.


TL;DR — Sorting

algotypicaluse in prod
bubbleO(n²)teaching only
mergeO(n log n) stableyes
native sortO(n log n)yes with comparator

Next steps


Before you go

Know which algorithm V8 uses for sort? Comment if the post has a mistake.

Subscribe to the Engineering Ledger

Get architecture and performance notes in your inbox. Same list as the timed prompt—subscribe here anytime.

No spam. No third-party APIs. Just me sending updates.

The Engineering Ledger

Bi-weekly transmissions on architecture, performance, and practical engineering. Subscribe from any article—no spam.