Unit 1 DSD Notes It Covers All The Topics in First Unit
Unit 1 DSD Notes It Covers All The Topics in First Unit
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.
The set of operations can be grouped into four categories. They are,
• Constructors: Functions used to initialize new instances of the ADT at the time of
instantiation.
• Accessors : Functions used to access the data elements contained in an instance
without making any modification to it.
• Mutators : Functions that modify the data elements of an ADT instance.
• Iterators : Functions that enable sequential access of the data elements.
Advantages of using ADTs
• ADTs give the feel of plug and play interface. So it is easy for the programmer to implement
ADT. For example, to store collection of items, it can be easily put the items in a list. That
is,
BirdsList=[‘Parrot’,’Dove’,’Duck’,’Cuckoo’]
• To find number of items in a list, the programmer can use len function without implementing
the code.
• The ADTs reduces the program developing time. Because the programmer can use
the predefined functions rather than developing the logic and implementing the same.
• The usage of ADTs reduces the chance of logical errors in the code, as the usage
of predefined functions in the ADTs are already bug free.
• ADTs provide well defined interfaces for interacting with the implementing code.
• ADTs increase the understandability and modularity of the code.
ADTs AND CLASSES
A data structure is the implementation for an ADT. In an object-oriented language, an ADT and its
implementation together make up a class. Each operation associated with the ADT is implemented
by a member function or method. The variables that define the space required by a data item are
referred to as data members. An object is an instance of a class that is created and takes up storage
during the execution of a computer program.
The class definition begins with the keyword class, followed by the name of
the class, a colon and an indented block of code that serves as the body of the class.
The body includes definitions for all methods of the class. These methods are
defined as functions, with a special parameter, named self, that is used to identify
the particular instance upon which a member is invoked.
When a class definition is entered, a new namespace is created, and used as
the local scope. Thus, all assignments to local variables go into this new namespace.
Example:
class customer:
def _ _init_ _(self,name,iden,acno):
self.custName=name
self.custID=iden
self.custAccNo=acno
def display(self):
print("Customer Name = ",self.custName)
print("Customer ID = ",self.custID)
print("Customer Account Number = ",self.custAccNo)
c = customer("Ramesh",10046,327659)
c.display()
Constructor
A constructor is a special method used to create and initialize an object of a class.
This method is defined in the class. The constructor is executed automatically at the time
of object creation. In python _ _init_ _() method is called as the constructor of the class. It
is always called when an object is created.The primary responsibility of _ _init_ _ method
is to establish the state of a newly created object with appropriate instance variables.
Syntax:
def _ _init_ _(self):
#body of the constructor
Where,
def keyword is used to define function.
_ _init_ _() method: It is a reserved method. This method gets called as soon as an
object of a class is instantiated.
self: The first argument self refers to the current object. It binds the instance to
the _ _init_ _() method. It is usually named self to follow the naming
convention.
Types of constructors
• There are three types of constructors
1. Default Constructor
2. Non-Parameterized Constructor
3. Parameterized Constructor
Python will provide a default constructor if no constructor is defined.
Python adds a default constructor when the programmer does not include
the constructor in the class or forget to declare it.
Default constructor does not perform any task but initializes the objects. It is
an empty constructor without a body.
Non-Parameterized Constructor:
A constructor without any arguments is called a non-parameterized constructor. This
type of constructor is used to initialize each object with default values.This constructor
does not accept the arguments during object creation. Instead, it initializes every object
with the same set of values.
Example:
class Employee:
def _ _init_ _(self):
self.name = "Ramesh"
self.EmpId = 100456
def display(self):
print("Employee Name = ", self.name, " \nEmployee Id = ",self.EmpId)
emp = Employee()
emp.display()
Output:
Employee Name = Ramesh
Employee Id = 100456
Parameterized constructor
Constructor with parameters is known as parameterized constructor. The first
parameter to constructor is self that is a reference to the being constructed, and the rest
of the arguments are provided by the programmer. A parameterized constructor can
have any number of arguments.
Example:
class Employee:
def init (self, name, age, salary):
self.name = name
self.age = age
self.salary = salary
def display(self):
print(self.name, self.age, self.salary)
# creating object of the Employee class
emp1 = Employee('Banu', 23, 17500)
emp1.display()
emp2 = Employee('Jack', 25, 18500)
emp2.display()
Output:
Banu 23 17500
Jack 25 18500
Destructor
It is a special method that is called when an object gets destroyed. A class can define
a special method called destructor with the help of del() . In Python, destructor is not
called manually but completely automatic, when the instance(object) is about to be
destroyed. It is mostly used to clean up non memory resources used by an instance(object).
Example: For Destructor
class Student:
# constructor
def init (self, name):
print('Inside
Constructor') self.name =
name print('Object
initialized')
def display(self):
print('Hello, my name is', self.name)
# destructor
def del (self):
print('Inside destructor')
print('Object destroyed')
s1 = Student('Raja') # create object
s1.display()
del s1 # delete object
Output:
Inside Constructor
Object initialized
Hello, my name is
Raja Inside destructor
Object destroyed
Iterators
Iteration is an important concept in the design of data structures. An iterator is
an object that contains a countable number of elements that can be iterated
upon.
Iterators allows to traverse through all the elements of a collection and
return one element at a time.
An iterator object must implement two special methods, iter() and
next() collectively called iterator protocol.
An object is called iterable if it gets an iterator from it. Most built-in containers
in python are, list, tuple, string etc.
The _ _iter_ _() method returns the iterator object itself. If required,
some initialization can be performed.
The _ _next_ _() method returns the next element of the collection. On reaching
the end, it must raise stop Iteration exception to indicate that there are no
further elements.
ENCAPSULATION
Encapsulation is one of the fundamental concepts in object-oriented programming.
Encapsulation in Python describes the concept of bundling data and methods within a single
unit. A class is an example of encapsulation as it binds all the data members (instance
variables) and methods into a single unit.
INHERITANCE
A natural way to organize various structural components of a software package is in a
hierarchical fashion. A hierarchical design is useful in software development, as common functionality
can be grouped at the most general level, thereby promoting reuse of code, while differentiated
behaviors can be viewed as extensions of the general case.
In object-oriented programming, the existing class is typically described as the base class,
parent class or super class, while the newly defined class is known as the subclass or
child class.
There are two ways in which a subclass can differentiate itself from its superclass. A
subclass may specialize an existing behavior by providing a new implementation that
overrides an existing method. A subclass may also extend its superclass by providing brand
new methods.
Inheritance is a mechanism through which we can create a class or object based on another
class or object. In other words, the new objects will have all the features or attributes of
the class or object on which they are based. It supports code reusability.
In Python, based upon the number of child and parent classes involved, there are five types of inheritance.
The types of inheritance are listed below:
i. Single inheritance
ii. Multiple Inheritance
iii. Multilevel inheritance
iv. Hierarchical Inheritance
v. Hybrid Inheritance
Single Inheritance
In single inheritance, a child class inherits from a single-parent class. Here is one child class and one
parent class
Example:
# Base class class
Vehicle:
def Vehicle_info(self):
print('Inside Vehicle class')
# Child class
class Car(Vehicle):
def car_info(self):
print('Inside Car class')
# Create object of Car
car = Car()
# access Vehicle's info using car object car.Vehicle_info()
car.car_info()
Output:
Inside Vehicle class
Inside Car class
Multiple Inheritance:
In multiple inheritance, one child class can inherit from multiple parent classes. So here is one child class and
multiple parent classes.
class SuperClass1:
# features of SuperClass1
class SuperClass2:
# features of SuperClass2
Multilevel Inheritance:
Multilevel Inheritance is a mechanism in object-oriented programming where a class inherits from
another class, which in turn inherits from another class. This process continues until the topmost class is
reached. In this way, inheritance relationships form a hierarchy of classes, with the base class being at the
top, and the derived classes being at the bottom. The derived class inherits the attributes and behavior of
the class it inherits from and can also add new attributes and behavior to those inherited.
Syntax :
class base1 :
body of base class
class derived1( base1 ) :
body of derived class
class derived2( derived1 ) :
body of derived class
Example Program:
class Vehicle:
def Vehicle_info(self):
print('Inside Vehicle class')
class Car(Vehicle):
def car_info(self):
print('Inside Car class')
class SportsCar(Car):
def sports_car_info(self):
print('Inside Sports Car class')
s_car = SportsCar()
s_car.Vehicle_info()
s_car.car_info()
s_car.sports_car_info()
Output:
Inside Vehicle class
Inside Car class
Inside Sports Car class
Hierarchical Inheritance:
In Hierarchical inheritance, more than one child class is derived from a single parent class. In
other words, a single base class is inherited by multiple derived classes. In this scenario,
each derived class shares common attributes and methods from the same base class,
forming a hierarchy of classes.
Syntax :
class BaseClass:
# Base class attributes and methods
class DerivedClass1(BaseClass):
# Additional attributes and methods specific to DerivedClass1
class DerivedClass2(BaseClass):
# Additional attributes and methods specific to DerivedClass2
Syntax:
class Vehicle:
def info():
print("This is Vehicle")
class Car(Vehicle):
def car_info():
print("Car name is BMW”)
class Truck(Vehicle):
def truck_info():
print("Truck name is Ford")
obj1 = Car()
obj1.info()
obj1.car_info()
obj2 = Truck()
obj2.info()
obj2.truck_info()
Output:
This is Vehicle
Car name is BMW
This is Vehicle
Truck name is
Ford
Hybrid Inheritance:
Hybrid inheritance is a blend of multiple inheritance types. In Python, the supported types of
inheritance are single, multiple, multilevel, hierarchical, and hybrid. In hybrid inheritance, classes are
derived from more than one base class, creating a complex inheritance structure.
Syntax:
class BaseClass1:
# Attributes and methods of BaseClass1
class BaseClass2:
# Attributes and methods of BaseClass2
class DerivedClass(BaseClass1, BaseClass2):
# Attributes and methods of DerivedClass
Example:
class Vehicle:
def vehicle_info(self):
print("Inside Vehicle class")
class Car(Vehicle):
def car_info(self):
print("Inside Car class")
class Truck(Vehicle):
def truck_info(self):
print("Inside Truck class")
class SportsCar(Car, Vehicle):
def sports_car_info(self):
print("Inside SportsCar
class") s_car = SportsCar()
s_car.vehicle_info()
s_car.car_info()
s_car.sports_car_info()
Output:
Inside Vehicle class
Inside Car class
Inside SportsCar
class
Namespaces
Whenever an identifier is assigned to a value, its definition is made with a specific scope. Top level
assignments are known as global scope. Assignments made with in the body of a function have
local scope to that function call.
In Python, when computing a sum with the syntax x + y, the names x and y must have been
previously associated with objects that serve as values. The process of determining the value
associated with an identifier is known as name resolution.
A namespace manages all of the identifiers that are defined in a particular scope, mapping each name
to its associated value. In Python, functions, classes and modules are class objects and so the value
associated with an identifier in a namespace may be a function, class or module.
A namespace is a collection of currently defined symbolic names along with information about the
object that each name references. We can think of a namespace as a dictionary, in which the keys
are the object names and the values are the objects themselves. Each key-value pair maps a name to
its corresponding object.
Types:
1) Built-in Namespace
It contains the names of all of Python’s built-in objects. These are available at all times when
Python is running. The Python interpreter creates the built-in namespace when it starts up. This
namespace remains in existence until the interpreter terminates.
Example:
Name=input("Enter your name:") #input() is built-in function
print(Name) #print() is built-in function
2) Global Namespace
It contains any names defined at the level of the main program. Python creates the global
namespace when the main program body starts, and it remains in existence until the interpreter
terminates. The interpreter creates a global namespace for any module that the program loads with
the import statement.
Example:
x=10 # global scope of variable in
python def f1(): #function definition
print(x) #variable accessed inside the function
f1()
3) Local Namespace
In Python, the interpreter creates a new namespace whenever a function executes. That
namespace is local to the function and remains in existence until the function terminates. A local
namespace can access global as well as built-in namespaces.
Example:
def f1(): #function definition
y=20 #Local scope of variable in python
print(y) #variable accessed inside the
function f1()
• The variable ‘y’ is declared in a local namespace and has a local scope of variable in python.
In this example, this code demonstrates shallow copying of a list with nested elements
using the ‘copy' module. Initially, it prints the original elements of li1, then performs
shallow copying into li2. Modifying an element in li2 affects the corresponding element
in li1, as both lists share references to the inner lists. This illustrates that shallow copying
creates a new list, but it doesn’t provide complete independence for nested elements.
Deep Copy:
A deep copy creates a new compound object before inserting copies of the items
found in the original into it in a recursive manner. It means first constructing a new
collection object and then recursively populating it with copies of the child objects found
in the original. In the case of deep copy, a copy of the object is copied into another object.
It means that any changes made to a copy of the object do not reflect in the original
object.
Syntax:
copy.deepcopy(x)
In the above example, the change made in the list did not affect other lists, indicating the
list is deeply copied.
Program:
import copy
li1 = [1, 2, [3,5], 4]
li2 = copy.deepcopy(li1)
print ("The original elements before deep copying")
for i in range(0,len(li1)):
print (li1[i],end=" ")
print("\r")
li2[2][0] = 7
print ("The new list of elements after deep copying ")
for i in range(0,len( li1)):
print (li2[i],end=" ")
print("\r")
print ("The original elements after deep copying")
for i in range(0,len( li1)):
print (li1[i],end=" ")
Output:
The original elements before deep copying
1 2 [3, 5] 4
The new list of elements after deep copying
1 2 [7, 5] 4
The original elements after deep copying
1 2 [3, 5] 4
This code illustrates deep copying of a list with nested elements using the copy module. It
initially prints the original elements of li1, then deep copies them to create li2. A
modification to an element in li2 does not affect li1, as demonstrated by the separate
printouts. This highlights how deep copying creates an independent copy, preserving the
original list’s contents even after changes to the copy.
1 Constant
log n Logarithmic
n Linear
n log n Linear logarithmic
n2 Quadratic
n3 Cubic
2n Exponential
Asymptotic Analysis
•Asymptotic analysis, gives an idea about the performance of the algorithm based on the input size.
It should not calculate the exact running time, but find the relation between the running time and the
input size. We should follow the running time when the size of the input is increased.
• For the space complexity, the goal is to get the relation or function that how much space in the
main memory is occupied to complete the algorithm.
• Algorithm analysis are broadly classified into three types such as
• Best case analysis: This analysis gives a lower bound on the run-time. It describes the behaviour of
an algorithm under optimal conditions.
Worst case analysis: This analysis gives the upper bound of the running time of algorithms. In
this case, a maximum number of operations are executed.
•Average case analysis: This analysis gives the region between the upper and lower bound of the
running time of algorithms. In this case, the number of executed operations is not minimum and
not maximum.
ASYMPTOTIC NOTATIONS
Asymptotic notation is one of the most efficient ways to calculate the time complexity of an
algorithm. Asymptotic notations are mathematical tools to represent time complexity of algorithms
for asymptotic analysis. The three asymptotic notations used to represent time complexity of
algorithms are,
The Big O (Big-Oh) Notation
The Big Ω (Big-Omega)
Notation The Big (Big-Theta)
Notation
It is represented as f(n) = O(g(n)). That is, at higher values of n, the upper bound of f(n) is g(n).
The above definition says, in the worst case, let the function f(n) be the algorithm's runtime,
and g(n) be an arbitrary time complexity. Then O(g(n)) says that the function f(n) never grows faster
than g(n) that is f(n)<=g(n) and g(n) is the maximum number of steps that the algorithm can attain.
In the above graph c. g(n) is a function that gives the maximum runtime (upper bound) and f(n) is
the algorithm’s runtime.
The Big Omega () notation:
Big Omega is an Asymptotic Notation for the best-case scenario. Big notation defines an
asymptotic lower bond.
The Mathematical Definition of Big-Omega
f(n) ∈ Ω(g(n)) if and only if there exist some positive constant c and some non-negative integer n₀
such that, f(n) ≥ c g(n) for all n ≥ n₀, n₀ ≥ 1 and c>0.
The above definition says, in the best case, let the function f(n) be the algorithm’s runtime
and g(n) be an arbitrary time complexity. Then Ω(g(n)) says that the function g(n) never grows more
than f(n) i.e. f(n)>=g(n), g(n) indicates the minimum number of steps that the algorithm will attain.
In the above graph, c.g(n) is a function that gives the minimum runtime (lower bound) and f(n) is the
algorithm’s runtime.
The Big Theta () Notation:
Big Theta is an Asymptotic Notation for the average case, which gives the average growth for a
given function. Theta Notation is always between the lower bound and the upper bound. It provides
an asymptotic average bound for the growth rate of an algorithm. If the upper bound and lower
bound of the function give the same result, then the Θ notation will also have the same rate of
growth.
The Mathematical Definition of Big-Theta
f(n) ∈ (g(n)) if and only if there exist some positive constant c₁ and c₂ some non-negative integer
n₀ such that, c₁ g(n) ≤ f(n) ≤ c₂ g(n) for all n ≥ n₀, n₀ ≥ 1 and c>0.
The above definition says in the average case, let the function f(n) be the algorithm’s runtime
and g(n) be an arbitrary time complexity. Then (g(n)) says that the function g(n) encloses the
function f(n) from above and below using c1.g(n) and c2.g(n).
In the above graph, c1.g(n) and c2.g(n) encloses the function f(n). This notation gives the realistic
time complexity of an algorithm.
RECURSION
Recursion is a technique by which a function makes one or more calls to itself during execution.
In computing, recursion provides an elegant and powerful alternative for performing repetitive tasks.
When one invocation of the function makes a recursive call, that invocation is suspended until the
recursive call completes.
The factorial function
• The factorial function is a classic mathematical function that has a natural recursive definition.
• The factorial of a positive integer n, is defined as the product of the integers from 1 to n. if n = 0, then
n! is defined as 1.
• For example, 5! = 5 . 4 . 3 . 2 . 1 = 120 and note that 5 ! = 5. (4.3.2.1) = 5. 4!. Generally, for a positive
integer n, we can define n ! = n. (n – 1) !. In this case, n = 0 is the base class. It is defined non recursively
is terms of fixed quantities. n (n-1)! is a recursive case.
Recursive implementation of the factorial function
A recursive implementation of the factorial function is,
def factorial(n):
if n = = 0:
return 1
else:
return n*factorial(n-1)
This function repetition is provided by the repeated recursive invocation of the function. When the
function is invoked, its argument is smaller by one and when a base case is reached, no further recursive
calls are made.
• The execution of a recursive function is illustrated using a recursive trace. Each entry of the trace
corresponds to a recursive call. Each new recursive function call is indicated by a downward arrow to
a new invocation. When the function returns, an arrow showing this return is drawn and the return
value may be indicated alongside this arrow.
• In python, when a function is called a structure known as an activation record or a frame is created to
store information about the progress of that invocation of the function. This activation record includes
a namespace for storing the function call’s parameters, local variables and information about which
command in the body of the function is currently executing.
• When the execution of a function leads to a nested function call, the execution of the former call is
suspended and its activation record stores the place where the control should continue upon return of
the nested call. That is, there is a different activation record for each active call.
Binary Search:
• When the sequence is unsorted the standard approach for searching a target value is sequential search.
When the sequence is sorted and indexable, then binary search is used to efficiently locate a target
value within a sorted sequence of n elements.
• For any index j, all the values stored at indices 0 to j-1 are less than or equal to the value at index j, and all
the values stored at indices j+1 to n-1 are greater than equal to that at index j. This allows us to quickly
search target value.
• The algorithm maintains two parameters, low and high, such that all the candidate entries have index
at least low and at most high. Initially, low = 0 and high = n−1. Then we compare the target value to
the median candidate, that is, the item data[mid] with index
mid = (low+high)/2.
Consider three cases:
If the target equals data[mid], then we have found the item, and the search terminates successfully.
If target < data[mid], then we recur on the first half of the sequence, that is, on the interval of
indices from low to mid−1.
If target > data[mid], then we recur on the second half of the sequence, that is, on the
interval of indices from mid+1 to high.
An unsuccessful search occurs if low > high, as the interval [low, high] is empty. This algorithm is known as
binary search.
Implementation
def binarysearch(data, target, low, high):
if low > high:
return False
else:
mid = (low + high) // 2 if
target == data[mid]:
return True
elif target < data[mid]:
return binarysearch(data, target, low, mid − 1)
else:
return binarysearch(data, target, mid + 1, high)
This binary search algorithm requires O(log n) time. Where as the sequential search algorithm uses O(n) time.
File Systems:
• File system for a computer has a recursive structure in which directories can be nested arbitrarily deep
within other directories. Recursive algorithms are widely to explore and manage these file systems.
• Modern operating systems define file-system directories in a recursive way. A file system consists of a top-
level directory, consists of files and other directories, which is turn contain files and other directories and
so on.
• The representation of such file system is,
• The file system representation uses recursive algorithms for copying a dictionary, deleting a dictionary
etc. In this example we consider computing the total disk usage for all files and directories nested within a
particular directory.
Tower of Hanoi Puzzle:
• The Tower of Hanoi puzzle consists of a board with three vertical poles and a stack of
disks. The diameter of the disks increases from the top to bottom creating tower structure.
• The illustration of Tower of Hanoi with three disks is shown in Fig.
The objective of tower of hanoi puzzle is to move all the disks from the starting pole to one of the other
two poles to create a new tower, adhering two conditions.
1. Only one disk can be moved at a time.
2. A larger disk can never be placed on top of a smaller disk.
This problem can be solved by recursive operation. Given n disks and three poles are named source(s)
destination (D) and intermediate (I).
Move the top n – 1 disks from pole S to pole I using pole D.
Move the remaining disks from pole S to pole D.
Move the n –1 disks from pole I to pole D using pole S.
The first and last steps are recursive calls to the same operation but using different poles
as the source, destination and intermediate designations. The second step is a process
where single disk to move. It is a base case.
Step 1 Step 2
Step 3 Step 4
Step 5 Step 6
Step 7
Implementation
def move (n, src, dest, temp):
if n > = 1 :
move (n-1, src, temp, dest)
print (“move %d %d” % (src, dest))
move (n-1, temp, dest, src)