0% found this document useful (0 votes)
31 views26 pages

AD3251-DSD Lecture Notes

The document provides an overview of Abstract Data Types (ADTs), data structures, and object-oriented programming principles, focusing on their definitions, classifications, and implementations in Python. It discusses the importance of data structures in organizing and managing data efficiently, and outlines the goals and principles of object-oriented design, such as modularity, abstraction, and encapsulation. Additionally, it explains the role of classes in Python, including the creation of objects, instance and class variables, and the use of constructors.

Uploaded by

praiselin
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
31 views26 pages

AD3251-DSD Lecture Notes

The document provides an overview of Abstract Data Types (ADTs), data structures, and object-oriented programming principles, focusing on their definitions, classifications, and implementations in Python. It discusses the importance of data structures in organizing and managing data efficiently, and outlines the goals and principles of object-oriented design, such as modularity, abstraction, and encapsulation. Additionally, it explains the role of classes in Python, including the creation of objects, instance and class variables, and the use of constructors.

Uploaded by

praiselin
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd

UNIT I

UNIT I ABSTRACT DATA TYPES


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

INTRODUCTION TO DATA STRUCTURES


Data structure is a branch of computer science. The study of data structure helps to understand how
data is organized and how data flow is managed to increase the efficiency of any process or program.
Data structure is the structural representation of logical relationship between data elements.
Definition
Data structure is a way of storing, organizing and retrieving data in a computer, so that it can be used
efficiently. It provides a way to manage large amount of data proficiently.
Need for Data Structures:
 It gives different level of organization of data.
 It tells how data can be stored and accessed.
 It provides a means to manage large amount of data efficiently.
 It provides fast searching and sorting of data.
Classification of Data structures
 Data structures can be classified into two categories.
i) Primitive Data structures
ii) Non-Primitive Data structures

Primitive Data Structures


• Primitive data structures are basic data structures. These can be manipulated or operated directly by
the machine level instructions. Basic data types such as integer, real, character and Boolean come
under this type. Example: int, char, float.
Non-Primitive Data Structures
 Non-primitive data structures are derived from primitive data structures.
 These data structures cannot be operated or manipulated directly by the machine level
instructions.
 They define the group of homogeneous and non-homogenous data items.
 Examples: Array, Lists, Graphs, Trees etc.
1. Linear Data Structures
 A data structure that maintains a linear relationship among the elements is called a linear data
structure.
 Here, the data are arranged in a sequential fashion. But in the memory the arrangement may
not be sequential.
 Examples: Arrays, linked lists, stack and queues.
2. Non-Linear Data Structures
1. A data structure that maintains the data elements in hierarchical order are known as nonlinear data
structure. Thus, they are present at various levels.
2. They are comparatively difficult to implement and understand as compared to the linear data
structures. Examples: Trees and Graphs.
3. Static Data Structures
• In static data structures the size of the structure is fixed.
• The content of the data structures can be modified without changing the memory space allocated
to it. Example: Array
4. Dynamic Data Structures
In dynamic data structures the size of the structure is not fixed and can be modified during the operations
performed on it.
• Dynamic data structures are designed to facilitate change of data structures in the run time.
Example: Linked list.

1. ABSTRACT DATA TYPES (ADTs)


Abstract Data Types (ADTs)
• In the real-world, the programs evolve as a result of new requirements or constraints. So, a modification
to a program commonly requires a change in one or more of its data structures.
• For example, to add a new field to a student record, to keep track of more information about each
student, then it will be better to replace an array with a linked structure to improve the program’s
efficiency. In such a scenario, rewriting every procedure that uses the changed structure is not desirable.
• Therefore, a better alternative is to separate the use of a data structure from the details of its
implementation. This is the principle underlying use of abstract data type.
Definition
• An abstract data type or ADT is a mathematical abstraction. It specifies a set of operations (or methods)
and the semantics of the operations (what they do), but it does not specify the implementation of the
operations.
• Examples: List ADT, Stack ADT, Queue ADT, Trees, Graphs etc.
• The definition of ADT only mentions what operations are to be performed but not how these operations
will be implemented. It does not specify how data will be organized in memory and what algorithms will
be used for implementing the operations. It is called “abstract” because it gives an implementation-
independent view. The process of hiding the nonessential details and providing only the essentials of
problem solving is known as data abstraction.
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.

2. ADTs and CLASSES


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.

Employee 3. INTRODUC
TION TO
OBJECT
ORIENTED

PROGRAMMING
Introduction to Object Oriented Programming
3.1 Goals, Principles and Patterns
• The main ‘actors’ in the object-oriented paradigm are called objects. Each object is an instance of a
class.
• Each class presents to the outside world a concise and consistent view of the objects that are instances
of this class, without going into too much unnecessary detail or giving others access to the inner
workings of the objects.
• The class definition typically specifies instance variables, also known as data members, that the object
contains as well as the methods also known as member functions that the object can execute.
• This view of computing is intended to fulfill several goals and incorporate several design principles.
3.2 Object-Oriented Design Goals
Software implementations should achieve robustness, adaptability, and reusability.
Robustness
• Every good programmer wants to develop software that is correct, which means that a program
produces the right output for all the anticipated inputs in the program’s application. The software is said
to be robust, if it is capable of handling unexpected inputs that are not explicitly defined for its
application.
• For example, if a program is expecting a positive integer and instead is given a negative integer, then
the program should be able to recover gracefully from this error.

Adaptability

• Modern software applications, such as Web browsers and Internet search engines, involve large
programs that are used for many years. Software, therefore, needs to be able to evolve over time in
response to changing conditions in its environment. Thus, another important goal of quality software is
that it achieves adaptability (also called evaluability).
• Related to this concept is portability, which is the ability of software to run with minimal change on
different hardware and operating system platforms. An advantage of writing software in Python is
the portability provided by the language itself.

Reusability

• Going hand in hand with adaptability, the software be reusable. That is, the same code should be usable
as a component of different systems in various applications.
• Developing quality software is designed in a way that makes it easily reusable in future applications.
Such reuse should be done with care.

3.3 Object-Oriented Design Principles


The important principles in object-oriented approach, are as follows
• Modularity
• Abstraction
• Encapsulation
Modularity

• Modern software systems consist of several different components that must interact correctly in order
for the entire system to work properly. Keeping these interactions requires that these different
components be well organized. Modularity refers to an organizing principle in which different
components of software system are divided into separate functional units.
• Modularity in a software system can provide a powerful organizing framework that brings clarity
to an implementation.
• In Python, a module is a collection of closely related functions and classes that are defined together in
a single file of source code.
• For example, Python’s standard library math module, provides definitions for key mathematical
constants and functions, and the OS module, provides support for interacting with the operating
system.

Uses of modularity:
• It increases the robustness of the program.
• It is easier to test and debug separate components of the program.
• It enables software reusability.
Abstraction

• Abstraction allows dealing with the complexity of the object. Abstraction allows picking out the relevant
details of the object, and ignoring the non-essential details.
• Applying the abstraction to the design of data structures gives rise to Abstract Data Types (ADTs). 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). An
abstract base class cannot be instantiated (i.e., you cannot directly create an instance of that class), but it
defines one or more common methods that all implementations of the abstraction must have.
• An ABC is realized by one or more concrete classes that inherit from the abstract base class while
providing implementations for those method declared by the ABC.
Encapsulation

• It hides the data defined in the class and separates implementation of the class from its interface. The
interaction with the class is through the interface provided by the set of methods defined in the class. This
separation of interface from its implementation allows changes to be made in the class without
affecting its interface.
• One of the advantages of encapsulation is that it gives freedom to the programmer to implement the
details of a component, without concern that other programmers will be writing the code that intricately
depends on those internal decisions.
• Encapsulation yields robustness and adaptability, for it allows the implementation details of parts of a
program to change without adversely affecting other parts, thereby making it easier to fix bugs or add
new functionality with relatively local changes to a component
3.4 Object Oriented Design Patterns
• Object-oriented design facilitates reusable, robust, and adaptable software. Designing good code
requires the effective use of object-oriented design techniques.
• Computing researchers and practitioners have developed a variety of organizational concepts and
methodologies for designing quality object-oriented software that is concise, correct, and reusable.
• The concept of a design pattern, describes a solution to a “typical” software design problem. A pattern
provides a general template for a solution that can be applied in many different situations.
• It describes the main elements of a solution in an abstract way that can be specialized for a specific
problem at hand. The design pattern can be consistently applied to implementations of data structures
and algorithms.
The algorithm design patterns include the following:
• Recursion
• Amortization
• Divide and conquer
• Prune and search
• Brute force
• Dynamic Programming
The greedy method The software engineering design patterns include:
• Iterator
• Adapter
• Position
• Composition
• Template method
• Locator
4. CLASSES IN PYTHON
Classes in Python

• A class serves as the primary means for abstraction in object-oriented programming. In python
everything is an object. Everything is an instance of some class. A class also serves as a blueprint for its
instances.
• The data values stored inside an object are called attributes. The state information for each instance is
represented in the form of attributes (also known as fields, instance variables, or data members).
• 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.
Defining a class
A class is the definition of data and methods for a specific type of object
Syntax:
class classname:
<statement1>
.
.
.
<statement>
 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()

4.1 The Self Identifier and Self Parameter


In python, the self-identifier places a key role. Self identifies the instance upon which a method is
invoked. While writing function in a class, at least one argument has to be passed, 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. In python programming self is a default variable that contains the memory address
of the instance of current class. So self is used to reuse all the instance variable and instance methods.

• Example:
self.custName, self.custID, self.custAccNo
4.2 Object Creation:
 An object is the runtime entity used to provide the functionality to the python class.
 The attributes defined inside the class are accessed only using objects of that class.
 The user defined functions also accessed by using the object.
 As soon as a class is created with attributes and methods, a new class object is created with
the same name as the class.
 This class object permits to access the different attributes as well as to instantiate
new objects of that class.
 Instance of the object is created using the name same as the class name and it is known as
object instantiation.
 One can give any name to a newly created object.
Syntax:
object_name = class_name
The dot(.) operator is used to call the functions.
Syntax:
object_name . function_name()

4.3 Class variable and Instance variable:


• Class variable is defined in the class and can be used by all the instances of that class.
 Instance variable is defined in a method and its scope is only with in the object that defines it.
 Every object of the class has its own copy of that variable. Any change made to
the variable don’t reflect in other objects of that class.
 Instance variables are unique for each instance, while class variables are shared by all instances
Example:
class Order:
def _ _init_ _(self, coffee_name, price):
self.coffee_name = coffee_name
self.price = price
ram_order = Order("Espresso", 210)
print(ram_order.coffee_name)
print(ram_order.price)
paul_order = Order("Latte", 275)
print(paul_order.coffee_name)
print(paul_order.price)

• In this example, coffee_name and price are the class variables. ram_order and paul_order are the two
instances of this class. Each of these instances has their own values set for the coffee_name and price
instance variables.
• When ram’s order details are printed in the console, the values Espresso and 210 are returned. When
Paul’s order details are printed in the console, the values Latte and 275 are returned.
• This shows that, instance variables can have different values for each instance of the class, whereas class
variables are the same across all instances.
4.4 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.

Data hiding using access modifiers


Encapsulation can be achieved by declaring the data members and methods of a class either as
private or protected. But in Python, there is no direct access modifiers like public, private, and
protected. This can be achieved by using single underscore and double underscores.
Single underscore represents the protected and double underscore represents the private members

Public Member:
Public data members are accessible within and outside of a class. All member variables of the class
are by default public.
Private Member:
The variables can be protected in the class by marking them as private. To define a private
member, prefix the variable name with two underscores. Private members are accessible only within
the class. Protected Member:
Protected members are accessible within the class and also available to its sub-classes. To define a
protected member, prefix the member name with a single underscore. Protected data members are
used in inheritance and to allow data members access to only child classes.
Advantages of Encapsulation:
1. The main advantage of using encapsulation is the security of the data. Encapsulation protects an
object from unauthorized access.
2. Encapsulation hide an object’s internal representation from the outside called data hiding.
3. It simplifies the maintenance of the application by keeping classes separated and preventing them
from tightly coupling with each other.
4. Bundling data and methods within a class makes code more readable and maintainable

OPERATOR OVERLOADING
Operator overloading means giving extended meaning beyond their predefined operational
meaning. For example, operator + is used to add two integers as well as join two strings and merge
two lists. The same built-in operator or function shows different behavior for objects of different
classes, this is called operator overloading.
Example:
# add 2 numbers
print(100 + 200)
# concatenate two strings
print('Python' +
'Programming') # merger
two list
print([10, 20, 30] + ['Data Structures', 'And', 'Algorithms'])
Output:
300
PythonProgramming
[10, 20, 30, 'Data Structures', 'And', 'Algorithms']
The operator + is used to carry out different operations for distinct data types. This is one of the
simplest occurrences of polymorphism in Python.
To perform operator overloading, python provides some special function or magic function that is
automatically invoked when it is associated with the particular object. If + operator is used, the magic
method _add_ is automatically invoked.
Example:
class item:
def _ _init_ _(self,
price): self.price =
price
# Overloading + operator with magic
method def _ _add_ _(self, other):
return self.price +
other.price b1 = item(400)
b2 = item(300)
print("Total Price: ", b1 + b2)
Output:
Total Price: 700
In this example, addition is implemented by a special method in python called the _add_ method. When two
integers are added together, this method is called to create a new integer object
 Executing x+y, calls the int class _add_ method when x is an integer, but it calls the float types
_add_ method when x is float. The operand on the left-hand side determines which add method is
called. Thus the + operator is overloaded.
 When a binary operator is applied to two instances of different types, as in 5 * ‘Hello’, Python
gives deference to the class of the left operand.
In this example, Python would effectively check if the int class provides a sufficient definition
for how to multiply an instance by a string, via the _ _mul_ _ method. If that class does not implement
such a behavior, Python checks the class definition for the right-hand operand, in the form of a special
method named _ _rmul_ _ (i.e., right multiply). This provides a way for a new user-defined class to
support mixed operations that involve an instance of an existing class.

5. 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
5.1 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
5.2 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
class MultiDerived(SuperClass1, SuperClass2):
# features of SuperClass1 + SuperClass2 + MultiDerived class

5.3 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

5.4 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
Vehi
cle:
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_in
fo()
Output:
This is
Vehicle Car
name is
BMW This
is Vehicle
Truck name
is Ford

5.5 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
6. NAMESPACE
Namespace
 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:
In a Python program, there are three types of namespaces:
1. Built-In
2. Global
3. Local
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.

7. SHALLOW AND DEEP COPYING


• In some applications, it is necessary to subsequently modify either the original or copy in an
independent manner.
• Consider an application to manage various lists of colors. Each color is represented by an instance of a
presumed color class. warmtones is an identifier denote an existing list of such colors (e.g., oranges,
browns).
Shallow Copy:
• A shallow copy creates a new compound object and then references the objects contained in the original
within it. The copying process does not create copies of the child objects themselves.
• In the case of shallow copy, a reference of an object is copied into another object. It means that any
changes made to a copy of an object do reflect in the original object.
• In python, this is implemented using the “copy()” function.
Syntax:
copy.copy(x)

Example: Create a copy using shallow copy import copy


old_list = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
new_list = copy.copy(old_list)
print("Old list:", old_list) print("New
list:", new_list)
The output will be:
Old list: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
New list: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
Example
Adding [4, 4, 4] to old_list, using shallow copy import copy
old_list = [[1, 1, 1], [2, 2, 2], [3, 3, 3]]
new_list = copy.copy(old_list)
old_list.append([4, 4, 4]) print("Old list:",
old_list)
print("New list:", new_list)
Output:
Old list: [[1, 1, 1], [2, 2, 2], [3, 3, 3], [4, 4, 4]]
New list: [[1, 1, 1], [2, 2, 2], [3, 3, 3]]
In the above program, shallow copy of old_list is created. The new_list contains references to original
nested objects stored in old_list. Then we add the new list i.e [4, 4, 4] into old_list. This new sublist was
not copied in new_list
Example: Adding new nested object using Shallow copy import copy
old_list = [[1, 1, 1], [2, 2, 2], [3, 3, 3]]
new_list = copy.copy(old_list) old_list[1][1] = 'AA'
print("Old list:", old_list)
print("New list:", new_list)
Output:
Old list: [[1, 1, 1], [2, 'AA', 2], [3, 3, 3]]
New list: [[1, 1, 1], [2, 'AA', 2], [3, 3, 3]]
In the above program, changes to old_list i.e old_list[1][1] = 'AA' affects both sublists old_list and new_list
at index [1][1]. This is because, both lists share the reference of same nested objects.
Deep Copy
A deep copy creates a new object and recursively adds the copies of nested objects present in the original
elements.
Example: Copying a list using deepcopy() import copy
old_list = [[1, 1, 1], [2, 2, 2], [3, 3, 3]]
new_list = copy.deepcopy(old_list) print("Old
list:", old_list) print("New list:", new_list)
Output:
Old list: [[1, 1, 1], [2, 2, 2], [3, 3, 3]]
New list: [[1, 1, 1], [2, 2, 2], [3, 3, 3]]
In the above program, changes to any nested objects in original object old_list, makes changes to the copy
new_list.
Example: Adding a new nested object in the list using Deep copy import copy
old_list = [[1, 1, 1], [2, 2, 2], [3, 3, 3]]
new_list = copy.deepcopy(old_list) old_list[1][0] = 'BB'
print("Old list:", old_list) print("New
list:", new_list)
Output:
Old list: [[1, 1, 1], ['BB', 2, 2], [3, 3, 3]]
New list: [[1, 1, 1], [2, 2, 2], [3, 3, 3]]
In the above program, changes in old_list, makes changes only in the old_list. This means, both the old_list
and the new_list are independent. This is because the old_list was recursively copied, which is true for all
its nested objects.

8. INTRODUCTION TO ANALYSIS OF ALGORITHMS


Introduction to Analysis of Algorithms

Data structure is a systematic way of organizing and accessing data, and an algorithm is a step-
by-step procedure for performing some task in a finite amount of time. Algorithm analysis helps
us to determine which algorithm is most efficient in terms of time and space consumed.
The running time of an algorithm can be calculated by executing it on various test inputs and
recording the time spent during each execution.

from time import time


start time = time( ) # record the starting time
run algorithm
end time = time( ) # record the ending time
elapsed = end time − start time # compute the elapsed time

Challenges of Experimental Analysis

 Experimental running times of two algorithms are difficult to directly compare unless the
experiments are performed in the same hardware and software environments.
 Experiments can be done only on a limited set of test inputs; hence, they leave out the
running times of inputs not included in the experiment (and these inputs may be important).
 An algorithm must be fully implemented in order to execute it to study its running time
experimentally.

Moving Beyond Experimental Analysis

Our goal is to develop an approach to analyzing the efficiency of algorithms that:

1. Allows us to evaluate the relative efficiency of any two algorithms in a way that is independent of the
hardware and software environment.
2. Takes into account all possible inputs.

3. Is performed by studying a high-level description of the algorithm without need for implementation.
Types of Analysis

Algorithm analysis depends on which inputs the algorithm takes less time (performing wel1) and with
which inputs the algorithm takes a long time.

Worst case
 Defines the input for which the algorithm takes a long time (slowest time to complete).
 Input is the one for which the algorithm runs the slowest.

Best case
 Defines the input for which the algorithm takes the least time (fastest time to complete).
 Input is the one for which the algorithm runs the fastest.

Average case
 Provides a prediction about the running time of the algorithm.
 Run the algorithm many times, using many different inputs and divide by the number of
trials.
 Assumes that the input is random.
Lower Bound <= Average Time <= Upper Bound

9. ASYMPTOTIC NOTATIONS
Asymptotic Notation

For the best, average and worst cases, we need to identify the upper and lower bounds. To represent these
upper and lower bounds, we need some kind of syntax, represented in the form of function f(n).
Big-O Notation [Upper Bounding Function]

This notation gives the tight upper bound of the given function. Thus, it gives the worst-case complexity of
an algorithm.

O(g(n)) = { f(n): there exist positive constants c and

n0 such that 0 ≤ f(n) ≤ cg(n) for all n ≥ n0 }

Generally, it is represented as f(n) = O(g(n)). That means, at larger values of n, the upper bound of f(n) is
g(n).

For example, if f(n) = n 4 + 100n 2 + 10n + 50 is the given algorithm, then n 4 is g(n).
That means g(n) gives the maximum rate of growth for f(n) at larger values of n.
Big-O Examples

Example-1 Find upper bound for f(n) = 3n + 8

Solution: 3n + 8 ≤ 4n, for all n ≥ 8

∴ 3n + 8 = O(n) with c = 4 and n0 =


8 Example-2 Find upper bound for
f(n) = n2 + 1 Solution: n2 + 1 ≤ 2n2,

∴ n2 + 1 = O(n2) with c = 2 and n0 = 1


for all n ≥ 1
Big Omega Notation [Lower Bounding Function]

This notation gives the tighter lower bound of the given algorithm and we represent it as f(n) = Ω(g(n)).
Thus, it provides the best-case complexity of an algorithm.

Ω(g(n)) = { f(n): there exist positive constants c


and n0 such that 0 ≤ cg(n) ≤ f(n) for all n ≥ n0 }

That means, at larger values of n, the tighter lower bound of f(n) is g(n).
For example, if f(n) = 100n2 + 10n + 50, g(n) is Ω(n2 ).
Example : 3nlog n−2n is Ω(nlog n).

Solution: 3nlog n− 2n = nlog n+ 2n(logn− 1) ≥ nlogn for n ≥ 2; hence, we can take c =1 and n0 = 2 in this
case

Big-Theta

There is a notation that allows us to say that two functions grow at the same rate, up to constant factors(ie.,
It encloses the function from above and below). Since it represents the upper and the lower bound of the
running time of an algorithm, it is used for sanalyzing the average-case complexity of an algorithm.

Θ(g(n)) = { f(n): there exist positive constants c1, c2 and n0 such that 0 ≤ c1g(n) ≤ f(n) ≤ c2g(n) for
all n ≥ n0 }

f(n) is Θ(g(n)), pronounced “ f(n) is big-Theta of g(n),” if f(n) is O(g(n)) and f(n) is Ω(g(n)) , that is, there
are real constants c > 0 and c > 0, and an integer constant n0 ≥1 such that
Example: 3nlog n+4n+5logn is Θ(nlog n).
Solution: 3nlogn ≤ 3nlog n+4n+5logn ≤ (3+4+5) nlogn for n≥2.
Asymptotic Analysis

There are some general rules to help us determine the running time of an algorithm.

1) Loops: The running time of a loop is, at most, the running time of the statements inside the loop
(including tests) multiplied by the number of iterations.
Example: // Executes n times
For(i=1;i<=n;i++)
M=m+2 //constant time, c

Total time = a constant c × n = c n = O(n).

2) Nested loops: Analyze from the inside out. Total running time is the product of the sizes of all the loops.

Example:

//outer loop
For(i=1;i<=n;i++)
For(j=1;j<=n;j++)
K=k+1 //constant time, c

Total time = c × n × n = c n2 = O(n2 ).

3) Consecutive statements: Add the time complexities of each statement


Example:
X=x+1

For(i=1;i<=n;i++)
M=m+2 //constant time, c
//outer loop

For(i=1;i<=n;i++)
For(j=1;j<=n;j++)
K=k+1 //constant time, c

Total time = c0 + c1n + c2n2 = O(n2 ).

4) If-then-else statements:

Worst-case running time: the test, plus either the then part or the else part (whichever is the larger).

//test : constant If(length()==0)


Return false;
Else:
For(int n=0;n<length();n++) If(!
list[n].equals(otherList.list[n]))
Total time = c0 + c1 + (c2 + c3 ) * n = O(n).
10.RECURSION

Recursion

Recursion is a technique by which a function makes one or more calls to itself during execution, until the
condition gets satisfied. Recursion provides a powerful alternative for performing repetitive tasks.
Example

 The factorial function (commonly denoted as n!) is a classic mathematical function that
has a natural recursive definition.
 An English ruler has a recursive pattern that is a simple example of a fractal structure.
 Binary search is among the most important computer algorithms. It allows us to
efficiently locate a desired value in a data set with upwards of billions of entries.
 The file system for a computer has a recursive structure in which directories can be
nested arbitrarily deeply within other directories. Recursive algorithms are widely used to
explore and manage these file systems.
1) The Factorial Function

The factorial of a positive integer n, denoted n!, is defined as the product of the integers from 1 to n. If n = 0,
then n! is defined as 1 by convention. More formally, for any integer n ≥ 0.

The factorial function is used to find the number of ways in which n distinct items can be arranged into a
sequence, that is, the number of permutations of n items. For example, the three characters a, b, and c can be
arranged in 3! = 3 · 2 · 1 = 6 ways: abc, acb, bac, bca, cab, and cba.
A Recursive Implementation of the Factorial Function

Recursion is not just a mathematical notation; we can use recursion to design a Python implementation of a
factorial function, as shown in Code Fragment 4.1
def factorial(n): if n == 0:
return 1 else:
return n*factorial(n−1)
Trace for the factorial function is,

A recursion trace for the call factorial(5)


2) Drawing an English Ruler
This is to draw the markings of a typical English ruler. For each inch, we place a tick with a numeric label.
We denote the length of the tick designating a whole inch as the major tick length. Between the marks for
whole inches, the ruler contains a series of minor ticks, placed at intervals of 1/2 inch, 1/4 inch, and so on.
In general, an interval with a central tick length L ≥ 1 is composed of:

• An interval with a central tick length L−1


• A single tick of length L
• An interval with a central tick length L−1

Python Code:

def draw_line(tick_length, tick_label=' '):

"""Draw one line with given tick length (followed by optional label)."""

line = '-'* tick_length

if tick_label:

line += ''+ tick_label Output

print(line)

def drawinterval(centerlength):

"""Draw tick interval based upon a central tick length."""

if centerlength > 0: # stop when length drops to 0

drawinterval(centerlength-1) # recursively draw top ticks

draw_line(centerlength) # draw center tick

drawinterval(centerlength-1)

def drawruler(numinches, majorlength):

"""DrawEnglish ruler with given number of inches, major tick length."""

draw_line(majorlength, 0 ) # draw inch 0 line

for j in range(1, 1 + numinches):

drawinterval(majorlength-1) # draw interior ticks for inch

draw_line(majorlength, str(j)) # draw inch j line and label

drawruler(2,4)

11.ANALYZING RECURSIVE ALGORITHMS


Analyzing Recursive Algorithm

Efficiency of the algorithm calculated as big-Oh to summarize the relationship between the number of
operations and the input size for a problem.

With a recursive algorithm, we will account for each operation that is performed based upon the particular
activation of the function that manages the flow of control at the time it is executed. Stated another way,
for each invocation of the function, we only account for the number of operations that are performed within
the body of that activation. We can then account for the overall number of operations that are executed as
part of the recursive algorithm by taking the sum, over all activations, of the number of operations that take
place during each individual activation
Computing Factorials

