Quicksort: A Complete Guide to Description, Performance, and Randomization

Quicksort is one of the most widely used sorting algorithms in practice, prized for its excellent average-case performance and in-place operation, despite having a poor theoretical worst case. This comprehensive guide covers the partition-based algorithm in detail, analyzes both its worst-case and expected running time, and explains how randomization transforms it into a reliably efficient algorithm regardless of input order.

Quicksort AlgorithmPartition MethodRandomized Quicksort

~6 min read · Updated Sep 7, 2026

The Divide-and-Conquer Structure of Quicksort

Quicksort follows the divide-and-conquer paradigm introduced earlier in this series, but with a distinctive twist: unlike merge sort, which does equal work dividing and combining, quicksort does all of its real work during the divide step, and the combine step is trivial.

QUICKSORT(A, p, r):
  if p < r:
      q = PARTITION(A, p, r)
      QUICKSORT(A, p, q - 1)
      QUICKSORT(A, q + 1, r)

The algorithm's entire logic rests on the PARTITION procedure, which rearranges the subarray A[p..r] around a chosen Pivot element, such that every element to the pivot's left is less than or equal to it, and every element to its right is greater than or equal to it. Once partitioned, the two resulting subarrays are recursively sorted independently, and since the pivot is already in its final sorted position, no explicit combine step is needed.

The Partition Procedure in Detail

The classic Lomuto partition scheme selects the last element of the subarray as the pivot and maintains an invariant as it scans through the array.

PARTITION(A, p, r):
  x = A[r]           // pivot
  i = p - 1
  for j = p to r - 1:
      if A[j] ≤ x:
          i = i + 1
          exchange A[i] with A[j]
  exchange A[i + 1] with A[r]
  return i + 1

Tracing through an example makes this concrete. Consider partitioning [2, 8, 7, 1, 3, 5, 6, 4] with pivot value 4 (the last element).

Initial: [2, 8, 7, 1, 3, 5, 6, 4]   pivot = 4, i = -1 (using 0-index p=0)

j=0: A[0]=2 ≤ 4 → i=0, swap A[0],A[0]: [2, 8, 7, 1, 3, 5, 6, 4]
j=1: A[1]=8 > 4 → no action
j=2: A[2]=7 > 4 → no action
j=3: A[3]=1 ≤ 4 → i=1, swap A[1],A[3]: [2, 1, 7, 8, 3, 5, 6, 4]
j=4: A[4]=3 ≤ 4 → i=2, swap A[2],A[4]: [2, 1, 3, 8, 7, 5, 6, 4]
j=5: A[5]=5 > 4 → no action
j=6: A[6]=6 > 4 → no action

Final swap: exchange A[3] with A[7]: [2, 1, 3, 4, 7, 5, 6, 8]
Return q = 3 — pivot 4 is now in its final position

After partitioning, elements [2, 1, 3] to the left are all ≤ 4, and elements [7, 5, 6, 8] to the right are all ≥ 4, with the pivot itself correctly positioned at index 3. This procedure runs in Θ(n) time for a subarray of size n, since it performs a single pass through the array.

The Worst Case: Already-Sorted or Reverse-Sorted Input

Quicksort's performance depends entirely on how balanced the partitions are. The worst case occurs when the partition is maximally unbalanced — one subarray of size n-1 and one of size 0 — which happens, using the Lomuto scheme above, whenever the input array is already sorted or reverse-sorted.

Worst-case recurrence: T(n) = T(n-1) + T(0) + Θ(n)
                              = T(n-1) + Θ(n)

Solving this recurrence (an arithmetic series):
T(n) = Θ(n) + Θ(n-1) + ... + Θ(1) = Θ(n²)

This Θ(n²) worst case is a genuine practical concern: naively implemented quicksort performs disastrously on already-sorted data, a scenario that occurs disturbingly often with real-world data, such as log files or pre-processed input.

The Best Case: Perfectly Balanced Partitions

At the opposite extreme, if the partition always splits the array into two equal halves, the recurrence becomes identical in structure to merge sort's:

Best-case recurrence: T(n) = 2T(n/2) + Θ(n)

Using the master method covered earlier in this series:
T(n) = Θ(n log n)

A Crucial Insight: Balanced Splits Are More Common Than Expected

A key insight makes quicksort's average behavior far better than the pessimistic worst case might suggest: even a consistently unbalanced split, such as a 9-to-1 ratio at every level, still produces Θ(n log n) running time overall, since the recursion depth remains logarithmic and the per-level work remains linear. Only splits that are unbalanced by a constant amount at every single level, such as always splitting off just one element, produce the true quadratic worst case.

