Lecture 11

Today:

  • A little more about slicing lists
  • Tuples
  • Recursion(Recursion(Recursion(...)))

A little more about slicing:

Recall the list slicing notation, which lets us get a sublist of a list.

In [1]:
xs = [1,2,3,4,5,6,7]
print(xs[2]) 
print(xs[2:4])    # from 2 to 4
print(xs[:2])     # up to 2
print(xs[2:])     # starting from 2 
print(xs[:])      # everything
3
[3, 4]
[1, 2]
[3, 4, 5, 6, 7]
[1, 2, 3, 4, 5, 6, 7]
In [2]:
# you can also put an increment amount
xs = list(range(20))
print(xs[0:10:3])
print(xs[1:15:2])
[0, 3, 6, 9]
[1, 3, 5, 7, 9, 11, 13]

An important thing about it is that the slicing notation actually makes a new list.

In [3]:
# compare this:
xs=[1,2,3,4]
ys = xs
ys[0] = 0
xs
Out[3]:
[0, 2, 3, 4]
In [4]:
# to this:
xs=[1,2,3,4]
ys = xs[0:4]   # could also have written ys=xs[:]
ys[0] = 0
xs
Out[4]:
[1, 2, 3, 4]

changing ys didn't change xs, which means that it's a new list in a separate memory location

Tuples

Like, you know, tuples.

In [5]:
t = (2,3)
In [6]:
t[0]
Out[6]:
2
In [7]:
t[1]
Out[7]:
3
In [8]:
t = (2,3,4)
In [9]:
print(list(range(10)))
print(tuple(range(10)))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)

So tuples are just like lists. What's the difference? Here it is:

In [10]:
t = (2,3,4)
t[0] = 999
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-10-56d2bae3e179> in <module>()
      1 t = (2,3,4)
----> 2 t[0] = 999

TypeError: 'tuple' object does not support item assignment

Once a tuple is made, it can never be changed. So tuples are immutable. That's why lists and tuples are different things.


Cool use of tuples: making functions return multiple values at the same time.

In [11]:
def division_alg(a,b):
    return (a//b, a%b)
In [12]:
(q, r) = division_alg(11,3)
In [13]:
print(q,r)
3 2

You can omit the parantheses around for a more slick look:

In [14]:
def division_alg(a,b):
    return a//b, a%b

q, r = division_alg(11,3)
print(q,r)
3 2


Zip:

In [15]:
list(zip([1,2,3,4], ["a","s","d","f"]))
Out[15]:
[(1, 'a'), (2, 's'), (3, 'd'), (4, 'f')]


Recursion

Many times it is very natural for a function to call itself. For example, we all know: $$n! = n.(n-1)!$$

In [16]:
def f(n):
    if n <= 1:
        return 1
    return n*(f(n-1))

What happens when I call f(5)? It looks at the definition of f and realizes it needs to evaluate 5*f(4), so it looks at the definition of f and realizes it needs to evaluate 4*f(5),... and then eventually it comes to f(1). It does not call itself anymore beucase it returns 1. Then it goes back up and finishes all the evaluations it was doing.

In [17]:
# let's print the first 20 values:
for i in range(20):
    print(f(i), end=" ")
1 1 2 6 24 120 720 5040 40320 362880 3628800 39916800 479001600 6227020800 87178291200 1307674368000 20922789888000 355687428096000 6402373705728000 121645100408832000 

What would happen if we didn't have the if statement?

def factorial(n): return n*(factorial(n-1))

If would never stop calling itself again and again and it would basically be like having an infinite loop.


Let's do another example:

In [18]:
# nth fibonacci number:
def fibonacci(n):
    if n <= 2:
        return 1
    return fibonacci(n-1) + fibonacci(n-2)
In [19]:
for i in range(1,21):
    print(fibonacci(i), end=" ")
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765 

This is actually very very inefficient. Can you say why? Make a diagram of which fibonacci(m)'s are called by which ones, you will see that each fibonacci is called multiple times. It will take $\operatorname{O}(2^n)$ function calls to compute fibonacci(n).

Recursion with lists

We want to write a function that returns the reversed version of a list. (In HW3, we had done in-place reversing using a for loop, now we just want to return the reversed list)

The idea is this:

reversed list = (last element of list) + (reverse of the rest of the list)
In [20]:
def reverso(xs):
    if len(xs) == 1:
        return xs
    return [xs[-1]] + reverso(xs[:-1])

Alternative:

In [21]:
def reverso(xs):
    if xs == []:
        return xs
    return [xs[-1]] + reverso(xs[:-1])

Note that this is not an efficient way of doing it.

In [22]:
xs = list(range(100))
In [23]:
print(reverso(xs))
[99, 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, 88, 87, 86, 85, 84, 83, 82, 81, 80, 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 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, 0]

Good exercises:

The idea of using recursion is not to use for loops.

  • Write a recursive function that will return the maximum element of a list.
  • Write a recursive function that will return the sum of elements of a list.
  • Write a recursive function that will return the average of the elements of a list.