#!/usr/bin/env python
# coding: utf-8
#
cs1001.py , Tel Aviv University, Spring 2020
#
# # Recitation 9
# We discussed OOP and special methods that allow us to define operators in Python.
# Then, we saw an implementation of a Linked list data structure.
#
# #### Takeaways:
# - OOP allows us to create classes at will and to define complex objects. Remember that the most important thing is to find a good inner representation of the object so that you will have an easy time working with it.
#
# - Important properties of Linked lists:
# - Not stored continuously in memory.
# - Allows for deletion and insertion after a __given__ element in $O(1)$ time.
# - Accessing the $i$'th element takes $O(i)$ time.
# - Search takes $O(n)$ time (no random access $\implies$ no $O(\log{n})$ search).
#
# #### Code for printing several outputs in one cell (not part of the recitation):
# In[1]:
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
# ## OOP
# A link to special method names in python: https://diveintopython3.net/special-method-names.html
# ### Basic polynomial class
#
# We represent a polynomial as a list of coefficients, where the $i$'th element in the list is the coefficient of $x^i$
# In[2]:
class Polynomial():
def __init__(self, coeffs):
self.coeffs = coeffs
def __repr__(self):
terms = [" + (" + str(self.coeffs[k]) + "*x^" + \
str(k) + ")" \
for k in range(len(self.coeffs)) \
if self.coeffs[k] != 0]
terms = "".join(terms)
if terms == "":
return "0"
else:
return terms[3:] #discard leftmost '+'
f = Polynomial([1, 1])
g = Polynomial([2, 3, 4])
f
g
# #### Add degree and evaluate functions (these are special to class polynomial and are not part of Python's recognized functions)
# In[3]:
class Polynomial():
def __init__(self, coeffs):
self.coeffs = coeffs
def __repr__(self):
terms = [" + (" + str(self.coeffs[k]) + "*x^" + \
str(k) + ")" \
for k in range(len(self.coeffs)) \
if self.coeffs[k] != 0]
terms = "".join(terms)
if terms == "":
return "0"
else:
return terms[3:] #discard leftmost '+'
def degree(self):
return len(self.coeffs) - 1
def evaluate(self, x):
result = 0
for i, c in enumerate(self.coeffs):
result += c * pow(x, i)
return result
f = Polynomial([1, 1])
f.degree()
f.evaluate(5)
# In[4]:
p = Polynomial([1,0,1])
q = Polynomial([1,0,1])
p == q
# #### Add \__eq\__ function for the '==' operator
# Without it, Python compares memory adderesses
# In[5]:
class Polynomial():
def __init__(self, coeffs):
self.coeffs = coeffs
def __repr__(self):
terms = [" + (" + str(self.coeffs[k]) + "*x^" + \
str(k) + ")" \
for k in range(len(self.coeffs)) \
if self.coeffs[k] != 0]
terms = "".join(terms)
if terms == "":
return "0"
else:
return terms[3:] #discard leftmost '+'
def degree(self):
return len(self.coeffs) - 1
def evaluate(self, x):
result = 0
for i, c in enumerate(self.coeffs):
result += c * pow(x, i)
return result
def __eq__(self, other):
assert isinstance(other, Polynomial)
return self.coeffs == other.coeffs
f = Polynomial([1, 1])
f2 = Polynomial([1, 1])
g = Polynomial([2, 3, 4])
f == g
f == f2
f.__eq__(f2)
# #### Add the \__add\__ function for the '+' operator
# In[6]:
class Polynomial():
def __init__(self, coeffs):
self.coeffs = coeffs
def __repr__(self):
terms = [" + (" + str(self.coeffs[k]) + "*x^" + \
str(k) + ")" \
for k in range(len(self.coeffs)) \
if self.coeffs[k] != 0]
terms = "".join(terms)
if terms == "":
return "0"
else:
return terms[3:] #discard leftmost '+'
def degree(self):
return len(self.coeffs) - 1
def evaluate(self, x):
result = 0
for i, c in enumerate(self.coeffs):
result += c * pow(x, i)
return result
def __eq__(self, other):
assert isinstance(other, Polynomial)
return self.coeffs == other.coeffs
def __add__(self, other):
assert isinstance(other, (Polynomial, int))
if isinstance(other, int):
terms = [c for c in self.coeffs]
terms[0] += other
return Polynomial(terms)
#else, other is a polynomial
size = min(self.degree(), other.degree()) + 1
terms = [self.coeffs[i] + other.coeffs[i] for i in range(size)]
terms += self.coeffs[size : self.degree() + 1]
terms += other.coeffs[size : other.degree() + 1]
#remove leading zeros
last = len(terms) - 1
while last > 0 and terms[last] == 0:
last -= 1
return Polynomial(terms[:last+1])
f = Polynomial([1, 1])
g = Polynomial([2, 3, 4])
f+g
f+100
100+f
# #### Add \__radd\__ function for right addition with int
# Since there is no support for addition with a Polynomial in the class int, python needs a definition of right addition for int in class Polynomial
#
# When executing the command $f + 3$, python will first search for an \__add\__ method in class int that can support a second operand of type polynomial (self = int, other = polynomial).
# If such method does not exist, python will then search for an \__radd\__ method in class polynomial that can support a second operand of type int (self= polynomial, other = int).
# If such method does not exist, an exception is thrown.
#
# Since + is commutative, then we set \__radd\__ to be identical to \__add\__
#
# In[7]:
class Polynomial():
def __init__(self, coeffs):
self.coeffs = coeffs
def __repr__(self):
terms = [" + (" + str(self.coeffs[k]) + "*x^" + \
str(k) + ")" \
for k in range(len(self.coeffs)) \
if self.coeffs[k] != 0]
terms = "".join(terms)
if terms == "":
return "0"
else:
return terms[3:] #discard leftmost '+'
def degree(self):
return len(self.coeffs) - 1
def evaluate(self, x):
result = 0
for i, c in enumerate(self.coeffs):
result += c * pow(x, i)
return result
def __eq__(self, other):
assert isinstance(other, Polynomial)
return self.coeffs == other.coeffs
def __add__(self, other):
assert isinstance(other, (Polynomial, int))
if isinstance(other, int):
terms = [c for c in self.coeffs]
terms[0] += other
return Polynomial(terms)
#else, other is a polynomial
size = min(self.degree(), other.degree()) + 1
terms = [self.coeffs[i] + other.coeffs[i] for i in range(size)]
terms += self.coeffs[size : self.degree() + 1]
terms += other.coeffs[size : other.degree() + 1]
#remove leading zeros
last = len(terms) - 1
while last > 0 and terms[last] == 0:
last -= 1
return Polynomial(terms[:last+1])
__radd__ = __add__ #to allow int+Polynomial
f = Polynomial([1, 1])
1 + f
# #### Add \__neg\__ function for unary minus (placing a '-' before a polynomial object)
# In[8]:
class Polynomial():
def __init__(self, coeffs):
self.coeffs = coeffs
def __repr__(self):
terms = [" + (" + str(self.coeffs[k]) + "*x^" + \
str(k) + ")" \
for k in range(len(self.coeffs)) \
if self.coeffs[k] != 0]
terms = "".join(terms)
if terms == "":
return "0"
else:
return terms[3:] #discard leftmost '+'
def degree(self):
return len(self.coeffs) - 1
def evaluate(self, x):
result = 0
for i, c in enumerate(self.coeffs):
result += c * pow(x, i)
return result
def __eq__(self, other):
assert isinstance(other, Polynomial)
return self.coeffs == other.coeffs
def __add__(self, other):
assert isinstance(other, (Polynomial, int))
if isinstance(other, int):
terms = [c for c in self.coeffs]
terms[0] += other
return Polynomial(terms)
#else, other is a polynomial
size = min(self.degree(), other.degree()) + 1
terms = [self.coeffs[i] + other.coeffs[i] for i in range(size)]
terms += self.coeffs[size : self.degree() + 1]
terms += other.coeffs[size : other.degree() + 1]
#remove leading zeros
last = len(terms) - 1
while last > 0 and terms[last] == 0:
last -= 1
return Polynomial(terms[:last+1])
__radd__ = __add__ #to allow int+Polynomial
def __neg__(self):
return Polynomial([-co for co in self.coeffs])
f = Polynomial([1, 1])
-f
# #### Add \__sub\__ function to subtract one poly form another or an int from a poly
# Using the fact we already have \__neg\__ and \__add\__ we define $f-g = f + (-g)$
# In[9]:
class Polynomial():
def __init__(self, coeffs):
self.coeffs = coeffs
def __repr__(self):
terms = [" + (" + str(self.coeffs[k]) + "*x^" + \
str(k) + ")" \
for k in range(len(self.coeffs)) \
if self.coeffs[k] != 0]
terms = "".join(terms)
if terms == "":
return "0"
else:
return terms[3:] #discard leftmost '+'
def degree(self):
return len(self.coeffs) - 1
def evaluate(self, x):
result = 0
for i, c in enumerate(self.coeffs):
result += c * pow(x, i)
return result
def __eq__(self, other):
assert isinstance(other, Polynomial)
return self.coeffs == other.coeffs
def __add__(self, other):
assert isinstance(other, (Polynomial, int))
if isinstance(other, int):
terms = [c for c in self.coeffs]
terms[0] += other
return Polynomial(terms)
#else, other is a polynomial
size = min(self.degree(), other.degree()) + 1
terms = [self.coeffs[i] + other.coeffs[i] for i in range(size)]
terms += self.coeffs[size : self.degree() + 1]
terms += other.coeffs[size : other.degree() + 1]
#remove leading zeros
last = len(terms) - 1
while last > 0 and terms[last] == 0:
last -= 1
return Polynomial(terms[:last+1])
__radd__ = __add__ #to allow int+Polynomial
def __neg__(self):
return Polynomial([-co for co in self.coeffs])
def __sub__(self, other):
assert isinstance(other, (int, Polynomial))
return (self + (-other))
f = Polynomial([1, 1])
g = Polynomial([2, 3, 4])
g - f
g - 1
# #### Add \__rsub\__ function for right subtraction
# Using the fact we already have \__neg\__ and \__sub\__ we define $f-g = -(g-f)$
#
# since oprator \- is not commutative, then we cannot set \__rsub\__ to be identical to \__sub\__
#
# Note that the following implementation of \__rsub\__ is promlematic, because it will keep calling itself (infinitely)
#
# def __rsub__(self, other):
# return other-self #PROBLEMATIC!
#
#
#
# In[10]:
class Polynomial():
def __init__(self, coeffs):
self.coeffs = coeffs
def __repr__(self):
terms = [" + (" + str(self.coeffs[k]) + "*x^" + \
str(k) + ")" \
for k in range(len(self.coeffs)) \
if self.coeffs[k] != 0]
terms = "".join(terms)
if terms == "":
return "0"
else:
return terms[3:] #discard leftmost '+'
def degree(self):
return len(self.coeffs) - 1
def evaluate(self, x):
result = 0
for i, c in enumerate(self.coeffs):
result += c * pow(x, i)
return result
def __eq__(self, other):
assert isinstance(other, Polynomial)
return self.coeffs == other.coeffs
def __add__(self, other):
assert isinstance(other, (Polynomial, int))
if isinstance(other, int):
terms = [c for c in self.coeffs]
terms[0] += other
return Polynomial(terms)
#else, other is a polynomial
size = min(self.degree(), other.degree()) + 1
terms = [self.coeffs[i] + other.coeffs[i] for i in range(size)]
terms += self.coeffs[size : self.degree() + 1]
terms += other.coeffs[size : other.degree() + 1]
#remove leading zeros
last = len(terms) - 1
while last > 0 and terms[last] == 0:
last -= 1
return Polynomial(terms[:last+1])
__radd__ = __add__ #to allow int+Polynomial
def __neg__(self):
return Polynomial([-co for co in self.coeffs])
def __sub__(self, other):
assert isinstance(other, (int, Polynomial))
return (self + (-other))
#__rsub__ = __sub__ #not what we want...
def __rsub__(self, other):
# other - self
return -(self - other) #why not just other-self ?
f = Polynomial([1, 1])
g = Polynomial([2, 3, 4])
1 - f
1 - g
# ## Linked Lists
# A linked list is a linear data structure (i.e. - nodes can be accessed one after the other). The list is composed of nodes, where each node contains a value and a "pointer" to the next node in the list.
#
# Linked lists support operations such as insertion, deletion, search and many more.
# In[3]:
class Node():
def __init__(self, val):
self.value = val
self.next = None
def __repr__(self):
#return "[" + str(self.value) + "," + str(id(self.next))+ "]"
#for today's recitation, we print the id of self instead of self.next
return "[" + str(self.value) + "," + str(id(self))+ "]"
class Linked_list():
def __init__(self, seq=None):
self.next = None
self.len = 0
if seq != None:
for x in seq[::-1]:
self.add_at_start(x)
def __repr__(self):
out = ""
p = self.next
while p != None:
out += str(p) + " " #str envokes __repr__ of class Node
p = p.next
return out
def __len__(self):
''' called when using Python's len() '''
return self.len
def add_at_start(self, val):
''' add node with value val at the list head '''
p = self
tmp = p.next
p.next = Node(val)
p.next.next = tmp
self.len += 1
def add_at_end(self, val):
''' add node with value val at the list tail '''
p = self
while (p.next != None):
p = p.next
p.next = Node(val)
self.len += 1
def insert(self, loc, val):
''' add node with value val after location 0<=loc
# Adding elements one by one. Try using Python tutor.
# In[5]:
l = Linked_list()
l.add_at_start("a")
l.add_at_start("b")
l
# ##### A short summary of methods complexity of Linked lists vs. lists:
#
#
# Method |
# List |
# Linked list |
#
#
# init |
# $O(1)$ |
# $O(1)$ |
#
#
# init with $k$ items |
# $O(k)$ |
# $O(k)$ |
#
#
# for the rest of the methods we assume that the structure contains $n$ items |
#
#
# add at start |
# $O(n)$ |
# $O(1)$ |
#
#
# add at end |
# $O(1)$ |
# $O(n)$ |
#
#
# $lst[k]$ (get the $k$th item) |
# $O(1)$ |
# $O(k)$ |
#
#
# delete $lst[k]$ |
# $O(n-k)$ |
# $O(k)$ |
#
#
# Reversing a linked list in $O(1)$ additional memory.
# See the code and demo here
# In[6]:
class Node():
def __init__(self, val):
self.value = val
self.next = None
def __repr__(self):
#return "[" + str(self.value) + "," + str(id(self.next))+ "]"
#for today's recitation, we print the id of self instead of self.next
return "[" + str(self.value) + "," + str(id(self))+ "]"
class Linked_list():
def __init__(self, seq=None):
self.next = None
self.len = 0
if seq != None:
for x in seq[::-1]:
self.add_at_start(x)
def __repr__(self):
out = ""
p = self.next
while p != None:
out += str(p) + " " #str envokes __repr__ of class Node
p = p.next
return out
def __len__(self):
''' called when using Python's len() '''
return self.len
def add_at_start(self, val):
''' add node with value val at the list head '''
p = self
tmp = p.next
p.next = Node(val)
p.next.next = tmp
self.len += 1
def add_at_end(self, val):
''' add node with value val at the list tail '''
p = self
while (p.next != None):
p = p.next
p.next = Node(val)
self.len += 1
def insert(self, loc, val):
''' add node with value val after location 0<=loc