DS Unit 1
DS Unit 1
Abstract Data Types (ADTs) – ADTs and classes – introduction to OOP – classes in Python –
inheritance – namespaces – shallow and deep copying Introduction to analysis of algorithms –
asymptotic notations – recursion – analyzing recursive algorithms
An ADT is a mathematical model of a data structure that specifies the type of data
stored, the operations supported on them, and the types of parameters of the operations. An ADT
specifies what each operation does, but not how it does it.
Python supports abstract data types using a mechanism known as an abstract base class
(ABC).
Some examples of ADT are Stack, Queue, List etc. Let us see
some operations of those mentioned ADT −
Stack − A stack is an abstract data type that stores elements in a lastin-first- out (LIFO)
order. Elements are added and removed to/from the top only.
Queue − A queue is an abstract data type that stores elements in a first-in- first-out
order. Elements are added at one end and removed from the other.
List − A list is an abstract data type that implements an ordered collection of values,
where the same value may occur more than once
ADTs and classes Class
Definitions
A class serves as the primary means for abstraction in object-oriented programming.A
class provides a set of behaviors in the form of member functions (also known as methods), with
implementations that are common to all instances of that class.
A class also serves as a blueprint for its instances, effectively determining the way that
state information for each instance is represented in the form of attributes
class CreditCard:
def init (self, customer, bank, acnt, limit): self.
customer = customer
self. bank = bank
self. account = acnt
self. limit = limit self.
balance = 0
def get customer(self):
return self. customer
def get bank(self):
return self. bank
def get account(self):
return self. account
def get limit(self): return
self. limit
def get balance(self):
return self. Balance def
charge(self, price):
if price + self. balance > self. limit: return
False
else:
self. balance += price return
True
def make payment(self, amount): self.
balance −= amount
The Constructor
A user can create an instance of the CreditCard class using a syntax as: cc =
CreditCard( John Doe, 1st Bank , 5391 0375 9387 5309 , 1000)
Internally, this results in a call to the specially named init method that serves as the
constructor of the class.
Introduction to OOP
Abstraction - An ADT is a mathematical model of a data structure that specifies the type
of data stored, the operations supported on them, and the types of parameters of the
operations. An ADT specifies what each operation does, but not how it does it
Private/ Internal interface: methods and properties, accessible from other methods of
the same class.
Public / External Interface: methods and properties, accessible also from outside the
class.
Inheritance - Child classes inherit data and behaviors from parent class. Inheritance
supports reusability.
Polymorphism: many methods can do the same task. Polymorphism means designing
objects to share behaviors. Polymorphism allows the same method to execute different
behaviors in two ways: method overriding and method overloading.
Classes in Python
• Class- Classes are defined by the user. The class provides the basic structure for an object.
It consists of data members and method members that are used by the instances(object) of the
class.
• Object- A unique instance of a data structure that is defined by its class. An object
comprises both data members and methods. Class itself does nothing but real functionality is
achieved through their objects.
• Data Member: A variable defined in either a class or an object; it holds the data associated
with the class or object.
• Instance variable: A variable that is defined in a method, its scope is only within the object
that defines it.
• Class variable: A variable that is defined in the class and can be used by all the instance of
that class.
• Method: They are functions that are defined in the definition of class and are used by
various instances of the class.
• Function Overloading: A function defined more than one time with different behavior.
(different arguments)
• Encapsulation: It is the process of binding together the methods and data variables as a
single entity i.e. class. It hides the data within the class and makes it available only through the
methods.
• Inheritance: The transfer of characteristics of a class to other classes that are derived from
it.
• Polymorphism: It allows one interface to be used for a set of actions. It means same
function name(but different signatures) being used for different types.
• Data abstraction: It is the process of hiding the implementation details and showing
only functionality to the user.
Classes-
• Python is OOP language. Almost everything in python is an object with its properties
and methods.
Creating Classes:
A class is a block of statement that combine data and operations, which are performed on the
data, into a group as a single unit and acts as a blueprint for the creation of objects.
Syntax:
class ClassName:
„ Optional class documentation string #list
of python class variables
# Python class constructor #Python
class method definitions
• In a class we can define variables, functions etc. While writing function in class we have to
pass atleast one argument that is called self parameter.
• The self parameter is a reference to the class itself and is used to access variables that belongs
to the class.
Example: Creating class in .py file class
student:
def display(self): # defining method in class
print("Hello Python")
• In python programming self is a default variable that contains the memory address of
the instance of the current class.
• So we can use self to refer to all the instance variable and instance methods.
Example:
class student:
def display(self): # defining method in class print("Hello
Python")
s1=student() #creating object of class s1.display()
#calling method of class using object Output:
Hello Python
• Instance variable is defined in a method and its scope is only within the object
that defines it.
• Every object of the class has its own copy of that variable. Any changes made to the variable
don‟t reflect in other objects of that class.
• Class variable is defined in the class and can be used by all the instances of that class.
• Instance variables are unique for each instance, while class variables are shared by
all instances.
Inheritance:
The mechanism of designing and constructing classes from other classes is called inheritance.
Inheritance is the capability of one class to derive or inherit the properties from some another
class.
The new class is called derived class or child class and the class from which this derived class
has been inherited is the base class or parent class. The benefits of inheritance are:
1. It represents real-world relationships well.
2. It provides reusability of a code. We don‟t have to write the same code again and again.
Also, it allows us to add more features to a class without modifying it.
3. It is transitive in nature, which means that if class B inherits from another class A, then all the
subclasses of B would automatically inherit from class
Syntax:
Class A:
# Properties of class A
Class B(A):
# Class B inheriting property of class A #
more properties of class B
Multilevel Inheritance:
In multilevel inheritance, features of the base class and the derived class are further inherited into
the new derived class. This is similar to a relationship representing a child and grandfather.
Syntax:
Class A:
# Properties of class A
Class B(A):
# Class B inheriting property of class A #
more properties of class B
Class C(B):
# Class C inheriting property of class B
# thus, Class C also inherits properties of class A # more
properties of class C
Example 1: Python program to demonstrate multilevel inheritance
#Mutilevel Inheritance
class c1:
def display1(self):
print("class c1") class
c2(c1):
def display2(self):
print("class c2") class
c3(c2):
def display3(self):
print("class c3") s1=c3()
s1.display3()
s1.display2()
s1.display1()
Output:
class c3
class c2
class c1
Example 2: Python program to demonstrate multilevel inheritance
# Base class
class Grandfather:
grandfathername =""
def grandfather(self):
print(self.grandfathername) #
Intermediate class
class Father(Grandfather):
fathername = ""
def father(self):
print(self.fathername) #
Derived class
class Son(Father):
def parent(self):
print("GrandFather :", self.grandfathername)
print("Father :", self.fathername)
# Driver's code s1
= Son()
s1.grandfathername = "Srinivas"
s1.fathername = "Ankush" s1.parent()
Output:
GrandFather : Srinivas
Father : Ankush
Multiple Inheritance:
When a class can be derived from more than one base classes this type of inheritance is called
multiple inheritance. In multiple inheritance, all the features of the base classes are inherited into
the derived class.
Syntax:
Class A:
# variable of class A #
functions of class A
Class B:
# variable of class B #
functions of class B
Class C(A,B):
# Class C inheriting property of both class A and B # more
properties of class C
Example: Python program to demonstrate multiple inheritance
# Base class1 class
Father:
def display1(self):
print("Father")
# Base class2 class
Mother:
def display2(self):
print("Mother") #
Derived class
class Son(Father,Mother):
def display3(self):
print("Son") s1
= Son()
s1.display3()
s1.display2()
s1.display1()
Output:
Son
Mother
Father
Hierarchical Inheritance:
When more than one derived classes are created from a single base this type of inheritence is
called hierarchical inheritance. In this program, we have a parent (base) class and two child
(derived) classes.
Namespaces
A namespace is an abstraction that manages all of the identifiers that are defined in a
particular scope, mapping each name to its associated value.
A name in Python is a way to access a variable like in any other languages. Python is more
flexible when it comes to the variable declaration. We can declare a variable by assigning a name
to it. We can use names to reference values.
Example:
num = 5
str = 'Z'
seq = [0, 1, 1, 2, 3, 5]
#We can even assign a name to a function. def
function():
print('It is a function.') foo
= function
foo()
instance namespace, which manages attributes specific to an individual object. For example,
each instance of our CreditCard class maintains a distinct balance, a
distinct account number, a distinct credit limit, and so on. Each credit card will have a
dedicated instance namespace to manage such values.
There is a separate class namespace for each class that has been defined. This namespace
is used to manage members that are to be shared by all instances of a class,
Types of Namespaces
1. Local Namespace
Creation of local functions creates the local namespace. This namespace covers the local names
inside a function. Python creates this namespace for every function called in a program. It
remains active until the function returns.
2. Global Namespace
When a user creates a module, a global namespace gets created. This namespace covers the
names from various imported modules used in a project. Python creates this namespace for every
module included in your program. It‟ll last until the program ends.
3. Built-in Namespace
This namespace covers the built-in functions and built-in exception names. Python creates it as
the interpreter starts and keeps it until you exit. The built-in namespace encompasses global
namespace and
global namespace encompasses local namespace. Some functions like print(), id() are examples
of built in namespaces.
Lifetime of a namespace:
A lifetime of a namespace depends upon the scope of objects, if the scope of an object ends, the
lifetime of that namespace comes to an end. It is not possible to access inner namespace‟s
objects from an outer namespace.
Scopes
A scope refers to a region of a program where a namespace can be directly accessed, i.e. without
using a namespace prefix. The scope of a name is the area of a program where this name can be
unambiguously used, for example, inside of a function. A name's namespace is identical to its
scope. Scopes are defined statically, but they are used dynamically.
During program execution there are the following nested scopes available: the
innermost scope is searched first and it contains the local names
the scopes of any enclosing functions, which are searched starting with the nearest enclosing
scope
the next-to-last scope contains the current module's global names
the outermost scope, which is searched last, is the namespace containing the built-in
names.
A shallow copy is one which makes a new object stores the reference of another object. While, in
deep copy, a new object stores the copy of all references of another object making it another list
separate from the original one.
Thus, when you make a change to the deep copy of a list, the old list doesn‟t get affected and
vice-versa. But shallow copying causes changes in both the new as well as in the old list.
This copy method is applicable in compound objects such as a list containing another list.
import copy
copy.copy(object_name) Syntax
copy.deepcopy(object_name)
Example:
import copy
a = [ [1, 2, 3], [4, 5, 6] ]
b = copy.copy(a)
c = [ [7, 8, 9], [10, 11, 12] ]
d = copy.deepcopy(c)
print(a)
print(b) a[1]
[2] = 23
b[0][0] = 98
print(a)
print(b)
print("\n")
print(c)
print(d) c[1]
[2] = 23
d[0][0] = 98
print(c)
print(d)
Output:
Shallow Copy [1,2,3]
[4,5,6]
[1,2,3][4,5,6]
[98,2,3][4,5,23]
[98,2,3][4,5,23]
Deep Copy
[7, 8, 9], [10, 11, 12]
[7, 8, 9], [10, 11, 12]
[7, 8, 9], [10, 11, 23]
[98, 8, 9], [10, 11, 12]
Introduction to analysis of algorithms
Efficiency of Algorithms:
The performances of algorithms can be measured on the scales of time and space. The
performance of a program is the amount of computer memory and time needed to run a program.
We use two approaches to determine the performance of a program. One is analytical and the
other is experimental.
In performance analysis we use analytical methods, while in performance measurement we
conduct experiments.
Time Complexity: The time complexity of an algorithm or a program is a function of the
running time of the algorithm or a program. In other words, it is the amount of computer time it
needs to run to completion.
Space Complexity: The space complexity of an algorithm or program is a function of the space
needed by the algorithm or program to run to completion. The time
complexity of an algorithm can be computed either by an empirical or theoretical
approach.
The empirical or posteriori testing approach calls for implementing the complete algorithms
and executing them on a computer for various instances of the problem. The time taken by the
execution of the programs for various instances of the problem are noted and compared. The
algorithm whose implementation yields the least time is considered as the best among the
candidate algorithmic solutions.
Analyzing Algorithms
Suppose M is an algorithm, and suppose n is the size of the input data. Clearly the complexity
f(n) of M increases as n increases. It is usually the rate of increase of f(n) with some standard
functions.
The most common computing times are O(1), O(log2 n), O(n), O(n log2 n), O(n2),
O(n3), O(2n)
Example:
The total frequency counts of the program segments A, B and C given by 1, (3n+1) and
(3n2+3n+1) respectively are expressed as O(1), O(n) and O(n2). These are referred to as the time
complexities of the program segments since they are indicative of the running times of the
program segments. In a similar manner space complexities of a program can also be
expressed in terms of mathematical
notations, which is nothing but the amount of memory they require for their execution.
Asymptotic Notations:
It is often used to describe how the size of the input data affects an algorithm‟s usage of
computational resources. Running time of an algorithm is described as a function of input size n
for large n.
Big oh(O): Definition: f(n) = O(g(n)) (read as f of n is big oh of g of n) if there exist a positive
integer n0 and a positive number c such that |f(n)| ≤ c|g(n)| for all n
≥ n0 . Here g(n) is the upper bound of the function f(n).
Theta(Θ): Definition: f(n) = Θ(g(n)) (read as f of n is theta of g of n), if there exists a positive
integer n0 and two positive constants c1 and c2 such that c1 |g(n)|
≤ |f(n)| ≤ c2 |g(n)| for all n ≥ n0. The function g(n) is both an upper bound and a lower bound for
the function f(n) for all values of n, n ≥ n0 .
Little oh(o): Definition: f(n) = O(g(n)) ( read as f of n is little oh of g of n), if f(n)
= O(g(n)) and f(n) ≠ Ω(g(n)).
Linear Recursion:
It is the most common type of Recursion in which function calls itself repeatedly until base
condition [termination case] is reached. Once the base case is reached the results are return to
the caller function.
If a recursive function is called only once then it is called a linear recursion.
Binary Recursion:
Functions with two recursive calls are referred to as binary recursive functions.
Example1: The Fibonacci function fib provides a classic example of binary recursion. The
Fibonacci numbers can be defined by the rule:
fib(n) = 0
if n is 0,
return 1 if
n is 1,
= fib(n-1) + fib(n-2) otherwise
For example, the first seven Fibonacci numbers are Fib(0)
=0
Fib(1) = 1
Fib(2) = Fib(1) + Fib(0) = 1
Fib(3) = Fib(2) + Fib(1) = 2
Fib(4) = Fib(3) + Fib(2) = 3
Fib(5) = Fib(4) + Fib(3) = 5
Fib(6) = Fib(5) + Fib(4) = 8
# Program to display the Fibonacci sequence up to n-th term where n is provided by the user
# change this value for a different result nterms =
10
# uncomment to take input from the user #nterms
= int(input("How many terms? ")) # first two
terms
n1 = 0
n2 = 1
count = 0
# check if the number of terms is valid if
nterms <= 0:
print("Please enter a positive integer") elif
nterms == 1:
print("Fibonacci sequence upto",nterms,":") print(n1)
else:
print("Fibonacci sequence upto",nterms,":") while count
< nterms:
print(n1,end=' , ') nth
= n1 + n2
# update values n1
= n2
n2 = nth count
+= 1
Tail Recursion:
Tail recursion is a form of linear recursion. In tail recursion, the recursive call is the last thing
the function does. Often, the value of the recursive call is returned. As such, tail recursive
functions can often be easily implemented in an iterative manner; by taking out the recursive call
and replacing it with a loop, the same effect can generally be achieved. In fact, a good
compiler can recognize tail
recursion and convert it to iteration in order to optimize the performance of the code.
A good example of a tail recursive function is a function to compute the GCD, or Greatest
Common Denominator, of two numbers:
def factorial(n):
if n == 0: return 1
else: return factorial(n-1) * n
def tail_factorial(n, accumulator=1):
if n == 0: return 1
else: return tail_factorial(n-1, accumulator * n)
Recursive algorithms for Factorial, GCD, Fibonacci Series and Towers of Hanoi:
Factorial(n) Input:
integer n ≥ 0 Output:
n!
1. If n = 0 then return (1)
2. else return prod(n, factorial(n − 1))
GCD(m, n)
Input: integers m > 0, n ≥ 0
Output: gcd (m, n)
12
1. If n = 0 then return (m)
2. else return gcd(n,m mod n)
Time-Complexity: O(ln n)
Fibonacci(n)
Input: integer n ≥ 0
Output: Fibonacci Series: 1 1 2 3 5 8 13………………………………..
1. if n=1 or n=2
2. then Fibonacci(n)=1
3. else Fibonacci(n) = Fibonacci(n-1) + Fibonacci(n-2)
Towers of Hanoi
Input: The aim of the tower of Hanoi problem is to move the initial n different sized disks from
needle
A to needle C using a temporary needle B. The rule is that no larger disk is to be placed above
the
smaller disk in any of the needle while moving or at any time, and only the top of the disk is to
be
moved at a time from any needle to any needle. Output:
1. If n=1, move the single disk from A to C and return,
2. If n>1, move the top n-1 disks from A to B using C as temporary.
3. Move the remaining disk from A to C.
4. Move the n-1 disk disks from B to C, using A as temporary. def
TowerOfHanoi(n , from_rod, to_rod, aux_rod):
if n == 1:
print "Move disk 1 from rod",from_rod,"to rod",to_rod return
TowerOfHanoi(n-1, from_rod, aux_rod, to_rod)
print "Move disk",n,"from rod",from_rod,"to rod",to_rod
TowerOfHanoi(n-1, aux_rod, to_rod, from_rod)
n=4
TowerOfHanoi(n, 'A', 'C', 'B')