Explanation of the quicksort algorithm

Sorting is a broad topic and various algorithms exist to order elements, just like a deck of cards can be sorted in many different ways, some of them being more efficient than others. In this article I will cover how quicksort works and will create the algorithm in JavaScript.

Assume that we take the hearts from a deck of cards and want to arrange them in ascending order starting with the 2 of hearts and ending with the ace of hearts. How would you do it?

The most natural approach is that you place the (shuffled) cards next to each other, scan them, find the 2 of hearts and place it at the beginning of the row. Then scan the rest of the cards, find the 3 of hearts and place it on the right of the 2 of hearts, and so on and on, until only one card (the ace) is left. This way the 13 cards will get sorted.

Let’s pause for a moment and think about the above sorting methodology. You scan the cards until you find the 2 of hearts. Then you scan them again for the 3 of hearts, then again for the 4 and so on.

Scanning is nothing else but iteration, which is an expensive operation, especially if it’s done nested or multiple times. If we need to sort an array of a million or even more numbers, the sorting method described above might not be the best solution.

Luckily, smart people developed various sorting algorithms that are less expensive and more efficient. One of them is quicksort.

The algorithm

The main principle behind quicksort is that the algorithm won’t iterate over the entire array as many times as we normally would in case of the deck of cards above. Instead, a number called pivot is picked from the array and other numbers will be grouped into three partitions based on their relationship to the pivot number.

Partitioning

If any given number is less than the pivot number, it goes into the left partition. The left partition will contain all numbers from the array that are less than the pivot number.

If a number is greater than the pivot number, it will be placed in the right partition, so this partition will be the group of numbers greater than the pivot.

It can happen that a number is equal to the pivot. In this case we place these numbers into the equal partition.

Repeat it

We have now three partitions (groups) of numbers and have made some progress as we grouped the numbers relative to the pivot number. What’s next?

Luckily, we have nothing to do with the equal group, they are at the right place now (i.e. in their final position), so we will focus on the left and right partitions in the following sections. Note that if all numbers are different, the equal partition will only contain one element.

Having only one element in the partition is a good thing because we can’t group one element further, so our goal should be to have only one number (or even none) in each partition.

Let’s take now the left partition. What should we do with it? Well, we will repeat the same procedure on this partition! We consider the left group as being the new input. Pick a pivot from this collection and divide it into three groups relative to the pivot. equal will contain the new pivot, numbers smaller than the new pivot goes to the new left and those that are greater go to the new right partition.

We will then repeat the procedure described above until we get only one (or zero) number in each group. When only one number is found in a partition, it cannot be split further.

We have finished with the original left partition, and we need to complete the same process on the original right group, and repeat the sorting mechanism until we get only one element in each partition.

It’s like a tree

The algorithm can be seen in the diagram below.

The quicksort tree

As you can see, the visual representation of the quicksort algorithm can be loosely compared to a tree graph where the leaves are partitions with only one element (bold numbers). It’s very similar to the algorithm you used at school when you had to write a number as the product of primes. (No more reference to maths class or school. At least in this post. Really.)

If we place the numbers written in bold next to each other from left to right, we will get the original numbers in ascending order. Cool!

Translating it to JavaScript

After this short introduction let’s see the actual code. As always, my solution is not the only one, and it can happen that you know a different interpretation of quicksort, which is absolutely fine.

Here is the array of numbers we are going to sort (same as in the graph above):

const inputArr = [5, 9, 4, 85, 17, 6, 24, 2, 8];

The array is creatively called inputArr and all numbers are different. Note that the algorithm also works for arrays of numbers where some of the numbers are equal.

Getting the building blocks

Let’s translate the algorithm to code.

First, we will need a pivot number, which can be picked at random or systematically. I will always use the first element in the array as my pivot. It’s easy to find, doesn’t require much code and makes the iteration a bit easier.

Yes, iteration. We need to iterate over the array of numbers to see which partition to put each number into and a for loop can be implemented here.

As for the partitions, we can use arrays called left, equal and right.

We will need to repeat the process on both left and right, so the algorithm involves some recursion.

We also have to ensure that the algorithm stops where only one element is left in any arrays.

Finally, we have to concatenate the many arrays to one final, sorted array.

The code

Our function will unbelievably called quickSort and let’s start it by defining the partitions:

const quickSort = (arr) => {
  const left = [];
  const right = [];
  const pivot = arr[0];
  const equal = [pivot];
};

