Quick Sort

Quick sort was invented by Tony Hoare. It is a divide and conquer strategy. Quicksort is sometimes referred to as partition exchange sort.

Quick sort picks one element as a pivot and shifts the pivot to the correct location in each iteration. By repositioning the pivot, the list is divided into two sublists of equal or unequal size.

Quick sort concept

Each sublist is solved in a recursive manner. In contrast to merge sort, the list is partitioned dynamically in this case. Merge sort partitions an array based on its position, whereas quicksort partitions it based on its actual value.

Merge sort splits the list from the center, but quicksort does not guarantee such balanced division. The value of the pivot, not the position, determines division. As a pivot, the first, last, or any random element can be chosen. However, the pivot position should remain constant throughout all the recursive calls.

Each sublist represents a new problem with a size smaller than n. A new pivot is chosen, and the procedure is repeated until it reaches the base case (i.e. list of size 1).

Algorithm

Algorithm for quicksort is shown below

Algorithm QUICKSORT (A, low, high)
// Description : Sort array A using Quicksort
// Input : Unsorted array A of size n, low = 0, high = n - 1
// Output : Sorted array A

if low ≤ high then
  q ← PARTITION (a, low, high)	
  QUICKSORT (A, low, q – 1)	
  QUICKSORT (A, q + 1, high)	
end

Subroutine PARTITION determines the correct position of pivot in a sorted array. The function PARTITION traverses the array from left to right. It detects elements bigger than the pivot using the low pointer, and elements less or equal to the pivot using the high pointer. The pseudo code of PARTITION subroutine is given below:

PARTITION (A, low, high)
x ← A[high]	// x is pivot element i.e. last element of list
i ← low – 1	 
for j ← low to high – 1 do
  if A[j] ≤ x then
    i ← i + 1
    swap (A[i], A[j])
  end
end
swap (A[i + 1], A[high])
return (i + 1)	// Correct index of pivot after sorting

On each recursive call, quick sort does following:

  • Procedure Scan array from left to right until an element greater than pivot is found.
  • Scan array from right to left until an element equal or smaller than pivot is found.
  • After finding such elements,
    • If low < high, then exchange A[low] and A[high].
    • If low ≥ high, then exchange A[pivot] and A[high], which will split the list into two sub-lists. Solve them recursively.

Example of Quick Sort

Problem: Trace quick-sort for the data set A = {44, 22, 33, 77, 11, 55, 66}

Solution:

According to its working principle, quick sort algorithm finds the element larger than pivot while moving from left to right, and finds element smaller than or equal to pivot while moving from right to left. When such elements are found,

  • If low < high, exchange A[low] and A [high].
  • If low ³ high, exchange A[pivot] and A[high]. This will split the list into two sub-lists and both sub-lists are sorted recursively.

Simulation of the quicksort for given data is shown below:

Step 1: Fix pivot point, low and high pointers.

Keep moving low and high pointer until A[low] > A[pivot] and A[high] ≤ A[pivot]. And if

  • low < high then swap A[low] and A[high]
  • low ≥ high then swap A[pivot] and A[high]
Quick sort - initial data

Step 2: A[low] = 22 and A[pivot]  = 44

A[low] < A[pivot] ⇒ low = low + 1 = 1 + 1 = 2

Quick sort - step 2

Step 3: A[low] = 33 and A[pivot] = 44

A[low] < A[pivot] ⇒ low = low + 1 = 2 + 1 = 3

Step 4: A[low] = 77 and A[pivot] = 44

A[low] > A[pivot] ⇒ fix low check for high pointer

A[high] = 66 and A[pivot] = 44

A[high > A[pivot] ⇒ high = high – 1 = 6 – 1 = 5

Step 5: A[high] = 55 and A[pivot] = 44

A[high] > A[pivot] ⇒ high = high – 1 = 5 – 1 = 4

Step 6: A[high] = 11 and A[pivot] = 44

A[high] ≤ A[pivot] ⇒ fix high and perform swap

Here, low < high, so swap A[low] with A[high] and again start moving low pointer

Step 7: A[low] = 11 and A[pivot] = 44

A[low] < A[pivot] ⇒ low = low + 1 = 3 + 1 = 4

Step 8: A[low] = 77 and A[pivot] = 44

A[low] > A[pivot] ⇒ fix low and check high

A[high] = 77 and A[pivot] = 44

A[high] > A[pivot] ⇒ high = high – 1 = 4 – 1 = 3

Step 9: A[high] = 11 and A[pivot] = 44

A[high] ≥ A[pivot] ⇒ fix high and perform swap

Here, low ≥ high so swap A[pivot] with A[high] divide list in two sublists and reset pointers. Recursively sort both the sublists.

Step 10: Let us first sort the left sub-list

A[low] = 22 and A[pivot] = 11

A[low] > A[pivot], so fix low and check high pointer

A[high] = 33 and A[pivot] = 11

A[high] > A[pivot] ⇒ high = high – 1 = 2 – 1 = 1

Step 11: A[high] = 22 and A[pivot] = 11

A[high] > A[pivot] ⇒ high = high – 1 = 1 – 1 = 0

Step 12: A[high] = 11 and A[pivot]  = 11

A[high] ≤ A[pivot] ⇒ so fix high and perform swap

Here, low ≥ high so swap A[pivot] with A[high], divide list in two sublists and reset pointers. Recursively sort both the sublists

Step 13 : A[low] = 33 and A[pivot] = 22

A[low] > A[pivot] Þ so fix low and check high pointer

A[high] = 33 and A[pivot] = 22

A[right] > A[pivot] ⇒ high = high – 1 = 2 – 1 = 1

Step 14: A[high] = 22 and A[pivot] = 22

A[high] ≤ A[pivot], so fix high and perform a swap

Here, low ≥ high so swap A[pivot] with A[high], divide list in two sub-lists. Left sub-list has size zero and right sub-list has only one element {33}, so no further process is required.

Step 15: Let us now sort the right sub-list of original array derived in step 9

A[low] = 55 and A[pivot] = 77

A[low] < A[pivot] ⇒ low = low + 1 = 5 + 1 = 6

Step 16: A[low] = 66 and A[pivot] = 77

A[low] < A[pivot] ⇒ low = low + 1 = 6 + 1 = 7

low > high so stop, and check for high index

A[high] = 66 and A[pivot] = 77

A[high] < A[pivot], so fix high and perform swap

Here, low ≥ high so swap A[pivot] with A[high], divide list in two sub lists and reset pointers. Right sub list has size zero. So only left sub list needs to be sorted

Step 17: A[low] = 55 and A[pivot] = 66

A[low] < A[pivot] ⇒ low = low + 1 = 5 + 1 = 6

low > high so stop, and check for high index

A[high] = 55 and A[pivot] = 66

A[high] < A[pivot], so fix high and perform swap Here, low ≥ high so swap A[pivot] with A[high], divide list in two sub lists and reset pointers. Right sub list has size zero and right sub list is of size 1, so sorting is not required at all.

The final sorted array is :

Quick Sort output

Complexity Analysis of Quick Sort

Best Case:

Best case for quicksort occurs when the list is divided from the middle. Partitions are balanced and it is identical to merge sort. Hence the complexity of the best case will be O( n log2 n). Let us prove it by solving the recurrence equation.

T(1) = 0

T(n) = 2T(n/2) + O(n) …(1)

2T(n/2) is the time required to solve two problems of size (n/2)

O(n) is the time required to fix the position of the pivot. Fixing of pivot requires a scan of the entire array.

The recurrence of the quicksort for the best case is identical to the recurrence of merge sort.

Depending upon how partitioning is done, running time of quick sort varies form O(nlog2n) to O(n2). Best and worst case partitioning is shown in following diagram. Best case occurs when partitioning happens from the center of list on each call. In this case, quick sort resembles to merge sort. Worst case occurs when list is already sorted. In this case, on subsist will be of size 0, and other will be always of size (n – 1)

Best case splitting for quick sort
Best case splitting for quick sort
Worst case splitting for quick sort
Worst case splitting for quick sort

We will derive the time complexity of merge sort using two methods:

  • Substitution method
  • Master method

Substitution Method:

Solving original recurrence for n/2,

T(n/2) = 2T(n/4) + n/2

Substituting this in equation (1),

T(n) = 2[ 2T(n/4) + n/2 ] + n

= 22 T(n/22) + 2n

.

.

.

After k substitutions,

T(n) = 2k T(n/2k) + k.n … (2)

Division of array creates binary tree, which has height log2n, so let us consider that k grows up to log2n,

k = log2n ⇒ n = 2k

Substitute these values in equation (2)

T(n) = nT(n/n) + log2n . n

T(n) = O(n.log2n) (because, T(1) = 0, from base case)

Master Method:

Recurrence of the merge sort, T(n) = 2T(n/2) + n is of the form T(n) = a.T(n/b) + f(n),

Comparing both the recurrence,

a = 2, b = 2 and f(n) = n with d = 1, where d is the degree of polynomial function f(n).

bd = 21 = 2 Here, a = bd, so from the Case 1 of Variant – I of master method,

T(n) = O(ndlog2n)

= O(nlog2n)

Worst Case:

The worst case scenario for quicksort is when the list has already been sorted. The pivot will be at either end in such scenario. Quick sort’s worst-case behavior generates subproblems with (n – 1) elements and zero elements. Following figure depicts the worst-case division of the list. In this scenario, the tree’s height would be n.

Worst case for quick sort
Worst case for quick sort

Because each call sets the right position of the pivot and no further work is required, the combination cost for quicksort is constant. The method scans the array and identifies the right location of the pivot to split the list, therefore the division cost of QUICK SORT is linear, i.e. O(n). In the worst-case scenario, one of the sublists has size 0 and the other has size (n – 1), thus the algorithm must solve a problem of size (n – 1) in the subsequent recursive call.

Thus,      

T(0) = 1 (Base case)

T(n)    =   T(conquer) + T(divide) + T(combine)

T (n)   =   T (n – 1) + O(n) + Q(1)

=   T (n – 1) + n …(1)

Using iterative approach,

Substitute n by (n – 1) in Equation (1),

T (n – 1) =   T (n – 2) + (n – 1) …(2)

⇒ T (n)   =   T (n – 2) + (n – 1) + n …(3)

Substitute n by (n – 1) in Equation (2),

T (n – 2) =   T (n – 3) + (n – 2)

From Equation (3),

T (n)   =   T (n – 3) + (n – 2) + (n – 1) + n

∶              ∶

After k iterations,

T(n) = T(n – k)  + (n – k + 1) + (n – k + 2) + … + (n – 1) + n

Let k = n,

T (n) = T (0) + 1 + 2 + 3 + . . . + (n – 2) + (n – 1) + n

= 1 + 2 + 3 + …+ n = S n 

= n(n + 1) / 2  

= (n2/2) + (n/2)

= O(n2)

Note :  In the worst case, quick sort behaves similar to selection sort.

Average case:

For quick sort, average-case running time is much closer to the best case.

That is, T (n) = O(n log2n)

The time complexity of all three cases is stated in the following table:

Best caseAverage caseWorst case
O(nlog2n)O(nlog2n)O(n2)

Simulation of Quick Sort

Quick Sort simulation

Image Source: https://commons.wikimedia.org/wiki/File:Quicksort-example.gif [Wiki]

Properties of Quick Sort

  • In place : It uses O(log n) amount of extra memory.
  • Quicksort is massively recursive.
  • In the best case, quicksort is extremely fast.
  • It is very complex.

Randomized Quick Sort

The worst-case scenario for quicksort is determined by how we choose a pivot element. The worst case scenario for quicksort is when the array has already been sorted and the first or last member is used as a pivot. To improve performance, we can add randomization to an algorithm.

In the worst-case scenario, choosing the first or last element as the pivot splits lists into (n – 1) and 0 size sub-lists. Partitions can be approximately balanced by using a random element as a pivot, and performance near to merging sort may be attained. The position of the pivot point is an important component in the performance of a quick sort.

Few choices for pivot selection:

  • Select middle element of the list as a pivot.
  • Select random element from the list as a pivot.
  • Use median as a pivot.

Discussion and Comparison

Despite the fact that quicksort’s worst-case running time is O(n2), it is regarded superior than merge sort for a variety of reasons. The sorting algorithm’s complexity is assessed by the number of comparison operations done by the algorithm. Quick sort performs fewer comparisons than merge sort. Quicksort is in-place algorithm that takes up O(log2n) addition space. Where as, merge sort requires O(n) additional space. Quicksort also has high cache locality, reducing the amount of memory accesses. Furthermore, by providing a randomized variant, the worst-case running time of quicksort can be reduced to O(nlog2n).

Merge Sort vs. Quick Sort

Sr. NoMerge sortQuicksort
1.Partitioning is always balancedPartitioning may not be balanced
2.Worst-case running time is O(nlog2n)Worst-case running time is O(n2)
3.It is not in place sorting methodIt is in place sorting method
4.Recurrence for worst-case: T(n) = 2T(n/2) + nRecurrence for worst-case: T(n) = T(n – 1) + n
5.Merge sort is not adaptiveQuicksort is adaptive
6.Does more comparison in the best case compared to quicksortDoes less comparison in the best case compared to merge sort
  • Quicksort performs worst when the data set is already sorted. Running time of quicksort in the worst case is O(n2), whereas merge sort runs in O(nlog2n) time in all three cases.
  • For data set S = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0} worst-case running time of quicksort and merge sort would be different.
  • For data set S = {0, 7, 2, 8, 1, 3, 9, 5, 4, 6}, both runs in O(nlog2n) time, but compare to merge sort, quick sort does less comparisons.

Following are the few popular sorting algorithms used in computer science:

Leave a Reply

Your email address will not be published. Required fields are marked *