What are the time complexities of quicksort, mergesort, and other sorting algorithms?

Medium
10 months ago

What are the time complexities of different sorting algorithms, such as quicksort and mergesort? Explain the best, average, and worst-case scenarios for each, and provide examples of when you might choose one algorithm over another. Additionally, discuss the space complexity of each algorithm.

Sample Answer

Sorting Algorithm Time Complexities

Sorting algorithms are fundamental to computer science, each offering different performance characteristics. Understanding their time and space complexities is crucial for selecting the most appropriate algorithm for a given task.

Quicksort

Description

Quicksort 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.

Code

def quicksort(arr):
    if len(arr) <= 1:
        return arr
    pivot = arr[len(arr) // 2]
    left = [x for x in arr if x < pivot]
    middle = [x for x in arr if x == pivot]
    right = [x for x in arr if x > pivot]
    return quicksort(left) + middle + quicksort(right)

arr = [3,6,8,10,1,2,1]
print(quicksort(arr)) # prints [1, 1, 2, 3, 6, 8, 10]

Best-Case Time Complexity

O(n log n): Occurs when the pivot is always the middle element, dividing the array into two equal halves.

Average-Case Time Complexity

O(n log n): On average, quicksort performs very well.

Worst-Case Time Complexity

O(n^2): Occurs when the pivot is always the smallest or largest element, leading to highly unbalanced partitions. This can happen when the array is already sorted or nearly sorted.

Space Complexity

O(log n) average, O(n) worst case: Due to the recursive calls. The space complexity can be reduced to O(log n) with tail-call optimization.

When to Choose

Quicksort is generally the fastest sorting algorithm in practice for many datasets. It's efficient and widely used. However, its worst-case performance should be considered.

Mergesort

Description

Mergesort is a divide-and-conquer algorithm that divides the array into equal halves, sorts each half recursively, and then merges the sorted halves.

Code

def merge_sort(arr):
    if len(arr) <= 1:
        return arr

    mid = len(arr) // 2
    left = arr[:mid]
    right = arr[mid:]

    left = merge_sort(left)
    right = merge_sort(right)

    return merge(left, right)


def merge(left, right):
    result = []
    i = j = 0

    while i < len(left) and j < len(right):
        if left[i] < right[j]:
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1

    result.extend(left[i:])
    result.extend(right[j:])
    return result

arr = [3,6,8,10,1,2,1]
print(merge_sort(arr)) # prints [1, 1, 2, 3, 6, 8, 10]

Best-Case Time Complexity

O(n log n): Mergesort's time complexity is not affected by the input data's arrangement.

Average-Case Time Complexity

O(n log n): Consistently performs well regardless of the input.

Worst-Case Time Complexity

O(n log n): Provides a guaranteed upper bound on performance.

Space Complexity

O(n): Mergesort requires extra space for merging the subarrays.

When to Choose

Mergesort is a good choice when a stable sort is required (i.e., elements with equal values maintain their relative order) and when worst-case performance is a concern.

Insertion Sort

Description

Insertion sort is a simple sorting algorithm that builds the final sorted array one item at a time. It is much less efficient on large lists than more advanced algorithms such as quicksort, heapsort, or merge sort.

Code

def insertion_sort(arr):
    for i in range(1, len(arr)):
        key = arr[i]
        j = i-1
        while j >= 0 and key < arr[j] :
                arr[j + 1] = arr[j]
                j -= 1
        arr[j + 1] = key

arr = [3,6,8,10,1,2,1]
insertion_sort(arr)
print(arr) # prints [1, 1, 2, 3, 6, 8, 10]

Best-Case Time Complexity

O(n): Occurs when the array is already sorted.

Average-Case Time Complexity

O(n^2):

Worst-Case Time Complexity

O(n^2): Occurs when the array is sorted in reverse order.

Space Complexity

O(1): Insertion sort sorts the array in-place.

When to Choose

Insertion sort is efficient for small datasets or nearly sorted data. It is also used as a subroutine in more complex algorithms like hybrid sorting algorithms.

Selection Sort

Description

Selection sort is a simple sorting algorithm. This sorting algorithm is an in-place comparison-based algorithm in which the list is divided into two parts, the sorted part at the left end and the unsorted part at the right end. Initially, the sorted part is empty, and the unsorted part is the entire list.

Code

def selection_sort(arr):
    for i in range(len(arr)):
        min_idx = i
        for j in range(i+1, len(arr)):
            if arr[min_idx] > arr[j]:
                min_idx = j
        arr[i], arr[min_idx] = arr[min_idx], arr[i]

arr = [3,6,8,10,1,2,1]
selection_sort(arr)
print(arr) # prints [1, 1, 2, 3, 6, 8, 10]

Best-Case Time Complexity

O(n^2):

Average-Case Time Complexity

O(n^2):

Worst-Case Time Complexity

O(n^2):

Space Complexity

O(1): Selection sort sorts the array in-place.

When to Choose

Selection sort is generally inefficient and rarely used in practice. It performs a minimal number of swaps, which can be useful in situations where writing to memory is costly.

Bubble Sort

Description

Bubble sort is a simple sorting algorithm that repeatedly steps through the list, compares adjacent elements, and swaps them if they are in the wrong order. The pass through the list is repeated until no swaps are needed, which indicates that the list is sorted.

Code

def bubble_sort(arr):
    n = len(arr)
    for i in range(n):
        for j in range(0, n-i-1):
            if arr[j] > arr[j+1] :
                arr[j], arr[j+1] = arr[j+1], arr[j]

arr = [3,6,8,10,1,2,1]
bubble_sort(arr)
print(arr) # prints [1, 1, 2, 3, 6, 8, 10]

Best-Case Time Complexity

O(n): Occurs when the array is already sorted.

Average-Case Time Complexity

O(n^2):

Worst-Case Time Complexity

O(n^2):

Space Complexity

O(1): Bubble sort sorts the array in-place.

When to Choose

Bubble sort is rarely used in practice due to its poor performance. It is primarily used for educational purposes to illustrate the basic concept of sorting algorithms.

Summary Table

AlgorithmBest-CaseAverage-CaseWorst-CaseSpace Complexity
QuicksortO(n log n)O(n log n)O(n^2)O(log n) to O(n)
MergesortO(n log n)O(n log n)O(n log n)O(n)
Insertion SortO(n)O(n^2)O(n^2)O(1)
Selection SortO(n^2)O(n^2)O(n^2)O(1)
Bubble SortO(n)O(n^2)O(n^2)O(1)

Conclusion

Choosing the right sorting algorithm depends on the specific requirements of the task, including the size of the dataset, the degree to which it is already sorted, and the importance of space complexity. Quicksort is often the fastest in practice, but mergesort provides a guaranteed O(n log n) performance. Simpler algorithms like insertion sort can be efficient for small datasets or nearly sorted data. Selection and Bubble Sort are less performant and are mainly used for educational purposes.