Home   About Me   Blog   Algorithms & datastructures Home  



import random
import time
from typing import Any


##################################################################################################
# 1. INSERTION SORT


# Insertion sort is:
# 1. stable
# 2. inplace
# 3. fast for small arrays(or almost sorted arrays)
# 4. O(n^2) time perfomance & O(1) space complexity
def insertion_sort(arr: list):
    """
    1. Iterate from arr[1] to arr[n]
    2. Compare the current_element to the previous_one
    3. If the current_element < previous_one, compare it to ALL the elements before
    4. Move the greater elements one position up to make space for the swapped element
    """
    for i in range(1, len(arr)):
        nums_b4 = i
        c_value = arr[i]
        while nums_b4 > 0:
            if c_value < arr[nums_b4 - 1]:
                arr[nums_b4], arr[nums_b4 - 1] = arr[nums_b4 - 1], arr[nums_b4]
            nums_b4 = nums_b4 - 1


arr = [1, 6, 2, 96, 346, 8, 5]
insertion_sort(arr)

if arr != sorted(arr):
    raise Exception(
        """The `insertion_sort` algo didnt work.
        \ngot = {}. \nexpected = {}.""".format(
            arr, sorted(arr)
        )
    )


# 2. SORT CUSTOM OBJECTS
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Point({self.x})"


def compare_func(a: Any, b: Any) -> bool:
    # We sort by the x coordinate, ascending
    return a.x < b.x


def custom_insertion_sort(arr: list, compare_func):
    for i in range(1, len(arr)):
        nums_b4 = i
        c_value = arr[i]
        while nums_b4 > 0:
            if compare_func(c_value, arr[nums_b4 - 1]):
                arr[nums_b4], arr[nums_b4 - 1] = arr[nums_b4 - 1], arr[nums_b4]
            nums_b4 = nums_b4 - 1


ct_array = [Point(4, 4), Point(1, 2), Point(10, 0), Point(3, 1)]
custom_insertion_sort(ct_array, compare_func)
if ct_array != sorted(ct_array, key=lambda a: a.x):
    raise Exception(
        """The `insertion_sort` algo didnt work.
        \ngot = {}. \nexpected = {}.""".format(
            ct_array, sorted(ct_array, key=lambda a: a.x)
        )
    )
##################################################################################################

# 3. MERGE SORT


# Merge sort is:
# 1. stable
# 2. NOT in-place
# 3. Uses Divide and Conquer
# 4. O(n log n) performance & O(n) space complexity
# 5. Good for large arrays, slower than insertion sort for small arrays.
def merge_sort(arr: list):
    if len(arr) > 1:
        middle = len(arr) // 2
        left_arr = arr[:middle]
        right_arr = arr[middle:]

        merge_sort(left_arr)
        merge_sort(right_arr)
        merge(arr, left_arr, right_arr)


def merge(arr: list, left: list, right: list):
    # Two iterators for traversing the two halves
    i = 0
    j = 0
    # Iterator for the main list
    k = 0

    while i < len(left) and j < len(right):
        if left[i] < right[j]:
            arr[k] = left[i]
            i += 1
        else:
            arr[k] = right[j]
            j += 1
        k += 1

    # For all the remaining values
    while i < len(left):
        arr[k] = left[i]
        i += 1
        k += 1
    while j < len(right):
        arr[k] = right[j]
        j += 1
        k += 1


arr = [1, 6, 2, 96, 346, 8, 5, 90]
merge_sort(arr)
if arr != sorted(arr):
    raise Exception(
        """The `merge_sort` algo didnt work.
        \ngot = {}. \nexpected = {}.""".format(
            arr, sorted(arr)
        )
    )


#### comparison of insertion_sort & merge_sort ####

arr = []
for i in range(45):
    arr.append(random.randint(0, 99_999_999))
start_insertion_sort = time.monotonic()
insertion_sort(arr)
end_insertion_sort = time.monotonic()
arr = []
if arr != sorted(arr):
    raise Exception(
        """The `insertion_sort` algo didnt work.
        \ngot = {}. \nexpected = {}.""".format(
            arr[:8], sorted(arr)[:8]
        )
    )
insertion_sort_duration = end_insertion_sort - start_insertion_sort
print(
    "`insertion_sort` of an array of length:{} took {}secs".format(
        len(arr), insertion_sort_duration
    )
)


arr = []
for i in range(45):
    arr.append(random.randint(0, 99_999_999))
start_merge_sort = time.monotonic()
merge_sort(arr)
end_merge_sort = time.monotonic()
arr = []
if arr != sorted(arr):
    raise Exception(
        """The `merge_sort` algo didnt work.
        \ngot = {}. \nexpected = {}.""".format(
            arr[:8], sorted(arr)[:8]
        )
    )
merge_sort_duration = end_merge_sort - start_merge_sort
print("`merge_sort` of an array of length:{} took {}secs".format(len(arr), merge_sort_duration))


# insertion_sort seems to perform better than merge_sort when len(arr)<=40