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

Unit 1 DSD Notes It Covers All The Topics in First Unit

The document provides an overview of the first unit of a Data Structures Design course, focusing on Abstract Data Types (ADTs), their classifications, and the principles of Object-Oriented Programming (OOP). It discusses the importance of data structures in organizing and managing data efficiently, as well as the concepts of modularity, abstraction, and encapsulation in software design. Additionally, it covers the implementation of classes in Python, including the use of instance and class variables.

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 views35 pages

Unit 1 DSD Notes It Covers All The Topics in First Unit

The document provides an overview of the first unit of a Data Structures Design course, focusing on Abstract Data Types (ADTs), their classifications, and the principles of Object-Oriented Programming (OOP). It discusses the importance of data structures in organizing and managing data efficiently, as well as the concepts of modularity, abstraction, and encapsulation in software design. Additionally, it covers the implementation of classes in Python, including the use of instance and class variables.

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
You are on page 1/ 35

Unit 1 dsd notes - It covers all the topics in first unit

Data structures design (Anna University)

Scan to open on Studocu

Studocu is not sponsored or endorsed by any college or university


Downloaded by Darla Krisy
AD3251 DATA STRUCTURES DESIGN
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.
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.
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.

INTRODUCTION TO OBJECT ORIENTED PROGRAMMING


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.
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 evolvability). 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.
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.
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
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()

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
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()

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.

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.

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

class MultiDerived(SuperClass1, SuperClass2):


# features of SuperClass1 + SuperClass2 + MultiDerived class

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.

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)
In this example, the change made in the list did affect another list, indicating the list is shallowly
copied. The shallow copy constructs a new compound object and then (to the extent possible)
inserts references into it to the objects found in the original.
Example:
import copy
li1 = [1, 2, [3,5], 4]
li2 = copy.copy(li1)
print ("The original elements before shallow copying")
for i in range(0,len(li1)):
print (li1[i],end=" ")
print("\r")
li2[2][0] = 7
print ("The original elements after shallow copying")
for i in range(0,len( li1)):
print (li1[i],end=" ")
Output:
The original elements before shallow copying
1 2 [3, 5] 4
The original elements after shallow copying
1 2 [7, 5] 4

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.

INTRODUCTION TO ANALYSIS OF ALGORITHMS


• To bake a cake, we can get different recipes from the internet. We can find ‘n’ number of
steps for different varieties of cakes. All those different step by step procedure to make a
cake can be called as an algorithm. We can choose a simple, easy and most convenient way
to make a cake.
• Similarly in computer science, multiple algorithms are available for solving the same
problem. The algorithm analysis helps us to determine which algorithm is most efficient
in terms of running time, memory and space consumed. etc.,

Why Analyze an Algorithm?


• The most straightforward reason for analyzing an algorithm is to discover its characteristics
in order to evaluate its suitability for various applications or compare it with other algorithms
for the same application. Moreover, the analysis of an algorithm can help us to understand it
better, and can suggest informed improvements. Algorithms tend to become shorter, simpler,
and more elegant during the analysis process.
• The efficiency of an algorithm can be decided based on
1. Amount of time required by an algorithm to execute.
2. Amount of storage required by an algorithm.
3. Size of the input set.
Complexities of an Algorithm:
• The complexity of an algorithm computes the amount of time and spaces required by
an algorithm for an input of size (n). The complexity of an algorithm can be two types.
Time Complexity:
Time complexity measures the amount of time required to run an algorithm, as input size of the
algorithm increases.
Space Complexity:
Space complexity measures the total amount of memory that an algorithm or operation needs to run
according to its input size.
Rate of Growth:
• The rate at which the performance of an algorithm increases as a function of input size
is called as rate of growth. The commonly used rate of growth is,
• The relationship between different rates of growth is given as,
Time complexity Name

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

The Big (O) Notation


Big Oh is an Asymptotic Notation for the worst-case scenario. The Big-O notation defines
asymptotic upper bound of an algorithm, it bounds a function only from above.

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 Mathematical Definition of Big-Oh


f(n) ∈ O(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 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)

You might also like