syndicated · dev.to / @markyu
JS Sorting Algorithms Every Developer Should Know
You call .sort() every day without thinking about it. But the sorting algorithm your language's...
- Published
- Jun 16
- Reading time
- 9 min read
- Reactions
- 5
algorithmsbeginnerstutorialjavascript
You call .sort() every day without thinking about it. But the sorting algorithm your language's standard library actually uses is a carefully engineered hybrid - and understanding what it's doing (and why) makes you a better engineer. Not just for interviews. For writing better code, debugging weird performance cliffs, and knowing when to reach for something different.
This post covers the eight classic sorting algorithms - the ones that appear in every CS curriculum and show up in every whiteboard interview - with an honest take on what they're actually useful for in 2025.
The two camps
Every classic sorting algorithm falls into one of two buckets:
| Time complexity | Examples | |
|---|---|---|
| Brute force | O(n²) average | Bubble, Selection, Insertion |
| Divide & conquer | O(n log n) average | Merge, Quick, Heap |
O(n²) sounds terrible, and at scale it is. But for arrays under ~50 elements, the constant factors often make the "slow" algorithms faster in practice - which is why Timsort (Python, JavaScript) and Introsort (C++) use insertion sort internally for small subarrays.
The other axis that matters: stable vs unstable. A stable sort preserves the relative order of equal elements. If you're sorting records by last name and want a secondary sort by first name to be preserved, you need stability. Quicksort is unstable. Merge sort is stable. This matters.
1. Bubble Sort - the one everyone knows, no one uses
How it works: Compare adjacent elements, swap if out of order. Repeat until no swaps happen.
def bubble_sort(arr):
n = len(arr)
for i in range(n):
for j in range(0, n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
return arrComplexity: O(n²) average and worst case, O(n) best case (already sorted)
Stable: Yes
Space: O(1)
Honest take: Bubble sort is for teaching, not production. The only place it wins is a nearly-sorted array where you add an early-exit optimization - it becomes O(n) on already-sorted data. But insertion sort does the same thing better.
That said: every CS interview will assume you know it. You should know it.
2. Selection Sort - simpler, not better
How it works: Find the minimum element in the unsorted portion, swap it to the front. Repeat.
def selection_sort(arr):
for i in range(len(arr)):
min_idx = i
for j in range(i + 1, len(arr)):
if arr[j] < arr[min_idx]:
min_idx = j
arr[i], arr[min_idx] = arr[min_idx], arr[i]
return arrComplexity: O(n²) in all cases - it always does the full scan
Stable: No (standard implementation)
Space: O(1)
Honest take: Selection sort makes fewer swaps than bubble sort - exactly n-1 swaps always - which matters if swapping is expensive (say, moving large records on disk). Otherwise it has no advantage. It's also not stable, which rules it out for anything that cares about order preservation.
3. Insertion Sort - the underrated one
How it works: Build a sorted subarray from left to right, inserting each new element into its correct position.
def insertion_sort(arr):
for i in range(1, len(arr)):
key = arr[i]
j = i - 1
while j >= 0 and arr[j] > key:
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = key
return arrComplexity: O(n²) worst case, O(n) best case (sorted data), O(nk) if each element is at most k positions from its sorted position
Stable: Yes
Space: O(1)
Honest take: This is the one people underestimate. Insertion sort is adaptive - it runs in O(n) on nearly-sorted data. It's also online, meaning it can sort a stream of data as it arrives. And it's the algorithm that Timsort (used by Python and JavaScript under the hood) falls back to for subarrays under 64 elements, because cache performance and constant factors make it beat merge sort at small sizes.
If you ever need to sort a small array yourself: use insertion sort.
4. Shell Sort - insertion sort, but turbocharged
How it works: Generalization of insertion sort. Instead of comparing adjacent elements, compare elements h positions apart. Decreasing h toward 1.
def shell_sort(arr):
n = len(arr)
gap = n // 2
while gap > 0:
for i in range(gap, n):
temp = arr[i]
j = i
while j >= gap and arr[j - gap] > temp:
arr[j] = arr[j - gap]
j -= gap
arr[j] = temp
gap //= 2
return arrComplexity: O(n log² n) with good gap sequences, though the exact bound depends heavily on the gap sequence used
Stable: No
Space: O(1)
Honest take: Shell sort is an interesting middle ground - better than insertion sort on large arrays, no extra memory needed, but it's been largely superseded by introsort and Timsort. Still worth knowing because the gap-sequence idea is non-obvious and comes up in other places (like some cache-oblivious algorithms).
5. Merge Sort - the stable workhorse
How it works: Divide the array in half, recursively sort both halves, then merge them back.
def merge_sort(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
return merge(left, right)
def merge(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return resultComplexity: O(n log n) in all cases - guaranteed, no worst case
Stable: Yes
Space: O(n) - this is the catch
Honest take: Merge sort has guaranteed O(n log n) performance - unlike quicksort which can degrade to O(n²) on bad pivot choices. It's also stable. Those two properties make it the go-to for sorting linked lists, external sorting (data that doesn't fit in memory), and anywhere stability matters.
The downside is memory: it needs O(n) extra space. For large arrays in memory-constrained environments, that's a real cost.
Java uses a variant of merge sort (Timsort) for object arrays. The stable guarantee matters when you're chaining comparators.
6. Quick Sort - the fast one (usually)
How it works: Pick a pivot, partition the array so everything smaller is to the left and everything larger is to the right, then recursively sort both sides.
def quick_sort(arr, low=0, high=None):
if high is None:
high = len(arr) - 1
if low < high:
pivot_idx = partition(arr, low, high)
quick_sort(arr, low, pivot_idx - 1)
quick_sort(arr, pivot_idx + 1, high)
return arr
def partition(arr, low, high):
pivot = arr[high]
i = low - 1
for j in range(low, high):
if arr[j] <= pivot:
i += 1
arr[i], arr[j] = arr[j], arr[i]
arr[i + 1], arr[high] = arr[high], arr[i + 1]
return i + 1Complexity: O(n log n) average, O(n²) worst case (sorted input with bad pivot selection)
Stable: No
Space: O(log n) average stack space
Honest take: Quicksort has the best average-case performance of any comparison sort in practice - the constant factors are small because it has great cache behavior and does comparisons in-place. That's why C's qsort and many other standard libraries are named after it.
The catch: it degrades to O(n²) on sorted or nearly-sorted input with naive pivot selection. Modern implementations use randomized pivots, median-of-three, or switch strategies entirely - which is how introsort was born.
Quicksort is fast. But never trust it on user-provided data without randomized pivots unless you want to be in a CVE.
7. Heap Sort - the Linux kernel's pick
How it works: Build a max-heap from the array, then repeatedly extract the max element to build the sorted array.
def heap_sort(arr):
n = len(arr)
# Build max heap
for i in range(n // 2 - 1, -1, -1):
heapify(arr, n, i)
# Extract elements
for i in range(n - 1, 0, -1):
arr[0], arr[i] = arr[i], arr[0]
heapify(arr, i, 0)
return arr
def heapify(arr, n, i):
largest = i
left, right = 2 * i + 1, 2 * i + 2
if left < n and arr[left] > arr[largest]:
largest = left
if right < n and arr[right] > arr[largest]:
largest = right
if largest != i:
arr[i], arr[largest] = arr[largest], arr[i]
heapify(arr, n, largest)Complexity: O(n log n) in all cases - including worst case
Stable: No
Space: O(1) - sorts in place
Honest take: Heap sort has the best worst-case guarantee among the in-place sorts: O(n log n) always, O(1) space. The Linux kernel uses it instead of quicksort for exactly this reason - when you can't tolerate O(n²) worst-case behavior, heapsort is the safe choice.
The downside: poor cache performance. Heap sort jumps around memory in a way that causes a lot of cache misses, which makes it slower in practice than quicksort or merge sort even though it has the same asymptotic complexity. You'll rarely use it directly, but knowing it exists and why is useful.
8. Radix Sort - breaking the comparison barrier
How it works: Sort integers digit by digit, from least significant to most significant, using a stable sort (usually counting sort) at each pass.
def radix_sort(arr):
max_val = max(arr)
exp = 1
while max_val // exp > 0:
counting_sort_by_digit(arr, exp)
exp *= 10
return arr
def counting_sort_by_digit(arr, exp):
n = len(arr)
output = [0] * n
count = [0] * 10
for i in range(n):
index = (arr[i] // exp) % 10
count[index] += 1
for i in range(1, 10):
count[i] += count[i - 1]
for i in range(n - 1, -1, -1):
index = (arr[i] // exp) % 10
output[count[index] - 1] = arr[i]
count[index] -= 1
for i in range(n):
arr[i] = output[i]Complexity: O(nk) where k is the number of digits - effectively O(n) for fixed-width integers
Stable: Yes
Space: O(n + k)
Honest take: Radix sort is the algorithm that breaks the O(n log n) lower bound - because it doesn't use comparisons. It works on the actual digit/bit structure of the data. For sorting large sets of integers, IP addresses, phone numbers, or any fixed-length data, radix sort can be dramatically faster than comparison sorts.
The catch: it only works on integers or things that can be mapped to integers (strings of fixed length, dates). It doesn't work on arbitrary comparable data. And the constant factors mean that for small arrays, comparison sorts win.
What .sort() is actually doing
Here's the part that makes the above useful beyond theory:
| Language | Algorithm used | Notes |
|---|---|---|
| Python | Timsort | Merge sort + insertion sort hybrid. Stable. Since Python 2.3. |
| JavaScript (V8) | Timsort | Same algorithm, switched from quicksort in 2019 for spec compliance (stable sort required by ES2019+). |
| Java (objects) | Timsort | Arrays.sort(Object[]) and Collections.sort(). |
| Java (primitives) | Dual-pivot quicksort | Arrays.sort(int[]). No stability needed, faster in practice. |
| C++ STL | Introsort | Quicksort + heapsort fallback + insertion sort for small arrays. Unstable. |
| Linux kernel | Heapsort | Chosen for guaranteed worst-case and no stack space. |
| Swift | Timsort |
Timsort is worth understanding at a high level: it looks for already-sorted "runs" in the input, then merges them with merge sort. Real-world data is often partially sorted (think: appending to a sorted list, or data with timestamps). Timsort exploits this and can run in O(n) on such data, beating theoretical alternatives.
Quick reference
| Algorithm | Best | Average | Worst | Space | Stable |
|---|---|---|---|---|---|
| Bubble | O(n) | O(n²) | O(n²) | O(1) | Yes |
| Selection | O(n²) | O(n²) | O(n²) | O(1) | No |
| Insertion | O(n) | O(n²) | O(n²) | O(1) | Yes |
| Shell | O(n log n) | O(n log² n) | O(n²) | O(1) | No |
| Merge | O(n log n) | O(n log n) | O(n log n) | O(n) | Yes |
| Quick | O(n log n) | O(n log n) | O(n²) | O(log n) | No |
| Heap | O(n log n) | O(n log n) | O(n log n) | O(1) | No |
| Radix | O(nk) | O(nk) | O(nk) | O(n+k) | Yes |
When do you actually implement one yourself?
Honestly? Almost never. But here's when the knowledge matters:
You need a stable sort in a language that doesn't guarantee one: Some older runtimes, embedded environments, or custom data structures. Reach for merge sort.
You're sorting a small array in a hot loop: Array.sort() has overhead from being general-purpose. For arrays under ~20 elements in a performance-critical path, inline insertion sort is faster.
You need guaranteed worst-case bounds and can't use O(n) space: Heapsort.
You're sorting integers and n is large: Radix sort. The O(n) vs O(n log n) difference shows up fast at a million+ items.
You're writing something that needs to handle adversarial input: Randomize your quicksort pivots. Or use merge sort. A sorted-input DoS via quicksort is a real vulnerability class.
The mental model to take away: the "best" sorting algorithm doesn't exist - it depends on the constraints. Timsort is the best general-purpose answer for unknown data. Quicksort is fastest when you know the data is random. Merge sort wins when you need stability and memory isn't a constraint. Heapsort wins when you need worst-case guarantees in O(1) space. Radix sort wins when you're sorting integers at scale.
Knowing which to reach for, and why, is the whole point of learning them.
Related reading
computerscience
Frontend Linear Data Structures Deep Dive: Arrays, Stacks, Queues, and Linked Lists
The Big Picture Before diving into stacks, queues, and linked lists, it helps to know...
algorithms
A* Search Finally Clicked When I Drew the Grid
A practical developer-friendly explanation of heuristic search, Greedy Best-First Search, and A* using a grid example instead of textbook language.
networking
Network Address Calculation: The Subnet Math That Matters
A practical subnetting guide showing how to calculate a network address from an IP address and mask using binary math and simple examples.
originally published
This post first ran on dev.to. Comments and reactions live there.
Continue on dev.to