# A smarter sorting algorithm¶

Our sorting algorithm had the following general operation on a list of size $n$:

1. Find the minimum element and put it at the beginning.
2. Sort the last $n-1$ elements.

As we saw the number of steps looked something like this:

We will show the merge sort algorithm that does the following on a list of size $n$:

1. Sort the first $n/2$ elements to get a list $L1$ and the last $n/2$ elements to get a list $L2$.
2. Merge the two lists together to one sorted list.

It turns out that the number of steps it takes looks like the following:

In [25]:
# illustration of number of steps sorting 20 numbers in selection sort vs merge sort

****************************** ****************************** ***************************** *************** **************************** ******* *************************** *** ************************** * ************************* * ************************ *** *********************** * ********************** * ********************* ******* ******************** *** ******************* * ****************** * ***************** *** **************** * *************** * ************** *************** ************* ******* ************ *** *********** * ********** * ********* *** ******** * ******* * ****** ******* ***** *** **** * *** * ** *** * * *
In [27]:
# comparison of running time of selection sort and merge sort

..............................plot_steps: False 1.000 micro-seconds per step ..............................plot_steps: True 0.611 micro-seconds per step

## Merge sort¶

In [11]:
#With Recursion
def merge_lists(L1,L2):
i=0
j=0
res = []
while i<len(L1) and j<len(L2):
if L1[i] < L2[j]:
res += [L1[i]]
i += 1
else:
res += [L2[j]]
j += 1
res += L1[i:]+L2[j:]
return res
x1=[3,9]
x2=[5,15]
x=merge_lists(x1,x2)

In [12]:
#With recursion
def merge_sort(L):
if len(L) <= 1:
return L
m = len(L)//2 #m=1
L1 = merge_sort(L[0:m]) #first half of list
L2 = merge_sort(L[m:]) #2nd half of list
return merge_lists(L1,L2)

L=[0,14,8,2,5,1,3,4]

In [13]:
merge_sort([3,1,4,1,5,9,2])

Out[13]:
[1, 1, 2, 3, 4, 5, 9]
In [14]:
#Without Recursion
def merge_sort_nr(L):
lists = [ [x] for x in L]
while len(lists)>1:
new_lists = []
if len(lists) % 2:
lists += []
for i in range(0,len(lists)-1,2):
new_lists += merge_lists(lists[i],lists[i+1])
lists = new_lists
return lists[0]

In [15]:
merge_sort_nr([3,1,4,1,5,9,2])

Out[15]:
[1, 1, 2, 3, 4, 5, 9]

## De-recursion¶

Many times, recursion gives us a clean way to think about problems and solve them.

But a recursive program is often slower than non recursive version.

So sometimes, after finding a recursive solution, we want to transform it to a non recursive solution.

## Remember Selection sort¶

In [40]:
def find_min_index(L):
current_index = 0
current_min = L[0]
for j in range(1,len(L)):
if current_min > L[j]:
current_min = L[j]
current_index = j
return current_index

In [ ]:
def selection_sort(L):
if len(L)<=1:
return L # a one-element list is always sorted
min_idx = find_min_index(L) #non-recursive helper function
L[0], L[min_idx] = L[min_idx], L[0]
return [L[0]] + sort(L[1:len(L)])

In [ ]:
def selection_sort_nr(L):
for i in range(len(L)):
min_idx = i+find_min_index(L[i:])
L[i], L[min_idx] = L[min_idx], L[i]
return L

In [41]:
selection_sort_nr([3,1,4,1,5,9,2])

Out[41]:
[1, 1, 2, 3, 4, 5, 9]
In [85]:
#write a function bin_search
#which takes in a sorted list L and an item i
#and returns the index of i if it exists & -1 if not.
#don't use recursion

def binarysearch(li,element):
top=len(li) #li=[0,1,2,3,4,5], element=1
bottom=0
while top>bottom: #top=6, bottom=0
middle=(top+bottom)//2 #middle 3
if element==li[middle]:
return middle
elif element < li[middle]:
top=middle
elif element > li[middle]:
bottom=middle
return -1
binarysearch([0,1,2,3,4,5],4)