Randomized Quicksort: Protecting Against the Worst Case

Since deterministic quicksort's worst case is triggered by specific, predictable input orderings, a simple but powerful fix exists: choose the pivot randomly rather than deterministically picking the last element.

RANDOMIZED-PARTITION(A, p, r):
  i = RANDOM(p, r)
  exchange A[r] with A[i]
  return PARTITION(A, p, r)

RANDOMIZED-QUICKSORT(A, p, r):
  if p < r:
      q = RANDOMIZED-PARTITION(A, p, r)
      RANDOMIZED-QUICKSORT(A, p, q - 1)
      RANDOMIZED-QUICKSORT(A, q + 1, r)

This change guarantees that no specific input, chosen by an adversary or arising naturally, can reliably trigger the worst case, since the pivot choice is randomized independently of the input's order. The expected running time becomes O(n log n) for every input, connecting directly to the probabilistic analysis techniques discussed earlier in this series.

Analyzing the Expected Running Time Using Indicator Variables

The expected running time of randomized quicksort can be derived rigorously using the indicator random variable technique introduced earlier in this series. The key insight is that the total running time is dominated by the number of comparisons performed, and two elements are compared at most once, exactly when one of them is chosen as a pivot while the other is still in the same subarray.

Let Xij = indicator that elements zi and zj
         (the i-th and j-th smallest elements) are ever compared

The expected total number of comparisons is:
E[X] = Σ Σ (i

This confirms rigorously that randomized quicksort achieves O(n log n) expected running time on any input, using the same indicator variable and harmonic series techniques that proved useful for the hiring problem discussed earlier in this series.

Why Quicksort Remains Popular Despite Its Worst Case

Despite merge sort and heapsort, both discussed elsewhere in this series, offering guaranteed Θ(n log n) worst-case performance, quicksort remains extremely popular in practice for several reasons: its constant factors are typically smaller than merge sort's, it sorts in place unlike merge sort, and its access pattern exhibits excellent cache locality, discussed earlier in this series in the context of computer architecture, since the partition step scans memory sequentially. Randomization removes the practical risk of the worst case, making randomized quicksort the standard choice in most real-world sorting library implementations.

Written & researched by Dr. Shahin Siami

Related Articles

Binary Search Trees: Querying, Inserting, and Deleting Efficiently

A binary search tree maintains elements in sorted order while supporting efficient search, insertion, and deletion, all in time proportional to the tree's height. This comprehensive guide covers the defining binary-search-tree property, the core query operations including search, minimum, maximum, and successor, and the more intricate insertion and deletion procedures that must carefully preserve the tree's structure.

Continue

Hash Tables Explained: From Direct Addressing to Open Addressing

Hash tables provide expected constant-time lookup, insertion, and deletion, making them one of the most widely used data structures in practice. This comprehensive guide covers the direct-addressing idea that motivates hashing, how collisions are handled through chaining, the properties of good hash functions, open addressing as a memory-efficient alternative, and practical considerations for real-world hash table implementations.

Continue

Elementary Data Structures: Stacks, Queues, Linked Lists, and Trees

Before tackling advanced data structures, mastering the elementary building blocks is essential, since nearly every complex structure is built from these fundamentals. This comprehensive guide covers array-based stacks and queues, singly and doubly linked lists, and the standard techniques for representing rooted trees, including the clever left-child right-sibling representation for trees with unbounded branching.

Continue

Finding the Median Without Fully Sorting: Linear-Time Selection Algorithms

Finding the k-th smallest element in an unsorted array does not require the full Θ(n log n) cost of sorting; it can be done in linear time. This comprehensive guide covers the trivial case of finding the minimum or maximum, an elegant randomized selection algorithm with linear expected time, and a more intricate deterministic algorithm that guarantees linear time even in the worst case.

Continue

Beating the n log n Barrier: Linear-Time Sorting Algorithms Explained

Every comparison-based sorting algorithm requires at least Ω(n log n) time in the worst case, but algorithms that avoid comparisons entirely can sort in linear time under the right conditions. This comprehensive guide proves the comparison-sort lower bound using a decision tree argument, then explains three linear-time algorithms — counting sort, radix sort, and bucket sort — along with the specific input assumptions each requires.

Continue

Heapsort and Priority Queues: A Complete Guide to the Binary Heap

The binary heap is one of the most elegant data structures in computer science, enabling both an efficient in-place sorting algorithm and the priority queue abstraction used throughout algorithm design. This comprehensive guide covers heap properties and array representation, the core heapify operation, building a heap from an unordered array, the complete heapsort algorithm, and priority queue operations built on top of heaps.

Continue