Quick sort is an efficient sorting algorithm that works by recursively dividing an array into smaller subarrays until they are fully sorted. Developed by Tony Hoare in 1959, quick sort adopts a divide-and-conquer approach to rearrange elements in an array relative to a pivot value.
The key steps involved in quick sort are:
- Select a pivot element from the array. This is usually done randomly.
- Partitioning: Rearrange the array into two subarrays such that all elements less than the pivot are moved to the left subarray, and all elements greater than the pivot are moved to the right subarray. The pivot element ends up in its final position.
- Recursively apply the above steps to the left and right subarrays until the entire array is sorted.
Quick sort has an average case time complexity of O(nlogn). However, its worst-case complexity is O(n^2) when the array is already sorted or contains many duplicate elements.
In this comprehensive Python tutorial, we will cover the following topics related to quick sort:
Table of Contents
Open Table of Contents
- Pseudocode for Quick Sort
- Python Implementation of Quick Sort
- Complexity Analysis of Quick Sort
- Lomuto vs Hoare Partition Schemes
- Comparison Between Quick Sort and Other Sorting Algorithms
- Optimizations to Improve Quick Sort Performance
- Common Mistakes to Avoid
- Applications of Quick Sort
- When to Use Quick Sort
- Real-World Examples of Quick Sort
- Conclusion
Pseudocode for Quick Sort
The pseudocode for the quick sort algorithm is as follows:
quickSort(array, leftIndex, rightIndex):
if leftIndex < rightIndex:
pivotIndex <- partition(array, leftIndex, rightIndex)
quickSort(array, leftIndex, pivotIndex - 1)
quickSort(array, pivotIndex + 1, rightIndex)
partition(array, leftIndex, rightIndex):
set pivot to array[rightIndex]
storeIndex <- leftIndex - 1
for i from leftIndex to rightIndex - 1:
if array[i] < pivot:
swap array[i] and array[storeIndex]
storeIndex++
swap array[storeIndex+1] and array[rightIndex]
return storeIndex + 1
This pseudocode depicts the high-level working of quick sort. It begins by checking the base case. If the left and right indexes have not crossed, it picks a pivot element and partitions the array around it such that elements less than the pivot are to the left and greater than pivot are to the right of it.
The partition()
function handles this partitioning logic. It iterates through the array, compares each element with the pivot, and swaps elements such that lesser elements are moved to the left side of the pivot.
Once partitioned, quick sort is recursively called on the left and right subarrays. This recursion continues until the array is fully sorted.
Python Implementation of Quick Sort
Here is how quick sort can be implemented in Python:
def partition(arr, low, high):
pivot = arr[high]
i = low - 1
for j in range(low, high):
if arr[j] < pivot:
i += 1
arr[i], arr[j] = arr[j], arr[i]
arr[i+1], arr[high] = arr[high], arr[i+1]
return i + 1
def quickSort(arr, low, high):
if low < high:
pi = partition(arr, low, high)
quickSort(arr, low, pi-1)
quickSort(arr, pi+1, high)
The key steps are:
- Choose the rightmost element as pivot
- Partition the array around the pivot
- Recursively sort the subarrays
To sort an array, we simply call quickSort(arr, 0, len(arr)-1)
.
Here is quick sort in action on an example array:
arr = [10, 7, 8, 9, 1, 5]
quickSort(arr, 0, len(arr)-1)
print(arr)
# [1, 5, 7, 8, 9, 10]
The key to implementing quick sort efficiently is the partition scheme. Many different partitioning methods are possible like Hoare partition, Lomuto partition, etc. The partition scheme shown above adopts the Lomuto partition approach.
Complexity Analysis of Quick Sort
Time Complexity
The time complexity of quick sort depends heavily on the pivot selection during partitioning.
-
Best Case: O(nlogn) - When the pivot divides the array into two equal halves, the subproblems will shrink maximally on each recursive call. This leads to the best case log-linear runtime.
-
Average Case: O(nlogn) - When the pivot divides the array into reasonably equal partitions, we still get the optimal O(nlogn) time complexity.
-
Worst Case: O(n^2) - When the pivot chosen leads to highly unbalanced partitions of sizes O(n) and O(0), the recursion depth increases linearly with array size. This leads to O(n^2) worst case.
So the pivot selection directly impacts the balance of partitions and recursion depth, driving quick sort’s time complexity.
Space Complexity
The base version of quick sort uses O(logn) additional space on the stack for recursive calls. But optimized in-place quick sort has a space complexity of O(1).
Lomuto vs Hoare Partition Schemes
The partition scheme I demonstrated above uses the Lomuto partition approach.
In Lomuto partitioning, the pivot is initially placed at the end of array. Elements lesser than pivot are placed to the left of it.
In Hoare partitioning, the pivot is initially placed at the start. Elements greater than pivot are moved to the right side.
Lomuto is generally preferred as it is simpler to implement and exhibits better performance by reducing swap operations. Hoare can suffer from worst-case O(n^2) runtime in some cases.
Comparison Between Quick Sort and Other Sorting Algorithms
Here is how quick sort compares to other popular sorting algorithms:
-
Merge Sort: Has O(nlogn) time complexity in all cases. Requires O(n) auxiliary space. More stable than quick sort.
-
Heap Sort: Has O(nlogn) time complexity in all cases. In-place, so O(1) auxiliary space. Slower than quick sort in practice.
-
Insertion Sort: Works well for small array sizes. Has O(n^2) worst and average time complexity. Stable sort.
-
Bubble Sort: Simple but extremely inefficient. Has O(n^2) time complexity. Stable sort.
-
Selection Sort: Unstable sort with O(n^2) time complexity. Faster than bubble sort.
So quick sort provides optimal O(nlogn) time complexity on average while also being in-place. However, it is less stable than merge sort.
Optimizations to Improve Quick Sort Performance
There are a few ways to optimize quick sort and improve its performance:
-
Choose median as pivot - Selecting median as pivot minimizes the chances of uneven partitions. A median can be estimated by taking the middle element or by picking median of medians.
-
Randomized quick sort - Choose pivot randomly instead of always picking the first or last element. This avoids the already sorted worst-case.
-
Tail recursion - Eliminate recursion overhead by using loops and tail recursion where possible.
-
Partitioning scheme - Choose an efficient partition algorithm like Hoare partition instead of naive partition.
-
In-place sorting - Avoid allocating extra memory by sorting in-place.
-
Insertion sort for small arrays - Use insertion sort instead of quicksort for small subarrays to improve performance.
Carefully selecting the pivot and partitioning scheme provides the biggest gains in optimizing quick sort.
Common Mistakes to Avoid
Here are some common mistakes to watch out for when implementing quick sort in Python:
-
Forgetting to handle base case in recursion, causing infinite loops.
-
Pivot selection can greatly impact efficiency. Always pick a good pivot using median or randomization.
-
Off-by-one errors during partitioning when comparing pivot with other elements.
-
Swapping wrong elements during partitioning, mixing up lesser and greater elements.
-
Incorrect low and high indexes passed to recursive calls, causing stack overflow.
-
Not returning pivot index from partition procedure leading to bugs.
-
Inefficient partition scheme leading to unbalanced splits. Use optimal schemes like Hoare partition.
-
Not optimizing for small arrays by using insertion sort.
Overall, pay close attention to pivot selection, partitioning logic, recursion handling and base cases to avoid these mistakes.
Applications of Quick Sort
Quick sort is widely used as an efficient sorting algorithm due to its optimal log-linear time complexity. Here are some common applications:
-
Sorting small to medium sized arrays: Quick sort works well for arrays containing less than 1000 elements.
-
Sorting large arrays of integers or floats: Used in math/statistics software for reordering large datasets.
-
Data and network routing algorithms: Quick sort can sort routing tables efficiently.
-
Web search result rankings: Search engines may use quick sort to rank web pages by relevance.
-
File system optimization: File systems often use quick sort to optimize directory lookups by sorting filenames.
-
Machine learning: Quick sort provides faster model training by rapidly sorting data for optimal splits.
When to Use Quick Sort
Quick sort is a good sorting algorithm to use when:
-
Speed is crucial - Quick sort is one of the fastest sorting algorithms.
-
Data set is relatively small - It performs best with fewer than 1000 elements.
-
Cache performance is important - It accesses data sequentially with good locality of reference.
-
Stability is not required - Quick sort is unstable, so order of equal elements may change.
-
Space complexity should be low - It can be implemented as an in-place sort.
However, quick sort is not ideal when:
-
Data set is already mostly sorted - Leads to O(n^2) worst-case.
-
Stability is required - Other stable sorts like merge sort may be better.
-
Data contains many duplicates - Again risks worst case quadratric runtime.
So in summary, quick sort provides lightning fast performance for small to medium datasets while also minimizing space overhead. Just be mindful of pitfalls around duplicate elements.
Real-World Examples of Quick Sort
Here are some real-world examples of quick sort usage:
-
Google uses optimized quick sort implementations to sort results by relevance. This allows fast delivery of search results.
-
Quick sort is applied in course scheduling software like UniTime to efficiently sort courses and class sections.
-
Statistics packages like SPSS leverage quick sort to rapidly sort and analyze large datasets in surveys and research.
-
MongoDB database uses quick sort as its default sorting algorithm due to fast performance on large datasets.
-
Github uses quick sort to reorder issues and pull requests based on created date, assignee, or comment count.
Conclusion
Quick sort is one of the most popular sorting algorithms used in practice due to its excellent O(nlogn) time complexity on average.
We learned how it adopts a divide-and-conquer approach by selecting a pivot and partitioning elements around it. Sorting is achieved by recursively sorting the subarrays.
An efficient implementation in Python requires careful selection of the pivot element and partitioning scheme. By picking the median pivot and using a technique like Hoare partition, we can optimize quick sort’s performance.
It works well for small and medium arrays where speed is important and stability is not required. The log-linear time complexity makes it well-suited for sorting large datasets. However, alternate algorithms may be better for sorted or duplicate data.
Quick sort will continue to be a staple of efficient sorting implementations due to its speed, minimal memory overhead, and adaptability using various optimizations. A solid understanding of this algorithm is key for any programmer.