0% found this document useful (0 votes)
21 views66 pages

Qbank DSD Cia-1

Uploaded by

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

Qbank DSD Cia-1

Uploaded by

Tamil K
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 66

PART-A

1. Abstract Data type (ADT) is a type (or class) for objects whose behavior is defined by a
set of values and a set of operations. 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.
2. In Python object-oriented Programming (OOPs) is a programming paradigm that uses
objects and classes in programming. It aims to implement real-world entities like
inheritance, polymorphisms, encapsulation, abstraction,etc. in the programming. The
main concept of object-oriented Programming (OOPs) or oops concepts in Python is to
bind the data and the functions that work together as a single unit so that no other part of
the code can access this data.
3. In Python, a class is a user-defined data type that contains both the data itself and the
methods that may be used to manipulate it. In a sense, classes serve as a template to create
objects. They provide the characteristics and operations that the objects will employ.
4. Objects are key to understanding object-oriented technology. The purpose of the object-
oriented programming is to implement the real word entities in programming. It also
emphasis on the binding of data. There are various OOPs concepts among them Object is
one of them.
5. One of the core concepts in object-oriented programming (OOP) languages is
inheritance. It is a mechanism that allows you to create a hierarchy of classes that share a
set of properties and methods by deriving a class from another class. Inheritance is the
capability of one class to derive or inherit the properties from another class.
6. The advantages of using classes in python are,
• Cross-Platform Compatibility
• Strong Community Support
• Integration and Extensibility
• Scalability and Performance
• Versatility and Flexibility

7.

Benefits of Super Function

• Need not remember or specify the parent class name to access its methods. This
function can be used both in single and multiple inheritances.
• This implements modularity (isolating changes) and code reusability as there is no
need to rewrite the entire function.

8.The definition for shallow copy is given below

• A shallow copy means constructing a new collection object and then populating it with
references to the child objects found in the original. In essence, a shallow copy is only one
level deep. The copying process does not recurse and therefore won’t create copies of the
child objects themselves. Making a shallow copy of an object won’t clone child objects.
Therefore, the copy is not fully independent of the original.

9. The definition for deep copy is given below


• A deep copy makes the copying process recursive. It means first constructing a new
collection object and then recursively populating it with copies of the child objects found
in the original. Copying an object this way walks the whole object tree to create a fully
independent clone of the original object and all of its children. A deep copy of an object
will recursively clone child objects. The clone is fully independent of the original, but
creating a deep copy is slower.

10. The main idea of asymptotic analysis is to have a measure of the efficiency of algorithms
that don’t depend on machine-specific constants and don’t require algorithms to be implemented
and time taken by programs to be compared.There are mainly three asymptotic notations:
1. Big-O Notation (O-notation)
2. Omega Notation (Ω-notation)
3. Theta Notation (Θ-notation)

11. Disadvantages of Recursion:

• Recursion can be less efficient than iterative solutions in terms of memory and
performance.
• Recursive functions can be more challenging to debug and understand than iterative
solutions.
• Recursion can lead to stack overflow errors if the recursion depth is too high.

12. The data is generally stored in key sequence in a list which has a head structure consisting
of count, pointers and address of compare function needed to compare the data in the list.
• The data node contains the pointer to a data structure and a self-referential
pointer which points to the next node in the list.

13. The basic operations in the Arrays are insertion, deletion, searching, display, traverse, and
update. These operations are usually performed to either modify the data in the array or to report
the status of the array

14. A linked list is a data structure that stores a sequence of elements. Each element in the list is
called a node, and each node has a reference to the next node in the list. The first node in the list is
called the head, and the last node in the list is called the tail.

15. There are four key types of linked lists:

• Singly linked lists

• Doubly linked lists

• Circular linked lists

• Circular doubly linked lists

PART-B

16.a) An Abstract Data Type (ADT) is a programming concept that defines a high-level view of a
data structure, without specifying the implementation details. In other words, it is a blueprint for
creating a data structure that defines the behavior and interface of the structure, without specifying
how it is implemented.

An ADT in the data structure can be thought of as a set of operations that can be performed on a
set of values. This set of operations actually defines the behavior of the data structure, and they
are used to manipulate the data in a way that suits the needs of the program.
ADTs are often used to abstract away the complexity of a data structure and to provide a simple
and intuitive interface for accessing and manipulating the data. This makes it easier for
programmers to reason about the data structure, and to use it correctly in their programs.

Examples of abstract data type in data structures are List, Stack, Queue, etc.

Abstract Data Type Model

List ADT

Lists are linear data structures that hold data in a non-continuous structure. The list is made up of
data storage containers known as "nodes." These nodes are linked to one another, which means
that each node contains the address of another block. All of the nodes are thus connected to one
another via these links. You can discover more about lists in this article: Linked List Data
Structure.

Some of the most essential operations defined in List ADT are listed below.

• front(): returns the value of the node present at the front of the list.

• back(): returns the value of the node present at the back of the list.

• push_front(int val): creates a pointer with value = val and keeps this pointer to the front
of the linked list.

• push_back(int val): creates a pointer with value = val and keeps this pointer to the back
of the linked list.

• pop_front(): removes the front node from the list.

• pop_back(): removes the last node from the list.


• empty(): returns true if the list is empty, otherwise returns false.

• size(): returns the number of nodes that are present in the list.

Stack ADT

A stack is a linear data structure that only allows data to be accessed from the top. It simply has
two operations: push (to insert data to the top of the stack) and pop (to remove data from the
stack). (used to remove data from the stack top).

Some of the most essential operations defined in Stack ADT are listed below.

• top(): returns the value of the node present at the top of the stack.

• push(int val): creates a node with value = val and puts it at the stack top.

• pop(): removes the node from the top of the stack.

• empty(): returns true if the stack is empty, otherwise returns false.

• size(): returns the number of nodes that are present in the stack.

Queue ADT

A queue is a linear data structure that allows data to be accessed from both ends. There are two
main operations in the queue: push (this operation inserts data to the back of the queue) and pop
(this operation is used to remove data from the front of the queue).

Some of the most essential operations defined in Queue ADT are listed below.
• front(): returns the value of the node present at the front of the queue.

• back(): returns the value of the node present at the back of the queue.

• push(int val): creates a node with value = val and puts it at the front of the queue.

• pop(): removes the node from the rear of the queue.

• empty(): returns true if the queue is empty, otherwise returns false.

• size(): returns the number of nodes that are present in the queue.

Advantages of ADT in Data Structures

The advantages of ADT in Data Structures are:

• Provides abstraction, which simplifies the complexity of the data structure and allows
users to focus on the functionality.

• Enhances program modularity by allowing the data structure implementation to be


separate from the rest of the program.

• Enables code reusability as the same data structure can be used in multiple programs with
the same interface.

• Promotes the concept of data hiding by encapsulating data and operations into a single
unit, which enhances security and control over the data.

• Supports polymorphism, which allows the same interface to be used with different
underlying data structures, providing flexibility and adaptability to changing requirements.

Disadvantages of ADT in Data Structures

There are some potential disadvantages of ADT in Data Structures:

• Overhead: Using ADTs may result in additional overhead due to the need for abstraction
and encapsulation.

• Limited control: ADTs can limit the level of control that a programmer has over the data
structure, which can be a disadvantage in certain scenarios.

• Performance impact: Depending on the specific implementation, the performance of an


ADT may be lower than that of a custom data structure designed for a specific application.

Conclusion
Abstract Data Types (ADTs) stand as a cornerstone in computer science, offering a high-level
blueprint for organizing and manipulating data. By encapsulating data and operations, ADTs
enable developers to create robust, reusable, and scalable solutions without concerning themselves
with intricate implementation details.

In essence, ADTs provide a bridge between the theoretical and practical realms, fostering the
development of versatile data structures and algorithms. Embracing ADTs empowers
programmers to focus on problem-solving and efficient design while enhancing the
maintainability and flexibility of their codebases.

As technology evolves, the significance of ADTs persists, guiding the creation of innovative
software systems and ensuring data is managed and manipulated effectively. Understanding and
harnessing the power of ADTs is key to navigating the complexities of modern computing.
16.b)

The class creates a user-defined data structure, which holds its own data members and member
functions, which can be accessed and used by creating an instance of that class. A class is like a
blueprint for an object.
Some points on Python class:
• Classes are created by keyword class.
• Attributes are the variables that belong to a class.
• Attributes are always public and can be accessed using the dot (.) operator. Eg.: My
class.Myattribute
Creating a Python Class
Here, the class keyword indicates that you are creating a class followed by the name of the class
(Dog in this case).
Python3
class Dog:
sound = "bark"
Object of Python Class
An Object is an instance of a Class. A class is like a blueprint while an instance is a copy of the
class with actual values. It’s not an idea anymore, it’s an actual dog, like a dog of breed pug who’s
seven years old. You can have many dogs to create many different instances, but without the class
as a guide, you would be lost, not knowing what information is required.
An object consists of:
• State: It is represented by the attributes of an object. It also reflects the properties of
an object.
• Behavior: It is represented by the methods of an object. It also reflects the response
of an object to other objects.
• Identity: It gives a unique name to an object and enables one object to interact with
other objects.

Declaring Class Objects (Also called instantiating a class)


