CmSc 250 Intro to Algorithms
Sorting Algorithms: QuickSort
Learning Goals
Exam-like questions
Here we don't have the merge step, at the end all the
elements are in the proper order.
STEP 1. Choosing the pivot Choosing the pivot is an essential step. This is a bad choice - the pivot may turn to be the smallest
or the largest element, Example: 8, 3, 25, 6, 10, 17, 1, 2, 18, 5 The first element is 8, the middle - 10, the last - 5.
STEP 2. Partitioning Partitioning is illustrated on the above example.
and to the right part
of the array.
Depending on the pivot the algorithm may run very fast, or in quadric time.:
then one of the partitions will be empty.
Choose the median of these three elements.
The three elements are sorted: [5, 8, 10] and the middle element is 8. This is the median.
1. The first action is to get the pivot out of the way - swap it with the next to the last element
5, 3, 25, 6, 18, 17, 1, 2, 8, 10
2. We want larger elements to go to the right and smaller elements to go to the left.
Two "fingers" are used to scan the elements from left to right and from right to left:
[5, 3, 25, 6, 18, 17, 1, 2, 8, 10] ^ ^ i j
Note: we know that the first element is smaller than the pivot, so the first element to be processed is the element to the right. The last two elements are the pivot and an element greater than the pivot, so they are not processed.
In the example the first swapping will be between 25 and 2, the second between 18 and 1.
3. Restore the pivot.
After restoring the pivot we obtain the following partitioning into three groups:
[5, 3, 2, 6, 1] [ 8 ] [18, 25, 17, 10]
STEP 3. Recursively quicksort the left and the right parts
Here is the code, that implements the partitioning.
left points to the first element
in the array currently processed, right
points to the last element.
if( left + 10 <= right)
{
int i = left, j = right - 1;
for ( ; ; )
{
while (a[++i] < pivot ) {} // move the left finger
while (pivot < a[--j] ) {} // move the right finger
if (i < j) swap (a[i],a[j]); // swap
else break; // break if fingers have crossed
}
swap (a[I], a[right-1]); // restore the pivot
quicksort ( a, left, i-1); // call quicksort for the left part
quicksort (a, i+1, right); // call quicksort for the left part
}
else insertionsort (a, left, right);
If the elements are less than 10, quicksort is not very
efficient.
Instead insertion sort is used at the last phase
of sorting.
Click here to see the above example worked out in details
Animation:
Compare the two versions:
A.
while (a[++i] < pivot) {}
while (pivot < a[--j]) {}
if (i < j) swap (a[i], a[j]);
else break;
B.
while (a[i] < pivot) {i++;}
while (pivot < a[j] ) {j--;}
if (i < j) swap (a[i], a[j]);
else break;
If we have an array of equal elements, the second code will
never increment i or decrement j,
and will
do infinite swaps. i and j will never cross.
Worst-case: O(N2)
Best-case O(NlogN) The best case is when the pivot is
the median of the array,
and then the left and the
right part will have same size.
There are logN partitions, and to obtain each partitions
we do N comparisons
(and not more than N/2 swaps).
Hence the complexity is O(NlogN)
Average-case - O(NlogN)
Analysis
T(N) = T(i) + T(N - i -1) + cN
The time to sort the file is equal to
6. 1. Worst case analysis
The pivot is the smallest element
T(N) = T(N-1) + cN, N > 1
Telescoping:
T(N-1) = T(N-2) + c(N-1)
T(N-2) = T(N-3) + c(N-2)
T(N-3) = T(N-4) + c(N-3)
T(2) = T(1) + c.2
Add all equations:
T(N) + T(N-1) + T(N-2) + … + T(2) =
= T(N-1) + T(N-2) + … + T(2) + T(1) + c(N) + c(N-1) + c(N-2) + … + c.2
T(N) = T(1) + c(2 + 3 + … + N)
T(N) = 1 + c(N(N+1)/2 -1)
6. 2. Best-case analysis:
The pivot is in the middle
T(N) = 2T(N/2) + cN
Divide by N:
T(N) / N = T(N/2) / (N/2) + c
Telescoping:
T(N/2) / (N/2) = T(N/4) / (N/4) + c
T(N/4) / (N/4) = T(N/8) / (N/8) + c
……
T(2) / 2 = T(1) / (1) + c
Add all equations:
T(N) / N + T(N/2) / (N/2) + T(N/4) / (N/4) + …. + T(2) / 2 =
= (N/2) / (N/2) + T(N/4) / (N/4) + … + T(1) / (1) + c.logN
After crossing the equal terms:
T(N)/N = T(1) + cLogN = 1 + cLogN
T(N) = N + NcLogN
Therefore T(N) = O(NlogN)6. 3. Average case analysis
Similar computations, resulting in T(N) = O(NlogN)
The average value of T(i) is 1/N times the sum of T(0) through T(N-1)
1/N S T(j), j = 0 thru N-1
T(N) = 2/N (S T(j)) + cN
Multiply by N
NT(N) = 2(S T(j)) + cN*N
To remove the summation, we rewrite the equation for N-1:
(N-1)T(N-1) = 2(S T(j)) + c(N-1)2, j = 0 thru N-2
and subtract:
NT(N) - (N-1)T(N-1) = 2T(N-1) + 2cN -c
Prepare for telescoping. Rearrange terms, drop the insignificant c:
NT(N) = (N+1)T(N-1) + 2cN
Divide by N(N+1):
T(N)/(N+1) = T(N-1)/N + 2c/(N+1)
Telescope:
T(N)/(N+1) = T(N-1)/N + 2c/(N+1)
T(N-1)/(N) = T(N-2)/(N-1)+ 2c/(N)
T(N-2)/(N-1) = T(N-3)/(N-2) + 2c/(N-1)
….
T(2)/3 = T(1)/2 + 2c /3
Add the equations and cross equal terms:
T(N)/(N+1) = T(1)/2 +2c S (1/j), j = 3 to N+1
T(N) = (N+1)(1/2 + 2c S(1/j))The sum S (1/j), j =3 to N-1, is about LogN
Thus T(N) = O(NlogN)
Advantages:
Disadvantages: The worst-case complexity is O(N2)
Applications:
Commercial applications use Quicksort - generally it runs fast,
no additional memory,
this compensates for the
rare occasions when it runs with O(N2)
Never use in applications which require
guaranteed response time:
Comparison with heapsort:
Comparison with mergesort:
So far, our best sorting algorithm has O(nlog n) performance: can we do any better?
In general, the answer is no.
Learning Goals
Exam-like questions
- Briefly describe the basic idea of quicksort.
- What is the complexity of quicksort?
- Analyze the worst-case complexity solving the recurrence relation.
- Analyze the best-case complexity solving the recurrence relation.
- Compare quicksort with mergesort and heapsort.
- What are the advantages and disadvantages of quicksort?
- Which applications are not suitable for quicksort and why?