Known to John von Neumann in 1945, 70+ years ago
In [129]:
import random
random.seed(0)
from resources.utils import run_tests
In [65]:
def split(input_list):
"""
Splits a list into two pieces
:param input_list: list
:return: left and right lists (list, list)
"""
input_list_len = len(input_list)
midpoint = input_list_len // 2
return input_list[:midpoint], input_list[midpoint:]
In [100]:
tests_split = [
({'input_list': [1, 2, 3]}, ([1], [2, 3])),
({'input_list': [1, 2, 3, 4]}, ([1, 2], [3, 4])),
({'input_list': [1, 2, 3, 4, 5]}, ([1, 2], [3, 4, 5])),
({'input_list': [1]}, ([], [1])),
({'input_list': []}, ([], []))
]
In [101]:
run_tests(tests_split, split)
In [93]:
def merge_sorted_lists(list_left, list_right):
"""
Merge two sorted lists
This is a linear operation
O(len(list_right) + len(list_right))
:param left_list: list
:param right_list: list
:return merged list
"""
# Special case: one or both of lists are empty
if len(list_left) == 0:
return list_right
elif len(list_right) == 0:
return list_left
# General case
index_left = index_right = 0
list_merged = [] # list to build and return
list_len_target = len(list_left) + len(list_right)
while len(list_merged) < list_len_target:
if list_left[index_left] <= list_right[index_right]:
# Value on the left list is smaller (or equal so it should be selected)
list_merged.append(list_left[index_left])
index_left += 1
else:
# Right value bigger
list_merged.append(list_right[index_right])
index_right += 1
# If we are at the end of one of the lists we can take a shortcut
if index_right == len(list_right):
# Reached the end of right
# Append the remainder of left and break
list_merged += list_left[index_left:]
break
elif index_left == len(list_left):
# Reached the end of left
# Append the remainder of right and break
list_merged += list_right[index_right:]
break
return list_merged
In [94]:
tests_merged_sorted_lists = [
({'list_left': [1, 5], 'list_right': [3, 4]}, [1, 3, 4, 5]),
({'list_left': [5], 'list_right': [1]}, [1, 5]),
({'list_left': [], 'list_right': []}, []),
({'list_left': [1, 2, 3, 5], 'list_right': [4]}, [1, 2, 3, 4, 5]),
({'list_left': [1, 2, 3], 'list_right': []}, [1, 2, 3]),
({'list_left': [1], 'list_right': [1, 2, 3]}, [1, 1, 2, 3]),
({'list_left': [1, 1], 'list_right': [1, 1]}, [1, 1, 1, 1]),
({'list_left': [1, 1], 'list_right': [1, 2]}, [1, 1, 1, 2]),
({'list_left': [3, 3], 'list_right': [1, 4]}, [1, 3, 3, 4]),
]
In [95]:
run_tests(tests_merged_sorted_lists, merge_sorted_lists)
In [127]:
def merge_sort(input_list):
if len(input_list) <= 1:
return input_list
else:
left, right = split(input_list)
# The following line is the most important piece in this whole thing
return merge_sorted_lists(merge_sort(left), merge_sort(right))
In [125]:
random_list = [random.randint(1, 1000) for _ in range(100)]
tests_merge_sort = [
({'input_list': [1, 2]}, [1, 2]),
({'input_list': [2, 1]}, [1, 2]),
({'input_list': []}, []),
({'input_list': [1]}, [1]),
({'input_list': [5, 1, 1]}, [1, 1, 5]),
({'input_list': [9, 1, 10, 2]}, [1, 2, 9, 10]),
({'input_list': range(10)[::-1]}, list(range(10))),
({'input_list': random_list}, sorted(random_list))
]
In [126]:
run_tests(tests_merge_sort, merge_sort)
merge_sort
keeps splitting until we get to single-element lists. Once we're there (the base case of recursion) the callers can start applying merge_sorted_list
. For the following example here's what's going on:
input_list=[9, 1, 10, 2]
left=[9, 1]
and right=[10, 2]
merge_sort([9, 1])
is responsible for sorting [9, 1]
, let's call it L1
.merge_sort([10, 2])
is reponsible for sorting [10, 2]
, let's call it R1
.For L1
:
left=[9]
and right=[1]
merge_sort([9])
returns [9]
since it's the base case and merge_sort([1])
returns [1]
merge_sorted_lists([9], [1])
returns [1, 9]
which is sortedSame thing happens for R1
and the result is [2, 10]
. Now merge_sorted_lists(L1, R1)
returns the final answer.