Skip to content

Sorting Algos QNA

Praveen Kumar Anwla edited this page Apr 9, 2024 · 16 revisions

Q1: What are different types of Sorting algorithms in Python?

Ans: There are various sorting algorithms that can be implemented in Python, each with its own advantages and disadvantages in terms of time and space complexity. Here are some of the commonly used sorting algorithms:

  1. Bubble Sort:

    • A simple sorting algorithm that repeatedly steps through the list, compares adjacent elements, and swaps them if they are in the wrong order.
  2. Selection Sort:

    • Sorts an array by repeatedly finding the minimum element from the unsorted part of the array and putting it at the beginning.
  3. Insertion Sort:

    • Builds the final sorted array one item at a time, taking each element from the input data and inserting it into its correct position in the already sorted portion of the array.
  4. Merge Sort:

    • A divide-and-conquer algorithm that divides the array into two halves, sorts each half, and then merges the sorted halves.
  5. QuickSort:

    • Another divide-and-conquer algorithm that selects a 'pivot' element and partitions the other elements into two sub-arrays according to whether they are less than or greater than the pivot.
  6. Heap Sort:

    • A comparison-based sorting algorithm that transforms an array into a binary heap and then repeatedly extracts the maximum element from the heap and rebuilds the heap.
  7. Counting Sort:

    • A non-comparison-based sorting algorithm that sorts elements based on their count occurrences in the input.
  8. Radix Sort:

    • A non-comparison-based sorting algorithm that works by distributing elements into buckets based on their digits, from the least significant digit to the most significant digit.
  9. TimSort:

    • A hybrid sorting algorithm derived from merge sort and insertion sort. It is designed to perform well on many kinds of real-world data.

Python provides a built-in sorted() function and a sort() method for lists that internally use an adaptive and stable sorting algorithm called Timsort, which is a hybrid sorting algorithm derived from merge sort and insertion sort. Timsort is designed to perform well on many kinds of real-world data.

The choice of which sorting algorithm to use depends on various factors, including the size of the data, whether the data is partially sorted, and the specific requirements of the task at hand.

Q2: Python code to perform Bubble sort on a list of numbers?

Ans: The time complexity of the Bubble Sort algorithm is O(n^2) in the worst and average cases, and O(n) in the best case.

Here's a breakdown of the cases:

  1. Worst Case (O(n^2)):

    • Occurs when the input array is in reverse order, and the maximum number of comparisons and swaps are needed.
  2. Average Case (O(n^2)):

    • The average case is also O(n^2), as it involves a nested loop where each element is compared and swapped.
  3. Best Case (O(n)):

    • The best case occurs when the input array is already sorted. In this case, Bubble Sort makes only one pass through the array, and no swaps are needed. The time complexity is linear, O(n).

Bubble Sort is not considered efficient for large datasets due to its quadratic time complexity, especially when compared to more advanced sorting algorithms with better average and worst-case time complexities.

def bubble_sort(nums):
    n = len(nums)
    
    for i in range(n):
        # Flag to optimize by stopping early if no swaps are made in a pass
        swapped = False
        
        # Last i elements are already sorted, so we don't need to check them
        for j in range(0, n-i-1):
            if nums[j] > nums[j+1]:
                # Swap elements if they are in the wrong order
                nums[j], nums[j+1] = nums[j+1], nums[j]
                swapped = True
        
        # If no swaps were made, the list is already sorted
        if not swapped:
            break

# Example usage:
my_list = [64, 34, 25, 12, 22, 11, 90]
bubble_sort(my_list)

print("Sorted list:", my_list)

Q3: Python code to perform Selection sort on a list of numbers?

Ans: Time Complexity: O(N2).

def selection_sort(nums):
    n = len(nums)
    
    for i in range(n):
        # Find the index of the minimum element in the unsorted part of the list
        min_index = i
        for j in range(i+1, n):
            if nums[j] < nums[min_index]:
                min_index = j
        
        # Swap the found minimum element with the first element
        nums[i], nums[min_index] = nums[min_index], nums[i]

# Example usage:
my_list = [64, 34, 25, 12, 22, 11, 90]
selection_sort(my_list)

print("Sorted list:", my_list)

Q4: Python code to perform Insertion sort on a list of numbers?

Ans: The time complexity of Insertion Sort is O(n^2) in the worst case and O(n) in the best case.

  • Worst case: O(n^2)
  • Best case: O(n)
  • Average case: O(n^2)

In the worst case, when the input array is in reverse order, each element needs to be compared and moved to its correct position, resulting in a quadratic time complexity. In the best case, when the array is already sorted, Insertion Sort performs very efficiently with a linear time complexity because it only needs to check each element once.

