#!/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