When an object of a class is created, the class is said to be instantiated. All the instances share the
attributes and the behavior of the class. But the values of those attributes, i.e. the state are unique
for each object. A single class may have any number of instances.
Example:
Example of Python Class and object
Creating an object in Python involves instantiating a class to create a new instance of that class.
This process is also referred to as object instantiation.
Python3
# Python3 program to
# demonstrate instantiating
# a class
class Dog:

# A simple class
# attribute
attr1 = "mammal"
attr2 = "dog"

# A sample method
def fun(self):
print("I'm a", self.attr1)
print("I'm a", self.attr2)

# Driver code
# Object instantiation
Rodger = Dog()

# Accessing class attributes


# and method through objects
print(Rodger.attr1)
Rodger.fun()
Output:
mammal
I'm a mammal
I'm a dog
In the above example, an object is created which is basically a dog named Rodger. This class only
has two class attributes that tell us that Rodger is a dog and a mammal.
Explanation :
In this example, we are creating a Dog class and we have created two class variables attr1 and
attr2. We have created a method named fun() which returns the string “I’m a, {attr1}” and I’m
a, {attr2}. We have created an object of the Dog class and we are printing at the attr1 of the
object. Finally, we are calling the fun() function.
Self Parameter
When we call a method of this object as myobject.method(arg1, arg2), this is automatically
converted by Python into MyClass.method(myobject, arg1, arg2) – this is all the special self is
about.
Python3
class GFG:
def __init__(self, name, company):
self.name = name
self.company = company

def show(self):
print("Hello my name is " + self.name+" and I" +
" work in "+self.company+".")

obj = GFG("John", "GeeksForGeeks")


obj.show()
The Self Parameter does not call it to be Self, You can use any other name instead of it. Here we
change the self to the word someone and the output will be the same.
Python3
class GFG:
def __init__(somename, name, company):
somename.name = name
somename.company = company

def show(somename):
print("Hello my name is " + somename.name +
" and I work in "+somename.company+".")

obj = GFG("John", "GeeksForGeeks")


obj.show()
Output: Output for both of the codes will be the same.
Hello my name is John and I work in GeeksForGeeks.
Explanation:
In this example, we are creating a GFG class and we have created the name, and
company instance variables in the constructor. We have created a method named say_hi() which
returns the string “Hello my name is ” + {name} +” and I work in “+{company}+”.”.We have
created a person class object and we passing the name John and Company GeeksForGeeks to
the instance variable. Finally, we are calling the show() of the class.
Pass Statement
The program’s execution is unaffected by the pass statement’s inaction. It merely permits the
program to skip past that section of the code without doing anything. It is frequently employed
when the syntactic constraints of Python demand a valid statement but no useful code must be
executed.
Python3
class MyClass:
pass
__init__() method
The __init__ method is similar to constructors in C++ and Java. Constructors are used to
initializing the object’s state. Like methods, a constructor also contains a collection of
statements(i.e. instructions) that are executed at the time of Object creation. It runs as soon as an
object of a class is instantiated. The method is useful to do any initialization you want to do with
your object.
Python3
# Sample class with init method
class Person:

# init method or constructor


def __init__(self, name):
self.name = name

# Sample Method
def say_hi(self):
print('Hello, my name is', self.name)

p = Person('Nikhil')
p.say_hi()
Output:
Hello, my name is Nikhil
Explanation:
In this example, we are creating a Person class and we have created a name instance variable in
the constructor. We have created a method named as say_hi() which returns the string “Hello, my
name is {name}”.We have created a person class object and we pass the name Nikhil to the
instance variable. Finally, we are calling the say_hi() of the class.
__str__() method
Python has a particular method called __str__(). that is used to define how a class object should
be represented as a string. It is often used to give an object a human-readable textual
representation, which is helpful for logging, debugging, or showing users object information.
When a class object is used to create a string using the built-in functions print() and str(),
the __str__() function is automatically used. You can alter how objects of a class are represented
in strings by defining the __str__() method.
Python3
class GFG:
def __init__(self, name, company):
self.name = name
self.company = company

def __str__(self):
return f"My name is {self.name} and I work in {self.company}."

my_obj = GFG("John", "GeeksForGeeks")


print(my_obj)
Output:
My name is John and I work in GeeksForGeeks.
Explanation:
In this example, We are creating a class named GFG.In the class, we are creating two instance
variables name and company. In the __str__() method we are returning the name instance
variable and company instance variable. Finally, we are creating the object of GFG class and we
are calling the __str__() method.
Class and Instance Variables
Instance variables are for data, unique to each instance and class variables are for attributes and
methods shared by all instances of the class. Instance variables are variables whose value is
assigned inside a constructor or method with self whereas class variables are variables whose
value is assigned in the class.
Defining instance variables using a constructor.
Python3
# Python3 program to show that the variables with a value
# assigned in the class declaration, are class variables and
# variables inside methods and constructors are instance
# variables.

# Class for Dog

class Dog:

# Class Variable
animal = 'dog'

# The init method or constructor


def __init__(self, breed, color):

# Instance Variable
self.breed = breed
self.color = color
# Objects of Dog class
Rodger = Dog("Pug", "brown")
Buzo = Dog("Bulldog", "black")

print('Rodger details:')
print('Rodger is a', Rodger.animal)
print('Breed: ', Rodger.breed)
print('Color: ', Rodger.color)

print('\nBuzo details:')
print('Buzo is a', Buzo.animal)
print('Breed: ', Buzo.breed)
print('Color: ', Buzo.color)

# Class variables can be accessed using class


# name also
print("\nAccessing class variable using class name")
print(Dog.animal)
Output:
Rodger details:
Rodger is a dog
Breed: Pug
Color: brown
Buzo details:
Buzo is a dog
Breed: Bulldog
Color: black
Accessing class variable using class name
dog
Explanation:
A class named Dog is defined with a class variable animal set to the string “dog”. Class variables
are shared by all objects of a class and can be accessed using the class name. Dog class has two
instance variables breed and color. Later we are creating two objects of the Dog class and we
are printing the value of both objects with a class variable named animal.
Defining instance variables using the normal method:
Python3
# Python3 program to show that we can create
# instance variables inside methods

# Class for Dog

class Dog:

# Class Variable
animal = 'dog'

# The init method or constructor


def __init__(self, breed):

# Instance Variable
self.breed = breed

# Adds an instance variable


def setColor(self, color):
self.color = color
# Retrieves instance variable
def getColor(self):
return self.color

# Driver Code
Rodger = Dog("pug")
Rodger.setColor("brown")
print(Rodger.getColor())
Output:
brown
Explanation:
In this example, We have defined a class named Dog and we have created a class variable animal.
We have created an instance variable breed in the constructor. The class Dog consists of two
methods setColor and getColor, they are used for creating and initializing an instance variable
and retrieving the value of the instance variable. We have made an object of the Dog class and
we have set the instance variable value to brown and we are printing the value in the terminal.

17.a) Types Of Inheritance:-


1. Single inheritance
2. Multilevel inheritance
3. Multiple inheritance
4. Hierarchical inheritance
5. Hybrid inheritance
Types of Inheritance in C++
1. Single Inheritance: In single inheritance, a class is allowed to inherit from only one class. i.e.
one subclass is inherited by one base class only.

Syntax:
class subclass_name : access_mode base_class
{
// body of subclass
};
OR
class A
{
... .. ...
};
class B: public A
{
... .. ...
};
• CPP
// C++ program to explain
// Single inheritance
#include<iostream>
using namespace std;

// base class
class Vehicle {
public:
Vehicle()
{
cout << "This is a Vehicle\n";
}
};

// sub class derived from a single base classes


class Car : public Vehicle {

};

// main function
int main()
{
// Creating object of sub class will
// invoke the constructor of base classes
Car obj;
return 0;
}

Output
This is a Vehicle

• C++

// Example:

#include<iostream>
using namespace std;

class A
{
protected:
int a;

public:
void set_A()
{
cout<<"Enter the Value of A=";
cin>>a;

}
void disp_A()
{
cout<<endl<<"Value of A="<<a;
}
};

class B: public A
{
int b,p;

public:
void set_B()
{
set_A();
cout<<"Enter the Value of B=";
cin>>b;
}

void disp_B()
{
disp_A();
cout<<endl<<"Value of B="<<b;
}

void cal_product()
{
p=a*b;
cout<<endl<<"Product of "<<a<<" * "<<b<<" = "<<p;
}

};

