Quick sort

Pick a pivot, move smaller values left and larger values right, then sort each side.

Pattern: Divide and conquer25 min read
Quick recall3 questions
  1. 01Why is merge sort O(n log n)?

    Halving gives about log n levels, and the merges on each level touch all n items once.

  2. 02What extra space does merge sort need?

    O(n), for the temporary array that each merge fills.

  3. 03Why is selection sort not stable?

    Its long swaps can jump one value past an equal value, so equal values can change order.

Merge sort is fast, but it needs a second array as big as the input. Quick sort sorts inside the array, and many standard libraries use it for numbers.

It has one tricky step, the partition, with two pointers that walk toward each other. This lesson walks through it slowly. Then it shows the one input that makes quick sort slow, and the fix.

Read the problem

Problem: Quick Sortingn ≤ 10⁵
it asks
Sort the array in increasing order, without a second array.
example
in a = [13, 46, 24, 52, 20, 9] out [9, 13, 20, 24, 46, 52]
limits
  • 1 ≤ n ≤ 10⁵
  • −10⁹ ≤ a[i] ≤ 10⁹
  • values can repeat
budget
About 10⁸ steps run in one second. With n up to 10⁵, aim for O(n log n) or faster.
Which pattern? Decide first, then open.

“sort, n up to 10⁵”O(n log n)

“without a second array”sort in place: partition around a pivot

pattern: Divide and conquer

Questions to ask before you code:

  • Can values repeat? Yes. Decide where values equal to the pivot go. Here they go left. Many repeats are a second trap.
  • Can the input already be sorted? Ask this one every time. It is quick sort's trap (see below).
  • n up to 10⁵ means the target is O(n log n), as with merge sort.

The idea: put one item in its final place

Remember it as: pick a pivot, smaller to the left, bigger to the right, repeat on each side.

  1. Pick one value, the . Here it is the first item.
  2. : move values ≤ pivot to its left and values > pivot to its right.
  3. The pivot is now in its final sorted place. It never moves again.
  4. Sort the left side and the right side the same way.
Each row is one partition. The pivot lands in its final place, and so does any piece of one item.

Merge sort does its work after the recursive calls (the merge). Quick sort does its work before them (the partition). Then there is nothing to combine.

Partition, step by step

Two pointers walk toward each other:

  • i moves right and stops at a value that is too big for the left (> pivot).
  • j moves left and stops at a value that belongs on the left (≤ pivot).
  • If they have not crossed, swap those two values. Both are now on the right side.
  • When they cross, swap the pivot with a[j].
one partition, pivot 13swaps 0
13
0
46
1
24
2
52
3
20
4
9
5

1/8The pivot is 13, the first item. i starts at the left end, j at the right end.

Red marks a value on the wrong side. Two swaps place the pivot.

The rule that makes it work, at every moment:

  • everything left of i is ≤ pivot,
  • everything right of j is > pivot.

When the pointers cross, j sits on the last value ≤ pivot. So that slot is where the pivot belongs.

The code

int partition(int[] a, int low, int high) {
    int pivot = a[low];
    int i = low, j = high;
    while (i < j) {
        while (i < high && a[i] <= pivot) i++;
        while (j > low && a[j] > pivot) j--;
        if (i < j) {
            int t = a[i];
            a[i] = a[j];
            a[j] = t;
        }
    }
    int t = a[low];
    a[low] = a[j];
    a[j] = t;
    return j;
}
  • The two inner loops are the pointer walks. i < high and j > low stop them at the edges of the piece.
  • The swap after the loop drops the pivot into slot j. The function returns that slot.
  • In Java, each swap goes through a temporary variable t. Python and TypeScript swap in one line.
void quickSort(int[] a, int low, int high) {
    if (low >= high) return;
    int p = partition(a, low, high);
    quickSort(a, low, p - 1);
    quickSort(a, p + 1, high);
}
  • Base case: a piece of one item (or none) is sorted.
  • The pivot at p is final, so the two calls skip it.
  • The first call: TypeScript fills in low and high by default. In Java, call quickSort(a, 0, a.length - 1). In Python, call quick_sort(a, 0, len(a) - 1).

Fast on average, slow on sorted input

The usual case: O(n log n)

One partition walks its whole piece once: O(n) per level of calls. On random data, the pivot usually lands somewhere near the middle. The pieces shrink by about half, so there are about log n levels, as in merge sort.

Extra space is only the call stack: about log n frames.

The trap: sorted input

Feed it a sorted array. The first item is now the smallest, so every partition splits off nothing.

Each call removes only the pivot. n calls deep, and the work adds up to about n² / 2.

This is not rare. Data is often sorted or nearly sorted already. Measured in Node.js:

Sorted input, n = 5,000ComparisonsDeepest call stack
First item as pivotabout 12.5 millionabout 5,000 calls
Random pivotabout 81,000about 30 calls

On larger sorted arrays, the first-item version did not finish. The call stack ran out, at a different size in each language:

Sorted input, first-item pivotStill workedCrashedError
Java 21, default stack25,00030,000StackOverflowError
Python 39901,000RecursionError
TypeScript, Node.js 268,0009,000"Maximum call stack size exceeded"

Python stops first because it allows only 1,000 nested calls by default. All three crash long before the limit of n = 10⁵.

The fix: a random pivot

Swap a random item to the front before each partition. The partition code stays the same.

void quickSort(int[] a, int low, int high) {
    if (low >= high) return;
    int r = low + (int) (Math.random() * (high - low + 1));
    int t = a[low];
    a[low] = a[r];
    a[r] = t;
    int p = partition(a, low, high);
    quickSort(a, low, p - 1);
    quickSort(a, p + 1, high);
}
  • Java and TypeScript scale Math.random(), a number from 0 up to 1, to a slot from low to high.
  • Python's random.randint(low, high) includes both ends, so it picks from the same slots.

The order of the input no longer matters. The worst case is still O(n²), but it now needs very bad luck. With this fix, a sorted array of 100,000 items took about 2.2 million comparisons, and the deepest call stack was about 40 calls.

One more trap: many equal values

A random pivot does not help when most values are equal. Every value is ≤ pivot, so the whole piece goes left again. 5,000 copies of the number 7 took about 12.5 million comparisons and a call stack 5,000 deep, random pivot or not.

The fix is a three-way partition: values smaller than the pivot, equal to it, and bigger. The equal group is final at once. You will build that split in the lesson "Sort 0s, 1s and 2s in one pass".

Merge sort or quick sort

Merge sortQuick sort
Average timeO(n log n)O(n log n)
Worst timeO(n log n)O(n²)
Extra spaceO(n)O(log n) call stack
Stableyesno
Real usePython, V8, Java objects (TimSort)Java numbers, Go

Quick sort is not stable: its long swaps can jump one value past an equal one. For plain numbers that does not matter, because two 5s look the same. For records, it does.

What the built-in sort in your language uses

CallAlgorithmStable
Java Arrays.sort(int[])Dual-Pivot Quicksortnot promised
Java Arrays.sort(Object[])TimSortyes
Python sorted(), list.sort()Timsortyes
TypeScript sort(), toSorted()TimSort in V8yes, since ES2019
  • Java promises stability only for objects, such as an Integer[] or a String[]. For an int[], two equal values look the same, so order does not matter.
  • The JavaScript spec requires a stable sort since ECMAScript 2019, but does not name the algorithm. V8, in Chrome and Node.js, uses TimSort.

How to spot when sorting helps

In interviews you rarely write a sort. You call one, in O(n log n), and then the problem gets easy. Look for these words:

Signal in the problemWhat sorting gives you
a pair that adds up to a targetsort, then two pointers from both ends
find or remove duplicatesequal values end up next to each other
meetings, intervals, overlapssort by start time, then one pass
smallest difference between two itemsonly neighbours need checking
k-th smallest, mediansort, then read the index

Sorting costs O(n log n). When the rest of the work is one O(n) pass, the whole answer is O(n log n), which fits n = 10⁵.

take U forwardYouTube
Quick Sort For Beginners: Striver's A2Z DSA Course
Remember4 points
  1. 01

    Pick a pivot, move smaller values left and bigger values right: the pivot is now in its final place. Repeat on each side.

  2. 02

    In the partition, everything left of i is ≤ pivot and everything right of j is > pivot. When they cross, the pivot goes to j.

  3. 03

    Average O(n log n) with only O(log n) extra space. A first-item pivot on sorted input gives O(n²), and a random pivot fixes it.

  4. 04

    Merge sort is stable and never slow. Quick sort is in place but not stable. Most interview problems call a sort, then do one O(n) pass.

Read more

  • Overview of quicksort on Khan Academy, partitioning with pictures
  • VisuAlgo: sorting, pick quick sort and its random pivot version
  • Arrays in the Java docs, the notes on Dual-Pivot Quicksort and TimSort
  • Quicksort on Wikipedia, with other partition schemes and pivot choices

Next: largest elements and rotations, the first lesson of the Arrays chapter.

Practice

Solve these on the sheet after the lesson. Try each one for 20 minutes before you open its solution.

Striver's A2Z sheet0/1 solved
Open the sheet
PreviousMerge sort
Coming soonLargest elements and rotations