Part 3 · Lesson 18 · 26 min read

Quick Sort, and When to Just Use .sort()

Partitioning around a pivot, quickselect, and sorting numbers and objects correctly in JavaScript.

On this page

  1. Do the work while splitting
  2. Partitioning around a pivot
  3. Traced: partition([7, 2, 9, 4, 5])
  4. Quick sort
  5. The worst case, and how to avoid it
  6. Quickselect: the k-th element without sorting
  7. JavaScript's .sort(): the text-order trap
  8. Comparators: numbers, text and objects
  9. Which sort to use
  10. Practice questions (7)
  11. Make it stick
  12. Part 3 complete — what's next

Do the work while splitting

Merge sort splits the array without looking at the values, and it does all its work when merging.Quick sort is a sorting algorithm that does the opposite. It picks one value, called the pivot. Then it rearranges the array so that everything smaller than the pivot is on its left and everything else is on its right. Now the pivot is exactly where it belongs in the sorted result. The two sides can then be sorted separately, and no merging is needed afterwards.

Partitioning around a pivot

This rearranging step is called partitioning. Partitioning splits an array into a small side and a big side around the pivot. It is done in place (inside the same array), in one pass. Keep a boundary i. Everything before it is smaller than the pivot. Walk through the array. Each time you find a smaller item, swap it to position i and move the boundary one place right. At the end, swap the pivot itself into position i.

Traced: partition([7, 2, 9, 4, 5])

step 1 / 12
Set up
pivot = arr[4] = 5

Choose the last item as the pivot. The goal is to get the smaller items on its left and the rest on its right.

1function partition(arr, lo, hi) {▶  const pivot = arr[hi];3  let i = lo;4  for (let j = lo; j < hi; j++) {5    if (arr[j] < pivot) {6      [arr[i], arr[j]] = [arr[j], arr[i]];7      i++;8    }9  }10  [arr[i], arr[hi]] = [arr[hi], arr[i]];11  return i;12}13const arr = [7, 2, 9, 4, 5];14console.log(partition(arr, 0, 4), arr);
Variables
arr[7, 2, 9, 4, 5]
pivot5
Console
(nothing printed yet)
One pass over the array. Items smaller than the pivot are swapped to the front; the pivot is then dropped in just after them. · interactive

Partitioning is O(n) time and O(1) space. It is also useful on its own. A three-way version sorts an array of 0s, 1s and 2s in one pass (Lesson 21).

Quick sort

Partition the array. Then quick sort the left side and the right side, using recursion. The pivot never moves again.

JavaScript
function quickSort(arr, lo = 0, hi = arr.length - 1) {  if (lo >= hi) return arr;              // 0 or 1 items: already sorted  const p = partition(arr, lo, hi);      // pivot is now at index p, in its final place  quickSort(arr, lo, p - 1);             // sort the smaller side  quickSort(arr, p + 1, hi);             // sort the larger side  return arr;}function partition(arr, lo, hi) {  const r = lo + Math.floor(Math.random() * (hi - lo + 1));   // random pivot  [arr[r], arr[hi]] = [arr[hi], arr[r]];  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;}console.log(quickSort([7, 2, 9, 4, 5, 1, 8])); // [ 1, 2, 4, 5, 7, 8, 9 ]

If each pivot lands near the middle, the array halves at every level, like merge sort. That gives log n levels with O(n) partitioning work on each, so the time is O(n log n). Unlike merge sort, quick sort needs no extra array. It needs only the recursion stack (Lesson 14), which is O(log n) on average. It is not stable, because swaps over a long distance can change the order of equal items.

The worst case, and how to avoid it

Suppose the array is already sorted and the pivot is always the last item. Then the pivot is the largest value every time. One side gets n − 1 items, and the other side gets none.

Dry run · last-item pivot on the sorted array [1, 2, 3, 4, 5]
Call sortsPivotLeft sideRight side
[1, 2, 3, 4, 5]5[1, 2, 3, 4][]
[1, 2, 3, 4]4[1, 2, 3][]
[1, 2, 3]3[1, 2][]
[1, 2]2[1][]

n levels instead of log n, each still O(n): O(n²) time and O(n) recursion depth.

The fix is simple: choose the pivot at random, as in the code above. Then no particular input can force bad pivots every time. The expected time (the average over the random choices) is O(n log n) for every input. Many libraries instead use the median (the middle value) of the first, middle and last items.

Merge sort or quick sort?

Merge sort: O(n log n) guaranteed, stable, but it needs O(n) extra space. Quick sort: O(n log n) expected, in place and usually faster in practice, but not stable, and O(n²) in the rare worst case. In interviews, you will be asked to explain these trade-offs more often than to write either sort.

Quickselect: the k-th element without sorting

Quickselect is an algorithm that finds the k-th smallest value without sorting the whole array. After one partition, the pivot is at its final index p. If p is the index you want, you are done. If not, the answer is on one side only. So continue with that side and ignore the other. (The code below uses a loop instead of recursion.)

JavaScript
function kthSmallest(arr, k) {          // k = 1 means the smallest  let lo = 0, hi = arr.length - 1;  const target = k - 1;                    // its index in sorted order  while (true) {    const p = partition(arr, lo, hi);    if (p === target) return arr[p];    if (p < target) lo = p + 1;           // answer is on the right side    else hi = p - 1;                       // answer is on the left side  }}function partition(arr, lo, hi) {  const r = lo + Math.floor(Math.random() * (hi - lo + 1));  [arr[r], arr[hi]] = [arr[hi], arr[r]];  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;}console.log(kthSmallest([7, 2, 9, 4, 5], 2)); // 4

On average, each step keeps about half of the remaining items. The total work is n + n/2 + n/4 + … ≈ 2n. So quickselect is O(n) on average, compared with O(n log n) for sorting first. (In the rare worst case it is O(n²), like quick sort.) It is the classic answer to “k-th largest element”, because the k-th largest is the (n − k + 1)-th smallest.

JavaScript's .sort(): the text-order trap

If you call sort with no arguments, it converts every item to a string(text) and sorts in alphabet order. For numbers this is almost never what you want:

JavaScript
console.log([10, 9, 1, 100].sort());                 // [ 1, 10, 100, 9 ]   ← text order!console.log([10, 9, 1, 100].sort((a, b) => a - b));  // [ 1, 9, 10, 100 ]console.log([10, 9, 1, 100].sort((a, b) => b - a));  // [ 100, 10, 9, 1 ]

As text, “100” comes before “9”, because the first character “1” comes before “9”. Always pass a comparator when you sort numbers. This bug passes small tests (single digits sort correctly as text) and fails on larger values. It is one of the most common JavaScript interview mistakes.

Comparators: numbers, text and objects

A comparator is a function that you give to sort to tell it how to order two items, a and b. It returns a negative number if a should come first, a positive number if b should come first, and 0 if they are equal for sorting. a - b does exactly that for numbers in ascending order (smallest first).

JavaScript
const people = [  { name: "Ravi", age: 30 },  { name: "Lena", age: 25 },  { name: "Omar", age: 30 },];// By one keypeople.sort((a, b) => a.age - b.age);// By textpeople.sort((a, b) => a.name.localeCompare(b.name));// By two keys: age descending, then name ascending for tiespeople.sort((a, b) => b.age - a.age || a.name.localeCompare(b.name));console.log(people.map((p) => p.name)); // [ 'Omar', 'Ravi', 'Lena' ]// sort() changes the array itself. toSorted() returns a sorted copy instead// (toSorted needs Node.js 20 or newer):const nums = [3, 1, 2];const sorted = nums.toSorted((a, b) => a - b);console.log(nums, sorted);               // [ 3, 1, 2 ] [ 1, 2, 3 ]
  • Two keys: first || second works because a 0 from the first comparison (a tie) is treated as “false”, so JavaScript uses the second comparison instead.
  • Never return a boolean such as a > b. It gives only true or false (which become 1 or 0). It never gives a negative number, so the order can be wrong.
  • Cost: sort is O(n log n) and stable. Count it in your Big-O.

Which sort to use

SituationUse
Normal interview codearr.sort((a, b) => a - b) — O(n log n)
Values in a small fixed range (letters, 0–100)Counting sort — O(n + range)
Only the k-th value, or the k smallestQuickselect — O(n) average (or a heap, Lesson 46)
“Do not use the built-in sort”Merge sort (guaranteed) or quick sort with a random pivot
Linked listMerge sort (Lesson 37)
Nearly sorted, or very smallInsertion sort

Practice questions

Several of these are about writing the right comparator. You will use this skill in almost every later part.

Q1Kth largest elementMedium

Return the k-th largest value in nums. Count repeated values separately (do not look for the k-th different value).

Example 1
Input: nums = [3, 2, 1, 5, 6, 4], k = 2
Output: 5
Explanation: Sorted largest first: 6, 5, 4 … The 2nd is 5.
Example 2
Input: nums = [3, 2, 3, 1, 2, 4, 5, 5, 6], k = 4
Output: 4
Explanation: Sorted largest first: 6, 5, 5, 4. Repeated values count separately, so the 4th is 4.
Stuck? Open one hint at a time
Hint 1
Sorting works and takes O(n log n). Can you avoid sorting everything?
Hint 2
The k-th largest is at index n - k of the array sorted smallest first. Use quickselect to find that index.
Show all 2 solutions with explanations
Approach 1Sort
The idea

Sort the copy with the largest value first, and take index k − 1.

JavaScript
function findKthLargest(nums, k) {  return [...nums].sort((a, b) => b - a)[k - 1];}console.log(findKthLargest([3, 2, 1, 5, 6, 4], 2));          // 5console.log(findKthLargest([3, 2, 3, 1, 2, 4, 5, 5, 6], 4)); // 4
How it works

O(n log n). This is a good first answer.

Approach 2Quickselect
The idea

Partition with a random pivot, again and again, until the pivot lands on index n − k.

JavaScript
function findKthLargest(nums, k) {  const a = [...nums];  const target = a.length - k;  let lo = 0, hi = a.length - 1;  while (true) {    const r = lo + Math.floor(Math.random() * (hi - lo + 1));    [a[r], a[hi]] = [a[hi], a[r]];    const pivot = a[hi];    let i = lo;    for (let j = lo; j < hi; j++) {      if (a[j] < pivot) { [a[i], a[j]] = [a[j], a[i]]; i++; }    }    [a[i], a[hi]] = [a[hi], a[i]];    if (i === target) return a[i];    if (i < target) lo = i + 1;    else hi = i - 1;  }}console.log(findKthLargest([3, 2, 1, 5, 6, 4], 2));          // 5console.log(findKthLargest([3, 2, 3, 1, 2, 4, 5, 5, 6], 4)); // 4
How it works

O(n) on average, and O(n²) in a very unlikely worst case. It uses O(1) extra space apart from the copy.

Which approach should you use?

Give the sort first, then quickselect as the improvement. Lesson 46 adds a third answer that uses a heap (a structure that always gives you the smallest or largest item quickly). It takes O(n log k) and also works well when the data arrives one item at a time. (LeetCode 215.)

Q2Sort colors (0s, 1s and 2s)Medium

Sort an array that contains only 0, 1 and 2, in place (change the same array), without the built-in sort.

Example 1
Input: [2, 0, 2, 1, 1, 0]
Output: [0, 0, 1, 1, 2, 2]
Explanation: All 0s, then 1s, then 2s.
Example 2
Input: [2, 0, 1]
Output: [0, 1, 2]
Explanation: Small case.
Stuck? Open one hint at a time
Hint 1
There are only three possible values, so counting sort works in two passes.
Hint 2
For one pass, split the array into three regions at once: 0s at the front, 2s at the back, and 1s in the middle.
Show all 2 solutions with explanations
Approach 1Count, then rewrite
The idea

Count the 0s, 1s and 2s. Then write the array again from the counts.

JavaScript
function sortColors(nums) {  const count = [0, 0, 0];  for (const x of nums) count[x]++;  let i = 0;  for (let v = 0; v < 3; v++) {    for (let c = 0; c < count[v]; c++) nums[i++] = v;  }  return nums;}console.log(sortColors([2, 0, 2, 1, 1, 0])); // [ 0, 0, 1, 1, 2, 2 ]
How it works

O(n) time, O(1) space, two passes.

Approach 2Three-way partition (one pass)
The idea
  1. low is the next place for a 0. high is the next place for a 2. mid is the index that scans the array.
  2. If the value is 0, swap it to low and move both low and mid. If it is 1, just move mid. If it is 2, swap it to high and move only high, because the value that was swapped in has not been checked yet.
JavaScript
function sortColors(nums) {  let low = 0, mid = 0, high = nums.length - 1;  while (mid <= high) {    if (nums[mid] === 0) {      [nums[low], nums[mid]] = [nums[mid], nums[low]];      low++; mid++;    } else if (nums[mid] === 1) {      mid++;    } else {      [nums[mid], nums[high]] = [nums[high], nums[mid]];      high--;    }  }  return nums;}console.log(sortColors([2, 0, 2, 1, 1, 0])); // [ 0, 0, 1, 1, 2, 2 ]console.log(sortColors([2, 0, 1]));          // [ 0, 1, 2 ]
How it works

One pass and O(1) space. This is partitioning with two boundaries instead of one. It is known as the Dutch national flag algorithm. Lesson 21 traces it step by step.

Which approach should you use?

Counting is the easiest. The one-pass partition is the follow-up that interviewers ask for. (LeetCode 75.)

Q3Sort by increasing frequencyEasy

Sort nums by how often each value appears, least frequent first. If two values appear equally often, put the larger value first.

Example 1
Input: [1, 1, 2, 2, 2, 3]
Output: [3, 1, 1, 2, 2, 2]
Explanation: 3 appears once, 1 twice, 2 three times.
Example 2
Input: [2, 3, 1, 3, 2]
Output: [1, 3, 3, 2, 2]
Explanation: 1 appears once. 2 and 3 appear twice each, so on a tie the larger value comes first.
Stuck? Open one hint at a time
Hint 1
Count the values with a Map first.
Hint 2
Use a comparator with two keys: frequency from small to large, then value from large to small.
Show the solution with explanations
Approach 1Frequency map + two-key comparator
The idea

Sort by count. When two counts are equal, sort by value, largest first.

JavaScript
function frequencySort(nums) {  const freq = new Map();  for (const x of nums) freq.set(x, (freq.get(x) ?? 0) + 1);  return nums.sort((a, b) => freq.get(a) - freq.get(b) || b - a);}console.log(frequencySort([1, 1, 2, 2, 2, 3])); // [ 3, 1, 1, 2, 2, 2 ]console.log(frequencySort([2, 3, 1, 3, 2]));    // [ 1, 3, 3, 2, 2 ]
How it works

O(n log n). The comparator reads the counts from the Map. Sorting by a value that you look up or work out is a very common pattern.

Which approach should you use?

The || trick (see the lesson) turns two rules into one comparator. (LeetCode 1636.)

Q4Sort the people by heightEasy

Given names and their heights (all different), return the names sorted by height, tallest first.

Example 1
Input: names = ["Mary", "John", "Emma"], heights = [180, 165, 170]
Output: ["Mary", "Emma", "John"]
Explanation: Tallest first: 180, 170, 165.
Stuck? Open one hint at a time
Hint 1
If you sort names alone, you lose the link to the heights. Sort the indexes instead, or sort pairs.
Show all 2 solutions with explanations
Approach 1Sort indices by height
The idea

Make a list of indexes [0, 1, 2 …]. Sort it by the height at each index. Then turn each index into its name.

JavaScript
function sortPeople(names, heights) {  return names    .map((_, i) => i)    .sort((a, b) => heights[b] - heights[a])    .map((i) => names[i]);}console.log(sortPeople(["Mary", "John", "Emma"], [180, 165, 170])); // [ 'Mary', 'Emma', 'John' ]
How it works

O(n log n). Sorting indexes keeps two parallel arrays (arrays with matching positions) together without building objects. The same trick appears in interval and scheduling problems.

Approach 2Sort pairs
The idea

Join each name and height into a pair. Sort the pairs, then take the names.

JavaScript
function sortPeople(names, heights) {  return names    .map((name, i) => [heights[i], name])    .sort((a, b) => b[0] - a[0])    .map(([, name]) => name);}console.log(sortPeople(["Mary", "John", "Emma"], [180, 165, 170])); // [ 'Mary', 'Emma', 'John' ]
How it works

The cost is the same, and the code is often easier to read.

Which approach should you use?

Both are fine. (LeetCode 2418.)

Q5Largest numberMedium

Arrange the non-negative whole numbers so that, when you write them one after another, they form the largest possible number. Return it as a string (text).

Example 1
Input: [10, 2]
Output: "210"
Explanation: "2" + "10" = "210" is larger than "102".
Example 2
Input: [3, 30, 34, 5, 9]
Output: "9534330"
Explanation: Not simply largest first: 3 must come before 30, because "330" > "303".
Example 3
Input: [0, 0]
Output: "0"
Explanation: Edge case: not "00".
Stuck? Open one hint at a time
Hint 1
Number order does not work, and plain text order does not work either (think of 3 and 30).
Hint 2
For two numbers a and b, which order makes the bigger string: a + b or b + a (joined as text)? Use exactly that as the comparator.
Show the solution with explanations
Approach 1Custom comparator on joined strings
The idea

Put a before b when the text a + b is larger than the text b + a.

JavaScript
function largestNumber(nums) {  const s = nums    .map(String)    .sort((a, b) => (b + a).localeCompare(a + b))    .join("");  return s[0] === "0" ? "0" : s;}console.log(largestNumber([10, 2]));           // 210console.log(largestNumber([3, 30, 34, 5, 9])); // 9534330console.log(largestNumber([0, 0]));            // 0
How it works
Dry run · comparing pairs
aba + bb + aFirst
3303303033
9595599
34334333434

b + a and a + b have the same length, so comparing them as text compares them as numbers.

Which approach should you use?

The whole problem is to choose the right comparator. It makes O(n log n) comparisons, and each one costs O(the number of digits). (LeetCode 179.)

Q6Partition around a pivot, keeping orderMedium

Rearrange nums so that values less than pivot come first, then values equal to it, then larger values. Keep the original order inside each group.

Example 1
Input: nums = [9, 12, 5, 10, 14, 3, 10], pivot = 10
Output: [9, 5, 3, 10, 10, 12, 14]
Explanation: Smaller values, then values equal to 10, then larger values — each group in its original order.
Stuck? Open one hint at a time
Hint 1
Quick sort's in-place partition does not keep the original order. Is extra space allowed here? (Yes.)
Hint 2
Use three lists: less, equal and greater.
Show the solution with explanations
Approach 1Three lists
The idea

Make one pass. Add each value to the list for its group. Then join the three lists.

JavaScript
function pivotArray(nums, pivot) {  const less = [], equal = [], greater = [];  for (const x of nums) {    if (x < pivot) less.push(x);    else if (x === pivot) equal.push(x);    else greater.push(x);  }  return [...less, ...equal, ...greater];}console.log(pivotArray([9, 12, 5, 10, 14, 3, 10], 10)); // [ 9, 5, 3, 10, 10, 12, 14 ]
How it works

O(n) time and O(n) space. It is a stable partition (equal items keep their order), and the price is extra memory. This is a small example of the trade-off between merge-style and quick-style algorithms.

Which approach should you use?

If the problem had said “in place, order does not matter”, the partition from the lesson (it is called the Lomuto partition) would be the answer. Read the question to see which one is asked. (LeetCode 2161.)

Q7Quick sort an arrayMedium

Sort nums with quick sort, so that it stays fast on sorted input and on input with many equal values.

Example 1
Input: [5, 2, 3, 1]
Output: [1, 2, 3, 5]
Explanation: There can be up to 5 × 10⁴ values, and they may be already sorted or all equal.
Stuck? Open one hint at a time
Hint 1
Use a random pivot, so that sorted input does not cause O(n²).
Hint 2
Many equal values also make the partition from the lesson unbalanced (one side gets almost everything). A three-way partition (less, equal, greater) fixes that.
Show the solution with explanations
Approach 1Quick sort, random pivot, three-way partition
The idea

Partition into three parts: values < pivot, values = pivot, and values > pivot (like Sort Colors). Then call the sort again only on the outer two parts.

JavaScript
function sortArray(nums) {  function sort(lo, hi) {    if (lo >= hi) return;    const pivot = nums[lo + Math.floor(Math.random() * (hi - lo + 1))];    let lt = lo, i = lo, gt = hi;    while (i <= gt) {      if (nums[i] < pivot) { [nums[lt], nums[i]] = [nums[i], nums[lt]]; lt++; i++; }      else if (nums[i] > pivot) { [nums[i], nums[gt]] = [nums[gt], nums[i]]; gt--; }      else i++;    }    sort(lo, lt - 1);       // values < pivot    sort(gt + 1, hi);       // values > pivot  }  sort(0, nums.length - 1);  return nums;}console.log(sortArray([5, 2, 3, 1]));             // [ 1, 2, 3, 5 ]console.log(sortArray([5, 1, 1, 2, 0, 0]));       // [ 0, 0, 1, 1, 2, 5 ]console.log(sortArray(new Array(8).fill(7)));     // [ 7, 7, 7, 7, 7, 7, 7, 7 ]
How it works

The expected time is O(n log n). If all values are equal, the first partition puts everything in the “equal” part and both recursive calls get empty ranges. That takes O(n) instead of the O(n²) that a two-way partition would take.

Which approach should you use?

A random pivot handles sorted input, and a three-way partition handles repeated values. Together they make quick sort safe to use. (LeetCode 912.)

Make it stick

Make it stick — close this page, then
  1. Write partition from memory and dry-run it on [3, 8, 1, 5].
  2. Explain why quick sort is O(n²) on sorted input with a last-item pivot, and how a random pivot fixes it.
  3. Say why quickselect is O(n) on average while sorting is O(n log n).
  4. Predict the output of [5, 25, 100].sort(), and fix it.
  5. Write a comparator for “by score descending, then name ascending”.

Revisit schedule: solve two of the questions again tomorrow, again in 3 days, and once more in a week — without looking at the answers. Each time it will feel easier. That is the memory forming.

Part 3 complete — what's next

You now know what sorting does, how the O(n log n) sorts work, and how to sort anything correctly in JavaScript. Sorting will often appear as the first step of later solutions.

Part 4 — Arrays and the First Patterns begins with the array problems that open almost every coding round. These are: finding the largest and second largest values, removing duplicates in place, rotating an array, and finding missing numbers.

Practise on LeetCode

Real interview problems that use exactly what this lesson teaches. Solve them in JavaScript after the practice questions above.

Published by Jainik · Last updated · How these lessons are made

Keep learning

Related series