Quick Sort in Python with O(n log n) Time Complexity


In this video lesson we will study how the Quick Sort algorithm works, we will analyze its time and space complexities and then we will implement it:


Problem - Quick Sort:

Given an array of integers nums, sort it in ascending order using Quick Sort

Example 1:

Input: nums = [3, 1, 3, 2, 5, 4]
Output: [1, 2, 3, 3, 4, 5]

Note:

Your algorithm should run in O(n log n) time and use O(log n) extra space.


## Understanding the Problem The core challenge of the problem is to sort an array of integers efficiently. Quick Sort is a popular sorting algorithm due to its average-case time complexity of O(n log n) and its in-place sorting capabilities, which means it uses minimal extra space. ### Significance and Applications Quick Sort is widely used in practice because of its efficiency and simplicity. It is particularly useful for large datasets and is often the algorithm of choice in many standard libraries and frameworks. ### Potential Pitfalls and Misconceptions - **Choosing the Pivot**: The efficiency of Quick Sort heavily depends on the choice of the pivot. A poor choice can lead to O(n^2) time complexity. - **In-Place Sorting**: While Quick Sort is in-place, it is not stable, meaning it does not preserve the relative order of equal elements. ## Approach ### Naive Solution A naive approach to sorting would be to use a simple algorithm like Bubble Sort or Insertion Sort. However, these algorithms have a time complexity of O(n^2), which is not efficient for large datasets. ### Optimized Solution: Quick Sort Quick Sort improves upon naive sorting algorithms by using a divide-and-conquer strategy. Here’s how it works: 1. **Choose a Pivot**: Select an element from the array as the pivot. 2. **Partitioning**: Rearrange the array so that elements less than the pivot are on the left, elements greater than the pivot are on the right. 3. **Recursively Apply**: Apply the same process to the sub-arrays formed by partitioning. ### Thought Process 1. **Choosing the Pivot**: A common strategy is to choose the last element as the pivot. 2. **Partitioning**: Use two pointers to rearrange elements around the pivot. 3. **Recursion**: Recursively apply Quick Sort to the left and right sub-arrays. ## Algorithm ### Step-by-Step Breakdown 1. **Base Case**: If the array has one or zero elements, it is already sorted. 2. **Partitioning**: - Choose the pivot. - Initialize two pointers. - Rearrange elements around the pivot. 3. **Recursive Calls**: Apply Quick Sort to the left and right sub-arrays. ## Code Implementation
def quick_sort(arr):
    # Helper function to perform the partitioning
    def partition(low, high):
        pivot = arr[high]  # Choose the last element as pivot
        i = low - 1  # Pointer for the greater element
        for j in range(low, high):
            # If current element is smaller than or equal to pivot
            if arr[j] <= pivot:
                i += 1  # Increment the pointer
                arr[i], arr[j] = arr[j], arr[i]  # Swap elements
        arr[i + 1], arr[high] = arr[high], arr[i + 1]  # Swap the pivot element
        return i + 1

    # Helper function to perform quick sort
    def quick_sort_recursive(low, high):
        if low < high:
            pi = partition(low, high)  # Partition the array
            quick_sort_recursive(low, pi - 1)  # Sort the left part
            quick_sort_recursive(pi + 1, high)  # Sort the right part

    quick_sort_recursive(0, len(arr) - 1)  # Initial call to quick sort

# Example usage
nums = [3, 1, 3, 2, 5, 4]
quick_sort(nums)
print(nums)  # Output: [1, 2, 3, 3, 4, 5]
### Key Parts of the Code - **Partition Function**: This function rearranges the elements around the pivot. - **Recursive Function**: This function applies Quick Sort to the sub-arrays. ## Complexity Analysis - **Time Complexity**: - Average Case: O(n log n) - Worst Case: O(n^2) (when the pivot is the smallest or largest element) - **Space Complexity**: O(log n) due to the recursion stack. ### Improvements Over Naive Approach Quick Sort is significantly faster than naive sorting algorithms like Bubble Sort and Insertion Sort, especially for large datasets. ## Edge Cases - **Empty Array**: The algorithm should handle an empty array gracefully. - **Array with One Element**: The algorithm should return the array as is. - **Array with All Identical Elements**: The algorithm should handle this without unnecessary swaps. ### Example Edge Cases - **Input**: `[]` - **Output**: `[]` - **Input**: `[1]` - **Output**: `[1]` - **Input**: `[2, 2, 2, 2]` - **Output**: `[2, 2, 2, 2]` ## Testing To test the solution comprehensively, consider the following test cases: - **Simple Case**: `[3, 1, 3, 2, 5, 4]` - **Empty Array**: `[]` - **Single Element**: `[1]` - **All Identical Elements**: `[2, 2, 2, 2]` - **Already Sorted**: `[1, 2, 3, 4, 5]` - **Reverse Sorted**: `[5, 4, 3, 2, 1]` ### Testing Frameworks You can use Python's built-in `unittest` framework to automate the testing process. ## Thinking and Problem-Solving Tips - **Understand the Problem**: Break down the problem into smaller parts. - **Choose the Right Algorithm**: Consider the time and space complexity. - **Practice**: Solve similar problems to improve your understanding. ## Conclusion Quick Sort is a powerful and efficient sorting algorithm that is widely used in practice. Understanding its mechanics and implementation can significantly improve your problem-solving skills. ## Additional Resources - [Quick Sort on Wikipedia](https://en.wikipedia.org/wiki/Quicksort) - [Sorting Algorithms - GeeksforGeeks](https://www.geeksforgeeks.org/sorting-algorithms/) - [LeetCode Sorting Problems](https://leetcode.com/tag/sorting/) By mastering Quick Sort, you can tackle a wide range of sorting problems efficiently and effectively. Happy coding!