In [1]:
alpha =      "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
In [2]:
# Multiplication cipher
def mult_cipher(plaintext, key):
    """Convert each char c to number n using alpha.
    Then n->n*key mod 26. Convert back to char"""
    ciphertext = []
    for i in range(len(plaintext)): 
        c = plaintext[i] 
        numc = alpha.find(c)
        multnumc = (numc*key) % 26  # only significant change vs shift_cipher
        codedc = alpha[multnumc]
        ciphertext.append(codedc)
    return ciphertext
In [4]:
mult_cipher("HELLO",3)
Out[4]:
['V', 'M', 'H', 'H', 'Q']
In [8]:
"".join(mult_cipher("ABCDEFGHIJKLMNOPQRSTUVWXYZ",3))
Out[8]:
'ADGJMPSVYBEHKNQTWZCFILORUX'
In [10]:
# To decipher, use mult_cipher(ciphertext, 9)
# we use 9 as the decryption key since 3*9 % 26 = 1
mult_cipher(['V', 'M', 'H', 'H', 'Q'], 9)
Out[10]:
['H', 'E', 'L', 'L', 'O']
In [11]:
mult_cipher("HELLO",2)
Out[11]:
['O', 'I', 'W', 'W', 'C']
In [12]:
"".join(mult_cipher("ABCDEFGHIJKLMNOPQRSTUVWXYZ",2))
Out[12]:
'ACEGIKMOQSUWYACEGIKMOQSUWY'
In [13]:
mult_cipher("A",2)
Out[13]:
['A']
In [14]:
mult_cipher("N",2)
Out[14]:
['A']
In [ ]:
# since above are the same, can't decrypt "A"
# so 2 is not a good encryption key to use, the problem is that 
# 2,26 share a common factor 
In [ ]:
# Want to have a way of deciding whether a given key can be inverted,
# need to compute greatest common divisor (gcd)
In [ ]:
# Digression: recursion 
In [15]:
# factorial function using loop (no recursion)
def fact_loop(n):
    """Return n*(n-1)*...*1 """
    run_prod = 1
    for i in range(1,n+1):
        run_prod = run_prod*i
    return run_prod
In [17]:
fact_loop(5)
Out[17]:
120
In [31]:
# Factorial function using recursion
def fact(n):
    #print(n)
    if n==1:
        return 1  # base case
    else:
        return n*fact(n-1) # calls same function: recursive call
In [33]:
fact(100)
Out[33]:
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
In [34]:
# CHALLENGE: compute nth term of Fibonacci sequence using recursion
# a0 = 1, a1 = 1, a2 = 2, a3 = 3, a4 = 5, a5 = 8,
# a_n =a_{n-1} + a_{n-2} for n>=1
def fib(n):
    """Returns nth fibonacci number an, uses recursion."""
    if n==0 or n==1:
        return 1
    return fib(n-1)+fib(n-2)
In [36]:
# for testing
[fib(n) for n in range(10)]
Out[36]:
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
In [39]:
fib(20)
Out[39]:
10946
In [ ]:
# this fib function is slower than the fib function from a previous class
# Reason: there is a slot of redunancy 
# (for instance, computes fib(n-2) multiple times)
In [ ]: