Quick Sort Visualization — Step by Step

Selects a pivot element, partitions the array around it, then recursively sorts both sides.

SortingintermediateUpdated

// quick answer

Quick Sort is a sorting algorithm that runs in O(n log n) time on average (O(n²) worst case) with O(log n) space. Selects a pivot element, partitions the array around it, then recursively sorts both sides.

How Quick Sort Works

Quick Sort picks a pivot element and rearranges the array so that everything smaller goes left and everything larger goes right (Lomuto partition scheme). Then it recursively sorts the two partitions. Average case O(n log n) but worst case O(n²) for already sorted input. In practice, it is often the fastest comparison sort due to cache efficiency.

Quick Sort Step by Step

The big idea: put one element exactly where it belongs

Quick Sort never merges anything. Instead, each round picks one element — the pivot — and rearranges the array so that everything smaller sits to the pivot’s left and everything larger to its right. After that single pass, the pivot is in its *final sorted position*, guaranteed: nothing on its left is bigger, nothing on its right is smaller. The two sides are then sorted recursively, each round locking another pivot in place. Where Merge Sort does its work combining on the way up, Quick Sort does all its work partitioning on the way down.

The Lomuto partition, step by step

This implementation uses the Lomuto scheme: the pivot is the last element, and a boundary index i marks the end of the "small values" zone. A scanner j walks left to right; every time it finds a value ≤ pivot, that value is swapped to the boundary and the boundary advances. Take [4, 8, 2, 7, 5], pivot 5. j=0: 4 ≤ 5, already at the boundary, boundary moves to 1. j=1: 8 > 5, skip. j=2: 2 ≤ 5, swap with 8 → [4, 2, 8, 7, 5], boundary moves to 2. j=3: 7 > 5, skip.

The pivot lands in its final home

When the scan ends, the boundary sits at the first element bigger than the pivot — in the example, index 2 (the 8). One last swap puts the pivot there: [4, 2, 5, 7, 8]. Look at the shape now: [4, 2] on the left are all ≤ 5, [7, 8] on the right are all > 5, and 5 will never move again. This is the moment to watch in the visualizer — the pivot bar flips to the "complete" color mid-run, long before the array is sorted, because its final position is already known.

Recurse on both sides

Quick Sort now calls itself on [4, 2] and on [7, 8] — the pivot itself is excluded, since it is done. Left side: pivot 2, nothing scans below it, so 2 swaps to the front → [2, 4], both locked. Right side: pivot 8, 7 ≤ 8 keeps the boundary moving, 8 stays put, both locked. The full array is [2, 4, 5, 7, 8]. Each level of recursion locks at least one element, and the ranges shrink until they hit single elements, which are trivially in position.

The worst case: when the pivot picks badly

Everything hinges on the pivot landing near the middle. If each partition splits roughly in half, there are about log₂ n levels doing O(n) work each — the O(n log n) average. But with the last element as pivot, an already-sorted array is the nightmare: every pivot is the maximum, every split is n−1 elements versus zero, and the recursion degrades to n levels — O(n²), plus deep recursion. Real implementations dodge this with randomized pivots, median-of-three selection, or introsort, which detects deep recursion and switches to a heap-based sort.

Why it is usually the fastest anyway

Despite the scary worst case, Quick Sort tends to beat other O(n log n) sorts on in-memory arrays. Partitioning is in place — no O(n) auxiliary array like Merge Sort — and the scan touches memory sequentially, which caches love. The inner loop is a couple of comparisons and an occasional swap: very little bookkeeping per element. The price is instability (partition swaps can reorder equal elements) and the need for good pivots. Race it against Merge Sort on random data to see the average case play out.

Time & Space Complexity

Best CaseAverage CaseWorst CaseSpace
O(n log n)O(n log n)O(n²)O(log n)

Quick Sort Pseudocode

function quickSort(arr, low, high):
  if low >= high: return
  pivotIdx = partition(arr, low, high)
  quickSort(arr, low, pivotIdx - 1)
  quickSort(arr, pivotIdx + 1, high)

function partition(arr, low, high):
  pivot = arr[high]
  i = low
  for j = low to high-1:
    if arr[j] <= pivot:
      swap(arr[i], arr[j])
      i++
  swap(arr[i], arr[high])
  return i

Press play in the visualizer above to watch each line execute with live highlighting.

Quick SortCode in JavaScript, Python & Java

JavaScript

function quickSort(arr, low = 0, high = arr.length - 1) {
  if (low < high) {
    const p = partition(arr, low, high);
    quickSort(arr, low, p - 1);
    quickSort(arr, p + 1, high);
  }
  return arr;
}

