Learn what is Quicksort and how to implement Quick sort (with all different partition variations) algorithm in javascript.
List of sorting algorithms
- Bubble sort algorithm in javascript
- Insertion sort algorithm in javascript
- Selection sort in javascript
- Merge Sort in javascript
- Radix sort in javascript
- Bucket sort in javascript
- Shell sort in javascript
- Heap sort in javascript
- Counting sort in javascript
Quicksort also know as partition-exchange sort, is one of the most efficient sorting algorithms.
It was developed by British computer scientist Tony Hoare in 1959 and published in 1961.
It does comparison based sorting but is not so stable, which means the relative order of the elements is not preserved.
Like merge sort, quick sort also uses the divide and conquer paradigm to sort the elements. It picks an element as a pivot and partitions the elements around this pivot. There are many different versions of quicksort that pick pivot in different ways.
- First element as a pivot.
- Last element as a pivot.
- Middle element as a pivot.
- Random element as a pivot.
It also has the advantage of sorting in place and it works well even in virtual memory environments.
How quick sort works?
Conceptually this is how it works.
As it is a divide and conquer algorithm, we will perform the each action in this way.
- Divide:- Pick a pivot
q
and partition the arrayarr[p...r]
into two possible sub arraysarr[p...q-1], arr[q+1...r]
such that all the elements of the left sub arrayarr[p...q-1]
is less than the pivot and all the elements of the right sub arrayarr[q+1...r]
is greater than the pivot. After this we are sure that the pivotq
is sorted, but the left and right sub arrays may or may not be sorted. - Conquer:- Sort the two sub arrays
arr[p...q-1], arr[q+1...r]
by calling itself recursively. - Combine:- As all the sub array is sorted, there is no need to combine them.
Psuedo Code
quickSort(arr, p, r){ if p < r q = partition(arr, p, r) quickSort(arr, p, q - 1) quickSort(arr, q + 1, r) } partition(arr, p, r){ //Last element as pivot q = arr[r] i = p - 1 for j = p to r - 1 if arr[j] <= q i = i + 1 swap arr[i] with arr[j] swap arr[i + 1] with arr[r] return i + 1 }
The most challenging part of quicksort is partitioning the array properly into two parts, if done properly it is two to three times faster than merge sort & heap sort.
The following image illustrates the working cycle of a partition in a single call.
Quick sort algorithm in Javascript.
Based on the above pseudo-code we will implement all the different variations of the quicksort in javascript.
The first two partition method in which we use first and last element as a pivot is based on Lomuto partition scheme.
In each implementation we will use a helper function to swap the two elements in the array.
const swap = (arr, left, right) => { const temp = arr[left] arr[left] = arr[right] arr[right] = temp; }
First element as a pivot
const partitionLow = (arr, low, high) => { //Pick the first element as pivot let pivot = arr[low]; let i = low; //Partition the array into two parts using the pivot for(let j = low; j <= high; j++){ if(arr[j] <= pivot){ swap(arr, i, j); i++; } } swap(arr, i - 1, low); //Return the pivot index return i - 1; } const quicksort = (arr, low, high) => { // base condition if (low >= high) { return; } // rearrange the elements across pivot const pivot = partitionLow(arr, low, high); // recur on sub-array containing elements less than pivot quicksort(arr, low, pivot - 1); // recur on sub-array containing elements more than pivot quicksort(arr, pivot + 1, high); }
Input: let arr = [10, 7, 8, 9, 1, 5]; quickSort(arr, 0, arr.length - 1); console.log(arr); Output: [1, 5, 7, 8, 9, 10]
Last element as pivot
const partitionHigh = (arr, low, high) => { //Pick the first element as pivot let pivot = arr[high]; let i = low; //Partition the array into two parts using the pivot for(let j = low; j < high; j++){ if(arr[j] <= pivot){ swap(arr, i, j); i++; } } swap(arr, i, high); //Return the pivot index return i; } const quicksort = (arr, low, high) => { // base condition if (low >= high) { return; } // rearrange the elements across pivot const pivot = partitionHigh(arr, low, high); // recur on sub-array containing elements less than pivot quicksort(arr, low, pivot - 1); // recur on sub-array containing elements more than pivot quicksort(arr, pivot + 1, high); }
Input: let arr = [10, 7, 8, 9, 1, 5]; quickSort(arr, 0, arr.length - 1); console.log(arr); Output: [1, 5, 7, 8, 9, 10]
Middle element as pivot
Using middle element as pivot is little tricky as it uses Hoare partition scheme.
This scheme works as follows.
- Use two indices that start at the ends of the array being partitioned.
- Then move towards each other until they discover an inversion, i.e a pair of elements, one greater than or equal to the pivot, one less than or equal, that are in the wrong order relative to each other.
- The inverted elements are then swapped. When the indices meet, the algorithm stops and returns the final index.
const partitionMiddle = (arr, low, high) => { //Get the mid index const mid = Math.floor((low + high) / 2); //Swap the mid element with first element swap(arr, mid, low); //Now use the first element as pivot let pivot = arr[low]; let lo = low + 1; let hi = high; //Partition the array based on the pivot while(lo <= hi){ //Move towards each other while(arr[hi] > pivot){ hi = hi - 1 } while(lo <= hi && arr[lo] <= pivot){ lo = lo + 1; } //When inversion found swap the elements if(lo <= hi){ swap(arr, lo, hi); lo = lo + 1; hi = hi - 1; } } swap(arr, low, hi); //Return the pivot index return hi; } const quicksort = (arr, low, high) => { // base condition if (low >= high) { return; } // rearrange the elements across pivot const pivot = partitionMiddle(arr, low, high); // recur on sub-array containing elements less than pivot quicksort(arr, low, pivot - 1); // recur on sub-array containing elements more than pivot quicksort(arr, pivot + 1, high); }
Input: let arr = [10, 7, 8, 9, 1, 5]; quickSort(arr, 0, arr.length - 1); console.log(arr); Output: [1, 5, 7, 8, 9, 10]
Random element as a pivot.
It is highly anticipated that the randomized version of quick sort as the sorting algorithm of choice for large enough inputs.
The idea is to choose a random index from the given index of the array and then perform the quick sort using either one of the above partitions. Generally the first or the last element as a pivot is used.
const partitionRandom = (arr, low, high) => { //Get random number between the given indexes. const random = Math.floor(Math.random() * (high - low + 1) + low); //Swap with the first element swap(arr, random, low); //Use first element as a pivot return partitionLow(arr, low, high); } const partitionLow = (arr, low, high) => { //Pick the first element as pivot let pivot = arr[low]; let i = low; //Partition the array into two parts using the pivot for(let j = low; j <= high; j++){ if(arr[j] <= pivot){ swap(arr, i, j); i++; } } swap(arr, i - 1, low); //Return the pivot index return i - 1; } const quicksort = (arr, low, high) => { // base condition if (low >= high) { return; } // rearrange the elements across pivot const pivot = partitionRandom(arr, low, high); // recur on sub-array containing elements less than pivot quicksort(arr, low, pivot - 1); // recur on sub-array containing elements more than pivot quicksort(arr, pivot + 1, high); }
Input: let arr = [10, 7, 8, 9, 1, 5]; quickSort(arr, 0, arr.length - 1); console.log(arr); Output: [1, 5, 7, 8, 9, 10]
Analysis of quick sort algorithm implemented in javascript.
If we break down the recurrence relation of quick sort and try to solve it with master method, then in mathematical term it can be written as.
T(n) = T(k) + T(n-k-1) + O(n)
T(k)
is the first recursive call, T(n-k-1)
is the second recursive call and O(n)
is the time taken for partitioning.
The time taken by quick sort depends upon the input array and partition strategy. The following are three cases.
Worst case
The worst case occurs when the partition process always picks the first or last element as pivot. If we consider the above partition strategy where the last element is always picked as a pivot, the worst case would occur when the array is already sorted in increasing or decreasing order.
T(n) = T(0) + T(n-1) + O(n) which is equivalent to T(n) = T(n-1) + O(n) Now after solving this we get O(n^2)
Best case
The best case occurs when the partition process always picks the middle element as pivot.
T(n) = 2T(n/2) + O(n) after solving this with master theorem we get O(nlogn)
Average case
For average case analysis, we need to consider all possible permutation of the array and calculate the time taken by every permutation which doesn’t look easy.
We can get an idea of average case by considering the case when partition puts O(n/9) elements in one sub array and O(9n/10) elements in other.
T(n) = T(n/9) + T(9n/10) + O(n) after solving this we get O(nlogn)
Best | Average | Worst |
---|---|---|
Θ(NlogN) | Θ(NlogN) | Θ(N^2) |
Although the worst case of quicksort is O(n ^ 2) which may seem worse than merge sort and heap sort. Quicksort is faster in practice because its inner loop can be efficiently implemented on most architectures, and in most real-world data.
Optimizing the quick sort using tail recursion.
To make sure at most O(logn) space is used, recur first into the smaller side of the partition, the use a tail call to recur into the other. With this we successfully sort the array in a way that it minimizes the recursive depth.
let partitionHigh = (arr, low, high) => { //Pick the first element as pivot let pivot = arr[high]; let i = low; //Partition the array into two parts using the pivot for(let j = low; j < high; j++){ if(arr[j] <= pivot){ swap(arr, i, j); i++; } } swap(arr, i, high); //Return the pivot index return i; } const tailRecursiveQuickSort = (arr, low, high) => { while(low < high){ const PI = partitionHigh(arr, low, high); // recur on smaller sub-array if (PI - low < high - PI) { tailRecursiveQuickSort(arr, low, PI - 1); low = PI + 1; } else { tailRecursiveQuickSort(arr, PI + 1, high); high = PI - 1; } } }
Input: let arr = [10, 7, 8, 9, 1, 5]; tailRecursiveQuickSort(arr, 0, arr.length - 1); console.log(arr); Output: [1, 5, 7, 8, 9, 10]