main()
{

B _b;
_b.set_B();
_b.cal_product();

return 0;

Output:- Enter the Value of A= 3 3 Enter the Value of B= 5 5 Product of 3 * 5 = 15

• C++

// Example:

#include<iostream>
using namespace std;

class A
{
protected:
int a;
public:
void set_A(int x)
{
a=x;
}

void disp_A()
{
cout<<endl<<"Value of A="<<a;
}
};

class B: public A
{
int b,p;

public:
void set_B(int x,int y)
{
set_A(x);
b=y;
}

void disp_B()
{
disp_A();
cout<<endl<<"Value of B="<<b;
}

void cal_product()
{
p=a*b;
cout<<endl<<"Product of "<<a<<" * "<<b<<" = "<<p;
}

};

main()
{
B _b;
_b.set_B(4,5);
_b.cal_product();

return 0;
}

Output
Product of 4 * 5 = 20

2. Multiple Inheritance: Multiple Inheritance is a feature of C++ where a class can inherit from
more than one class. i.e one subclass is inherited from more than one base class.
Syntax:
class subclass_name : access_mode base_class1, access_mode base_class2, ....
{
// body of subclass
};
class B
{
... .. ...
};
class C
{
... .. ...
};
class A: public B, public C
{
... ... ...
};
Here, the number of base classes will be separated by a comma (‘, ‘) and the access mode for
every base class must be specified.
• CPP

// C++ program to explain


// multiple inheritance
#include <iostream>
using namespace std;

// first base class


class Vehicle {
public:
Vehicle() { cout << "This is a Vehicle\n"; }
};

// second base class


class FourWheeler {
public:
FourWheeler()
{
cout << "This is a 4 wheeler Vehicle\n";
}
};

// sub class derived from two base classes


class Car : public Vehicle, public FourWheeler {
};

// main function
int main()
{
// Creating object of sub class will
// invoke the constructor of base classes.
Car obj;
return 0;
}

Output
This is a Vehicle
This is a 4 wheeler Vehicle

• C++

// Example:

#include<iostream>
using namespace std;

class A
{
protected:
int a;

public:
void set_A()
{
cout<<"Enter the Value of A=";
cin>>a;

void disp_A()
{
cout<<endl<<"Value of A="<<a;
}
};

class B: public A
{
protected:
int b;

public:
void set_B()
{
cout<<"Enter the Value of B=";
cin>>b;
}

void disp_B()
{
cout<<endl<<"Value of B="<<b;
}
};

class C: public B
{
int c,p;

public:
void set_C()
{
cout<<"Enter the Value of C=";
cin>>c;
}

void disp_C()
{
cout<<endl<<"Value of C="<<c;
}

void cal_product()
{
p=a*b*c;
cout<<endl<<"Product of "<<a<<" * "<<b<<" * "<<c<<" = "<<p;
}
};

main()
{

C _c;
_c.set_A();
_c.set_B();
_c.set_C();
_c.disp_A();
_c.disp_B();
_c.disp_C();
_c.cal_product();

return 0;

To know more about it, please refer to the article Multiple Inheritances.
3. Multilevel Inheritance: In this type of inheritance, a derived class is created from another
derived class.
Syntax:-
class C
{
... .. ...
};
class B:public C
{
... .. ...
};
class A: public B
{
... ... ...
};
• CPP

// C++ program to implement


// Multilevel Inheritance
#include <iostream>
using namespace std;

// base class
class Vehicle {
public:
Vehicle() { cout << "This is a Vehicle\n"; }
};

// first sub_class derived from class vehicle


class fourWheeler : public Vehicle {
public:
fourWheeler()
{
cout << "Objects with 4 wheels are vehicles\n";
}
};
// sub class derived from the derived base class fourWheeler
class Car : public fourWheeler {
public:
Car() { cout << "Car has 4 Wheels\n"; }
};

// main function
int main()
{
// Creating object of sub class will
// invoke the constructor of base classes.
Car obj;
return 0;
}

Output
This is a Vehicle
Objects with 4 wheels are vehicles
Car has 4 Wheels
4. Hierarchical Inheritance: In this type of inheritance, more than one subclass is inherited from
a single base class. i.e. more than one derived class is created from a single base class.

Syntax:-
class A
{
// body of the class A.
}
class B : public A
{
// body of class B.
}
class C : public A
{
// body of class C.
}
class D : public A
{
// body of class D.
}
• CPP

// C++ program to implement


// Hierarchical Inheritance
#include <iostream>
using namespace std;

// base class
class Vehicle {
public:
Vehicle() { cout << "This is a Vehicle\n"; }
};

// first sub class


class Car : public Vehicle {
};

// second sub class


class Bus : public Vehicle {
};

// main function
int main()
{
// Creating object of sub class will
// invoke the constructor of base class.
Car obj1;
Bus obj2;
return 0;
}

Output
This is a Vehicle
This is a Vehicle

5. Hybrid (Virtual) Inheritance: Hybrid Inheritance is implemented by combining more than


one type of inheritance. For example: Combining Hierarchical inheritance and Multiple
Inheritance.
Below image shows the combination of hierarchical and multiple inheritances:
• CPP

// C++ program for Hybrid Inheritance

#include <iostream>
using namespace std;

// base class
class Vehicle {
public:
Vehicle() { cout << "This is a Vehicle\n"; }
};

// base class
class Fare {
public:
Fare() { cout << "Fare of Vehicle\n"; }
};

// first sub class


class Car : public Vehicle {
};

// second sub class


class Bus : public Vehicle, public Fare {
};

// main function
int main()
{
// Creating object of sub class will
// invoke the constructor of base class.
Bus obj2;
return 0;
}

Output
This is a Vehicle
Fare of Vehicle

• C++

// Example:

#include <iostream>
using namespace std;

class A
{
protected:
int a;
public:
void get_a()
{
cout << "Enter the value of 'a' : ";
cin>>a;
}
};

class B : public A
{
protected:
int b;
public:
void get_b()
{
cout << "Enter the value of 'b' : ";
cin>>b;
}
};
class C
{
protected:
int c;
public:
void get_c()
{
cout << "Enter the value of c is : ";
cin>>c;
}
};

class D : public B, public C


{
protected:
int d;
public:
void mul()
{
get_a();
get_b();
get_c();
cout << "Multiplication of a,b,c is : " <<a*b*c;
}
};

int main()
{
D d;
d.mul();
return 0;
}

6. A special case of hybrid inheritance: Multipath inheritance:


A derived class with two base classes and these two base classes have one common base class is
called multipath inheritance. Ambiguity can arise in this type of inheritance.
Example:
• CPP

// C++ program demonstrating ambiguity in Multipath


// Inheritance

#include <iostream>
using namespace std;

class ClassA {
public:
int a;
};

class ClassB : public ClassA {


public:
int b;
};

class ClassC : public ClassA {


public:
int c;
};

class ClassD : public ClassB, public ClassC {


public:
int d;
};

int main()
{
ClassD obj;

// obj.a = 10; // Statement 1, Error


// obj.a = 100; // Statement 2, Error

obj.ClassB::a = 10; // Statement 3


obj.ClassC::a = 100; // Statement 4

obj.b = 20;
obj.c = 30;
obj.d = 40;

cout << " a from ClassB : " << obj.ClassB::a;


cout << "\n a from ClassC : " << obj.ClassC::a;

cout << "\n b : " << obj.b;


cout << "\n c : " << obj.c;
cout << "\n d : " << obj.d << '\n';
}

Output
a from ClassB : 10
a from ClassC : 100
b : 20
c : 30
d : 40
Output:
a from ClassB : 10
a from ClassC : 100
b : 20
c : 30
d : 40
In the above example, both ClassB and ClassC inherit ClassA, they both have a single copy of
ClassA. However Class-D inherits both ClassB and ClassC, therefore Class-D has two copies of
ClassA, one from ClassB and another from ClassC.
If we need to access the data member of ClassA through the object of Class-D, we must specify
the path from which a will be accessed, whether it is from ClassB or ClassC, bcoz compiler can’t
differentiate between two copies of ClassA in Class-D.
There are 2 Ways to Avoid this Ambiguity:
1) Avoiding ambiguity using the scope resolution operator: Using the scope resolution
operator we can manually specify the path from which data member a will be accessed, as shown
in statements 3 and 4, in the above example.
• CPP

obj.ClassB::a = 10; // Statement 3


obj.ClassC::a = 100; // Statement 4

Note: Still, there are two copies of ClassA in Class-D.


2) Avoiding ambiguity using the virtual base class:
• CPP

#include<iostream>

class ClassA
{
public:
int a;
};

class ClassB : virtual public ClassA


{
public:
int b;
};

class ClassC : virtual public ClassA


{
public:
int c;
};

class ClassD : public ClassB, public ClassC


{
public:
int d;
};
int main()
{
ClassD obj;

obj.a = 10; // Statement 3


obj.a = 100; // Statement 4

obj.b = 20;
obj.c = 30;
obj.d = 40;

cout << "\n a : " << obj.a;


cout << "\n b : " << obj.b;
cout << "\n c : " << obj.c;
cout << "\n d : " << obj.d << '\n';
}

Output:
a : 100
b : 20
c : 30
d : 40
According to the above example, Class-D has only one copy of ClassA, therefore, statement 4
will overwrite the value of a, given in statement 3.

17.b) Accessing Parent Class Functions


When a class inherits from another class it inherits the attributes and methods of another class.
A class that inherits from another class is known as child class and the class from which the
child class inherits is known as Parent class. But have you ever wondered how to access the
parent’s class methods? This is really simple, you just have to call the constructor of parent
class inside the constructor of child class and then the object of a child class can access the
methods and attributes of the parent class.
Example:

# Python code to demonstrate


# how parent constructors are called.

# parent class
class Person( object ):

# __init__ is known as the constructor


def __init__(self, name, idnumber):
self.name = name
self.idnumber = idnumber

def display(self):
print(self.name)
print(self.idnumber)

# child class
class Employee( Person ):
def __init__(self, name, idnumber, salary):
self.salary = salary

# invoking the constructor of


# the parent class
Person.__init__(self, name, idnumber)

def show(self):
print(self.salary)

# creation of an object
# variable or an instance
a = Employee('Rahul', 886012, 30000000)

# calling a function of the


# class Person using Employee's
# class instance
a.display()
a.show()

Output:
Rahul
886012
30000000
Note: For more information, refer to Inheritance in Python.
Accessing Parent class method from inner class
An inner class or nested class is a defined inside the body of another class. If an object is
created using a class, the object inside the root class can be used. A class can have one or more
than one inner classes.
Types of Inner Classes:
• Multiple Inner Class
• Multilevel Inner Class
Multiple Inner Class: A class containing more than one inner class.
Example:

class Electronics:
def __init__(self):
print('SINGLA ELECTRONICS')
self.laptop=self.Laptop()
self.mobile=self.Mobile()

# Inner Class 1
class Laptop:
def operation(self):
print('DELL Inspiron 15')

# Inner Class 2
class Mobile:
def operation(self):
print('Redmi Note 5')

# Driver Code
ele = Electronics()
ele.laptop.operation()
ele.mobile.operation()

Output:
SINGLA ELECTRONICS
DELL Inspiron 15
Redmi Note 5
Multilevel Inner Class: In multilevel inner classes, the inner class contains another class which
is inner classes to the previous one.

Example:

class Vehicle:

def __init__(self):

# instantiating the 'Inner' class


self.inner = self.Car()

# instantiating the multilevel


# 'InnerInner' class
self.innerinner = self.inner.Maruti()

def show_classes(self):
print("This is in Outer class that is Vehicle")
# inner class
class Car:
# First Inner Class

def __init__(self):

# instantiating the
# 'InnerInner' class
self.innerinner = self.Maruti()

def show_classes(self):
print("This is in Inner class that is Car")

# multilevel inner class


class Maruti:

def inner_display(self, msg):


print("This is in multilevel InnerInner\
class that is Maruti")
print(msg)

# Driver Code
outer = Vehicle()
outer.show_classes()
inner = outer.Car()
inner.show_classes()
innerinner = inner.Maruti()

# Calling the method inner_display


innerinner.inner_display("Just Print It!")

Output:
This is in Outer class that is Vehicle
This is in Inner class that is Car
This is in multilevel InnerInner class that is Maruti
Just Print It!
18.a)
What is namespace:
A namespace is a system that has a unique name for each and every object in Python. An object
might be a variable or a method. Python itself maintains a namespace in the form of a Python
dictionary. Let’s go through an example, a directory-file system structure in computers.
Needless to say, that one can have multiple directories having a file with the same name inside
every directory. But one can get directed to the file, one wishes, just by specifying the absolute
path to the file.
Real-time example, the role of a namespace is like a surname. One might not find a single
“Alice” in the class there might be multiple “Alice” but when you particularly ask for “Alice
Lee” or “Alice Clark” (with a surname), there will be only one (time being don’t think of both
first name and surname are same for multiple students).
On similar lines, the Python interpreter understands what exact method or variable one is trying
to point to in the code, depending upon the namespace. So, the division of the word itself gives
a little more information. Its Name (which means name, a unique identifier) + Space(which
talks something related to scope). Here, a name might be of any Python method or variable and
space depends upon the location from where is trying to access a variable or a method.

Types of namespaces :

When Python interpreter runs solely without any user-defined modules, methods, classes, etc.
Some functions like print(), id() are always present, these are built-in namespaces. When a user
creates a module, a global namespace gets created, later the creation of local functions creates
the local namespace. The built-in namespace encompasses the global namespace and the
global namespace encompasses the local namespace.

The lifetime of a namespace :


A lifetime of a namespace depends upon the scope of objects, if the scope of an object ends, the
lifetime of that namespace comes to an end. Hence, it is not possible to access the inner
namespace’s objects from an outer namespace.

Example:
• Python3

# var1 is in the global namespace


var1 = 5
def some_func():

# var2 is in the local namespace


var2 = 6
def some_inner_func():

# var3 is in the nested local


# namespace
var3 = 7

As shown in the following figure, the same object name can be present in multiple namespaces
as isolation between the same name is maintained by their namespace.

But in some cases, one might be interested in updating or processing global variables only, as
shown in the following example, one should mark it explicitly as global and the update or
process. Note that the line “count = count +1” references the global variable and therefore uses
the global variable, but compare this to the same line written “count = 1”. Then the line “global
count” is absolutely needed according to scope rules.

• Python3
# Python program processing
# global variable

count = 5
def some_method():
global count
count = count + 1
print(count)
some_method()

Output:
6
Scope of Objects in Python :

Scope refers to the coding region from which a particular Python object is accessible. Hence one
cannot access any particular object from anywhere from the code, the accessing has to be
allowed by the scope of the object.
Let’s take an example to have a detailed understanding of the same:

Example 1:
• Python3

# Python program showing


# a scope of object

def some_func():
print("Inside some_func")
def some_inner_func():
var = 10
print("Inside inner function, value of var:",var)
some_inner_func()
print("Try printing var from outer function: ",var)
some_func()

Output:
Inside some_func
Inside inner function, value of var: 10

Traceback (most recent call last):


File "/home/1eb47bb3eac2fa36d6bfe5d349dfcb84.py", line 8, in
some_func()
File "/home/1eb47bb3eac2fa36d6bfe5d349dfcb84.py", line 7, in some_func
print("Try printing var from outer function: ",var)
NameError: name 'var' is not defined

18.b)
Last Updated : 08 Jun, 2023

••
Shallow Copy: Shallow repetition is quicker. However, it’s “lazy” it handles
pointers and references. Rather than creating a contemporary copy of the
particular knowledge the pointer points to, it simply copies over the pointer price.
So, each of the first and therefore the copy can have pointers that reference
constant underlying knowledge.
Deep Copy: Deep repetition truly clones the underlying data. It is not shared
between the first and therefore the copy.

Below is the tabular Difference between the Shallow Copy and Deep Copy:

Shallow Copy Deep Copy

Shallow Copy stores the references of Deep copy stores copies of the object’s
objects to the original memory address. value.

Deep copy doesn’t reflect changes made to


Shallow Copy reflects changes made to the
the new/copied object in the original
new/copied object in the original object.
object.

Shallow Copy stores the copy of the Deep copy stores the copy of the original
original object and points the references to object and recursively copies the objects as
the objects. well.

A shallow copy is faster. Deep copy is comparatively slower.

Below is the program to explain the shallow and deep copy of the class.

• C++
• Java
• Python3
// C++ program to illustrate the deepcopy and shallow copy

#include <algorithm>

#include <iostream>

#include <memory>

#include <vector>

using namespace std;

// Class of Car

class Car {

public:

string name;

vector<string> colors;

Car(string name, vector<string> colors)

this->name = name;

this->colors = colors;

};

int main()

// Create a Honda car object

vector<string> honda_colors = { "Red", "Blue" };

Car honda = Car("Honda", honda_colors);

// Deepcopy of Honda

Car deepcopy_honda = honda;

deepcopy_honda.colors.push_back("Green");

cout << "Deepcopy: ";

