Quick Sort in Java 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 Quick Sort algorithm is to efficiently sort an array of integers in ascending order. Quick Sort is a divide-and-conquer algorithm that works by selecting a 'pivot' element from the array and partitioning the other elements into two sub-arrays, according to whether they are less than or greater than the pivot. The sub-arrays are then sorted recursively.

Quick Sort is significant due to its average-case time complexity of O(n log n), making it one of the fastest sorting algorithms for large datasets. However, its worst-case time complexity is O(n^2), which can occur if the pivot elements are not chosen wisely.

Potential pitfalls include choosing a poor pivot, which can lead to unbalanced partitions and degrade performance. Misunderstanding the partitioning process can also lead to incorrect implementations.

Approach

To solve the problem, we can follow these steps:

  1. Choose a pivot element from the array.
  2. Partition the array into two sub-arrays: elements less than the pivot and elements greater than the pivot.
  3. Recursively apply the above steps to the sub-arrays.
  4. Combine the sorted sub-arrays and the pivot to get the final sorted array.

A naive solution might involve selecting the first element as the pivot, but this can lead to poor performance on already sorted arrays. A better approach is to use the median-of-three method or random pivot selection to improve performance.

Algorithm

Here is a step-by-step breakdown of the Quick Sort algorithm:

  1. Partitioning: Choose a pivot and rearrange the array so that all elements less than the pivot come before it, and all elements greater come after it.
  2. Recursive Sorting: Recursively apply the partitioning process to the sub-arrays.
  3. Combining: Combine the sorted sub-arrays and the pivot to form the final sorted array.

Code Implementation

public class QuickSort {
    // Main function to sort an array using Quick Sort
    public static void quickSort(int[] nums, int low, int high) {
        if (low < high) {
            // Partition the array and get the pivot index
            int pi = partition(nums, low, high);
            
            // Recursively sort the sub-arrays
            quickSort(nums, low, pi - 1);
            quickSort(nums, pi + 1, high);
        }
    }

    // Function to partition the array and return the pivot index
    private static int partition(int[] nums, int low, int high) {
        // Choose the pivot element
        int pivot = nums[high];
        int i = (low - 1); // Index of smaller element

        for (int j = low; j < high; j++) {
            // If current element is smaller than or equal to pivot
            if (nums[j] <= pivot) {
                i++;
                // Swap nums[i] and nums[j]
                int temp = nums[i];
                nums[i] = nums[j];
                nums[j] = temp;
            }
        }

        // Swap nums[i + 1] and nums[high] (or pivot)
        int temp = nums[i + 1];
        nums[i + 1] = nums[high];
        nums[high] = temp;

        return i + 1;
    }

    // Main method to test the Quick Sort algorithm
    public static void main(String[] args) {
        int[] nums = {3, 1, 3, 2, 5, 4};
        quickSort(nums, 0, nums.length - 1);
        System.out.println(Arrays.toString(nums)); // Output: [1, 2, 3, 3, 4, 5]
    }
}

Complexity Analysis

The time complexity of Quick Sort is:

The space complexity is O(log n) due to the recursive stack space used during the sorting process.

Edge Cases

Potential edge cases include:

To test these edge cases, we can use the following examples:

int[] emptyArray = {};
int[] singleElementArray = {1};
int[] identicalElementsArray = {2, 2, 2, 2, 2};

Testing

To test the Quick Sort implementation comprehensively, we should use a variety of test cases, including:

We can use JUnit or another testing framework to automate these tests and ensure the correctness of our implementation.

Thinking and Problem-Solving Tips

When approaching sorting problems like Quick Sort, consider the following tips:

To improve problem-solving skills, practice solving similar problems and study different sorting algorithms and their trade-offs.

Conclusion

Quick Sort is a powerful and efficient sorting algorithm with an average-case time complexity of O(n log n). Understanding its partitioning process and pivot selection strategies is crucial for optimizing performance. By practicing and implementing Quick Sort, you can enhance your problem-solving skills and gain a deeper understanding of sorting algorithms.

Additional Resources

For further reading and practice problems related to Quick Sort, consider the following resources: