• Home
  • About
    • Thoughts To Pen photo

      Thoughts To Pen

      My thoughts on Computer Programming || Psychology || Personal Finances || & much more...

    • Learn More
    • Twitter
    • Instagram
    • Github
    • StackOverflow
  • Posts
    • All Posts
    • All Tags
  • Projects
  • Portfolio
  • Resources
  • About

Quick Sort — The Versatile Performer

02 Feb 2026

Introduction

In the previous post, we learned Merge Sort — a guaranteed $O(n \log n)$ algorithm that needs $O(n)$ extra space. Quick Sort takes a different approach: it achieves $O(n \log n)$ on average while sorting in-place (only $O(\log n)$ extra space for the recursion stack).

Quick Sort is the default sorting algorithm in many standard libraries (Java’s Arrays.sort() for primitives uses a variant of Quick Sort). Let’s understand why.

Think of this analogy: you’re organising books on a shelf. You pick one book at random — call it the pivot. You place all books that come before the pivot alphabetically to its left, and all books that come after it to its right. Now the pivot book is in its final correct position. You then repeat the same process for the left group and the right group separately.


How Quick Sort Works

  1. Choose a pivot element from the array.
  2. Partition the array: rearrange elements so that everything less than the pivot goes to the left, and everything greater goes to the right.
  3. After partitioning, the pivot is in its final sorted position.
  4. Recursively apply the same process to the left sub-array and the right sub-array.

Step-by-Step Walkthrough

Let’s sort [33, 15, 10, 45, 27, 8, 42] using the last element as pivot (Lomuto partition scheme).

Round 1 — Pivot = 42

We need to rearrange so that everything ≤ 42 goes left and everything > 42 goes right.

Walk through the array with a pointer i that tracks where the “small side” ends:

j arr[j] arr[j] ≤ 42? Action Array State
0 33 Yes Swap arr[0] with arr[0], i→1 [33, 15, 10, 45, 27, 8, 42]
1 15 Yes Swap arr[1] with arr[1], i→2 [33, 15, 10, 45, 27, 8, 42]
2 10 Yes Swap arr[2] with arr[2], i→3 [33, 15, 10, 45, 27, 8, 42]
3 45 No Skip [33, 15, 10, 45, 27, 8, 42]
4 27 Yes Swap arr[3] with arr[4], i→4 [33, 15, 10, 27, 45, 8, 42]
5 8 Yes Swap arr[4] with arr[5], i→5 [33, 15, 10, 27, 8, 45, 42]

Now swap the pivot (42) into position i (index 5):

After partition: [33, 15, 10, 27, 8, **42**, 45] — 42 is in its final position ✅

  • Left sub-array: [33, 15, 10, 27, 8]
  • Right sub-array: [45] (single element, already sorted ✅)

Round 2 — Sort [33, 15, 10, 27, 8], Pivot = 8

j arr[j] arr[j] ≤ 8? Action Result
0 33 No Skip  
1 15 No Skip  
2 10 No Skip  
3 27 No Skip  

Swap pivot (8) into position 0:

After partition: [**8**, 33, 15, 10, 27, 42, 45] — 8 is in place ✅

  • Left sub-array: [] (empty)
  • Right sub-array: [33, 15, 10, 27]

Round 3 — Sort [33, 15, 10, 27], Pivot = 27

j arr[j] arr[j] ≤ 27? Action Result
0 33 No Skip  
1 15 Yes Swap, i→1 [15, 33, 10, 27]
2 10 Yes Swap, i→2 [15, 10, 33, 27]

Swap pivot into position 2:

After partition: [15, 10, **27**, 33] — 27 is in place ✅

Remaining rounds sort [15, 10] and [33]:

  • Pivot = 10 → [**10**, 15] ✅
  • [33] is a single element ✅

Final sorted array: [8, 10, 15, 27, 33, 42, 45] ✅


Java Implementation

public class QuickSort {

    /**
     * Entry point for Quick Sort.
     */
    public static void quickSort(int[] arr, int low, int high) {
        if (low < high) {
            // Partition the array and get the pivot's final index
            int pivotIndex = partition(arr, low, high);

            // Recursively sort elements before and after the pivot
            quickSort(arr, low, pivotIndex - 1);
            quickSort(arr, pivotIndex + 1, high);
        }
    }