for (string color : deepcopy_honda.colors) {

cout << color << " ";

cout << endl << "Original: ";

for (string color : honda.colors) {


cout << color << " ";

cout << endl;

// Shallow Copy of Honda

Car* copy_honda = &honda;

copy_honda->colors.push_back("Green");

cout << "Shallow Copy: ";

for (string color : copy_honda->colors) {

cout << color << " ";

cout << endl << "Original: ";

for (string color : honda.colors) {

cout << color << " ";

cout << endl;

return 0;

Output
Deepcopy: ['Red', 'Blue', 'Green']
Original: ['Red', 'Blue']
Shallow Copy: ['Red', 'Blue', 'Green']
Original: ['Red', 'Blue', 'Green']

PART-A
1. Analyzing algorithms in data structures using the C programming language involves
evaluating their efficiency and performance. There are two types of analysis
namely,
i) Apriori analysis and
ii) Posterior analysis

2. In a classification problem, the category or classes of data is identified based on training data.
The model learns from the given dataset and then classifies the new data into classes or groups
based on the training.To evaluate the performance of a classification model, different metrics are
used, and some of them are as follows:

o Accuracy
o Confusion Matrix
o Precision
o Recall
o F-Score
o AUC(Area Under the Curve)-ROC

3.

o Python also accepts function recursion, which means a defined function


can call itself.

o Recursion is a common mathematical and programming concept. It means


that a function calls itself. This has the benefit of meaning that you can
loop through data to reach a result.

4.
There are mainly three asymptotic notations:
1. Big-O Notation (O-notation)
2. Omega Notation (Ω-notation)
3. Theta Notation (Θ-notation)
5.

Shallow Copy Deep Copy

It is fast as no new memory is allocated. It is slow as new memory is allocated.

Changes in one entity is reflected in other Changes in one entity are not reflected in changes in an
entity. identity.

The default version of the clone() method In order to make the clone() method support the deep copy
supports shallow copy. has to override the clone() method.

A shallow copy is less expensive. Deep copy is highly expensive.

Cloned object and the original object are not Cloned object and the original object are disjoint.
disjoint.

6. To give a basic definition of both terms, class attributes are class variables that are inherited by
every object of a class. The value of class attributes remain the same for every new object.

7.
Advantages of Recursion:

• Recursion can simplify complex problems by breaking them down into smaller, more
manageable pieces.
• Recursive code can be more readable and easier to understand than iterative code.
• Recursion is essential for some algorithms and data structures.
• Also with recursion, we can reduce the length of code and become more readable and
understandable to the user/ programmer.
8.
A recurrence relation is a mathematical expression that defines a sequence in terms of its
previous terms. In the context of algorithmic analysis, it is often used to model the time
complexity of recursive algorithms.

9.
When working with classes in Python, the term “self” refers to the instance of the class that is
currently being used. It is customary to use “self” as the first parameter in instance methods of a
class. Whenever you call a method of an object created from a class, the object is automatically
passed as the first argument using the “self” parameter. This enables you to modify the object’s
properties and execute tasks unique to that particular instance.
10.
Yes, the inheritance is possible in python.
One of the core concepts in object-oriented programming (OOP) languages is
inheritance. It is a mechanism that allows you to create a hierarchy of classes that
share a set of properties and methods by deriving a class from another class.
Inheritance is the capability of one class to derive or inherit the properties from
another class.
Benefits of inheritance are:
Inheritance allows you to inherit the properties of a class, i.e., base class to another,
i.e., derived class.

11. __init__ method in Python is used to initialize objects of a class. It is also


called a constructor. Constructors are used to initialize the object’s state.

12.

As an introduction we show that the following recursive function has linear time
complexity.

// Sum returns the sum 1 + 2 + ... + n, where n >= 1.

func Sum(n int) int {

if n == 1 {

return 1

}
return n + Sum(n-1)

Let the function T(n) denote the number of elementary operations performed by the
function call Sum(n).

We identify two properties of T(n).

• Since Sum(1) is computed using a fixed number of operations k1, T(1) = k1.
• If n > 1 the function will perform a fixed number of operations k2, and in
addition, it will make a recursive call to Sum(n-1). This recursive call will
perform T(n-1) operations. In total, we get T(n) = k2 + T(n-1).

If we are only looking for an asymptotic estimate of the time complexity, we don’t
need to specify the actual values of the constants k1 and k2. Instead, we
let k1 = k2 = 1. To find the time complexity for the Sum function can then be
reduced to solving the recurrence relation

• T(1) = 1, (*)
• T(n) = 1 + T(n-1), when n > 1. (**)

13. Worst Case Analysis (Mostly used)


In the worst-case analysis, we calculate the upper bound on the running
time of an algorithm. We must know the case that causes a maximum
number of operations to be executed. For Linear Search, the worst case
happens when the element to be searched (x) is not present in the array.
When x is not present, the search() function compares it with all the
elements of arr[] one by one. Therefore, the worst-case time complexity of
the linear search would be O(n).

14. Average Case Analysis (Rarely used)


In average case analysis, we take all possible inputs and calculate the
computing time for all of the inputs. Sum all the calculated values and divide
the sum by the total number of inputs. We must know (or predict) the
distribution of cases. For the linear search problem, let us assume that all
cases are uniformly distributed (including the case of x not being present in
the array). So we sum all the cases and divide the sum by (n+1). Following is
the value of average-case time complexity.

15. Best Case Analysis (Very Rarely used)


In the best-case analysis, we calculate the lower bound on the running time
of an algorithm. We must know the case that causes a minimum number of
operations to be executed. In the linear search problem, the best case occurs
when x is present at the first location. The number of operations in the best
case is constant (not dependent on n). So time complexity in the best case
would be ?(1)

PART-B
16.a) The process in which a function calls itself directly or indirectly is called recursion and the
corresponding function is called a recursive function. Using a recursive algorithm, certain
problems can be solved quite easily. Examples of such problems are Towers of Hanoi
(TOH), Inorder/Preorder/Postorder Tree Traversals, DFS of Graph, etc. A recursive function
solves a particular problem by calling a copy of itself and solving smaller subproblems of the
original problems. Many more recursive calls can be generated as and when required. It is
essential to know that we should provide a certain case in order to terminate this recursion
process. So we can say that every time the function calls itself with a simpler version of the
original problem.
Need of Recursion
Recursion is an amazing technique with the help of which we can reduce the length of our code
and make it easier to read and write. It has certain advantages over the iteration technique which
will be discussed later. A task that can be defined with its similar subtask, recursion is one of
the best solutions for it. For example; The Factorial of a number.
Properties of Recursion:
• Performing the same operations multiple times with different inputs.
• In every step, we try smaller inputs to make the problem smaller.
• Base condition is needed to stop the recursion otherwise infinite loop will occur.
Algorithm: Steps
The algorithmic steps for implementing recursion in a function are as follows:

Step1 - Define a base case: Identify the simplest case for which the solution is known or trivial.
This is the stopping condition for the recursion, as it prevents the function from infinitely
calling itself.

Step2 - Define a recursive case: Define the problem in terms of smaller subproblems. Break the
problem down into smaller versions of itself, and call the function recursively to solve each
subproblem.

Step3 - Ensure the recursion terminates: Make sure that the recursive function eventually
reaches the base case, and does not enter an infinite loop.

step4 - Combine the solutions: Combine the solutions of the subproblems to solve the original
problem.
A Mathematical Interpretation
Let us consider a problem that a programmer has to determine the sum of first n natural
numbers, there are several ways of doing that but the simplest approach is simply to add the
numbers starting from 1 to n. So the function simply looks like this,
approach(1) – Simply adding one by one
f(n) = 1 + 2 + 3 +……..+ n
but there is another mathematical approach of representing this,
approach(2) – Recursive adding
f(n) = 1 n=1
f(n) = n + f(n-1) n>1
There is a simple difference between the approach (1) and approach(2) and that is
in approach(2) the function “ f( ) ” itself is being called inside the function, so this
phenomenon is named recursion, and the function containing recursion is called recursive
function, at the end, this is a great tool in the hand of the programmers to code some problems
in a lot easier and efficient way.
How are recursive functions stored in memory?
Recursion uses more memory, because the recursive function adds to the stack with each
recursive call, and keeps the values there until the call is finished. The recursive function uses
LIFO (LAST IN FIRST OUT) Structure just like the stack data
structure. https://www.geeksforgeeks.org/stack-data-structure/

What is the base condition in recursion?


In the recursive program, the solution to the base case is provided and the solution to the bigger
problem is expressed in terms of smaller problems.

int fact(int n)
{
if (n < = 1) // base case
return 1;
else
return n*fact(n-1);
}
In the above example, the base case for n < = 1 is defined and the larger value of a number can
be solved by converting to a smaller one till the base case is reached.
How a particular problem is solved using recursion?
The idea is to represent a problem in terms of one or more smaller problems, and add one or
more base conditions that stop the recursion. For example, we compute factorial n if we know
the factorial of (n-1). The base case for factorial would be n = 0. We return 1 when n = 0.
Why Stack Overflow error occurs in recursion?
If the base case is not reached or not defined, then the stack overflow problem may arise. Let us
take an example to understand this.
int fact(int n)
{
// wrong base case (it may cause
// stack overflow).
if (n == 100)
return 1;

else
return n*fact(n-1);
}
If fact(10) is called, it will call fact(9), fact(8), fact(7), and so on but the number will never
reach 100. So, the base case is not reached. If the memory is exhausted by these functions on the
stack, it will cause a stack overflow error.
What is the difference between direct and indirect recursion?
A function fun is called direct recursive if it calls the same function fun. A function fun is called
indirect recursive if it calls another function say fun_new and fun_new calls fun directly or
indirectly. The difference between direct and indirect recursion has been illustrated in Table 1.
// An example of direct recursion
void directRecFun()
{
// Some code....

directRecFun();

// Some code...
}

// An example of indirect recursion


void indirectRecFun1()
{
// Some code...

indirectRecFun2();

// Some code...
}
void indirectRecFun2()
{
// Some code...

indirectRecFun1();

// Some code...
}
What is the difference between tailed and non-tailed recursion?
A recursive function is tail recursive when a recursive call is the last thing executed by the
function. Please refer tail recursion article for details.
How memory is allocated to different function calls in recursion?
When any function is called from main(), the memory is allocated to it on the stack. A recursive
function calls itself, the memory for a called function is allocated on top of memory allocated to
the calling function and a different copy of local variables is created for each function call.
When the base case is reached, the function returns its value to the function by whom it is called
and memory is de-allocated and the process continues.
Let us take the example of how recursion works by taking a simple function.
• CPP
• Java
• Python3
• C#
• PHP
• Javascript

// A C++ program to demonstrate working of


// recursion
#include <bits/stdc++.h>
using namespace std;

void printFun(int test)


{
if (test < 1)
return;
else {
cout << test << " ";
printFun(test - 1); // statement 2
cout << test << " ";
return;
}
}

// Driver Code
int main()
{
int test = 3;
printFun(test);
}

Learn Data Structures & Algorithms with GeeksforGeeks


Output
321123
Time Complexity: O(1)
Auxiliary Space: O(1)
When printFun(3) is called from main(), memory is allocated to printFun(3) and a local
variable test is initialized to 3 and statement 1 to 4 are pushed on the stack as shown in below
diagram. It first prints ‘3’. In statement 2, printFun(2) is called and memory is allocated
to printFun(2) and a local variable test is initialized to 2 and statement 1 to 4 are pushed into
the stack.
Similarly, printFun(2) calls printFun(1) and printFun(1) calls printFun(0). printFun(0) goes
to if statement and it return to printFun(1). The remaining statements of printFun(1) are
executed and it returns to printFun(2) and so on. In the output, values from 3 to 1 are printed
and then 1 to 3 are printed. The memory stack has been shown in below diagram.

Recursion VS Iteration
SR
Recursion Iteration
No.

Terminates when the condition


1) Terminates when the base case becomes true.
becomes false.

2) Used with functions. Used with loops.