function partition(arr, low, high) {
  const pivot = arr[high]; // Lomuto: last element is the pivot
  let i = low; // boundary of the "<= pivot" zone
  for (let j = low; j < high; j++) {
    if (arr[j] <= pivot) {
      [arr[i], arr[j]] = [arr[j], arr[i]];
      i++;
    }
  }
  [arr[i], arr[high]] = [arr[high], arr[i]]; // pivot to final spot
  return i;
}

Python

def quick_sort(arr, low=0, high=None):
    if high is None:
        high = len(arr) - 1
    if low < high:
        p = partition(arr, low, high)
        quick_sort(arr, low, p - 1)
        quick_sort(arr, p + 1, high)
    return arr

def partition(arr, low, high):
    pivot = arr[high]  # Lomuto: last element is the pivot
    i = low  # boundary of the "<= pivot" zone
    for j in range(low, high):
        if arr[j] <= pivot:
            arr[i], arr[j] = arr[j], arr[i]
            i += 1
    arr[i], arr[high] = arr[high], arr[i]  # pivot to final spot
    return i

Java

public static void quickSort(int[] arr, int low, int high) {
    if (low < high) {
        int p = partition(arr, low, high);
        quickSort(arr, low, p - 1);
        quickSort(arr, p + 1, high);
    }
}

private static int partition(int[] arr, int low, int high) {
    int pivot = arr[high]; // Lomuto: last element is the pivot
    int i = low; // boundary of the "<= pivot" zone
    for (int j = low; j < high; j++) {
        if (arr[j] <= pivot) {
            int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp;
            i++;
        }
    }
    int tmp = arr[i]; arr[i] = arr[high]; arr[high] = tmp;
    return i; // pivot's final index
}

When to Use Quick Sort

  • General-purpose in-memory sorting — on random-access arrays it is typically the fastest comparison sort in practice, which is why C’s qsort and many standard libraries build on it.
  • When memory is tight — it sorts in place with only O(log n) stack space for recursion, versus Merge Sort’s O(n) auxiliary array.
  • When average-case speed matters more than worst-case guarantees — with randomized or median-of-three pivots, hitting O(n²) is vanishingly unlikely on real data.
  • Avoid it when stability is required — partitioning reorders equal elements; use Merge Sort or your library’s stable sort instead.
  • Avoid the naive last-element-pivot version on sorted or nearly-sorted input — that is exactly its O(n²) worst case; if you must handle such input, randomize the pivot or use introsort.
  • Avoid it for latency-critical or adversarial settings — an attacker who controls input can force the worst case of a predictable-pivot quicksort; guaranteed O(n log n) algorithms are safer.

Quick Sort — Frequently Asked Questions

Is Quick Sort stable?

No, not in its standard in-place forms. Partitioning swaps elements across long distances, so equal values can end up in a different relative order than they started. Stable variants exist but require extra memory, which sacrifices quicksort’s main space advantage. If stability matters — sorting by one field while preserving order on another — use merge sort or your language’s built-in stable sort.

What is the time complexity of Quick Sort?

O(n log n) on average and in the best case, when pivots split the array into roughly equal halves. The worst case is O(n²), which occurs when every pivot is the smallest or largest remaining element — for the last-element Lomuto scheme, already-sorted or reverse-sorted input triggers exactly this. Space is O(log n) on average for the recursion stack, since it partitions in place.

Why is Quick Sort called quick?

Its inventor, Tony Hoare, named it in 1961 for its speed in practice, and the name stuck because it is deserved: on typical in-memory arrays quicksort beats other comparison sorts. It partitions in place with no auxiliary array to copy, scans memory sequentially (cache-friendly), and has a very tight inner loop. Same asymptotic class as merge sort and heapsort, but smaller constants.

How do real implementations avoid the O(n²) worst case?

Three common defenses: pick the pivot randomly, so no fixed input pattern is reliably bad; use median-of-three, taking the median of the first, middle, and last elements, which handles sorted input well; or use introsort, which monitors recursion depth and switches to heapsort if it grows too deep, guaranteeing O(n log n). Many libraries also hand small partitions to insertion sort.

Quick Sort vs Merge Sort — which should I use?

Quicksort is usually faster on in-memory arrays and uses O(log n) space instead of O(n), so it wins when memory matters and average speed is the goal. Merge sort wins when you need stability, a guaranteed O(n log n) worst case, linked-list sorting, or external sorting of data larger than RAM. In practice, most standard libraries choose a quicksort-derived hybrid for primitives and a merge-based one for objects.

Compare Quick Sort

Related Algorithms

  • Merge Sort VisualizationDivides the array in half recursively, sorts each half, then merges the sorted halves back together.
  • Bubble Sort VisualizationRepeatedly steps through the list, compares adjacent elements, and swaps them if out of order.

References & Further Reading