Out[85]:
4
In [34]:
#With recursion
def bin_search_1(L,item):
n = len(L)  #L=[1,2,3,4,5], m=2
if n==0:
return -1
m = n//2
if L[m]==item:
return m
#print(m)
if L[m]>item:
return bin_search_1(L[:m],item) #Search left half

res = bin_search_1(L[m+1:],item) #Search right half #what happens if I don't have the following 3 lines?
if res==-1:
return -1
return m+1+res

bin_search_1([1,2,3,4,5], 4)

Out[34]:
3
In [65]:
#Without recursion
def bin_search_nr(L,item):
left = 0
right= len(L)
while right-left >0:
#print('*')
m = int((left+right)/2)
#print(m) #what would this print?
if L[m]==item:
return m
if L[m]>item: #Search left half
right = m
else:
left  = m+1 #Search right half
return -1

In [71]:
L=range(10) #=[0,1,2,3,4,5,6,7,8,9]
#print(bin_search_nr(L,2))


# Lab work¶

### Exercise 1¶

Write a function sort4 that sorts a list of 4 elements. The function should make two calls to sort2

In [29]:
def sort4(L):
L_first_sorted = sort2(L[0:2])
L_last_sorted = sort2(L[2:4])
#
# do something to return a sorted list
#

In [ ]:
sort4([10,2,5,7])


### Exercise 2¶

Write a function merge_lists that takes two sorted lists L1 and L2 and returns a sorted list that of length len(L1)+len(L2) that contains all their elements.

### Exercise 3¶

Write a function sort32 that sorts a list of 32 elements. The function should make two recursive calls to a provided function sort16

In [30]:
def sort32(L):
L_first_sorted = sort16(L[0:4])
L_last_sorted = sort16(L[4:8])
#
# do something to return a sorted list
#

In [31]:
sort16 = sorted

In [32]:
sort32([32,31,30,29,28,27,26,25,24,23,22,21,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1])


### Exercise 4¶

Write a function merge_sort that will sort a list of any size. The function should make two recursive calls to itself

In [34]:
def merge_sort(L):
#
# do something here
#
L_first_sorted = merge_sort(L[0:int(len(L)/2)])
L_last_sorted = merge_sort(L[int(len(L)/2):len(L)])
#
# do something to return a sorted list

In [ ]:
merge_sort([78, 39, 50, 43, 3, 30, 34, 75, 33, 7, 30, 71, 76, 44, 27, 4, 68, 21, 51, 78, 11, 53, 71, 60, 64, 9, 28, 63, 55, 34, 44, 52, 28, 52, 43, 44, 4, 41, 40, 17])


### Exercise 5¶

Use a stopwatch to compare selection sort and merge sort on random lists of 4000 numbers. Run each one of them 10 times andrecord the average time.

(if your computer crashes then you can use smaller lists, you can also use the %timeit option as below)

In [36]:
def gen_random_list(n):
return [random.randint(0,2*n) for i in range(n)]

In [37]:
# run this code so that Python allows you to run the sorts on inputs larger than 100
import sys
sys.setrecursionlimit(10**6)

In [ ]:
% timeit merge_sort(gen_random_list(4000));

In [ ]:
% timeit sort(gen_random_list(4000));
# your selection sort function from the previous labwork or from the lecture


### Exercise 6 (bonus)¶

Write a function cannon_ball_no_gravity(angle,speed,t) that on input an angle angle between $0$ and $90$ and a speed speed in meters per second, and time $t$ in seconds, returns two values height,distance .

The value height will be which are the height in meters that a cannon ball shot at angle $angle$ from the ground and speed $speed$ would be at after $t$ seconds if there is no gravity.

The value distance will be the distance in meters that the projection of this ball on the ground would be from the initial point after $t$ seconds.

Write a function cannon_ball_earth(angle,speed,t) that does the same thing if the ball is shot on earth and takes gravity into account.

(You don't need to worry about air resistance nor about whether height becomes negative).

You can use the following helper functions:

In [39]:
import math
def sine(angle):
return math.sin((angle/360.0)*2*math.pi)
def cosie(angle):
return math.cos((angle/360.0)*2*math.pi)