Every recursive call needs extra space in the Every iteration does not require any
3)
stack memory. extra space.

4) Smaller code size. Larger code size.

Now, let’s discuss a few practical problems which can be solved by using recursion and
understand its basic working. For basic understanding please read the following articles.
Basic understanding of Recursion.
Problem 1: Write a program and recurrence relation to find the Fibonacci series of n where n>2
.
Mathematical Equation:
n if n == 0, n == 1;
fib(n) = fib(n-1) + fib(n-2) otherwise;
Recurrence Relation:
T(n) = T(n-1) + T(n-2) + O(1)
Recursive program:
Input: n = 5
Output:
Fibonacci series of 5 numbers is : 0 1 1 2 3
Implementation:
• C++
• C
• Java
• Python3
• C#
• Javascript

// C++ code to implement Fibonacci series


#include <bits/stdc++.h>
using namespace std;

// Function for fibonacci

int fib(int n)
{
// Stop condition
if (n == 0)
return 0;

// Stop condition
if (n == 1 || n == 2)
return 1;

// Recursion function
else
return (fib(n - 1) + fib(n - 2));
}

// Driver Code
int main()
{
// Initialize variable n.
int n = 5;
cout<<"Fibonacci series of 5 numbers is: ";

// for loop to print the fibonacci series.


for (int i = 0; i < n; i++)
{
cout<<fib(i)<<" ";
}
return 0;
}

Learn Data Structures & Algorithms with GeeksforGeeks


Output
Fibonacci series of 5 numbers is: 0 1 1 2 3
Time Complexity: O(2n)
Auxiliary Space: O(n)
Here is the recursive tree for input 5 which shows a clear picture of how a big problem can be
solved into smaller ones.
fib(n) is a Fibonacci function. The time complexity of the given program can depend on the
function call.
fib(n) -> level CBT (UB) -> 2^n-1 nodes -> 2^n function call -> 2^n*O(1) -> T(n) = O(2^n)
For Best Case.
T(n) = ?(2^n\2)
Working:

Problem 2: Write a program and recurrence relation to find the Factorial of n where n>2 .
Mathematical Equation:
1 if n == 0 or n == 1;
f(n) = n*f(n-1) if n> 1;
Recurrence Relation:
T(n) = 1 for n = 0
T(n) = 1 + T(n-1) for n > 0
Recursive Program:
Input: n = 5
Output:
factorial of 5 is: 120
Implementation:
• C++
• C
• Java
• Python3
• C#
• Javascript

// C++ code to implement factorial


#include <bits/stdc++.h>
using namespace std;

// Factorial function
int f(int n)
{
// Stop condition
if (n == 0 || n == 1)
return 1;

// Recursive condition
else
return n * f(n - 1);
}

// Driver code
int main()
{
int n = 5;
cout<<"factorial of "<<n<<" is: "<<f(n);
return 0;
}

Learn Data Structures & Algorithms with GeeksforGeeks


Output
factorial of 5 is: 120
Time complexity: O(n)
Auxiliary Space: O(n)
Working:

Diagram of factorial Recursion function for user input 5.

Example: Real Applications of Recursion in real problems

Recursion is a powerful technique that has many applications in computer science and
programming. Here are some of the common applications of recursion:
• Tree and graph traversal: Recursion is frequently used for traversing and searching
data structures such as trees and graphs. Recursive algorithms can be used to explore
all the nodes or vertices of a tree or graph in a systematic way.
• Sorting algorithms: Recursive algorithms are also used in sorting algorithms such as
quicksort and merge sort. These algorithms use recursion to divide the data into
smaller subarrays or sublists, sort them, and then merge them back together.
• Divide-and-conquer algorithms: Many algorithms that use a divide-and-conquer
approach, such as the binary search algorithm, use recursion to break down the
problem into smaller subproblems.
• Fractal generation: Fractal shapes and patterns can be generated using recursive
algorithms. For example, the Mandelbrot set is generated by repeatedly applying a
recursive formula to complex numbers.
• Backtracking algorithms: Backtracking algorithms are used to solve problems that
involve making a sequence of decisions, where each decision depends on the
previous ones. These algorithms can be implemented using recursion to explore all
possible paths and backtrack when a solution is not found.
• Memoization: Memoization is a technique that involves storing the results of
expensive function calls and returning the cached result when the same inputs occur
again. Memoization can be implemented using recursive functions to compute and
cache the results of subproblems.
These are just a few examples of the many applications of recursion in computer science and
programming. Recursion is a versatile and powerful tool that can be used to solve many
different types of problems.

Explanation: one real example of recursion:

Recursion is a programming technique that involves a function calling itself. It can be a


powerful tool for solving complex problems, but it also requires careful implementation to avoid
infinite loops and stack overflows.
Here’s an example of implementing recursion in Python:

• C++
• Java
• Python3
• C#
• Javascript

#include <iostream>
using namespace std;
int factorial(int n)
{

// Base case: if n is 0 or 1, return 1


if (n == 0 || n == 1) {
return 1;
}

// Recursive case: if n is greater than 1,


// call the function with n-1 and multiply by n
else {
return n * factorial(n - 1);
}
}
int main()
{

// Call the factorial function and print the result


int result = factorial(5);
cout << result <<endl; // Output: 120
return 0;
}

Learn Data Structures & Algorithms with GeeksforGeeks


Output
120
In this example, we define a function called factorial that takes an integer n as input. The
function uses recursion to compute the factorial of n (i.e., the product of all positive integers up
to n).
The factorial function first checks if n is 0 or 1, which are the base cases. If n is 0 or 1, the
function returns 1, since 0! and 1! are both 1.
If n is greater than 1, the function enters the recursive case. It calls itself with n-1 as the
argument and multiplies the result by n. This computes n! by recursively computing (n-1)!.
It’s important to note that recursion can be inefficient and lead to stack overflows if not used
carefully. Each function call adds a new frame to the call stack, which can cause the stack to
grow too large if the recursion is too deep. In addition, recursion can make the code more
difficult to understand and debug, since it requires thinking about multiple levels of function
calls.
However, recursion can also be a powerful tool for solving complex problems, particularly those
that involve breaking a problem down into smaller subproblems. When used correctly, recursion
can make the code more elegant and easier to read.
What are the disadvantages of recursive programming over iterative programming?
Note that both recursive and iterative programs have the same problem-solving powers, i.e.,
every recursive program can be written iteratively and vice versa is also true. The recursive
program has greater space requirements than the iterative program as all functions will remain
in the stack until the base case is reached. It also has greater time requirements because of
function calls and returns overhead.
Moreover, due to the smaller length of code, the codes are difficult to understand and hence
extra care has to be practiced while writing the code. The computer may run out of memory if
the recursive calls are not properly checked.
What are the advantages of recursive programming over iterative programming?
Recursion provides a clean and simple way to write code. Some problems are inherently
recursive like tree traversals, Tower of Hanoi, etc. For such problems, it is preferred to write
recursive code. We can write such codes also iteratively with the help of a stack data structure.
For example refer Inorder Tree Traversal without Recursion, Iterative Tower of Hanoi.

16.b)
Class attributes are an important aspect of object-oriented programming and play a crucial role in
creating organized and efficient code in Python. Below are some of a few reasons why attributes
are indispensable items in OOP:

1. Define default values: Class attributes provide a way to define default values for objects. With
this, developers can create objects with pre-set values, reducing the need for manual initialization
and minimizing the risk of errors.

2. Share information among objects: They allow developers to share information among different
objects. This is useful in cases where a single instance of an object needs to be shared across
different parts of the codebase.

3. Create singletons: They can be used to create singletons, which are objects that are instantiated
only once and shared among different parts of the code. Again, this is particularly useful in
situations where a single instance of an object needs to be shared across different parts of the
codebase.

4. Improve code organization and efficiency: Class attributes are a powerful tool in the OOP
paradigm, improving the organization and efficiency of the code. They let developers create code
that is more readable, understandable, and maintainable, as they provide a way to define common
characteristics among objects in a clear and concise manner.

5. Prevent unintended consequences: Python provides a way to define class methods that can be
used to change class attributes without affecting all instances of a class. This is a useful technique
to avoid unintended consequences when modifying class attributes.
Understanding class attributes

Let’s explore Python class attributes in more depth.

Defining class attributes

Python classes allow for the creation of objects that can have both attributes (data) and methods
(functions). Attributes are defined within a class and can be accessed and modified by both the
class and its objects.

