Last time: list comprehension, numbers in binary, how computer memory works, mutable vs immutable.
Where we are in the course: We are still learning programming basics and working on mathematical examples to do that. Don't worry, more serious math is coming :)
Today: Example: sorting. Complexity, big-O notation.
This under the hood behaviour difference between mutable and immutable objects is very important.
x = 1
y = x
print(x,y)
y = 999
print(x,y)
x
didn't change.
But doing a similar thing for lists:
x = [1,2,3]
y = x
print(x,y)
y[0] = 999
print(x,y)
They both change. We explained the reason why: it's because for lists and other mutable objects, running x = [1,2,3]
creates a list [1,2,3]
somewhere in memory, and stores, in x
the address of that spot. When we say y=x
, x
and y
are pointing to the same object. So changing y[0]
changes x[0]
as well.
In function arguments too:
def f(x):
x += 1
return x
x = 0
print(f(x), x)
The value of x
didn't change because a copy of x
was made inside the function.
But for lists:
def f(zs):
zs[0] = 999
return zs
xs = [1,2,3]
print(xs, f(xs))
changing x[0]
inside the function changed it for good.
But there's more funny stuff:
def f(zs):
zs = [1,2,3]
return zs
xs = [9,9,9,9]
f(xs)
xs
's was changed inside the function right? so if I print xs
...
print(xs)
It hadn't changed at all! What is going on?
In the function, zs
is a local variable, and a copy of xs
, but not a copy of the list that xs
is pointing to. It's a copy of the address where the list lives. This is why we were able to change xs
when we did zs[0]=999
before, because a change in the list that zs
and xs
are both ponting to was made. Now, if we say, zs=[1,2,3]
, then a totally new list [1,2,3]
is created, and zs
is now pointing to it. But xs
is still pointing to what it was pointing to before, which was [9,9,9,9]
in this case.
We want to write a function that will sort a list of numbers:
e.g. we want sort([2, 15 ,-1 ,8 ,7])
to return:
[15,8,7,2,-1]
Idea for algorithm: Move the maximum element to the top of the list, then move the maxiumum of the rest to the top of the list...
Pseudo-code first level:
input: xs output: a list with the same entries as xs but x[i]>=x[j] for all i>j N = length of xs for i=0,...,N-1 mloc = the location of the maximum of xs from i to N-1 swap xs[mloc] and xs[i]
This algorithm is called selection sort. There are much better algorithms like merge sort, quick-sort.
Of course we need to expand "the location of the maximum of xs from i to N-1" as code as well.
# returns the index of the max of the list
def max_loc_of_part(xs, start, end): # this is actually officially called argmax
current_max = xs[start]
current_max_location = start
for i in range(start, end):
if current_max < xs[i]:
current_max = xs[i]
current_max_location = i
return current_max_location
# let's test:
max_loc_of_part([1,2,3,4,5],0,5)
max_loc_of_part([6,2,3,4,5],0,2)
max_loc_of_part([6,2,3,4,5],2,3)
During the sorting, we'll also need to swap things. Let's make that into a function too:
# note that this swaps *in place*
def swap(xs, i, j):
dum = xs[i]
xs[i] = xs[j]
xs[j] = dum
#return xs #(we don't need to return because xs is changed by the function, but we could do it)
# let's test:
xs = [1,2,3]
swap(xs,0,1)
print(xs)
def sort(xs):
N = len(xs)
for i in range(N):
swap(xs, max_loc_of_part(xs,i,N), i)
return xs
xs = [2, 15 ,-1 ,8 ,7]
sort(xs)
Note that we could have written the same algorithm in one go like this:
def sort_with_not_great_code(xs):
N = len(xs)
for i in range(N):
current_max = xs[i]
current_max_location = i
for j in range(i, N):
if current_max < xs[j]:
current_max = xs[j]
current_max_location = j
dum = xs[i]
xs[i] = xs[current_max_location]
xs[current_max_location] = dum
return xs
# test
xs = [2, 15 ,-1 ,8 ,7]
sort_with_not_great_code(xs)
But it is harder to read, and most importantly, you don't get any parts that you can test indepdently and make sure are ok. So it's better to break the problem down to smaller parts.
Next time, we will make some improvements to this code.
xs
, make a new list ys = []
. For each element x
in xs
, insert x
into ys
in a way so that ys
stays sorted. e.g. if ys = [10,8,4,1]
and we are inserting x = 5
, ys
will be [10,8,5,4,1]
. (you can use ys.insert(place, new_element)
if you want or write it yourself)