Quick Sort#
https://en.wikipedia.org/wiki/Quicksort
Contents#
Introduction#
Tony Hoare 1961
most implementations of quicksort are not stable
complexity
\(\Theta(\log n)\) storage is required for the recursive callback but otherwise can be performed without an auxiliary array
\(\Theta(n^2)\) worst-case time complexity
the performance of quicksort is heavily dependent on the pivot selected and so has an expected time complexity of \(\Theta(n \log n)\)
Divide (quicksort uses a value-partitioning scheme, as opposed to mergesort which uses a size-based/index-based partitioning scheme)
select an arbitrary element to serve as the pivot
partition the array into sub arrays based on the value of the pivot: elements are swapped such that elements smaller than the pivot appear before it and elements larger than the pivot appear after it (elements equal to the pivot can go on either side)
partition(A, start, end)
is an in-place operation defined to select a pivot value from the part of the array between thestart
andend
indexes, perform the swapping, and return the index of the final location of the pivot element (the pivot is left in its final sorted position)
Conquer
recursively sort the two sub arrays: the two operations of pivot selection and partitioning are recursively applied until the array being processed is a single element
Combine
NOT REQUIRED, since once this operation has been recursively applied to each element in the array it will be in sorted order
Merge sort vs quick sort
mergesort requires more auxiliary space than quicksort (there are versions of mergesort that require no auxiliary array but these versions perform the worst in practice if not asymptotically)
mergesort has better worst-case time complexity than quicksort
quicksort performs as good as mergesort in expectation
quicksort usually performs better in practice because the constants associated with its time complexity are lower
Algorithm#
INPUT an array A[0:n - 1], the index of the first element of A, the index of the last element of A
OUTPUT nothing (in-place)
QUICKSORT (A, stt, end)
1 if stt == end
2 return A[stt] // the single-element array A[stt]
3
4 pvt = partition(A, stt, end)
5
6 QUICKSORT(A, stt, pvt - 1)
7 QUICKSORT(A, pvt + 1, end)
INPUT an array A, the index of the first element of A, the index of the last element of A
OUTPUT the index of the pivot element
partition (A, stt, end)
1 pvt = choose_pivot(A, stt, end) // select a pivot element and return its index
2
3 swap(A[pvt], A[end]) // swap the pivot element to the end of the array
4
5 j = stt
6 for i = stt to end - 1
7 if A[i] <= A[pvt]
8 swap(A[j], A[i])
9 j++
10
11 swap(A[j], A[end]) // index j is pointing to the first element larger than the pivot element, so swap them
12
13 return j // return the index of the pivot element
choose_pivot
There are different ways of going about selecting a pivot element. One can select the first element as the pivot element; the last element; a random element; the middle element; or some other approach.
partition
Start from the leftmost element and track the index of elements that are less than or equal to the pivot element.
Example
i|
A = 5 3 1 6 4 9 11 2, stt = 0, end = 7
j|
|
A = 5 3 1 6 4 9 11 2, stt = 0, end = 7
|
|
A = 5 3 1 6 4 9 11 2, stt = 0, end = 7
|
|
A = 1 3 5 6 4 9 11 2, stt = 0, end = 7
|
...
|
A = 1 2 5 6 4 9 11 3, stt = 0, end = 7
|
Complexity#
Since quicksort is a divide and conquer algorithm we can analyze its time complexity in those terms. The combination component of quicksort is the partition operation which runs in linear time, and there are two subproblems per recursion.
\(T(n) = 2T(\cdot) + \Theta(n)\)
In the best case, the pivot is selected so as to divide the array in half. This yields two sub problems of size \(n/2\).
\(\text{best}\quad T(n) = 2T(n/2) + \Theta(n)\)
In the worst case, either the smallest element or the largest element is selected. This yields one sub problem of size \(0\) and another sub problem of size \(n - 1\).
\(\text{worst}\quad T(n) = T(n - 1) + \Theta(n) = \Theta(n^2)\)
This analysis shows why how the pivot element is selected is significant. If the pivot element is selected at random then selecting the worst-case pivot element every time is unlikely. The expected-case complexity is
\(\text{expected}\quad T(n) = \Theta(n \log n)\)
Quicksort sorts in-place, which means that no auxiliary array is required and so the space cost is just a function of the depth of the call stack.
In the balanced case
\(S(n) = S(n/2) + \Theta(1) = \Theta(\log n)\)
In the worst case
\(S(n) = S(n - 1) + \Theta(1) = \Theta(n)\)
There are optimizations to ensure that even in the worst case \(\Theta(\log n)\) space is all that is required. In practice then the worst-case space complexity of quicksort is usually stated as logarithmic as well.
Implementations#
C#
/* sort_quick_last.c (QUICKSORT)
*
* this implementation of quick sort does not use any fancy pivot selection operation
* rather it just selects the last element of the array as the pivot element
*/
#include <stdio.h> // printf
/**************************************************
*
* auxiliary
*
**************************************************/
void print_arr (int A[], int n, char *str) {
printf("%s: ", str);
for (int i = 0; i < n; i++)
printf("%d ", A[i]);
printf("\n");
}
void swap (int *a, int *b) {
int t = *a;
*a = *b;
*b = t;
}
/**************************************************
*
* QUICKSORT
*
**************************************************/
int partition (int A[], int stt, int stp) {
int pvt = stp; // select the last elem as the pivot elem
swap(&A[pvt], &A[stp]); // swap the pivot elem to the end of the array (this is redundant in this implementation)
int j = stt;
for (int i = stt; i < stp; i++) { // iterate over all elems except the last one, the pivot elem
if (A[i] <= A[pvt]) { // if elem i is less than or equal to the pivot elem
swap(&A[i], &A[j]); // then swap elem i to the lefthand side of the array
j++; // and increment the index of the pivot elem
}
}
// A[j] is the first elem larger than the pivot elem, so swap them
// the pivot elem is now in its proper position in the array
swap(&A[j], &A[stp]);
// return the index of the pivot elem
return j;
}
void quicksort (int A[], int stt, int stp) {
/* ^^^^^^^ index of the first elem
* ^^^^^^^ index of the last elem
*/
// base case
if (stt >= stp) // if the array consists of a single elem
return;
// combine step
int pvt = partition(A, stt, stp);
// divide step
quicksort(A, stt, pvt - 1);
quicksort(A, pvt + 1, stp);
}
/**************************************************
*
* driver
*
**************************************************/
int main (void) {
int A[] = {2, 1, 5, 8, 5, 6, 7, 4, 8, 7, 5, 1, 7, 3, 0, 3, 1, 5, 0, 7};
int n = sizeof(A) / sizeof(A[0]);
print_arr(A, n, " array");
quicksort(A, 0, n - 1);
print_arr(A, n, "sorted array");
return 0;
}
Acknowledgments#
https://www.khanacademy.org/computing/computer-science/algorithms/quick-sort/a/analysis-of-quicksort