In Python, class attributes are defined directly within the class definition and are shared among all
instances of the class. They can be accessed using the class name and through an instance of the
class.

Class attributes are defined outside of any method, including the init method, and are typically
assigned a value directly.

Below is a code snippet to show how this works:

class MyClass:
class_attribute = "I am a class attribute"
print(MyClass.class_attribute)

It's also possible to define class attributes within the class constructor (init) method. This isn’t
common practice, however, as the class attributes are shared among all instances of the class and
should be constant for all instances.

class MyClass:
def init(self):
self.class_attribute = "I am a class attribute"

On the other hand, instance attributes are defined within the class constructor (init) method and
are unique to each instance of the class. They can be accessed using the instance name and
through the class name.

An example of this is:

class MyClass:
def init(self):
self.instance_attribute = "I am an instance attribute"

my_object = MyClass()
print(my_object.instance_attribute) # Output: "I am an instance attribute"

Accessing class attributes

Class attributes can be accessed using the dot notation, just like you would with instance
variables.

Here’s an example:

class Cat:
species = "Felis catus"

def init(self, name, breed):

self.name = name
self.breed = breed

cat1 = Cat("Whiskers", "Siamese")

cat2 = Cat("Fluffy", "Persian")

print(cat1.species) # prints "Felis catus"


print(cat2.species) # also prints "Felis catus"

Modifying class attributes

Modifying class attributes is just as easy as accessing them. However, be careful when doing so as
it changes for all instances of that class. If you change the species of one cat, you're changing it
for all cats.

class Pig:
species = "Sus scrofa"

def init(self, name, breed):

self.name = name

self.breed = breed

pig1 = Pig("Hamlet", "Large White")

pig2 = Pig("Babe", " Hampshire")

print(pig1.species) # prints "Sus scrofa"

print(pig2.species) # also prints "Sus scrofa"

Pig.species = "Sus domesticus"

print(pig1.species) # prints "Sus domesticus"


print(pig2.species) # also prints "Sus domesticus"
Class methods

When we talk about attributes in OOP, we can’t forget the existence of the method they work
with. Let’s look at it in detail.

Methods are functions that are associated with an object and can be called on that object. In OOP,
methods allow objects to perform operations and interact with each other. They are defined inside
classes and can take parameters, perform calculations, and modify object data.

Methods provide a way to encapsulate behavior and logic into objects, making it easier to
understand and maintain the code. In Python, they can be defined using the def keyword, just like
any other function. However, they are associated with an object and can be called on that object
using dot notation.

They play a crucial role in the implementation of object-oriented programming principles and are
an essential aspect of creating organized and maintainable code.

Creating class methods

When it comes to creating class methods, there are a few things to keep in mind. First, it's
important to use the @classmethod decorator to indicate that the method is a class method. This is
because class methods behave differently than regular instance methods and need to be treated as
such.

Here's an example of how to create a class method:

class Book:
books_sold = 0

def init(self, title, author):

self.title = title

self.author = author

@classmethod
def update_books_sold(cls, amount):
cls.books_sold += amount

In the code snippet above, we have as object a book with attributes title, author and with a method
update_books_sold, which has a function to update the quantity of the total books sold. The
methods defined will help us interact with the object and update some of its content.

Using class methods to modify class attributes

One of the most common use cases for class methods is to modify class attributes. Class attributes
are shared among all instances of a class. By using class methods, you can manipulate these
shared attributes in a controlled and organized way.

For example, you have a Book class and want to keep track of the total number of pages in all the
books. Here's how you could do that using class methods:

class Book:
total_pages = 0
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
@classmethod
def update_total_pages(cls, pages):
cls.total_pages += pages
def add_to_total_pages(self):
Book.update_total_pages(self.pages)

In this example, we've added a class attribute total_pages to keep track of the total number of
pages across all books. We've also added a class method update_total_pages to modify this class
attribute. Finally, we've added an instance method add_to_total_pages that calls the
update_total_pages method, passing in the number of pages for the current book.
Practical uses of class attributes

Python class attributes are a versatile feature of OOP, allowing you to add class-level data to
classes. This data can serve as default values for instance attributes, track class-level statistics, and
much more.

In this section, we'll dive into the practical uses of Python class attributes, showcasing how they
can be used in real-world scenarios to enhance the organization, maintainability, and performance
of code.

Whether you have extensive experience with Python or are just starting out with OOP, this section
will provide valuable insights and techniques for leveraging class attributes to elevate your code.

Setting default values for objects

One of the most practical uses of class attributes is setting default values for objects. In object-
oriented programming, objects are instances of a class and can have their own attributes.
However, sometimes it's useful to set a default value for an attribute that applies to all instances of
the class. This is where class attributes come in handy.

1. Setting a default tip percentage in a Restaurant class:

class Restaurant:
tip = 18
def __init__(self, name, cuisine, bill, tip=None):
self.name = name
self.cuisine = cuisine
self.bill = bill
if tip is not None:
self.tip = tip

2. Setting a default discount percentage in a ClothingStore class:

class ClothingStore:
discount = 10
def __init__(self, name, clothes, price, discount=None):
self.name = name
self.clothes = clothes
self.price = price
if discount is not None:
self.discount = discount

3. Setting a default age limit in a MovieTheater class:

class MovieTheater:
age_limit = 17
def init(self, name, movie, age_limit=None):
self.name = name
self.movie = movie
if age_limit is not None:
self.age_limit = age_limit

Sharing information among objects

Class attributes can also be used to share information among objects in your code. Say you want
to keep track of the number of books in your library. You can create a Book class with a class
attribute count that will keep track of the total number of books in the library:
class Book:
count = 0
def init(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
Book.count += 1

Now, every time you create a new Book object, the count class attribute will be updated
accordingly:

book1 = Book("Pride and Prejudice", "Jane Austen", 279)


book2 = Book("To Kill a Mockingbird", "Harper Lee", 324)
book3 = Book("The Great Gatsby", "F. Scott Fitzgerald", 218)

print(Book.count) # Output: 3

This demonstrates how class attributes can be used to share information among objects, making it
easier to keep track of important information in your code. It's also a great way to maintain
consistency and ensure that all objects use the same information.

Creating singletons

Class attributes can be used to create singleton objects, which are objects that are created only
once and can be shared among multiple parts of your code. Singletons are commonly used for
shared resources, such as database connections, caches, and other objects that should only exist
once in a system.

To create a singleton in Python, you can create a class with a class attribute that holds the
singleton object:

class Database:
_instance = None
def new(cls):
if cls._instance is None:
cls._instance = super().new(cls)
return cls._instance

This class uses the new method to ensure that only one instance of the class is created. Every time
you try to create a new Database object, the new method will check if an instance already exists
and return that instead of creating a new one.

Here's an example of how you can use the Database class:

db1 = Database()
db2 = Database()

print(db1 is db2) # Output: True

In this example, db1 and db2 refer to the same object, demonstrating that the singleton pattern has
been implemented successfully. This allows you to share the same database connection across
your code, ensuring that all parts of the code use the same database and avoiding the creation of
multiple connections.
Best practices for using class attributes and methods

Last but certainly not least, let's talk about some best practices for using class attributes and
methods. After all, you want to make sure they’re used in the most effective and efficient way
possible.

1. Avoid mutating class attributes from instance methods

Class attributes are meant to be shared among all instances of a class, so mutating them from an
instance method can lead to unexpected behavior. Instead, you should use instance attributes if
you need to store information that is specific to an individual object.

2. Use class methods sparingly

Class methods are useful in certain cases but they can also make your code more complex and
harder to maintain. Only use them when you really need to modify class attributes or when you
want to provide a way to create objects that is different from the standard "init" method.

3. Name your class attributes and methods clearly

Make sure your class attributes and methods have clear and descriptive names so that other
developers (and your future self!) can understand what they do. This makes it easier to maintain
your code and reduces the risk of bugs.

4. Keep your classes small and focused


Classes that have too many attributes and methods can become hard to understand and maintain.
Try to keep them small and focused, and consider breaking them up into multiple classes if they
get too large.

5. TRY new methods!

Object-oriented programming is a powerful tool that can help you write better code, so don't be
afraid to experiment and try new things. The more you play with class attributes and methods, the
better you'll get at using them effectively.

Conclusion

We've covered a lot of ground on the topic of Python class attributes and methods. We've
discussed what class attributes and methods are, how to create and use them, and their practical
uses in object-oriented programming. We've also explored some best practices for using them
effectively.

Class attributes and methods play a vital role in OOP, allowing you to structure code in a way that
is easy to understand, maintain, and debug. By using class attributes and methods, you can create
objects that share information, store default values, and provide a way to modify class-level data.

17.a)

Asymptotic Analysis: Big-O Notation and More

The efficiency of an algorithm depends on the amount of time, storage and other resources
required to execute the algorithm. The efficiency is measured with the help of asymptotic
notations.

An algorithm may not have the same performance for different types of inputs. With the increase
in the input size, the performance will change.

The study of change in performance of the algorithm with the change in the order of the input size
is defined as asymptotic analysis.

Asymptotic Notations

Asymptotic notations are the mathematical notations used to describe the running time of an
algorithm when the input tends towards a particular value or a limiting value.

