Tuesday, August 11, 2026

Quick Sort Explained: How It Works, Partitioning, Complexity & JavaScript Implementation

Quick Sort Explained: Partitioning, Recursion & Time Complexity

A practical explanation of Quick Sort using the first element as the pivot, with JavaScript implementation and a visual walkthrough of the partition process.


Sorting an array looks simple until you start asking a more interesting question:

How can we sort an array without repeatedly creating new arrays or moving every element around?

This is where Quick Sort becomes interesting.

Quick Sort doesn't try to find the smallest element one by one. Instead, it chooses an element called a pivot and rearranges the array so that elements smaller than the pivot move toward one side and elements greater than the pivot move toward the other.

Once the pivot reaches its correct position, the same process is repeated on the left and right portions of the array.

The algorithm sounds simple.

The interesting part is how the partitioning actually works.

Quick Sort in One Picture

At a high level, Quick Sort follows three steps:

```
1️⃣
Choose a Pivot

Pick an element from the current range.

2️⃣
Partition

Move smaller and larger elements to their appropriate sides.

3️⃣
Repeat

Recursively sort the two sides.

```

In this implementation, we'll always choose the first element of the current range as the pivot.

Let's Start With an Example

Consider this array:

[2, 5, 6, 3, 1, 4, 7, 9, 8]

We start with the complete array.

Since we're using the first element as the pivot:

Pivot = 2

Now our job is to rearrange the array so that values smaller than 2 end up on the left and values greater than 2 end up on the right.

In this particular array, there is only one value smaller than 2:

1

Once partitioning finishes, the pivot will be placed between those two groups.

Understanding the Partition Function

The most important part of Quick Sort is not the recursion.

It is the partition function.

Our partition function receives:

partition(arr, low, high)

Here:

  • arr is the array.
  • low is the beginning of the current section.
  • high is the end of the current section.

The first thing we do is select the pivot:

const pivot = arr[low];

Because we're using the first element, the pivot is simply:

pivot = arr[low]

Now We Need Two Pointers

This is where the algorithm becomes interesting.

We use two pointers:

```
Left pointer

Starts at the pivot and moves toward the right.

Right pointer

Starts at the end and moves toward the left.

```

In code:

let left = low;
let right = high;

You can think of the pointers as two people searching from opposite directions.

LEFT → → → → → → ← ← ← ← ← RIGHT

What Does the Left Pointer Look For?

The left pointer searches for an element that is greater than the pivot.

Why?

Because a value greater than the pivot doesn't belong on the left side. We want to eventually move it toward the right.

while (left < high && arr[left] <= pivot) {
    left++;
}

As long as the current value is less than or equal to the pivot, we keep moving.

The pointer stops when it finds a value that is greater than the pivot.

Left pointer's job:
Find something that belongs on the right side.

What Does the Right Pointer Look For?

The right pointer does the opposite.

It searches for an element that is smaller than the pivot.

while (right > low && arr[right] >= pivot) {
    right--;
}

As long as the current value is greater than or equal to the pivot, the pointer moves left.

It stops when it finds a value that is smaller than the pivot.

Right pointer's job:
Find something that belongs on the left side.

The Key Idea Behind the Swap

Now both pointers have found something useful.

The left pointer found an element that is too large for the left side.

The right pointer found an element that is too small for the right side.

So we swap them.

[arr[left], arr[right]] = [arr[right], arr[left]];

This is the heart of the partition operation.

Too large ← LEFT       RIGHT → Too small
↓ SWAP ↓
Too small → LEFT       RIGHT ← Too large

We continue doing this until the two pointers meet or cross.

Then Comes the Important Pivot Swap

Once the two pointers are finished, we still haven't placed the pivot in its final position.

Remember that our pivot is still sitting at:

arr[low]

The right pointer has now stopped at the position where the pivot belongs.

So we perform:

[arr[low], arr[right]] = [arr[right], arr[low]];

This final swap is what puts the pivot into its correct sorted position.

After partition:

Every element to the left of the pivot is smaller than or equal to it, and every element to the right is greater than or equal to it.

Quick Sort's Secret: Recursion

Once the pivot is in its correct position, we have solved one part of the problem.

We don't need to move that pivot again.

Instead, we divide the problem into two smaller problems.

Left side    | PIVOT |    Right side



Quick Sort left       Quick Sort right

That's exactly what these two lines do:

quickSort(arr, low, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, high);

The same partitioning process keeps happening until each section contains zero or one element.

Visualizing the Recursion

You can imagine Quick Sort breaking the array down like this:

[2, 5, 6, 3, 1, 4, 7, 9, 8]
        ↓
[1]   [2]   [5, 6, 3, 4, 7, 9, 8]
                   ↓
               [3, 4]   [5]   [6, 7, 9, 8]
                            ↓
                             [6] [7] [8] [9]

Eventually, every element reaches a position where there is nothing left to sort around it.

When Does the Recursion Stop?

Every recursive algorithm needs a base case.

For Quick Sort, the base case is very simple:

if (low >= high) {
    return;
}

Why?

Because an array section containing zero or one element is already sorted.

0 elements → already sorted
1 element → already sorted

The Complete JavaScript Implementation

Putting everything together, the implementation looks like this:

const arr = [2, 5, 6, 3, 1, 4, 7, 9, 8];

function partition(arr, low, high) {
const pivot = arr[low];


let left = low;
let right = high;

while (left < right) {

    // Find an element greater than the pivot
    while (left < high && arr[left] <= pivot) {
        left++;
    }

    // Find an element smaller than the pivot
    while (right > low && arr[right] >= pivot) {
        right--;
    }

    // Swap the elements
    if (left < right) {
        [arr[left], arr[right]] = [arr[right], arr[left]];
    }
}

// Put pivot in its correct position
[arr[low], arr[right]] = [arr[right], arr[low]];

return right;


}

function quickSort(arr, low, high) {


// Base case: zero or one element
if (low >= high) {
    return;
}

const pivotIndex = partition(arr, low, high);

// Sort left side of pivot
quickSort(arr, low, pivotIndex - 1);

// Sort right side of pivot
quickSort(arr, pivotIndex + 1, high);


}

quickSort(arr, 0, arr.length - 1);

console.log(arr);

Time Complexity: Where Quick Sort Gets Interesting

Quick Sort doesn't always divide the array equally.

And that is exactly why its time complexity depends heavily on the pivot.

Best Case: O(n log n)

The best situation occurs when the pivot divides the array into roughly equal halves every time.

n

n/2    n/2
↓        ↓
n/4 n/4  n/4 n/4

...

Each level processes approximately n elements, and there are roughly log n levels.

Therefore:

O(n log n)

Average Case: O(n log n)

Even when the partitions aren't perfectly balanced, Quick Sort generally performs very well on average.

With a reasonably good pivot selection strategy, the expected complexity is:

O(n log n)

Worst Case: O(n²)

Here's where our choice of the first element as the pivot becomes important.

Imagine the array is already sorted:

[1, 2, 3, 4, 5, 6, 7, 8, 9]

We choose 1 as the pivot.

There are no elements smaller than 1, so the pivot ends up at the beginning.

Now Quick Sort has to process:

9 → 8 → 7 → 6 → 5 → 4 → 3 → 2 → 1

Instead of dividing the problem in half, we're effectively removing only one element at each step.

Worst-case complexity: O(n²)

So Why Not Just Always Use the First Element?

This is the obvious question.

If choosing the first element works and keeps the implementation simple, why do many implementations use a random pivot?

The answer isn't that the first-element approach is wrong.

It's that the input data can make its performance predictable.

If you know your data is already sorted, reverse sorted, or has a structure that repeatedly produces poor partitions, always choosing the first element can lead to the worst case.

Randomizing the pivot makes it much harder for a particular input arrangement to consistently produce terrible partitions.

First element pivot
Simple, predictable, easy to understand.

``` Random pivot
Reduces the likelihood of repeatedly getting highly unbalanced partitions. ```

For learning Quick Sort, however, using the first element is an excellent way to understand the algorithm because the partition logic is easier to reason about.

What About Space Complexity?

Quick Sort is an in-place sorting algorithm in this implementation because we're rearranging elements inside the original array instead of creating separate arrays for every partition.

However, recursion still consumes stack space.

Case Time Recursion Stack
Best O(n log n) O(log n)
Average O(n log n) O(log n)
Worst O(n²) O(n)

Quick Sort vs Merge Sort

Quick Sort is often compared with Merge Sort because both have an average time complexity of O(n log n).

Feature Quick Sort Merge Sort
Average time O(n log n) O(n log n)
Worst case O(n²) O(n log n)
Extra array memory Low Higher
In-place Yes Typically no
Main idea Partition around pivot Split and merge

The Easiest Way to Remember Quick Sort

If you forget the implementation details, remember these three questions:

```

1. What is my pivot?
In our implementation: the first element.

2. What is partition doing?
Finding misplaced elements on both sides and swapping them.

3. What happens after partition?
The pivot is in its correct position, so recursively sort the left and right sides.

```
Pick → Partition → Place Pivot → Recurse

Final Thoughts

Quick Sort is a great example of how an algorithm can look complicated in code while being surprisingly simple at the conceptual level.

We choose a pivot, use two pointers to find misplaced elements, swap them, place the pivot in its correct position, and repeat the same process on the two smaller sections.

The implementation above deliberately uses the first element as the pivot. That makes the algorithm easy to understand and is a good starting point for learning partitioning and recursion.

But it also teaches an important lesson about algorithms:

The algorithm itself is only part of the story.

How you choose the pivot can determine whether Quick Sort behaves like O(n log n) or falls all the way to O(n²).

Once you understand that trade-off, you're no longer just memorizing Quick Sort.

You're understanding why the algorithm behaves the way it does.

Quick Takeaways

  • Quick Sort uses a pivot to partition an array.
  • This implementation chooses the first element as the pivot.
  • The left pointer searches for an element greater than the pivot.
  • The right pointer searches for an element smaller than the pivot.
  • Those misplaced elements are swapped.
  • The pivot is finally placed in its correct position.
  • Quick Sort recursively sorts the two sides.
  • Average and best-case time complexity is O(n log n).
  • Worst-case time complexity is O(n²).
  • Pivot selection can significantly affect performance.

Tags: Quick Sort, Quick Sort JavaScript, Sorting Algorithms, Algorithms, Data Structures, JavaScript Algorithms, DSA, Recursion, Partition Algorithm, Time Complexity, Space Complexity, Coding Interview, Software Development

No comments:

Post a Comment

Quick Sort Explained: How It Works, Partitioning, Complexity & JavaScript Implementation

Quick Sort Explained: Partitioning, Recursion & Time Complexity A practical explanation of Quick Sort using the ...

horizontal ads