The actual performance may also depend on the specific implementation and the nature of the input data. Insertion Sort is often considered inefficient for large datasets but can be efficient for small datasets or partially sorted datasets.

def insertion_sort(nums):
    for i in range(1, len(nums)):
        j = i
        while j > 0 and nums[j - 1] > nums[j]:
            # Swap adjacent elements if they are in the wrong order
            nums[j - 1], nums[j] = nums[j], nums[j - 1]
            j -= 1

# Example usage:
my_list = [64, 34, 25, 12, 22, 11, 90]
insertion_sort(my_list)

print("Sorted list in ascending order:", my_list)

Method 2:

def insertion_sort(nums):
    for i in range(1, len(nums)):
        for j in range(i, 0, -1):
            if nums[j - 1] > nums[j]:
                # Swap adjacent elements if they are in the wrong order
                nums[j - 1], nums[j] = nums[j], nums[j - 1]
            else:
                break

# Example usage:
my_list = [64, 34, 25, 12, 22, 11, 90]
insertion_sort(my_list)

print("Sorted list in ascending order:", my_list)

Q5: Python code to perform Shell sort on a list of numbers?

Ans: The time complexity of Shell Sort depends on the sequence of gaps used. It's challenging to determine an exact time complexity because it heavily relies on the gap sequence chosen. However, the worst-case time complexity for Shell Sort is generally considered to be O(n^2), which is the same as the worst-case time complexity of the Insertion Sort.

When a simple gap sequence like "n/2, n/4, n/8, ..., 1" is used, the worst-case time complexity is O(n^2). However, Shell Sort can have better average-case time complexity than other quadratic sorting algorithms like Bubble Sort and Insertion Sort, making it more efficient in practice for certain datasets.

In some cases, the time complexity of Shell Sort with a carefully chosen gap sequence can approach O(n log n). Nevertheless, the optimal gap sequence for achieving the best performance is an open research problem, and different gap sequences may perform differently on various types of data.

def shell_sort(nums):
    n = len(nums)
    gap = n // 2

    while gap > 0:
        for i in range(gap, n):
            j = i
            while j >= gap and nums[j - gap] > nums[j]:
                nums[j - gap], nums[j] = nums[j], nums[j - gap]
                j -= gap

        gap //= 2

# Example usage:
my_list = [64, 34, 25, 12, 22, 11, 90]
shell_sort(my_list)

print("Sorted list:", my_list)

Method 2:

def shell_sort(nums):
    n = len(nums)
    gap = n // 2

    while gap > 0:
        for i in range(gap, n):
            for j in range(i, gap-1, -gap):
                if nums[j - gap] > nums[j]:
                    nums[j - gap], nums[j] = nums[j], nums[j - gap]
                else:
                    break

        gap //= 2

# Example usage:
my_list = [64, 34, 25, 12, 22, 11, 90]
shell_sort(my_list)

print("Sorted list:", my_list)

Q6: Python code for Quicksort algorithm for a list of numbers?

Ans: The average time complexity of QuickSort is O(n log n), where "n" is the number of elements in the array being sorted. This average-case time complexity holds for a random distribution of elements.

However, it's important to note that QuickSort has a worst-case time complexity of O(n^2), which occurs when the pivot element is always chosen in a way that one partition is empty. This worst-case scenario is less likely to occur with a good pivot selection strategy.

QuickSort is often favored in practice due to its good average-case performance and simplicity. Various optimizations, such as using a randomized pivot selection or the three-way partitioning technique, can further improve its performance and reduce the likelihood of encountering the worst-case scenario.

def quick_sort(arr):
    if len(arr) <= 1:
        return arr
    else:
        pivot = arr[0]
        less_than_pivot = [x for x in arr[1:] if x <= pivot]
        greater_than_pivot = [x for x in arr[1:] if x > pivot]
        return quick_sort(less_than_pivot) + [pivot] + quick_sort(greater_than_pivot)

# Example usage:
my_list = [5, 2, 9, 1, 5, 6]
sorted_list = quick_sort(my_list)
print("Original list:", my_list)
print("Sorted list:", sorted_list)

Method 2:

Partitioning shown above in the quicksort code is a common approach, but there are alternative methods for choosing the pivot value and partitioning the array. One such approach is to use the "median-of-three" strategy, where you choose the pivot as the median of three values: the first, middle, and last elements of the array.

Here's an updated version of your quicksort code incorporating the median-of-three strategy:

def quick_sort(arr):
    if len(arr) <= 1:
        return arr
    else:
        # Choosing pivot as the median of three values
        first, middle, last = arr[0], arr[len(arr)//2], arr[-1]
        pivot = sorted([first, middle, last])[1]
        
        less_than_pivot = [x for x in arr if x < pivot]
        equal_to_pivot = [x for x in arr if x == pivot]
        greater_than_pivot = [x for x in arr if x > pivot]
        
        return quick_sort(less_than_pivot) + equal_to_pivot + quick_sort(greater_than_pivot)

# Example usage:
my_list = [5, 2, 9, 1, 5, 6]
sorted_list = quick_sort(my_list)
print("Original list:", my_list)
print("Sorted list:", sorted_list)

This implementation chooses the pivot as the median of the first, middle, and last elements of the array. This can help improve the performance of quicksort on certain inputs.

Method 3: The "Median of Medians" algorithm is a technique to choose a pivot for quicksort that guarantees a good balance in the partitioning, improving worst-case performance. It involves recursively dividing the array into groups of five elements, finding the median of each group, and then finding the median of those medians.

Here's an example implementation of quicksort using the Median of Medians algorithm:

def quick_sort(arr):
    if len(arr) <= 1:
        return arr
    else:
        # Choosing pivot using Median of Medians
        pivot = median_of_medians(arr)
        
        less_than_pivot = [x for x in arr if x < pivot]
        equal_to_pivot = [x for x in arr if x == pivot]
        greater_than_pivot = [x for x in arr if x > pivot]
        
        return quick_sort(less_than_pivot) + equal_to_pivot + quick_sort(greater_than_pivot)

def median_of_medians(arr):
    sublists = [arr[i:i+5] for i in range(0, len(arr), 5)]
    medians = [sorted(sublist)[len(sublist)//2] for sublist in sublists]
    
    if len(medians) <= 5:
        return sorted(medians)[len(medians)//2]
    else:
        return median_of_medians(medians)

# Example usage:
my_list = [5, 2, 9, 1, 5, 6]
sorted_list = quick_sort(my_list)
print("Original list:", my_list)
print("Sorted list:", sorted_list)

In this implementation, the median_of_medians function calculates the median of medians, and it is then used as the pivot for the quicksort algorithm. The choice of the pivot in this way helps to ensure better partitioning and improve worst-case performance.

Q7: Python code for merge sort?

Ans:

def merge_sort(nums):

    # define the base case: that we keep splitting the lists until
    # the sub-lists have just 1 item - arrays with a single item is sorted by default
    if len(nums) == 1:
        return

    # DIVIDE PHASE
    middle_index = len(nums) // 2
    left_half = nums[:middle_index]
    right_half = nums[middle_index:]
    
    merge_sort(left_half)
    merge_sort(right_half)
    
    # CONQUER PHASE
    i = 0
    j = 0
    k = 0

    while i < len(left_half) and j < len(right_half):
        if left_half[i] > right_half[j]:
            nums[k] = left_half[i]
            i = i + 1
        else:
            nums[k] = right_half[j]
            j = j + 1

        k = k + 1

    # after that there may be additional items in the left (right) sub-array
    while i < len(left_half):
        nums[k] = left_half[i]
        i = i + 1
        k = k + 1

    while j < len(right_half):
        nums[k] = right_half[j]
        j = j + 1
        k = k + 1



my_list = [1, 5, -2, 0, 10, 100, 55, 12, 10, 2, -10, -3]

merge_sort(my_list)
print(my_list)

Q8: Python code to perform Radix sort?

Ans:

import random

ITEMS_IN_BUCKET = 10


class RadixSort:

    def __init__(self, data):
        self.data = data

    # to get the max. number of digits in all numbers; E.g., 12,123,1234 -> Max #digits=4
    # So, we can identify LSD, and move towards the MSD for sorting.
    def get_digits(self): 
        return len(str(max(self.data)))

    def sort(self):
        for digit in range(self.get_digits()):
            self.counting_sort(digit)

    #For each digit, perform counting sorting; Put numbers in right bucket, and perform sorting for the next iteration.
    def counting_sort(self, d):

        count_array = [[] for _ in range(ITEMS_IN_BUCKET)]

        # store the count of each element in count array O(N)
        for num in self.data:
            # calculate the index of the given bucket
            index = (num // (10 ** d)) % 10
            count_array[index].append(num)

        # we have to consider all the items in the count array (list)
        z = 0
        for i in range(len(count_array)):
            while len(count_array[i]) > 0:
                # it takes O(N) linear running time complexity - dictionary O(1)
                self.data[z] = count_array[i].pop(0)
                z += 1


if __name__ == '__main__':

    n = [5, 3, 10, 12, 9, 8, 20, 100, 325, 1023]
    random.shuffle(n)
    print(n)
    radix_sort = RadixSort(n)
    radix_sort.sort()
    print(radix_sort.data)

Q8.1 : Time and Space complexity of a Radix sort?

Ans: The time complexity of Radix Sort is O(nk), where n is the number of elements in the array and k is the number of digits in the largest number. This is because the algorithm performs counting sort for each digit position, and counting sort has a time complexity of O(n + k).

The space complexity of Radix Sort is O(n + k), where n is the number of elements in the array and k is the number of buckets (in this case, 10). This is because additional space is required for the count array used in counting sort.

Here's an example of using Radix Sort on an array:

arr = [170, 45, 75, 90, 802, 24, 2, 66]
radix_sort = RadixSort(arr)
radix_sort.sort()
print("Sorted array is:", radix_sort.data)

For this example, let's analyze the time and space complexity:

  • n (number of elements) = 8
  • k (number of digits in the largest number) = 3 (since the largest number is 802)

Time complexity: O(nk) = O(8 * 3) = O(24) Space complexity: O(n + k) = O(8 + 10) = O(18)

So, for this specific example, the time complexity is O(24) and the space complexity is O(18).

Q 9: Can you arrange the following sorting algorithms in terms of time complexity: Bubble Sort, Selection Sort, Insertion Sort, Shell sort, Quick Sort, Merge Sort, and Radix Sort?

Ans: Sure, here's the list of sorting algorithms arranged in terms of their time complexity:

  1. Radix Sort: O(nk), where n is the number of elements and k is the number of digits in the largest number.
  2. Merge Sort: O(n log n), where n is the number of elements. This is both its average and worst-case time complexity.
  3. Quick Sort: O(n log n) on average, but O(n^2) in the worst case. However, with proper pivot selection techniques, its worst-case performance is rarely encountered in practice.
  4. Shell Sort: The time complexity of Shell Sort depends on the chosen sequence of increments. In the best known case (using the Hibbard sequence), it's O(n^3/2). In the average and worst cases, it's often considered close to O(n log n).
  5. Insertion Sort: O(n^2) in the worst case but can be more efficient on nearly sorted data.
  6. Selection Sort: O(n^2) in all cases, making it less efficient than Insertion Sort on nearly sorted data.
  7. Bubble Sort: O(n^2) in the worst and average cases, making it one of the least efficient sorting algorithms.

Keep in mind that these complexities represent the comparison-based sorting algorithms (except Radix Sort), where the number of comparisons between elements is the primary factor influencing time complexity. Radix Sort operates differently by considering the digits of the elements rather than comparing them directly, which leads to its linear time complexity.

Q10: Can you arrange the following sorting algorithms in terms of space complexity: Bubble Sort, Selection Sort, Insertion Sort, Shell sort, Quick Sort, Merge Sort, and Radix Sort.

Ans: Certainly! Here's the list of sorting algorithms arranged in terms of their space complexity:

  1. Merge Sort: O(n), where n is the number of elements. Merge Sort typically requires additional space proportional to the size of the input array for the merging step. This additional space can be significant for large arrays.

  2. Radix Sort: O(n + k), where n is the number of elements and k is the number of buckets (in this case, typically 10). Radix Sort requires additional space for the count array used in counting sort, but the extra space is generally not proportional to the input size and is determined by the range of values being sorted.

  3. Quick Sort: O(log n) due to the recursive call stack. Quick Sort is an in-place sorting algorithm, meaning it doesn't require additional space proportional to the input size beyond the space needed for storing the array itself. However, the recursive calls consume space on the call stack, leading to a space complexity of O(log n).

  4. Shell Sort: O(1). Shell Sort is an in-place sorting algorithm, meaning it sorts the elements within the original array without requiring additional space.

  5. Insertion Sort: O(1). Insertion Sort is also an in-place sorting algorithm. It operates directly on the input array, making it space-efficient.

  6. Selection Sort: O(1). Selection Sort, like Insertion Sort, operates directly on the input array without requiring additional space.

  7. Bubble Sort: O(1). Similar to Insertion Sort and Selection Sort, Bubble Sort is an in-place sorting algorithm and doesn't require additional space beyond the original array.

In summary, Merge Sort and Radix Sort typically require additional space beyond the input array, making them less space-efficient compared to Quick Sort, Shell Sort, Insertion Sort, Selection Sort, and Bubble Sort, which are all in-place sorting algorithms.

Clone this wiki locally