For example: In bubble sort, when the input array is already sorted, the time taken by the
algorithm is linear i.e. the best case.

But, when the input array is in reverse condition, the algorithm takes the maximum time
(quadratic) to sort the elements i.e. the worst case.
When the input array is neither sorted nor in reverse order, then it takes average time. These
durations are denoted using asymptotic notations.

There are mainly three asymptotic notations:

• Big-O notation

• Omega notation

• Theta notation

Big-O Notation (O-notation)

Big-O notation represents the upper bound of the running time of an algorithm. Thus, it gives the
worst-case complexity of an algorithm.
Big-
O gives the upper bound of a function

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


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

The above expression can be described as a function f(n) belongs to the set O(g(n)) if there exists
a positive constant c such that it lies between 0 and cg(n), for sufficiently large n.
For any value of n, the running time of an algorithm does not cross the time provided by O(g(n)).
Since it gives the worst-case running time of an algorithm, it is widely used to analyze an
algorithm as we are always interested in the worst-case scenario.
Omega Notation (Ω-notation)

Omega notation represents the lower bound of the running time of an algorithm. Thus, it provides
the best case complexity of an algorithm.

Omega gives the lower bound of a function

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


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

The above expression can be described as a function f(n) belongs to the set Ω(g(n)) if there exists
a positive constant c such that it lies above cg(n), for sufficiently large n.
For any value of n, the minimum time required by the algorithm is given by Omega Ω(g(n)).
Theta Notation (Θ-notation)

Theta notation 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 analyzing the average-case
complexity of an algorithm.

Theta bounds the function within constants factors

For a function g(n), Θ(g(n)) is given by the relation:

Θ(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 }

The above expression can be described as a function f(n) belongs to the set Θ(g(n)) if there exist
positive constants c1 and c2 such that it can be sandwiched between c1g(n) and c2g(n), for
sufficiently large n.
If a function f(n) lies anywhere in between c1g(n) and c2g(n) for all n ≥ n0, then f(n) is said to be
asymptotically tight bound.
er recurrencesUpdated : 15 Feb, 2023
17.b)

The•• Master Theorem is a tool used to solve recurrence relations that arise in the
analysis of divide-and-conquer algorithms. The Master Theorem provides a
systematic way of solving recurrence relations of the form:
T(n) = aT(n/b) + f(n)
1. where a, b, and f(n) are positive functions and n is the size of the
problem. The Master Theorem provides conditions for the solution of the
recurrence to be in the form of O(n^k) for some constant k, and it gives a
formula for determining the value of k.
2. The advanced version of the Master Theorem provides a more general
form of the theorem that can handle recurrence relations that are more
complex than the basic form. The advanced version of the Master
Theorem can handle recurrences with multiple terms and more complex
functions.
3. It is important to note that the Master Theorem is not applicable to all
recurrence relations, and it may not always provide an exact solution to a
given recurrence. However, it is a useful tool for analyzing the time
complexity of divide-and-conquer algorithms and provides a good
starting point for solving more complex recurrences.
Master Theorem is used to determine running time of algorithms (divide and
conquer algorithms) in terms of asymptotic notations.
Consider a problem that is solved using recursion.

function f(input x size n)


if(n < k)
solve x directly and return
else
divide x into a subproblems of size n/b
call f recursively to solve each subproblem
Combine the results of all sub-problems
The above algorithm divides the problem into a subproblems, each of size n/b and
solve them recursively to compute the problem and the extra work done for
problem is given by f(n), i.e., the time to create the subproblems and combine
their results in the above procedure.
So, according to master theorem the runtime of the above algorithm can be
expressed as:

T(n) = aT(n/b) + f(n)


where n = size of the problem
a = number of subproblems in the recursion and a >= 1
n/b = size of each subproblem
f(n) = cost of work done outside the recursive calls like dividing into subproblems
and cost of combining them to get the solution.
Not all recurrence relations can be solved with the use of the master theorem i.e.
if

• T(n) is not monotone, ex: T(n) = sin n


• f(n) is not a polynomial, ex: T(n) = 2T(n/2) + 2n
This theorem is an advance version of master theorem that can be used to
determine running time of divide and conquer algorithms if the recurrence is of
the following form :-

where n = size of the problem


a = number of subproblems in the recursion and a >= 1
n/b = size of each subproblem
b > 1, k >= 0 and p is a real number.
Then,

1. if a > bk, then T(n) = θ(nlog a)


b

2. if a = bk, then
(a) if p > -1, then T(n) = θ(nlog a logp+1n)
b

(b) if p = -1, then T(n) = θ(nlog a loglogn)


b

(c) if p < -1, then T(n) = θ(nlog a)


b

3. if a < bk, then


(a) if p >= 0, then T(n) = θ(nk logpn)
(b) if p < 0, then T(n) = θ(nk)

Time Complexity Analysis –

• Example-1: Binary Search – T(n) = T(n/2) + O(1)


a = 1, b = 2, k = 0 and p = 0
bk = 1. So, a = bk and p > -1 [Case 2.(a)]
T(n) = θ(nlog a logp+1n)
b

T(n) = θ(logn)
• Example-2: Merge Sort – T(n) = 2T(n/2) + O(n)
a = 2, b = 2, k = 1, p = 0
bk = 2. So, a = bk and p > -1 [Case 2.(a)]
T(n) = θ(nlog a logp+1n)
b

T(n) = θ(nlogn)
• Example-3: T(n) = 3T(n/2) + n2
a = 3, b = 2, k = 2, p = 0
bk = 4. So, a < bk and p = 0 [Case 3.(a)]
T(n) = θ(nk logpn)
T(n) = θ(n2)
• Example-4: T(n) = 3T(n/2) + log2n
a = 3, b = 2, k = 0, p = 2
bk = 1. So, a > bk [Case 1]
T(n) = θ(nlog a )
b

T(n) = θ(nlog 3)
2

• Example-5: T(n) = 2T(n/2) + nlog2n


a = 2, b = 2, k = 1, p = 2
bk = 2. So, a = bk [Case 2.(a)]
T(n) = θ(nlog alogp+1n )
b

T(n) = θ(nlog 2log3n)


2

T(n) = θ(nlog3n)

• Example-6: T(n) = 2nT(n/2) + nn


This recurrence can’t be solved using above method since function is not
of form T(n) = aT(n/b) + θ(nk logpn)

Here are some important points to keep in mind regarding the Master
Theorem:

1. Divide-and-conquer recurrences: The Master Theorem is specifically


designed to solve recurrence relations that arise in the analysis of divide-
and-conquer algorithms.
2. Form of the recurrence: The Master Theorem applies to recurrence
relations of the form T(n) = aT(n/b) + f(n), where a, b, and f(n) are
positive functions and n is the size of the problem.
3. Time complexity: The Master Theorem provides conditions for the
solution of the recurrence to be in the form of O(n^k) for some constant
k, and it gives a formula for determining the value of k.
4. Advanced version: The advanced version of the Master Theorem
provides a more general form of the theorem that can handle recurrence
relations that are more complex than the basic form.
5. Limitations: The Master Theorem is not applicable to all recurrence
relations, and it may not always provide an exact solution to a given
recurrence.
6. Useful tool: Despite its limitations, the Master Theorem is a useful tool
for analyzing the time complexity of divide-and-conquer algorithms and
provides a good starting point for solving more complex recurrences.
7. Supplemented with other techniques: In some cases, the Master
Theorem may need to be supplemented with other techniques, such as
the substitution method or the iteration method, to completely solve a
given recurrence relation.

d. A
ll rights reserve
18.a) A factorial is positive integer n, and denoted by n!. Then the product of
all positive integers less than or equal to n.

For example:

In this article, we are going to calculate the factorial of a number


using recursion.
Examples:
Input: 5
Output: 120

Input: 6
Output: 720
Implementation:
If fact(5) is called, it will call fact(4), fact(3), fact(2) and fact(1). So it means
keeps calling itself by reducing value by one till it reaches 1.

• Python3

# Python 3 program to find

# factorial of given number

def factorial(n):

# Checking the number

# is 1 or 0 then

# return 1

# other wise return

# factorial

if (n==1 or n==0):

return 1

else:

return (n * factorial(n - 1))

# Driver Code
num = 5;

print("number : ",num)

print("Factorial : ",factorial(num))

Output
number : 5
Factorial : 120

Time complexity: O(n)


Space complexity: O(n)
18.b)

A Fibonacci sequence is a sequence of integers which first two terms are 0 and 1 and all
other terms of the sequence are obtained by adding their preceding two numbers.

For example: 0, 1, 1, 2, 3, 5, 8, 13 and so on...

See this example:

Backward Skip 10sPlay VideoForward Skip 10s

1. def recur_fibo(n):
2. if n <= 1:
3. return n
4. else:
5. return(recur_fibo(n-1) + recur_fibo(n-2))
6. # take input from the user
7. nterms = int(input("How many terms? "))
8. # check if the number of terms is valid
9. if nterms <= 0:
10. print("Plese enter a positive integer")
11. else:
12. print("Fibonacci sequence:")
13. for i in range(nterms):
14. print(recur_fibo(i))

Output:

You might also like

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy