Python
Python
A Linear Data Structure is a type of data structure in which elements are arranged in a
sequential or linear order, where each element is connected to its previous and next
element.
In a linear data structure, all elements are stored in a single level, and traversal of data can
occur in a single run, either forward or backward.
Example :
Array: Representing a 2D Matrix for Addition
Linked List: Managing Dynamic Data
Stack: Implementing Browser Backtracking
Queue: Simulating a Print Job Queue
*********************************************************************
Applications of Data Structures:
1. Artificial Intelligence: Representing and searching decision trees, graphs, and state
spaces.
2. Operating Systems: Process scheduling, memory management, and file systems.
3. Networking: Routing algorithms, data packet organization, and bandwidth allocation.
4. Game Development: Pathfinding (A* algorithm), scene graphs, and event systems.
5. Machine Learning: Storing and processing datasets efficiently.
*************************************************************************
Comprehensions
1. List Comprehensions
List comprehensions provide a concise way to create lists in Python.
They use a single line of code with an expression and an optional condition to generate a
list based on an iterable.
Syntax:
[expression for item in iterable if condition]
Example:
print("List Comprehension:")
squares = [x**2 for x in range(10)]
print(f"Squares of numbers from 0 to 9: {squares}\n")
2. Dictionary Comprehensions
Dictionary comprehensions allow you to create dictionaries in a concise way by specifying keys
and values derived from an iterable.
Syntax:
{key_expression: value_expression for item in iterable if condition}
Example:
print("Dictionary Comprehension:")
number_map = {x: x**2 for x in range(5)}
print(f"Mapping of numbers to their squares (0 to 4): {number_map}\n")
3. Set Comprehensions
Set comprehensions enable the creation of sets in a concise manner.
Since sets only contain unique elements, duplicate values are automatically removed.
Syntax:
{expression for item in iterable if condition}
Example:
print("Set Comprehension:")
squares = {x**2 for x in [1, 2, 2, 3, 4, 4, 5]}
print(f"Unique squares from the list [1, 2, 2, 3, 4, 4, 5]: {unique_squares}\n")
4. Generator Comprehensions
Generator comprehensions create a generator object instead of storing elements in
memory.
They are memory-efficient and generate items lazily, i.e., only when requested.
Syntax:
(expression for item in iterable if condition)
Example:
print("Generator Comprehension:")
square_gen = (x**2 for x in range(5))
print("Squares of numbers from 0 to 4 generated one by one:")
for square in square_gen:
print(square)
List Comprehensions: Creates a list where each element is the square of numbers from 0
to 9.
Dictionary Comprehensions: Creates a dictionary where each key is a number, and the
value is its square for numbers from 0 to 4.
Set Comprehensions: Creates a set of unique squares from the given list.
Generator Comprehensions: Generates squares of numbers one by one in memory-
efficient manner.
**************************************************************************
List Comprehension Works in Python
List comprehensions provide a concise way to create lists in Python.
They use a single line of code with an expression and an optional condition to generate a
list based on an iterable.
List comprehension provides a concise way to create lists. It consists of:
1. Expression: An operation or value to include in the new list.
2. Iterable: A sequence or collection to iterate over.
3. Optional Condition: A filter to decide which elements to include.
Syntax:
[expression for item in iterable if condition]
Example:
squares = []
for x in range(10):
squares.append(x**2)
Output:
Traditional: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
Comprehension: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
***************************************************************************
Linked lists
Linked lists are considered linear data structures. Despite their flexible memory allocation and
pointer-based connections, they maintain a sequential order of elements, which is a key
characteristic of linear data structures.
Why Linked Lists are Linear:
1. Sequential Access: Elements in a linked list are accessed sequentially, one after the other,
from the head node to the tail node.
2. Single Path: At any point in a linked list, there is only one logical next element to traverse,
making the structure linear in nature.
************************************************************************
Generators in Python:
Generators are a special type of iterable in Python that allows you to produce values lazily,
meaning values are generated one at a time and only when requested.
This is achieved using the yield keyword, which pauses the generator's state and can resume
from where it left off, making them more memory-efficient than lists.
Key Features of Generators:
1. Lazy Evaluation
2. State Retention
3. Iterables:
**************************************************************************
Inorder Traversal:
To solve the problem of finding all possible binary trees for a given inorder traversal and
printing their preorder traversals, we can use recursion. Each element in the inorder traversal
can serve as the root of the tree, and the elements to the left of the root in the array form the
left subtree, while the elements to the right form the right subtree.
def get_trees(inorder):
if not inorder:
return [None]
trees = []
for i in range(len(inorder)):
left = get_trees(inorder[:i])
right = get_trees(inorder[i+1:])
for l in left:
for r in right:
trees.append((inorder[i], l, r))
return trees
def preorder(tree):
if not tree:
return []
return [tree[0]] + preorder(tree[1]) + preorder(tree[2])
class Quadrilateral:
def __init__(self, l, w):
self.l, self.w = l, w
def area(self): return self.l * self.w
def perimeter(self): return 2 * (self.l + self.w)
class Pentagon:
def __init__(self, s):
self.s = s
def area(self): return self.s ** 2 * 1.72
def perimeter(self): return 5 * self.s
if shape == "triangle":
a, b, c, h = map(float, input("Enter sides and height: ").split())
poly = Triangle(a, b, c, h)
elif shape == "quadrilateral":
l, w = map(float, input("Enter length and width: ").split())
poly = Quadrilateral(l, w)
elif shape == "pentagon":
s = float(input("Enter side length: "))
poly = Pentagon(s)
else:
print("Invalid shape")
exit()
class Human(Animal):
def move(self):
print("Humans walk on two legs.")
class Snake(Animal):
def move(self):
print("Snakes slither on the ground.")
class Dog(Animal):
def move(self):
print("Dogs run on four legs.")
class Lion(Animal):
def move(self):
print("Lions run swiftly on four legs.")
Program
from itertools import combinations
def generate_combinations(input_list, n):
# Generate all combinations of n distinct objects
result = list(combinations(input_list, n))
return result
# Example usage
original_list = [1, 2, 3, 4, 5, 6, 7, 8, 9]
n=2
print(f"Original list: {original_list}")
print(f"Combinations of {n} distinct objects:")
combinations_result = generate_combinations(original_list, n)
for combo in combinations_result:
print(list(combo))
Output :
[1, 2]
[1, 3]
[1, 4]
...
[7, 8]
[7, 9]
[8, 9]
***************************************************************************
Python Lambdas Useful
Python lambda functions are useful for writing small, anonymous functions inline without
the need for a full def statement.
They are especially helpful in scenarios where you need a quick, one-time function, such
as with map(), filter(), and reduce().
Output:
Even numbers from 1 to 100:
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20, ..., 98, 100]
handler = FileHandlerWithLogging()
handler.log("Starting the file operation")
print(handler.read("example.txt"))
*******************************************************************
factorial in Python
def factorial(n):
return 1 if n == 0 else n * factorial(n - 1)
# Test the function
num = 5
print(f"Factorial of {num} is {factorial(num)}")
Output:
Factorial of 5 is 120
***********************************************************************
Convert your List to a Set in Python
A set is an unordered collection of unique elements, so converting a list to a set will
automatically remove any duplicate elements.
list1 = [1, 2, 3]
list2 = [3, 4, 5]
set1 = set(list1)
set2 = set(list2)
print(set1 | set2) # Union: {1, 2, 3, 4, 5}
print(set1 & set2) # Intersection: {3}
print(set1 - set2) # Difference: {1, 2}
*************************************************************
Concept of inheritance and method overriding:
# Base class
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound."
# Derived class 1
class Dog(Animal):
def speak(self):
return f"{self.name} barks."
# Derived class 2
class Cat(Animal):
def speak(self):
return f"{self.name} meows."
# Main program
if __name__ == "__main__":
# Creating instances of the base and derived classes
generic_animal = Animal("Generic Animal")
dog = Dog("Rover")
cat = Cat("Whiskers")
# Demonstrating method overriding
print(generic_animal.speak()) # Calls the speak method from the Animal class
print(dog.speak()) # Calls the overridden speak method from the Dog class
print(cat.speak()) # Calls the overridden speak method from the Cat class
Output:
Generic Animal makes a sound.
Rover barks.
Whiskers meows.
********************************************************************8
append() and extend() methods
append() Method
Purpose: Adds a single element to the end of the list.
How it Works: The argument passed to append() is added as a single item, meaning if you
append a list, the entire list will be added as a single element.
extend() Method
Purpose: Adds each element of an iterable (e.g., list, tuple, string) to the list individually.
How it Works: The elements of the iterable are added to the list as individual items, not
as a single object.
Key Differences:
append() Adds a single element (as an object) my_list.append([4, 5]) [1, 2, 3, [4, 5]]
******************************************************************************
call static method from within class:
It is possible to call a static method from within the same class without qualifying its name
in Python. Static methods belong to the class rather than any specific instance, so they can be called
using the class name, but they can also be invoked directly without qualification from within the
class scope.
Key Points:
1. Direct Call: When calling a static method within the same class, you can call it directly
by its name.
2. Using @staticmethod: Static methods are decorated with @staticmethod, which allows
them to be accessed without needing an instance or self.
3. No Access to Instance or Class (self or cls): Static methods do not take self or cls as their
first parameter.
class Example:
@staticmethod
def static_method():
print("Static method called")
def instance_method(self):
static_method()
example = Example()
example.instance_method()
Why It Works:
Python looks up the method name in the local class scope, so the static method is found
and executed.
The static method doesn’t depend on self or cls, so it can be invoked without needing an
instance or the class name.
**************************************************************************
Add an object to the global namespace: globals() or dir()
It is possible to add an object to the global namespace in Python using the globals()
function. The globals() function returns a dictionary representing the current global symbol
table, which can be modified to add or remove objects in the global namespace. However, dir()
cannot be used for this purpose, as it only lists the names in the current scope and is not modifiable.
Example: Adding an Object to the Global Namespace Using globals() .
# Define a class
class MyClass:
def __init__(self, name):
self.name = name
def greet(self):
return f"Hello, {self.name}!"
Defining an Interface
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
"""Calculate the area of the shape."""
pass
@abstractmethod
def perimeter(self):
"""Calculate the perimeter of the shape."""
pass
def area(self):
return self.length * self.width
def perimeter(self):
return 2 * (self.length + self.width)
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
def perimeter(self):
return 2 * 3.14159 * self.radius
*****************************************************************
Constructor not overridden
In Object-Oriented Programming (OOP), constructors are not overridden because they are
not inherited in the same way as regular methods. A constructor is specific to the class in which it
is defined, and each class has its own constructor. When an object of a subclass is created, the
constructor of the subclass is executed, and if it explicitly calls the constructor of the superclass, it
does so using the super() keyword.
Key Points:
1. Not Overridden: Constructors are not overridden because they are designed to initialize
specific properties for their class.
2. Superclass Constructor: If the subclass does not define its constructor, the superclass
constructor is called by default (if it has a no-argument constructor).
3. Explicit Call: The super() keyword is used in the subclass constructor to call the
superclass constructor explicitly.
class Parent:
def __init__(self):
print("Constructor of Parent class")
class Child(Parent):
def __init__(self):
print("Constructor of Child class")
super().__init__() # Explicitly calling Parent class constructor
To create a namespace package in Python, you can follow the PEP 420 method, which does not
require an __init__.py file in the package directories.
Steps:
1. Create the directory structure without an __init__.py file in the top-level package.
2. Distribute modules across multiple directories that share the same namespace.
Step-by-Step Example:
Step 1: Create the Folder Structure
Make two directories for the same package:
project/
├── part1/
│ └── module1.py
└── part2/
└── module2.py
part1 and part2 are sub-packages of the same namespace.
Note: Do not add an __init__.py file in part1 or part2. This tells Python they are part of a
namespace package.
Step 2: Add Python Code in Modules
part1/module1.py:
def greet():
print("Hello from module1 in part1!")
part2/module2.py:
def greet():
print("Hello from module2 in part2!")
Step 3: Using the Namespace Package
You can now import and use the modules from both parts:
import part1.module1 # Import module from part1
import part2.module2 # Import module from part2
4. No Wastage of Memory
Linked List: Allocates memory for only the required number of elements, avoiding
wastage.
Array: May allocate extra memory to accommodate future growth in dynamic arrays or
leave unused space in static arrays.
Reasoning: Arrays often allocate excess memory to handle potential resizing, which can lead to
unused memory overhead.
***************************************************************************
return stack
# Example usage
if __name__ == "__main__":
# Create a queue (using a list)
queue = [10, 20, 30, 40, 50]
Example Output
For an input queue [10, 20, 30, 40, 50]:
Original Queue: [10, 20, 30, 40, 50]
Converted Stack: [10, 20, 30, 40, 50]
Explanation
1. Queue Representation:
o The queue is represented as a list, where the front of the queue is the first element
(queue[0]).
2. Conversion:
o Using a while loop, elements are removed from the front of the queue (pop(0)) and
added to the stack (append()).
3. Result:
o The stack will contain the reversed order of the original queue.
******************************************************************************
Detect a loop in a linked list
Detecting a loop in a linked list is a common problem that can be solved using several methods.
The most efficient approach is the Floyd’s Cycle Detection Algorithm (also known as the
tortoise and hare algorithm).
def detect_loop(head):
slow, fast = head, head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return False
Usage Example
class Node:
def __init__(self, data):
self.data = data
self.next = None
head = Node(1)
head.next = Node(2)
head.next.next = Node(3)
head.next.next.next = Node(4)
head.next.next.next.next = head.next # Creates a loop
# Detect loop
print("Loop detected!" if detect_loop(head) else "No loop detected!")
**************************************************************************
Advantages of Linked Lists over Arrays
1. Dynamic Size:
o A linked list can dynamically grow or shrink during execution, while the size of an
array is fixed once declared.
2. Efficient Insertions and Deletions:
o Insertions and deletions in a linked list are efficient as they involve updating
pointers, whereas in arrays, elements need to be shifted, which is time-consuming.
3. Memory Utilization:
o Linked lists use memory efficiently as memory is allocated on demand. Arrays may
lead to unused memory (if the array is under-utilized) or overflow (if the array is
fully utilized).
4. No Contiguous Memory Requirement:
o Linked lists do not require contiguous memory allocation, unlike arrays, making
them suitable for systems with fragmented memory.
5. Ease of Implementation for Certain Data Structures:
o Data structures such as stacks, queues, and hash chains are easier to implement with
linked lists due to their dynamic nature.
When to Use a Linked List?
Frequent Insertions/Deletions:
Unknown Data Size:
Memory Fragmentation
Complex Relationships
When to Use an Array?
1. Frequent Access by Index:
2. Static and Predictable Data Size:
3. Cache-Friendly Operations:
4. Simple Applications:
5. Sorting and Searching:
****************************************************************
Remove Duplicates From a List in Python
1. Using set()
The set data structure inherently removes duplicates because it doesn't allow duplicate values.
numbers = [1, 2, 2, 3, 4, 4, 5, 6, 6, 7]
unique_numbers = list(set(numbers))
Output:
Original List: [1, 2, 2, 3, 4, 4, 5, 6, 6, 7]
List After Removing Duplicates: [1, 2, 3, 4, 5, 6, 7]
Using a Loop
You can iterate through the list and add elements to a new list only if they are not already
present.
numbers = [1, 2, 2, 3, 4, 4, 5, 6, 6, 7]
unique_numbers = []
for num in numbers:
if num not in unique_numbers:
unique_numbers.append(num)
print("Original List:", numbers)
print("List After Removing Duplicates:", unique_numbers)
Output:
Original List: [1, 2, 2, 3, 4, 4, 5, 6, 6, 7]
List After Removing Duplicates: [1, 2, 3, 4, 5, 6, 7]
***********************************************************************
Super()
In Python, super() is a built-in function that is used to call methods from a parent (or
superclass) from within a subclass.
It is commonly used to invoke the constructor (__init__()) of the parent class to ensure
proper initialization of the inherited attributes and methods, especially when working with
inheritance.
The main purpose of super() is to provide a way for a child class to call the methods or
attributes of its parent class without explicitly naming the parent class.
This supports cleaner, more maintainable, and more flexible code, especially in complex
class hierarchies.
super().__init__()
Purpose: The super().__init__() is used to call the constructor of the parent class in a way
that respects the method resolution order (MRO) in multiple inheritance scenarios. It
ensures that Python calls the appropriate method in the inheritance chain.
Explicit Superclass __init__()
Purpose: Calling the superclass's __init__() directly (i.e., by explicitly naming the class)
bypasses the method resolution order (MRO) and directly calls the constructor of the
specified parent class.
When to Use Each Approach
Use super().__init__() when you want to:
o Ensure that the method resolution order (MRO) is respected.
o Work with multiple inheritance, where you may have more than one parent class.
o Write code that is more maintainable and flexible.
Use explicit Superclass.__init__(self) when:
o You have a simple inheritance structure (single inheritance) and want to directly
call the constructor of the parent class.
o You are certain about which class's constructor should be called and don't need to
rely on the MRO.
******************************************************************************
This () and super () both in a constructor:
you can use both self() and super() in a constructor in Python.
It's important to understand their respective purposes and how they interact when used in
the context of a constructor.
Understanding self() and super() in Constructors
self() is used to refer to the current instance of the class. In a constructor (__init__), it is
used to assign or initialize the instance variables of that class.
super() is used to call a method (usually the constructor) from the parent or superclass.
It allows the child class to access the parent class’s methods, including its constructor.
When to Use super() in a Constructor
super() is typically used in a child class’s constructor (__init__) to call the parent class’s
constructor, especially if the parent class has its own initialization logic.
This ensures that the initialization logic of the parent class is executed before or after the
initialization in the child class.
************************************************************************8
classes inherit object
All classes (whether explicitly inherited or not) inherit from a special base class called
object.
This is because, in Python, every class is an instance of the object class, which is the root
of the class hierarchy.
This inheritance from object is a fundamental part of Python's object-oriented design.
Python Classes Inherit from object
Unified Base Class
Object-Oriented Behavior
Access to Core Methods
Consistency and Extensibility
New-Style Classes
All classes in Python inherit from object, whether explicitly or implicitly.
Inheritance from object provides a unified base class, giving all Python classes consistent
behaviors and features.
The inheritance from object enables features like method resolution, polymorphism, and
special methods (__str__, __init__, etc.).
In Python 3, all classes are new-style classes by default, which means they implicitly inherit
from object.
In Python, classes inherit from object to ensure that they are part of a unified object-
oriented model, which allows the usage of common methods and behaviors across all
classes. This inheritance from object is fundamental to Python's design, enabling
consistency, flexibility, and extensibility in object-oriented programming.
*************************************************************************
Differences Between References and Pointers
An abstract class in Python is a class that cannot be instantiated on its own. It serves as a
blueprint for other classes, defining methods that must be implemented by any subclass.
The primary purpose of an abstract class is to define a common interface or a set of methods
that derived classes must implement
Key Concepts of Abstract Classes in Python:
1. Abstract Methods:
o An abstract class can have one or more abstract methods. An abstract method is a
method that is declared but contains no implementation in the abstract class.
o Subclasses of the abstract class are required to implement these abstract methods.
2. Cannot Instantiate Abstract Classes:
o You cannot create an instance of an abstract class directly. It can only be used as a
base class for other classes.
3. Abstract Base Class (ABC) Module:
o Python's abc module provides the functionality to define abstract classes and
methods.
o To create an abstract class, the class must inherit from the ABC (Abstract Base
Class) class provided by the abc module.
4. Use of the @abstractmethod Decorator:
o The @abstractmethod decorator is used to mark methods as abstract methods,
indicating that they must be implemented by any subclass.
****************************************************************************
Lists, Tuples, and Sets
Property List Tuple Set
Ordered, mutable Ordered, immutable Unordered, mutable collection
Definition
collection of elements. collection of elements. of unique elements.
Syntax
Key Characteristics
***********************************************************************
Brute Force and Backtracking
Brute Force Approach
It involves systematically generating and evaluating all possible solutions to find the
correct one.
Key Features:
o Exhaustive Search.
o No Optimization.
o High Time Complexity
Example:
o Solving the Traveling Salesman Problem by evaluating all permutations of
cities to find the shortest route.
Backtracking Approach
A smarter way of exploring possible solutions by incrementally building candidates and
abandoning (or "backtracking") when a candidate fails to satisfy constraints.
Key Features:
o Pruned Search Space
o Recursive Approach
o More Efficient
Example:
o Solving the n-Queens Problem by placing queens one by one on the board and
backtracking if a conflict arises.
Comparison Table
Feature Brute Force Backtracking
******************************************************************************
Python program to convert a comma-separated list into a string:
# Input: A list with comma-separated values
comma_separated_list = ["apple", "banana", "cherry", "date"]
Output
Comma-separated string: apple, banana, cherry, date
***********************************************************************
Python program to capitalize the first letter of a string:
# Input: A string from the user
input_string = input("Enter a string: ")
Example Output
Enter a string: hello world
Output:
Capitalized string: Hello world
**********************************************************************
Depth-First Search (DFS) and Breadth-First Search (BFS) algorithms
Depth-First Search (DFS):
Traversal Order: DFS explores as deep as possible along each branch before
backtracking.
Leaf Nodes: In a full tree, DFS can encounter a leaf node quickly if it dives deep into one
branch.
Behavior:
o DFS does not examine nodes at the current level completely before moving
deeper.
o It may reach a leaf in the first branch it explores.
Breadth-First Search (BFS):
Traversal Order: BFS explores all nodes at the current depth level before moving to the
next level.
Leaf Nodes: In a full tree, BFS only encounters leaves after traversing all nodes at every
level above the leaves.
Behavior:
o BFS takes longer to reach a leaf because it must process all nodes at intermediate
levels first.
Which Traverses a Leaf Soonest?
DFS reaches a leaf node sooner because it directly explores one branch fully, reaching
the deepest nodes (leaves) quickly.
BFS will only reach a leaf node after traversing all levels above it, making it slower to
encounter leaves in a full tree.
A
/ \
B C
/ \ /\
D E F G
DFS (Preorder):
Traversal order: A → B → D (leaf encountered after 3 steps).
BFS:
Traversal order: A → B → C → D (leaf encountered after 4 steps).
*********************************************************************
Use Python Lists , Use Tuples, Dictionaries or Sets
Comparison Table
Data
Order Duplicates Mutable Use Case
Structure
*************************************************************************
Split a Python List into Evenly Sized Chunks
def split_list(lst, chunk_size):
return [lst[i:i + chunk_size] for i in range(0, len(lst), chunk_size)]
my_list = [1, 2, 3, 4, 5, 6, 7, 8, 9]
chunk_size = 3
Example Input:
Original list: [1, 2, 3, 4, 5, 6, 7, 8, 9].
Chunk size: 3.
Output:
Chunks: [[1, 2, 3], [4, 5, 6], [7, 8, 9]].
*****************************************************
Advantages of a binary search over a linear search
Binary search has several advantages over linear search, particularly when dealing with large,
sorted datasets.
Faster Search Time
Efficiency with Sorted Data
Predictable Performance
Optimal for Static Datasets
Example Comparison
Dataset: [1, 3, 5, 7, 9, 11, 13, 15]
Searching for 13:
Linear Search:
o Comparisons: 1 → 3 → 5 → 7 → 9 → 11 → 13 (7 steps).
Binary Search:
o Comparisons: 9 → 13 (2 steps).
Binary Search is far superior in terms of performance for large, sorted datasets due to
its logarithmic time complexity and minimal number of comparisons.
Linear Search is more flexible and can be used in unsorted data structures or small
datasets, but it is inefficient as the data grows in size.
***********************************************************
Sort a List in Python
The sort() method sorts the list in place, meaning it modifies the original list and doesn't return
a new list.
my_list = [3, 1, 4, 1, 5, 9, 2]
my_list.sort()
print(my_list) # Output: [1, 1, 2, 3, 4, 5, 9]
my_list.sort(reverse=True)
*******************************************************************
Types of Data Structures
*****************************************************************************
Time complexity and space complexity
Time complexity
Time complexity refers to the amount of time an algorithm takes to run as a function of the input
size. It helps to analyze how the runtime of an algorithm increases as the size of the input data
increases.
Space Complexity:
Space complexity refers to the amount of memory or space an algorithm needs to run as a function
of the input size. It measures the total memory consumed by an algorithm, including both the space
required for the input and any auxiliary space used during the algorithm's execution.
**********************************************************************
Python list and iterates using the range() function What will be the output?
arr = [1, 2, 3, 4, 5, 6]
for i in range(1, 6):
arr[i - 1] = arr[i]
for i in range(0, 6):
print(arr[i], end = " ")
Answer:
Input List Initialization:
First for Loop
Key Theoretical Observations:
The last element (arr[5]) is not modified during the loop.
The loop effectively shifts elements to the left by one position, and the last two elements
become identical.
Theoretical Concepts Involved
List Mutability
Index-Based Access
Data Shifting
Element Duplication
Final Output Explanation
After both loops:
The first loop shifts elements left, and the list becomes [2, 3, 4, 5, 6, 6].
The second loop prints all elements of the modified list consecutively.
The final output is: Output : 234566
******************************************************************
single inheritance
class Animal:
def __init__(self, name):
self.name = name
def make_sound(self):
print(f"{self.name} makes a sound.")
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name)
self.breed = breed
def make_sound(self):
print(f"{self.name}, the {self.breed}, barks.")
def make_sound(self):
print(f"{self.name} makes a {self.sound} sound.")
class Dog(Animal):
def fetch(self):
print(f"{self.name} is fetching the ball.")
class Cat(Animal):
def climb(self):
print(f"{self.name} is climbing a tree.")
class Bird(Animal):
def fly(self):
print(f"{self.name} is flying.")
**********************************************************************
multiple subclasses like Car, Bike, and Truck
# Base class - Vehicle
class Vehicle:
def start(self):
print("The vehicle is starting.")
# Create instances
car = Car()
bike = Bike()
# Call methods
car.start() # Output: The vehicle is starting.
car.display_info() # Output: This is a car.
Arrangement of
Sequentially, one after the other Hierarchical or network-based
Elements
Element Access Sequential or direct index access Traversal algorithms (DFS, BFS)
One-to-many or many-to-many
Relationships One-to-one (between adjacent elements)
relationships
Linear Data Structures are best for scenarios where elements are processed sequentially,
and the relationship between them is simple and direct.
Non-Linear Data Structures are suited for applications where elements are
interconnected in a more complex way, such as in trees and graphs, representing
hierarchical or network relationships.
*************************************************************************
Abstraction
Abstraction in OOP (Object-Oriented Programming) refers to the concept of hiding
the implementation details and showing only the necessary features of an object.
It allows focusing on what an object does, rather than how it does it.
In OOP, abstraction is achieved through: Abstract classes and Interfaces.
Encapsulation
Encapsulation in Object-Oriented Programming (OOP) is the concept of bundling the
data (variables) and methods (functions) that operate on the data into a single unit known
as a class.
It also refers to the practice of restricting access to some of an object's components,
thereby preventing unintended interference and misuse.
Encapsulation is achieved by using access modifiers such as:
Private
Public
Protected
************************************************************************
Class and Object in OOP:
Class: A class in Object-Oriented Programming (OOP) is a blueprint or template for
creating objects. It defines a set of attributes (variables) and methods (functions) that the
created objects will have. A class is essentially a user-defined data type that describes the
properties and behaviors of objects.
Object: An object is an instance of a class. It is a concrete entity created using the class as
a template. Each object has its own attributes and methods defined by the class.
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
# Create objects (instances) of the Car class
car1 = Car("Toyota", "Corolla", 2020)
car2 = Car("Honda", "Civic", 2022)
************************************************************************
Self-parameter
In Python, the self parameter refers to the instance of the class. It is a way for an object to
refer to itself within the class methods.
The self parameter is used in instance methods to access and manipulate the instance's
attributes and other methods.
needed
1. Binding Methods to Objects:
o When you create an object, it has its own set of data (attributes). The self keyword
is needed to bind the method to the object (instance) and ensure it operates on the
object’s data, not on the class itself.
2. Referring to the Current Instance
Every instance of a class has its own state (data), and self refers to that state. It allows each
object to have its own set of attributes and behaviors, even if they come from the same
class.
class Car:
def __init__(self, make, model):
self.make = make
self.model = model
def display_info(self):
print(f"Car: {self.make} {self.model}")
*******************************************************************************
Inheritance
Inheritance is one of the key concepts in Object-Oriented Programming (OOP) that allows
a class (called a subclass or derived class) to inherit attributes and methods from another
class (called a superclass or base class).
This promotes code reuse and establishes a relationship between classes.
In Python, inheritance allows a subclass to:
Inherit
Override or extend
Access
Types of Inheritance in Python: Single Inheritance, Multiple Inheritance, Multilevel
Inheritance, Hierarchical Inheritance and Hybrid Inheritance.
**************************************************************************
Exception Handling in Python and Java
Both Python and Java support exception handling to manage runtime errors, but their
approaches, syntax, and features differ in several ways.
Catching Python allows catching specific Java allows catching specific exceptions and
Specific exceptions by listing them after supports hierarchical exception handling
Exceptions the except keyword. with inheritance.
Exception In Python, you can use raise to In Java, exceptions can be re-thrown using
Propagation re-throw an exception. throw or throws.
*********************************************************************************
One-dimensional Array AND Multi-dimensional Array
One-dimensional Array
A one-dimensional array is a simple linear collection of elements. It is essentially a list of values
arranged in a single row or column.
# One-dimensional array
one_dim_array = [1, 2, 3, 4, 5]
# Accessing elements
print("First element:", one_dim_array[0]) # Output: 1
print("Last element:", one_dim_array[-1]) # Output: 5
Multi-dimensional Array
A multi-dimensional array, on the other hand, is an array of arrays. It represents data in a matrix
form, which can be two-dimensional, three-dimensional, or even higher-dimensional.
# Two-dimensional array (2D)
two_dim_array = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
# Accessing elements
print("Element at row 1, column 1:", two_dim_array[0][0]) # Output: 1
print("Element at row 3, column 2:", two_dim_array[2][1]) # Output: 8
Key Differences:
Aspect One-dimensional Array Multi-dimensional Array
One-dimensional Array:
Advantages:
1. Simplicity
2. Efficient Memory Usage
3. Faster Access
4. Ease of Use
Disadvantages:
1. Limited Structure
2. Scalability
3. Limited Representation
Multi-dimensional Array:
Advantages:
1. Complex Data Representation
2. Efficient for Mathematical Operations
3. Organized Data
4. Support for Higher Dimensions
Disadvantages:
1. Complexity
2. Memory Usage
3. Slower Access
4. Overhead
************************************************************************8
List Methods:
Explanation:
1. append(): Adds an item (6) to the end of the list.
2. extend(): Adds elements [7, 8, 9] from another iterable.
3. insert(): Inserts item 10 at index 3.
4. remove(): Removes the first occurrence of the item 10.
5. pop(): Pops and removes the element at index 2, which is 3.
6. clear(): Clears the list, making it empty.
7. index(): Finds the index of the first occurrence of 3.
8. count(): Counts how many times 2 appears in the list.
9. sort(): Sorts the list in ascending order.
10. reverse(): Reverses the order of the list.
11. copy(): Creates a shallow copy of the list.
12. sort(reverse=True): Sorts the list in descending order.
13. reverse() again: Reverses the list again.
my_list = [1, 2, 3, 4, 5]
my_list.append(6)
print("After append:", my_list)
my_list.extend([7, 8, 9])
print("After extend:", my_list)
my_list.clear()
print("After clear:", my_list)
my_list = [1, 2, 3, 4, 5]
index_of_item = my_list.index(3)
print("Index of 3:", index_of_item)
count_of_item = my_list.count(2)
print("Count of 2:", count_of_item)
my_list.sort()
print("After sort:", my_list)
my_list.reverse()
print("After reverse:", my_list)
copied_list = my_list.copy()
print("Copied list:", copied_list)
my_list.sort(reverse=True)
print("After sort with reverse=True:", my_list)
my_list.reverse()
print("After reverse again:", my_list)
Output :
After append: [1, 2, 3, 4, 5, 6]
After extend: [1, 2, 3, 4, 5, 6, 7, 8, 9]
After insert: [1, 2, 3, 10, 4, 5, 6, 7, 8, 9]
After remove: [1, 2, 3, 4, 5, 6, 7, 8, 9]
After pop: [1, 2, 4, 5, 6, 7, 8, 9]
Popped item: 3
After clear: []
Index of 3: 2
Count of 2: 1
After sort: [1, 2, 3, 4, 5]
After reverse: [5, 4, 3, 2, 1]
Copied list: [5, 4, 3, 2, 1]
After sort with reverse=True: [5, 4, 3, 2, 1]
After reverse again: [1, 2, 3, 4, 5]
***************************************************************************8