quickSort accepts one parameter, the array we want to sort. We define the partitions and store the first element of the array, which becomes the pivot element in the pivot variable (5 in our example). We also immediately place this number into the equal array as it belongs there. This way we can ignore the pivot element when iterating over the original array because it’s already in place, so we can start the iteration with the second (that is of index 1) element. This is one reason why it’s worth choosing the first element as our pivot number.

Now that I mentioned the word iteration, let’s create the for loop:

for (let i = 1; i < arr.length; i++) {
  if (arr[i] < pivot) {
    left.push(arr[i]);
  } else if (arr[i] > pivot) {
    right.push(arr[i]);
  } else {
    equal.push(arr[i]);
  }
}

As discussed above, we start with the second element, so i starts at 1, otherwise there is nothing fancy here. If the current number is less than pivot, we will push it into the left array. However, if it’s greater than pivot, we will place it in right. Finally, in case we have numbers that are equal to pivot, we will move it into the equal partition.

If you log left, equal and right in this order, you will see that left only contains numbers less than pivot and right only has numbers greater than pivot. We can concatenate them to one array to see the result better:

return [...left, ...equal, ...right];

Let’s call the function and log the returned value to the console:

console.log(quickSort(inputArr)); // [ 4, 2, 5, 9, 85, 17, 6, 24, 8 ]

If you look at the graph above, the returned array represents the second level of the tree. We have made a small step towards our goal by grouping 4 and 2 on the left of 5 and placing any other numbers on the right as they are all greater than 5. We can also see that equal only contains one element (which is pivot), so we don’t have to worry about it for now.

But how can we get through the rest of the tree in the graph? We should repeat the iteration and the partitioning on both left and right, so need to call quickSort with left and then right being the arguments. Remember, both left and right are arrays.

So we need to modify the return statement as follows:

return [...quickSort(left), ...equal, ...quickSort(right)];

As a result, we will get many arrays of length one (or zero in those cases where you can’t see left or right in the graph above) and we need to concatenate them to one final sorted array. We use the ... spread operator, which is a convenient way of concatenating arrays.

One last step is missing. If you run this function as is, you will get an error (“Maximum call stack size exceeded”), because we didn’t specify when to finish the recursion. We need to explicitly declare that no more runs of quickSort is needed on any branch if the length of the current array is 1 or zero.

Let’s include this line at the top of the function:

if (arr.length < 2) return arr;

We are simply returning the current array in this case.

The full code looks like this:

const quickSort = (arr) => {
  if (arr.length < 2) return arr;

  const left = [];
  const right = [];
  const pivot = arr[0];
  const equal = [pivot];

  for (let i = 1; i < arr.length; i++) {
    if (arr[i] < pivot) {
      left.push(arr[i]);
    } else if (arr[i] > pivot) {
      right.push(arr[i]);
    } else {
      equal.push(arr[i]);
    }
  }

  return [...quickSort(left), ...equal, ...quickSort(right)];
};

Let’s call the function on inputArr:

console.log(quickSort(inputArr)); // [ 2, 4, 5, 6, 8, 9, 17, 24, 85 ]

The array is sorted and we are done!

Why is quicksort efficient?

After doing that much work, we can raise the question: Why is quicksort more efficient than the way we sorted the cards above?

The detailed explanation is beyond the scope of this post and will be the topic of a later article.

The short answer is that quicksort does less iteration than the other sorting methodology. Iteration is a long and therefore expensive operation, especially if we have nested iterations. In the average case the execution time required for quicksort is more favourable than the time required for the card sorting “algorithm” described at the beginning of the post.

By dividing the array in partitions and apply quickSort on them again, we can greatly reduce the number of operations and we can achieve an efficiency similar to the case of binary search.

To prove the power of division, start dividing i.e. 32 by 2 until we get 1:

32 --> 16 --> 8 --> 4 --> 2 --> 1

We got to 1 in just five steps, so if we want to run the an iteration at each phase, we have to do it only five times.

Now compare this with the case when we subtract 1 from 32 each time until we get 1:

32 --> 31 --> 30 --> .. --> 1

We need 31 steps, so we run the iteration 31 times. The difference is clear.

Note that this little example is meant to show the power of division and how it reduces the number of operations needed in quicksort. It’s not subtraction what happens in the case of the card sorting example above.

Again, this is just to show that quicksort (which is a divide and conquer algorithm) can be an efficient technique in average cases. It’s true though that the worst case scenario of quicksort is less favourable than its average case and other sorting algorithms can be more efficient.

Conclusion

In this post I created a quicksort algorithm, which is an great way of sorting numbers in many cases. I hope you found the article useful. If so, see you next time!