    /**
     * Lomuto partition scheme: uses the last element as pivot.
     * Returns the final index of the pivot after partitioning.
     */
    private static int partition(int[] arr, int low, int high) {
        int pivot = arr[high]; // choose the last element as pivot
        int i = low;           // i tracks the boundary of the "small" side

        for (int j = low; j < high; j++) {
            if (arr[j] <= pivot) {
                // Swap arr[i] and arr[j], then advance i
                swap(arr, i, j);
                i++;
            }
        }

        // Place the pivot in its correct position
        swap(arr, i, high);
        return i;
    }

    private static void swap(int[] arr, int a, int b) {
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }

    public static void main(String[] args) {
        int[] data = {33, 15, 10, 45, 27, 8, 42};
        quickSort(data, 0, data.length - 1);
        // Output: [8, 10, 15, 27, 33, 42, 45]
        for (int num : data) System.out.print(num + " ");
    }
}

Key Lines Explained

Line Purpose
int pivot = arr[high]; Pick the last element as pivot. Simple but can lead to worst-case on sorted input (see below).
int i = low; i is the boundary: everything to the left of i is ≤ pivot.
if (arr[j] <= pivot) If the current element is small enough, swap it into the “small” side and advance i.
swap(arr, i, high); After the loop, position i is the correct final position for the pivot.

Pivot Strategies

The pivot choice dramatically affects performance:

Strategy How Pros Cons
Last element pivot = arr[high] Simplest to implement $O(n^2)$ on already-sorted arrays
First element pivot = arr[low] Simple Same worst-case problem
Random element pivot = arr[random(low, high)] Avoids worst-case in practice Adds a random call
Median-of-three Median of first, middle, last Excellent balance Slightly more code

Java’s Arrays.sort() for primitives uses dual-pivot Quick Sort — a variant that picks two pivots and partitions into three sections, offering even better real-world performance.


Time & Space Complexity

Case Time Explanation
Best $O(n \log n)$ Pivot always lands in the middle, splitting evenly.
Average $O(n \log n)$ Random input leads to reasonably balanced partitions.
Worst $O(n^2)$ Pivot is always the smallest or largest element (sorted array + last-element pivot). Each partition only removes one element.

| Space | $O(\log n)$ — for the recursion stack | |:—:|:—:|

Tip: To avoid the $O(n^2)$ worst case in practice, use randomised pivot selection or the median-of-three strategy.


Merge Sort vs Quick Sort

Feature Merge Sort Quick Sort
Worst-case time $O(n \log n)$ ✅ $O(n^2)$ ❌
Average-case time $O(n \log n)$ $O(n \log n)$
Space $O(n)$ ❌ $O(\log n)$ ✅
In-place? No Yes ✅
Stable? Yes ✅ No (Lomuto/Hoare)
Practical speed Good Usually faster (better cache locality) ✅
Best for Linked lists, guaranteed worst-case Arrays in practice

Quick Sort wins in practice because it operates on contiguous memory (great for CPU cache), makes fewer data copies, and has a smaller constant factor.


When to Use / When NOT to Use

✅ Use When ❌ Avoid When
You need fast in-place sorting (most common scenario) Worst-case $O(n^2)$ is unacceptable (e.g., real-time systems) — use Merge Sort
Memory is limited — only $O(\log n)$ extra space Stability is required — use Merge Sort instead
General-purpose sorting of arrays with randomised pivot The array is already sorted and you are using naive pivot selection

Practice Problems

  1. Sort an Array – LeetCode #912 — Implement Quick Sort with random pivot. Compare its runtime against your Merge Sort solution.
  2. Kth Largest Element – LeetCode #215 — Use the partition step (Quick Select) to find the k-th element in $O(n)$ average time without fully sorting.
  3. Sort Colors – LeetCode #75 — A three-way partition problem inspired by Quick Sort (Dutch National Flag).

What’s Next?

We have now seen comparison-based sorts that top out at $O(n \log n)$. But can we do even better? Yes — when the data has special properties (integers in a known range), we can sort in linear time $O(n)$. Next up: Counting Sort, Radix Sort & Bucket Sort.



programmingalgorithmssortingdivide-and-conquer Share Tweet Msg