It is relatively easy to analyze the efficiency of our function for computing factorials.
Sample recursion trace is,

To compute factorial(n), there are a total of n+1 activations, as the parameter decreases from n in the first
call, to n−1 in the second call, and so on, until reaching the base case with parameter 0. Each individual
activation of factorial executes a constant number of operations. Therefore, the overall number of
operations for computing factorial(n) is O(n), as there are n+1 activations, each of which accounts for O(1)
operations.

Drawing an English Ruler

In analyzing the English ruler application, the fundamental question of how many total lines of output are
generated by an initial call to draw interval(c), where c denotes the center length. This is a reasonable
benchmark for the overall efficiency of the algorithm as each line of output is based upon a call to the draw
line utility, and each recursive call to draw interval with nonzero parameter makes exactly one direct call to
draw line. Some intuition may be gained by examining the source code and the recursion trace. We know that
a call to draw interval(c) for c > 0 spawns two calls to draw interval(c−1) and a single call to draw line. We
will rely on this intuition to prove the following claim. Proposition 4.1: For c ≥ 0, a call to draw interval(c)
results in precisely 2c − 1 lines of output

Justification:

In fact, induction is a natural mathematical technique for proving the correctness and efficiency of a
recursive process. In the case of the ruler, we note that an application of draw interval (0) generates no
output, and that 20 −1 = 1−1 = 0. This serves as a base case for our claim. More generally, the number of
lines printed by draw interval(c) is one more than twice the number generated by a call to draw
interval(c−1), as one center line is printed between two such recursive calls. By induction, we have that the
number of lines is thus 1+2 ·(2c−1 −1) = 1+2c −2 = 2c −1. This proof is indicative of a more
mathematically rigorous tool, known as a recurrence equation that can be used to analyze the running time
of a recursive algorithm.
Performing a Binary Search
Considering the running time of the binary search algorithm, a constant number of primitive operations are
executed at each recursive call of method of a binary search. Hence, the running time is proportional to the
number of recursive calls performed. The most log n+1 recursive calls are made during a binary search of a
sequence having n elements, leading to the following claim. The binary search algorithm runs in O(logn)
time for a sorted sequence with n elements.
Justification:
Each recursive call the number of candidate entries still to be searched is given by the value high−low+1.
Moreover, the number of remaining candidates is reduced by at least one half with each recursive call.
Specifically, from the definition of mid, the number of remaining candidates is either.

Initially, the number of candidates is n; after the first call in a binary search, it is at most n/2; after the
second call, it is at most n/4; and so on. In general, after the j th call in a binary search, the number of
candidate entries remaining is at most n/2j . In the worst case (an unsuccessful search), the recursive calls
stop when there are no more candidate entries. Hence, the maximum number of recursive calls performed,
is the smallest integer r such that n 2r < 1. In other words (recalling that we omit a logarithm’s base when it
is 2), r > logn. Thus, we have r = logn+1, which implies that binary search runs in O(logn) time
Computing Disk Space Usage
To characterize the “problem size” for our analysis, we let n denote the number of file- system entries in
the portion of the file system that is considered. (For example, the file system portrayed in Figure 4.6 has n
= 19 entries.) To characterize the cumulative time spent for an initial call to the disk usage function, we
must analyze the total number of recursive invocations that are made, as well as the number of operations
that are executed within those invocations.

Intuitively, a call to disk usage for a particular entry ‘e’ of the file system is only made from ‘e’, and that
entry will only be explored once.

The fact that each iteration of that loop makes a recursive call to disk usage, and yet we have already
concluded that there are a total of n calls to disk usage (including the original call). We therefore conclude
that there are O(n) recursive calls, each of which uses O(1) time outside the loop, and that the overall
number of operations due to the loop is O(n). Summing all of these bounds, the overall number of
operations is O(n)

You might also like