Python Basics: Lists, Tuples, and Operators
Python Basics: Lists, Tuples, and Operators
Unit – 1
PART A (2 Marks) - Short Answer Questions
Q1. What is the difference between a List and a Tuple in Python?
List: Lists are mutable (can be changed after creation), defined using square brackets [], and are generally
slower than tuples. Example: my_list = [1, 2, 3]
Tuple: Tuples are immutable (cannot be changed), defined using parentheses (), and are faster than lists.
Example: my_tuple = (1, 2, 3)
Mutable: Objects whose value can be changed after they are created.
Slicing is a technique to extract a substring from a string (or sublist from a list) using indices. The syntax is
string[start:stop:step].
Example:
Python
s = "PYTHON"
print(s[0:2]) # Output: PY
A dictionary is an unordered collection of data values used to store data values like a map. Unlike lists that
hold only a single value as an element, dictionaries hold a key:value pair.
The __init__ method is a special method (constructor) in Python classes. It is automatically called when an
object of a class is created. It is used to initialize the state (attributes) of the object.
Break: Terminates the loop immediately and transfers execution to the statement following the loop.
Continue: Skips the rest of the code inside the current loop iteration and jumps to the next iteration of the
loop.
Q7. What is Data Abstraction and Encapsulation?
Encapsulation: Wrapping up data (variables) and methods (functions) together into a single unit (Class).
Abstraction: Hiding the implementation details and showing only the essential features of the object to the
user.
A namespace is a system to ensure that all the names in a program are unique and can be used without any
conflict. It is essentially a mapping of names to objects (e.g., Local, Global, and Built-in namespaces).
Answer:
1. Introduction
Operators are special symbols in Python that carry out arithmetic or logical computation. The value that the
operator operates on is called the operand. For example, in a + b, + is the operator and a, b are operands.
1. Arithmetic Operators
2. Comparison (Relational) Operators
3. Logical Operators
4. Assignment Operators
5. Membership Operators
6. Identity Operators
7. Bitwise Operators
1. Arithmetic Operators
These are used to perform mathematical operations like addition, subtraction, multiplication, etc.
Example Code:
Python
a = 10
b=3
print("Addition:", a + b)
print("Floor Division:", a // b) # Output: 3
print("Exponent:", a ** b) # Output: 1000
These operators compare the values on either side and decide the relation between them. They always return a
Boolean value (True or False).
== Equal to x == y → False
Example Code:
Python
x = 10
y = 20
print(x > y) # Output: False
print(x != y) # Output: True
3. Logical Operators
and Returns True if both statements are true x < 5 and x < 10
not Reverse the result, returns False if the result is true not(x < 5 and x < 10)
Example Code:
Python
a = True
b = False
print(a and b) # Output: False
print(a or b) # Output: True
4. Assignment Operators12
5. Membership Operators3536
These operators test 37if a sequence is presented in an object (like strings, lists, or tuples).
in: Returns True if a sequence with the specified value is present in the object.
not in: Returns True if a sequence with the specified value is not present in the object.
Example Code:
Python
fruits = ["apple", "banana"]
print("banana" in fruits) # Output: True
print("orange" not in fruits) # Output: True
6. Identity Operators
These operators compare the memory locations of two objects, not just if they are equal.
is: Returns True if both variables point to the same object in memory.
is not: Returns True if both variables point to different objects.
Example Code:
Python
x = ["apple", "banana"]
y = ["apple", "banana"]
z=x
& (AND), | (OR), ^ (XOR), ~ (NOT), << (Left Shift), >> (Right Shift).
Conclusion:
Python provides a rich set of built-in operators that form the foundation of logic building in programming,
ranging from basic arithmetic to complex logical and object identity checks.
2. Discuss Python Control Flow statements (Conditionals and Loops) with flowcharts and syntax.
Answer:
1. Introduction to Control Flow Control flow refers to the order in which individual statements, instructions,
or function calls are executed or evaluated in a program. By default, Python executes code sequentially (line by
line). However, we often need to alter this flow based on decisions or repetitions.
These statements allow the program to execute a block of code only if a specific condition is true.
A. The if Statement
This is the simplest form of decision-making. It executes a block of code only if the condition is True.
Syntax:
Python
if condition:
# statement(s) to execute if condition is true
Example:
Python
age = 18
if age >= 18:
print("You are eligible to vote.")
B. The if...else Statement
This handles two possibilities. If the condition is True, the if block executes; otherwise, the else block executes.
Syntax:
Python
if condition:
# Executes if True
else:
# Executes if False
Example:
Python
num = 10
if num % 2 == 0:
print("Even Number")
else:
print("Odd Number")
C. The if...elif...else Ladder
Used when checking multiple conditions sequentially. As soon as one condition is true, its block is executed,
and the rest are skipped.
Syntax:
Python
if condition1:
# block 1
elif condition2:
# block 2
else:
# block 3 (default)
Example:
Python
marks = 75
if marks >= 90:
print("Grade A")
elif marks >= 70:
print("Grade B")
else:
print("Grade C")
Loops allow us to execute a block of code repeatedly until a certain condition is met.
A. The while Loop
A while loop repeatedly executes a target statement as long as a given condition remains true. It is known as an
entry-controlled loop.
Syntax:
Python
while condition:
# statements
# update iterator
Python
count = 1
while count <= 5:
print(count)
count = count + 1
B. The for Loop
The for loop in Python is used to iterate over a sequence (like a list, tuple, dictionary, or string). It is a definite
loop (we know how many times it will run based on the sequence length).
Syntax:
Python
Python
Python
Example of break:
Python
for i in range(10):
if i == 5:
break # Stops loop when i is 5
print(i)
Conclusion: Control flow statements are the building blocks of logical programming. Conditionals allow for
decision-making, while loops facilitate efficient repetition of tasks without rewriting code.
Answer:
1. Introduction to Python Lists A List is a collection of items ordered in a sequence. It is very flexible and is
one of the most used data types in Python.
Ordered: The items have a defined order, and that order will not change.
Mutable: We can change, add, and remove items in a list after it has been created.
Heterogeneous: A list can hold items of different data types (integers, strings, floats, etc.).
Syntax: Lists are created by placing elements inside square brackets [], separated by commas.
Python
You can access individual elements using their index. Python supports both positive (0 to n-1) and negative
indexing (-1 starts from the end).
Example:
Python
Python
nums = [0, 1, 2, 3, 4, 5]
print(nums[1:4]) # Output: [1, 2, 3] (Stop index is excluded)
print(nums[:3]) # Output: [0, 1, 2]
C. Concatenation (+)
Example:
Python
L1 = [1, 2]
L2 = [3, 4]
print(L1 + L2) # Output: [1, 2, 3, 4]
D. Repetition (*)
Example:
Python
L = ["Hi"]
print(L * 3) # Output: ['Hi', 'Hi', 'Hi']
E. Membership (in)
Example:
Python
A. Adding Elements
Python
L = [1, 2]
[Link](3)
# L is now [1, 2, 3]
2. extend(iterable): Adds elements of a list (or any iterable) to the end of the current list.
Python
L = [1, 2]
[Link]([3, 4])
# L is now [1, 2, 3, 4]
Python
L = [1, 3]
[Link](1, 2) # Insert 2 at index 1
# L is now [1, 2, 3]
B. Removing Elements
Python
2. pop(index): Removes and returns the element at the specified index. If no index is specified, it
removes the last item.
Python
Python
[Link]()
# L is now []
C. Utility Methods
Python
L = [3, 1, 2]
[Link]()
# L is now [1, 2, 3]
Python
L = [1, 2, 3]
[Link]()
# L is now [3, 2, 1]
Python
L = [1, 1, 2, 3]
print([Link](1)) # Output: 2
4. index(value): Returns the index of the first occurrence of the specified value.
Python
Answer:
1. Introduction to OOP
Object-Oriented Programming (OOP) is a programming paradigm that structures programs so that properties
and behaviors are bundled into individual objects. It models real-world entities (like a Car, Student, or
Employee) into code.
A Class is a user-defined data type that acts as a blueprint or template for creating objects. It defines the
attributes (variables) and methods (functions) that the objects created from the class will have.
Python
class ClassName:
# attributes
# methods
B. Object (The Instance)
An Object is an instance of a class. When a class is defined, no memory is allocated until an object is created.
An object has three characteristics: State (attributes), Behavior (methods), and Identity.
Syntax: object_name = ClassName()
class Student:
# Constructor method to initialize attributes
def __init__(self, name, roll_no):
[Link] = name
self.roll_no = roll_no
# Creating Objects
s1 = Student("Alice", 101)
s2 = Student("Bob", 102)
# Accessing methods
[Link]()
[Link]()
3. Inheritance
Inheritance is a powerful feature of OOP that allows a class (Child Class) to derive or inherit the properties and
methods of another class (Parent Class).
Syntax:
Python
class ParentClass:
# body of parent
class ChildClass(ParentClass):
# body of child
4. Types of Inheritance
Python supports several types of inheritance. You should explain these with block diagrams.
A. Single Inheritance
Python
class Animal:
def speak(self):
print("Animal Speaking")
d = Dog()
[Link]() # Calls Parent method
[Link]() # Calls Child method
B. Multiple Inheritance
A child class inherits from more than one parent class. Python supports this (unlike Java).
Python
class Father:
def height(self):
print("Tall height")
class Mother:
def color(self):
print("Fair color")
c = Child()
[Link]()
[Link]()
C. Multilevel Inheritance
A child class inherits from a parent, who in turn inherits from a grandparent. It forms a chain.
D. Hierarchical Inheritance
5. Explain Dictionaries in Python. Discuss the various methods to access and modify dictionary elements.
Answer:
1. Introduction to Dictionaries
A Dictionary in Python is an unordered collection of data values, used to store data values like a map. Unlike
other Data Types that hold only a single value as an element, Dictionary holds a key:value pair.
Key-Value Pair: Each element is defined by a unique key that maps to a specific value.
Mutable: Dictionaries can be changed (add, remove, or modify items) after creation.
Unordered: Items are not stored in a specific index order (prior to Python 3.7).
Unique Keys: Duplicate keys are not allowed. Keys must be immutable (e.g., Strings, Numbers,
Tuples).
Syntax:
Dictionaries are enclosed in curly braces {} and separated by commas.
Python
my_dict = {
"brand": "Ford",
"model": "Mustang",
"year": 1964
}
2. Creating a Dictionary
You can create a dictionary by placing a sequence of elements within curly braces, separated by ‘comma’.
Empty Dictionary:
Python
d = {}
Python
Python
d = dict(name="John", age=36)
Since dictionaries are unordered, we cannot use integer indexes (like 0, 1) to access values. We use Keys.
Python
Returns the value for the given key. If the key is not available, it returns None (or a default value you specify)
instead of an error.
Python
print([Link]('age')) # Output: 20
print([Link]('gender')) # Output: None
print([Link]('gender', 'N/A')) # Output: N/A
Python
Python
student['age'] = 21
# Result: {'name': 'Alice', 'age': 21, 'city': 'New York'}
C. Using update()
Updates the dictionary with elements from another dictionary or an iterable of key/value pairs.
Python
5. Removing Elements
pop(key): Removes the item with the specified key name and returns its value.
Python
[Link]("age")
Python
[Link]()
del keyword: Deletes the item with the specified key name.
Python
del student["name"]
Python
[Link]() # Result is {}
Example of Iteration:
Python
d = {'a': 1, 'b': 2}
Class (The Blueprint): A Class is a user-defined data type that acts as a blueprint for creating
objects. In this scenario, we define a class named Employee. It serves as a template that
defines what data an employee has (Name, ID, Salary) and what actions they can perform
(Calculate Salary, Show Details).
Object (The Instance): An object is a specific instance of a class. For example, if Employee is
the class, then Employee("John", 101, 50000) is an object representing a specific person.
The _init_ Method (Constructor): This is a special method in Python. It is automatically
invoked when a new object is created. We use it to initialize the object's attributes.
The self Keyword: In Python, self represents the instance of the class. It binds the attributes
with the specific arguments provided. For instance, [Link] = name ensures that the name
belongs to that specific object, not another one.
Before writing the code, explain the step-by-step logic. This shows the evaluator you
understand the flow
Step 2: Inside the class, define the _init_ method with parameters: self, name, empid, and
salary.
Logic:
Calculate HRA (House Rent Allowance) as 10% of basic salary (0.10 * salary).
Step 4: Define a method display(self) to print the employee's name, ID, and the calculated net
salary.
3. Program Implementation
Python
class Employee:
[Link] = name
[Link] = empid
self.basic_salary = basic_salary
def calculate_net_salary(self):
return net_salary
def display(self):
[Link]()
4. Output
Plaintext
You can add this small section to fill space and show thoroughness:
Basic: 25,000
2: Write a Python program that accepts a sentence and performs the following operations:
To solve this problem, we need to manipulate text data. In Python, text is handled using Strings. A
thorough understanding of String methods and Control Flow is required.
Python
# Function to perform string operations
def process_sentence():
# 1. Accept Input
sentence = input("Enter a sentence: ")
# Driver Code
process_sentence()
5. Output Trace
Scenario:
Execution Trace:
1. Vowels Count:
o P (no), y (no), t (no), h (no), o (yes), n (no)...
o Vowels found: 'o', 'i', 'E', 'a' = 4
2. Word Count:
o split() creates ['Python', 'is', 'Easy']
o Length = 3
3. Reversal:
o Python -> nohtyP
o is -> si
o Easy -> ysaE
Plaintext
Enter a sentence: Python is Easy
3: Explain the concept of Exception Handling in Python. Write a program to handle the
"Division by Zero" error.
To write a robust program, handling errors gracefully is essential. In Python, this is achieved through
Exception Handling.
Risk: If the user enters 0 as the denominator, mathematically, the result is undefined. Python
raises a ZeroDivisionError.
Risk 2: If the user enters text (e.g., "hello") instead of a number, Python raises a
ValueError. We must handle both to make the program "crash-proof."
Here is the code using all four blocks for a complete answer.
Python
def division_calculator():
print("--- Safe Division Program ---")
except ZeroDivisionError:
# 2. CATCH SPECIFIC ERROR (Division by 0)
print(">> Error: You cannot divide by Zero! Please try
again.")
except ValueError:
# 3. CATCH WRONG INPUT TYPE (Text instead of number)
print(">> Error: Invalid input! Please enter numbers
only.")
else:
# 4. THE ELSE BLOCK
# Executes only if NO exceptions occurred
print(f">> Success! The result is: {result}")
break # Exit the loop on success
finally:
# 5. THE FINALLY BLOCK
# Executes always
print("--- Execution attempt finished ---\n")
# Driver Code
division_calculator()
5. Output Trace (Case Studies)
In a 15-mark question, showing different test cases proves you understand how the flow changes
based on input.
Plaintext
--- Safe Division Program ---
Enter Numerator: 10
Enter Denominator: 2
>> Success! The result is: 5.0
--- Execution attempt finished ---
Plaintext
--- Safe Division Program ---
Enter Numerator: 10
Enter Denominator: 0
>> Error: You cannot divide by Zero! Please try again.
--- Execution attempt finished ---
Plaintext
--- Safe Division Program ---
Enter Numerator: Ten
>> Error: Invalid input! Please enter numbers only.
--- Execution attempt finished ---
6. Key Takeaways for the Evaluator
Robustness: The program does not crash even if the user gives bad input.
Hierarchy: Specific errors (ZeroDivisionError) are handled separately from generic
errors, providing better feedback to the user.
Cleanup: The finally block ensures that any necessary concluding steps happen
(represented here by the "finished" message).
4: Explain the concept of Exception Handling in Python. Write a program to handle
"Division by Zero" error.
1. Introduction to Exception Handling
In Python, there are two types of errors that occur during coding:
1. Syntax Errors: Errors caused by wrong grammar (e.g., missing a colon, wrong indentation).
The code will not run at all.
2. Exceptions (Runtime Errors): Errors that occur during execution. The code syntax is
correct, but something goes wrong while the program is running (e.g., trying to divide by
zero, trying to open a file that doesn't exist).
Exception Handling is the method of handling these runtime errors gracefully so that the program
does not crash abruptly. Instead of showing a scary technical error message to the user, the program
catches the error and displays a friendly message or takes alternative action.
A. try block
This block contains the "suspicious" code that might raise an exception.
Python "tries" to execute this code. If everything is fine, it skips the except block.
If an error occurs here, execution immediately jumps to the except block.
B. except block
Here is the complete program illustrating all the blocks discussed above.
The Code:
Python
def division_program():
print("--- Start of Program ---")
try:
# 1. We ask the user for input inside the try block
numerator = int(input("Enter the numerator: "))
denominator = int(input("Enter the denominator: "))
except ZeroDivisionError:
# 3. This runs if the user enters 0 for the denominator
print("Error: You cannot divide a number by zero!")
except ValueError:
# 4. This runs if the user enters text instead of numbers
print("Error: Invalid input! Please enter numeric values only.")
else:
# 5. This runs ONLY if the division was successful
print(f"Success! The result is: {result}")
finally:
# 6. This runs NO MATTER WHAT happens above
print("--- Execution Completed (Cleaning up resources) ---")
To get full marks, you should explain the different outputs this code generates.
Plaintext
Plaintext
UNIT – 2
PART A (2 Marks) - Short Answer Questions
An Abstract Data Type (ADT) is a logical description of a data structure. It defines what operations
can be performed on the data but does not specify how these operations are implemented.
Shallow Copy: Creates a new object but stores references to the original elements. Changes
to mutable elements in the copied object will affect the original object. (Module: [Link]())
Deep Copy: Creates a new object and recursively copies all objects found in the original.
Changes to the copy do not affect the original. (Module: [Link]())
Asymptotic notations are mathematical tools used to describe the running time or space complexity of
an algorithm as the input size grows.
A namespace is a mapping from names to objects. It ensures that object names in a program are
unique and can be used without conflict. Examples: Local Namespace, Global Namespace, Built-in
Namespace.
A Queue follows FIFO (First In First Out). Elements are inserted at the rear and deleted from the
front.
Applications: Printer scheduling, CPU task scheduling, Breadth-First Search (BFS) in graphs.
1: Explain the Stack ADT and its implementation using a Python List.
1. Definition of Stack ADT
A Stack is a linear data structure that follows the LIFO (Last In First Out) principle.
This means the element that is inserted last will be the first one to be removed.
A real-life example is a stack of plates: you place a new plate on top, and you also remove
the plate from the top. You cannot remove a plate from the middle without removing the top
ones first.
Key Characteristics:
Insertion and Deletion happen at the same end, known as the TOP.
It is often called a "Push-Down List".
2. Operations on Stack
3. Diagrammatic Representation
Plaintext
| |
| 30 | <--- TOP (Last Element Pushed)
| 20 |
| 10 | <--- Bottom (First Element Pushed)
+-------+
In Python, a simple List can be used as a Stack because it supports append() (add to end/top) and
pop() (remove from end/top).
The Code:
Python
class Stack:
def __init__(self):
"""Initialize an empty stack"""
[Link] = []
def is_empty(self):
"""Check if the stack is empty"""
return [Link] == []
def pop(self):
"""Remove and return the top item"""
if self.is_empty():
return "Error: Stack Underflow"
return [Link]()
def peek(self):
"""Return the top item without removing it"""
if self.is_empty():
return "Error: Stack is Empty"
return [Link][-1]
def size(self):
"""Return the number of elements in the stack"""
return len([Link])
def display(self):
"""Print the stack contents"""
print("Current Stack:", [Link])
6. Complexity Analysis
7. Applications of Stack
1. Function Call Management: Python uses a stack (Call Stack) to manage function calls and
recursion.
2. Expression Evaluation: Used to convert Infix expressions to Postfix (Reverse Polish
Notation).
3. Undo Mechanism: Editors use stacks to store changes so you can "Undo" (Pop) the last
action.
4. Balanced Parentheses: Checking if code has matching () or {}.
2: Explain the Queue ADT and its operations with a Python program.
1. Definition of Queue ADT
A Queue is a linear data structure that follows the FIFO (First In First Out) principle.
This means the element that is inserted first will be the first one to be removed.
A real-life example is a queue of people standing at a ticket counter: the person who comes
first gets the ticket first and leaves the line first.
Key Characteristics:
2. Operations on Queue
3. Diagrammatic Representation
In Python, you can implement a Queue using a standard list, but it is not efficient for large data
because inserting/deleting at the beginning of a list takes $O(n)$ time (shifting elements). The
efficient way is using [Link].
Python
class QueueList:
def __init__(self):
[Link] = []
def is_empty(self):
return len([Link]) == 0
def size(self):
return len([Link])
def display(self):
print("Queue:", [Link])
Note: For a full 13 marks, mentioning this method shows deeper knowledge.
Python
from collections import deque
class Queue:
def __init__(self):
# deque is a double-ended queue, optimized for adding/removing from both ends
[Link] = deque()
def dequeue(self):
if self.is_empty():
return "Error: Queue Underflow"
return [Link]() # Optimized O(1) removal
def is_empty(self):
return len([Link]) == 0
def size(self):
return len([Link])
6. Applications of Queue
1. Job Scheduling: Operating systems use queues to schedule processes (CPU Scheduling).
2. Printer Spooling: Documents sent to a printer are lined up in a queue.
3. Breadth-First Search (BFS): Graph traversal algorithms use queues to explore nodes.
4. Handling Requests: Web servers use queues to handle incoming user requests in order.
Divide and Conquer is an algorithm design paradigm. It solves a problem by breaking it down into
smaller sub-problems, solving them recursively, and then combining their solutions to get the final
result.
1. Divide: Break the original problem into smaller sub-problems that are similar to the original
problem but smaller in size.
2. Conquer: Solve the sub-problems recursively. (Base case: if the problem is small enough,
solve it directly).
3. Combine: Merge the solutions of the sub-problems to create the solution for the original
problem.
Merge Sort is a classic sorting algorithm that perfectly demonstrates this strategy.
Divide: The array is divided into two halves using the middle index: mid = len(array) // 2.
Conquer: We recursively call Merge Sort on both the left half and the right half. This
continues until the sub-arrays have only one element (which is already sorted).
Combine: The sorted halves are merged back together to form a complete sorted array.
Diagrammatic Representation
Example Trace:
Input List: [38, 27, 43, 3]
1. Divide:
Split into [38, 27] and [43, 3]
o
Split again into [38], [27], [43], [3] (Base Case reached).
o
2. Conquer & Combine (Merge):
o Merge [38] and [27] $\rightarrow$ [27, 38]
o Merge [43] and [3] $\rightarrow$ [3, 43]
o Merge [27, 38] and [3, 43] $\rightarrow$ [3, 27, 38, 43] (Final Sorted List).
3. Python Implementation
Python
def merge_sort(arr):
# Base Case: If list has 0 or 1 element, it is already sorted
if len(arr) <= 1:
return arr
# Step 1: Divide
mid = len(arr) // 2
left_half = arr[:mid]
right_half = arr[mid:]
# Compare elements from both lists and add smaller one to result
while i < len(left) and j < len(right):
if left[i] < right[j]:
sorted_list.append(left[i])
i += 1
else:
sorted_list.append(right[j])
j += 1
return sorted_list
Time Complexity
The running time of Merge Sort can be expressed using the recurrence relation:
Space Complexity
Merge Sort is not an in-place sort. It requires auxiliary (extra) memory to store the temporary
sub-arrays during the merge process.
Space Complexity: $O(n)$
Question: Discuss Shallow Copy and Deep Copy in Python with a clear code example.
1. Introduction: Assignment vs. Copying
In Python, if you use the assignment operator (=) to assign one list to another (e.g., list2 = list1), it
does not create a copy. Instead, it creates a reference (or alias). Both variables point to the same
memory location.
To create actual duplicates of data, Python provides the copy module, which supports two types of
copying:
1. Shallow Copy
2. Deep Copy
2. Shallow Copy
A Shallow Copy creates a new object (a new container), but it inserts references into it to the objects
found in the original.
Function: [Link](x)
Behavior:
o It creates a copy of the outer list.
o However, the inner elements (like nested lists) are shared between the original and
the copy.
o If you modify a nested object in the copy, the change will reflect in the original
object.
3. Deep Copy
A Deep Copy creates a new object and then recursively copies the objects found in the original.
Function: [Link](x)
Behavior:
o It creates a fully independent copy of the original object and all its children.
o It copies the outer list and the inner nested lists.
o If you modify a nested object in the copy, the change will NOT reflect in the original
object.
To demonstrate the difference, we must use a nested list (a list inside a list), because flat lists (lists
with only numbers) behave similarly for both operations.
Python
import copy
def demonstrate_copying():
print("--- 1. ASSIGNMENT OPERATION (=) ---")
original = [1, [2, 3], 4]
alias = original # Just a reference, not a copy
print(f"Original: {original}")
print(f"Shallow: {shallow}")
print("Result: Outer element change didn't affect original, but NESTED change did.\n")
print(f"Original: {original}")
print(f"Deep: {deep}")
print("Result: Original remains completely untouched.")
A. In Shallow Copy:
B. In Deep Copy:
1: Write a Python program to define a class Employee with attributes name, id, and
salary. Include methods to Initialize, Calculate Net Salary, and Display Details.
1. Problem Definition
Data (Attributes): Every employee has a Name, an Employee ID, and a Basic Salary.
Actions (Methods):
1. __init__: To set up these values when a new employee is created.
2. calculate_net_salary: To perform math (Net Salary = Basic + Allowances -
Deductions).
3. display_details: To print the information neatly.
Class: The template. Think of it like a "Form" that has blank spaces for Name, ID, Salary.
Object: The filled-in form. (e.g., Employee "John", ID 101).
Encapsulation: We are bundling the data (salary, id) and the functions that use them into a
single unit.
3. Class Diagram
Here is the complete code. I have added comments to explain each part.
Python
class Employee:
"""
A class to represent an Employee in an organization.
"""
# Creating Object 1
print("--- Creating First Employee Object ---")
emp1 = Employee("Alice", 101, 50000)
emp1.display_details()
# Creating Object 2
print("\n--- Creating Second Employee Object ---")
emp2 = Employee("Bob", 102, 35000)
emp2.display_details()
Output on Screen:
Plaintext
--- Creating First Employee Object ---
==============================
EMPLOYEE PAY SLIP
==============================
Name : Alice
Employee ID : 101
Basic Salary : Rs. 50000.00
HRA (20%) : Rs. 10000.00
DA (10%) : Rs. 5000.00
------------------------------
NET SALARY : Rs. 59000.00
==============================
6. Key Concepts to Explain (for 15 Marks)
To ensure full marks, write a short paragraph on these keywords used in the code:
The Tower of Hanoi is a classic mathematical puzzle consisting of three rods and n disks of different
sizes.
Initial State: All disks are stacked on the first rod (Source) in decreasing order of size
(largest at the bottom, smallest at the top).
Goal: Move the entire stack to the last rod (Destination).
The problem might look complex for many disks, but it uses a simple "Divide and Conquer"
strategy. We can solve the problem for n disks if we know how to solve it for n-1 disks.
1. Move n-1 disks from Source (A) to Auxiliary (B). (Using C as the helper).
2. Move the nth (largest) disk directly from Source (A) to Destination (C).
3. Move the n-1 disks from Auxiliary (B) to Destination (C). (Using A as the helper).
3. Python Program Implementation
Python
def tower_of_hanoi(n, source, destination, auxiliary):
"""
Function to solve Tower of Hanoi puzzle.
n: number of disks
source: rod where disks initially reside
destination: rod where disks must go
auxiliary: helper rod
"""
# BASE CASE: If there is only 1 disk, just move it
if n == 1:
print(f"Move disk 1 from {source} to {destination}")
return
To get full marks, you must show the step-by-step output for a small number like n=3.
Total Moves: 7
This is the most critical part for a 15-mark question. You must derive the formula.
Recurrence Relation:
Solving by Substitution:
Time Complexity:
Since the number of moves grows exponentially with n, the time complexity is:$O(2^n)$
(Exponential Time)
Space Complexity:
The space complexity is determined by the maximum depth of the recursion stack (how many
function calls are waiting in memory).$O(n)$ (Linear Space)
Question: Implement a "Circular Queue" ADT. Explain why it is better than a simple
Queue implemented using Arrays.
1. The Problem with Linear Queues (Why do we need Circular?)
A Circular Queue is a linear data structure in which the operations are performed based on a FIFO
(First In First Out) principle, but the last position is connected back to the first position to make a
circle.
In a circular queue of size N, we use Modulo Arithmetic (%) to wrap the pointers around.
1. Index Calculation:
4. Python Implementation
Since Python lists are dynamic, we simulate a Fixed Size Circular Queue to demonstrate the concept
properly (as expected in an algorithm exam).
Python
class CircularQueue:
def __init__(self, size):
[Link] = size
# Initialize queue with None
[Link] = [None] * size
[Link] = -1
[Link] = -1
# 4. Insert Element
[Link][[Link]] = item
print(f"Enqueued: {item}")
def dequeue(self):
# 1. Check if Queue is Empty
if [Link] == -1:
print("Queue is Empty! (Underflow)")
return
# 2. Retrieve item
item = [Link][[Link]]
print(f"Dequeued: {item}")
return item
def display(self):
if [Link] == -1:
print("Queue is Empty")
return
[Link](10)
[Link](20)
[Link](30)
[Link](40)
[Link](50)
Overflow Happens when Rear reaches End, even Happens only when the Queue is genuinely
Condition if Front is empty. full (Count = Size).
UNIT – 4
PART A (2 Marks) - Short Answer Questions
Binary Tree: A tree data structure where each node has at most two children,
referred to as the left child and the right child.
Difference: A general tree can have any number of children per node, whereas a
binary tree is restricted to a maximum of two.
The value of the left child is less than the parent's value.
The value of the right child is greater than the parent's value.
This property applies to every node in the tree.
AVL Tree: A self-balancing Binary Search Tree where the difference between
heights of left and right subtrees cannot be more than 1 for all nodes.
Balance Factor (BF): $BF = Height(Left Subtree) - Height(Right Subtree)$. Allowed
values are $\{-1, 0, 1\}$.
A binary tree where all levels are completely filled except possibly the last level, which is
filled from left to right. This structure is essential for Heap implementation using arrays.
A tree where each node can hold more than one key and can have more than two children. A
common example is a B-Tree of order $m$, where a node can have up to $m$ children and
$m-1$ keys.
1: Explain the three Tree Traversal techniques (Inorder, Preorder, Postorder) with
recursive algorithms and an example diagram.
1. Introduction to Tree Traversal
Tree Traversal is the process of visiting every node in a tree data structure exactly once. Unlike linear
data structures (Arrays, Linked Lists) where there is only one way to traverse (start to end), trees can
be traversed in different ways.
Let us consider the following Binary Tree for all our examples.
Structure:
Root: A
Left Subtree: B (with children D, E)
Right Subtree: C (no children)
In this traversal, we visit the left child first, then the root, and finally the right child.
Algorithm Steps:
Python Implementation:
Python
def inorder_traversal(root):
if root:
# Step 1: Recur on Left
inorder_traversal([Link])
Start at A. Go Left to B.
At B, Go Left to D.
At D (Leaf), Left is None $\rightarrow$ Print D $\rightarrow$ Right is None. Return to B.
Back at B $\rightarrow$ Print B $\rightarrow$ Go Right to E.
At E (Leaf) $\rightarrow$ Print E. Return to B, then return to A.
Back at A $\rightarrow$ Print A $\rightarrow$ Go Right to C.
At C (Leaf) $\rightarrow$ Print C.
Output: D B E A C
Note: In a Binary Search Tree (BST), Inorder traversal always gives sorted output.
In this traversal, we visit the root first, then the left child, and finally the right child.
Algorithm Steps:
Python Implementation:
Python
def preorder_traversal(root):
if root:
# Step 1: Visit Node
print([Link], end=" ")
Output: A B D E C
In this traversal, we visit the left child first, then the right child, and finally the root. The root is
always visited last.
Algorithm Steps:
Python Implementation:
Python
def postorder_traversal(root):
if root:
# Step 1: Recur on Left
postorder_traversal([Link])
Start at A. Go Left to B.
At B, Go Left to D.
At D (Leaf) $\rightarrow$ Print D. Return to B.
At B, Go Right to E.
At E (Leaf) $\rightarrow$ Print E. Return to B.
Back at B (Left/Right done) $\rightarrow$ Print B. Return to A.
At A, Go Right to C.
At C (Leaf) $\rightarrow$ Print C. Return to A.
Back at A (Left/Right done) $\rightarrow$ Print A.
Output: D E B C A
Application: Used to delete the tree (delete children before deleting parent).
Output
DBEAC ABDEC DEBCA
(Example)
Getting sorted data from Copying Trees, Expression Deleting Trees, Expression
Primary Use
BST Prefix Postfix
2: Explain Binary Search Tree (BST) ADT. Discuss the Insert and Delete
operations with examples.
1. Definition: What is a Binary Search Tree (BST)?
A Binary Search Tree (BST) is a special type of Binary Tree that maintains a sorted order
of elements. It allows for efficient Searching, Insertion, and Deletion operations.
Properties of BST:
1. Left Subtree: All values in the left child (and its subtrees) are smaller than the parent
node.
2. Right Subtree: All values in the right child (and its subtrees) are greater than the
parent node.
3. No Duplicates: Typically, BSTs do not allow duplicate values.
Shutterstock
2. Operation 1: Insertion
To insert a new value into a BST, we must find the correct spot so that the BST properties
(Left < Root < Right) are maintained.
Algorithm:
Example Trace:
Deletion is more complex than insertion because removing a node might break the tree
structure. We handle this in three distinct cases.
Plaintext
50 50
/ \ / \
30 70 ---> 30 70
/
20 (Delete this)
Scenario: The node has only one child (either Left or Right).
Action: Bypass the node. Make the parent of the node point directly to the node's
single child.
Example: Deleting 30 (which has child 20). Parent 50 now connects directly to 20.
Tree:
Plaintext
50
/ \
30 70
/ \
60 80
Result:
Plaintext
60
/ \
30 70
\
80
Python
class Node:
def __init__(self, key):
[Link] = None
[Link] = None
[Link] = key
5. Complexity Analysis
Time Complexity:
o Best/Average Case: $O(\log n)$ (Because at every step, we eliminate half the
tree).
o Worst Case: $O(n)$ (If the tree is skewed, looking like a linked list).
Space Complexity: $O(h)$ where $h$ is the height of the tree (for recursion stack).
A Heap is a specialized tree-based data structure that satisfies two specific properties:
1. Shape Property: It must be a Complete Binary Tree. This means all levels are
completely filled except possibly the last level, which is filled from left to right.
2. Heap Property: It must satisfy a specific ordering between parent and children nodes
(either Min or Max).
2. Types of Heaps
A. Max-Heap
In a Max-Heap, the value of every Parent node is greater than or equal to the values of its
Children.
Root: The root contains the Maximum element of the entire tree.
Rule: $Parent \ge Children$
B. Min-Heap
In a Min-Heap, the value of every Parent node is less than or equal to the values of its
Children.
Root: The root contains the Minimum element of the entire tree.
Rule: $Parent \le Children$
There are two main operations: Insertion and Deletion (Extract Max).
When we add a new element, we must maintain the Complete Binary Tree shape first, and
then fix the Heap Order.
Algorithm:
1. Add the new key at the end of the tree (last available position in the array).
2. Compare the new key with its Parent.
3. Swap: If the new key is greater than its parent (in Max-Heap), swap them.
4. Repeat: Continue moving up the tree until the property is satisfied or the root is
reached.
Algorithm:
A Priority Queue is an abstract data type where each element has a "priority". In a standard
Queue (FIFO), elements leave in the order of arrival. In a Priority Queue, the element with
the highest priority is served first.
We could use Arrays or Linked Lists, but Heaps are much more efficient:
Array
How it works:
1. Enqueue (Insert): We use the Heap Insertion algorithm. This ensures the highest
priority item bubbles up to the root efficiently.
2. Dequeue (Remove): We use the Heap Deletion (Extract Root) algorithm. This
instantly gives us the highest priority item and reorganizes the rest efficiently.
Real-world Applications:
CPU Scheduling: Processes with higher priority (system tasks) are executed before
lower priority ones (user apps).
Dijkstra's Algorithm: Used to find the shortest path in graph algorithms.
5. Complexity Analysis
Operation Time Complexity Reason
Delete Max $O(\log n)$ We traverse down the height of the tree.
Here is the elaborated answer for Q4: Explain AVL Tree Rotations with clear diagrams.
This is a diagram-heavy question. In an exam, the text can be brief, but the diagrams must
be perfect to score the full 13 marks.
4: Explain AVL Tree Rotations with clear diagrams.
1. What is an AVL Tree?
An AVL tree (named after Adelson-Velsky and Landis) is a self-balancing Binary Search
Tree (BST). It ensures the tree remains balanced to guarantee $O(\log n)$ search time.
For every node in the tree, the difference between the height of the left subtree and the height
of the right subtree must be -1, 0, or +1.
If the Balance Factor of any node becomes +2 or -2, the tree is "unbalanced," and we perform
Rotations to fix it.
2. Types of Rotations
There are four types of rotations depending on where the new node was inserted:
When to use: When a new node is inserted into the Left child of the Left subtree of a node
that becomes critical.
Diagrammatic Explanation:
When to use: When a new node is inserted into the Right child of the Right subtree.
Problem: The tree is "Right Heavy" (BF = -2).
Solution: Perform a Left Rotation.
Diagrammatic Explanation:
When to use: When a new node is inserted into the Right child of the Left subtree.
Problem: The path is "Zig-Zag" (Left then Right). Single rotation won't fix it.
Solution: Double Rotation.
1. Left Rotate the child node (to convert it into an LL case).
2. Right Rotate the critical node (to solve the LL case).
Example:
When to use: When a new node is inserted into the Left child of the Right subtree.
Example:
Left of Left Child LL Case Single Right Rotate Pull middle up, push root right.
Right of Right Child RR Case Single Left Rotate Pull middle up, push root left.
Right of Left Child LR Case Left then Right Straighten Zig-Zag, then fix.
Left of Right Child RL Case Right then Left Straighten Zig-Zag, then fix.
Question:
Construct an AVL Tree by inserting the following elements in sequence: 10, 20, 30, 40,
50, 25.
Answer:
1. Introduction
An AVL Tree is a self-balancing Binary Search Tree (BST) where the difference between
the heights of left and right subtrees (Balance Factor) cannot be more than 1 for all nodes.
2. Step-by-Step Construction
Step 1: Insert 10
Plaintext
10 (BF=0)
Step 2: Insert 20
Plaintext
10 (-1)
\
20 (0)
Step 3: Insert 30
$30 > 10$, $30 > 20$. Insert 30 as right child of 20.
BF Calculation:
o Node 20: $0 - 1 = -1$
o Node 10: $0 - 2 = -2$ (Unbalanced)
Imbalance Type: The imbalance is in the Right child of the Right subtree (RR
Case).
Action: Perform Single Left Rotation (LL Rotation) on node 10.
o Node 20 moves up to become the root.
o Node 10 becomes the left child of 20.
Resulting Tree:
Plaintext
20 (0)
/ \
10 (0) 30 (0)
Step 4: Insert 40
$40 > 20$, $40 > 30$. Insert 40 as right child of 30.
BF Calculation:
o Node 30: $0 - 1 = -1$
o Node 20: $1 - 2 = -1$
Status: The tree is Balanced.
Tree:
Plaintext
20 (-1)
/ \
10 30 (-1)
\
40 (0)
Step 5: Insert 50
$50 > 20$, $50 > 30$, $50 > 40$. Insert 50 as right child of 40.
BF Calculation:
o Node 40: $0 - 1 = -1$
o Node 30: $0 - 2 = -2$ (Unbalanced)
Imbalance Type: The imbalance is in the Right child of the Right subtree of node 30
(RR Case).
Action: Perform Single Left Rotation on node 30.
o Node 40 moves up.
o Node 30 becomes the left child of 40.
o Node 50 remains the right child of 40.
Resulting Tree:
Plaintext
20 (-1)
/ \
10 40 (0)
/ \
30 50
Step 6: Insert 25
Plaintext
BF Calculation:
o Node 30: $1 - 0 = 1$
o Node 40: $H(Left=2) - H(Right=1) = 1$
o Node 20: $H(Left=1) - H(Right=3) = -2$ (Unbalanced)
Imbalance Type: The insertion happened in the Right subtree of 20, and then in the
Left subtree of 40. This is an RL Case (Right-Left Case).
Action: Perform Double Rotation (RL Rotation).
Plaintext
20
\
30
/ \
25 40
\
50
Plaintext
30
/ \
20 40
/ \ \
10 25 50
3. Final Output
Plaintext
30
/ \
20 40
/ \ \
10 25 50
Verification:
BF(10) = 0
BF(25) = 0
BF(50) = 0
BF(20) = $1 - 1 = 0$
BF(40) = $0 - 1 = -1$
BF(30) = $2 - 2 = 0$
Conclusion: All nodes satisfy the AVL property $|BF| \le 1$. The tree is balanced.
Question:
Answer:
1. Introduction to B-Tree
A B-Tree is a self-balancing search tree designed to work well on magnetic disks or other
direct-access secondary storage devices.1 It generalizes the binary search tree concept,
allowing nodes to have more than two children.2
2. Properties of a B-Tree of Order 5
3. Insertion Algorithm
1. Search: Traverse the tree to find the appropriate leaf node where the key should be
inserted.
2. Insert: Insert the key into the leaf node in sorted order.
3. Check for Overflow:
o If the node contains $\le m-1$ keys (i.e., $\le 4$ keys), the operation is
complete.
o If the node contains 9$m$ keys (i.e., 5 keys), an Overflow occurs.10
4. Split Operation (Handling Overflow):
o The node is split into two nodes.
o The median key (the middle element) is promoted to the parent node.11
o The keys smaller than the median go to the left new node, and keys larger go
to the right new node.
o If the parent becomes full, the split propagates upward (potentially splitting
the root).12
Task: Construct a B-Tree of Order 5 by inserting numbers: 10, 20, 30, 40, 50, 60, 70, 80, 90.
Plaintext
[ 30 ]
/ \
[10, 20] [40, 50]
$60 > 30$, go to right child [40, 50]. Insert 60 $\rightarrow$ [40, 50, 60].
$70 > 30$, go to right child. Insert 70 $\rightarrow$ [40, 50, 60, 70].
The right node is now full (4 keys), but valid.
Tree:
Plaintext
[ 30 ]
/ \
[10, 20] [40, 50, 60, 70]
Plaintext
[ 30, 60 ]
/ | \
[10, 20] [40, 50] [70, 80]
Step 5: Insert 90
$90 > 60$, go to the rightmost child [70, 80].
Insert 90: [70, 80, 90].
Node size is 3 (Valid).
Final Tree:
Plaintext
[ 30, 60 ]
/ | \
[10, 20] [40, 50] [70, 80, 90]
UNIT – 3
Linear Search: Scans elements sequentially. Works on both sorted and unsorted lists.
Time Complexity: $O(n)$.
Binary Search: Divide and conquer approach. Works only on sorted lists. Time
Complexity: $O(\log n)$.
The load factor ($\lambda$) is the ratio of the number of elements stored in the hash table to
the total size of the table.
6. What is Rehashing?
Rehashing is the process of increasing the size of the hash table (usually doubling it) and re-
inserting all existing elements into the new table when the load factor exceeds a certain
threshold.
Stable Sort: Preserves the relative order of equal elements. Example: Merge Sort,
Insertion Sort.
Unstable Sort: Does not guarantee the order of equal elements. Example: Quick Sort,
Selection Sort.
Merge Sort guarantees $O(n \log n)$ time complexity in the worst case, whereas Quick Sort
can degrade to $O(n^2)$. However, Merge Sort requires extra space ($O(n)$).
Q1. Explain the Quick Sort algorithm with an illustrative example. Discuss its
complexity.
Answer:
1. Introduction:
Quick Sort is a highly efficient sorting algorithm based on the Divide and Conquer paradigm.
It works by selecting a 'pivot' element from the array and partitioning the other elements into
two sub-arrays, according to whether they are less than or greater than the pivot.
2. Algorithm (Steps):
1. Choose a Pivot: Pick an element from the array to serve as the pivot (commonly the
first, last, or middle element).
2. Partitioning: Reorder the array so that all elements with values less than the pivot
come before the pivot, and all elements with values greater than the pivot come after
it. After this partitioning, the pivot is in its final position.
3. Recursive Sorting: Recursively apply the above steps to the sub-array of elements
with smaller values and the sub-array of elements with greater values.
3. Illustrative Example:
Pass 1:
o Pivot: 70
o Pointer i: Starts at -1 (tracks elements smaller than pivot).
o Pointer j: Scans from index 0 to 5.
o Comparison:
10 < 70: Increment i, swap (no change). Array: [10, 80, 30...]
80 > 70: Do nothing.
30 < 70: Increment i, swap 80 and 30. Array: [10, 30, 80, 90, 40...]
90 > 70: Do nothing.
40 < 70: Increment i, swap 80 and 40. Array: [10, 30, 40, 90, 80, 50,
70]
50 < 70: Increment i, swap 90 and 50. Array: [10, 30, 40, 50, 80, 90,
70]
o Final Step: Swap Pivot (70) with i+1 (80).
o Result after Partition: [10, 30, 40, 50, **70**, 90, 80]
o (Pivot 70 is now at its correct sorted position).
Pass 2 (Recursion):
o Left Sub-array: [10, 30, 40, 50] (Already sorted in this case, but recursion
continues).
o Right Sub-array: [90, 80] $\rightarrow$ Pivot 80. Swap 90, 80 $\rightarrow$
[80, 90].
Final Sorted Array: [10, 30, 40, 50, 70, 80, 90]
4. Complexity Analysis:
Best Case: $O(n \log n)$. Occurs when the pivot always divides the array into two
nearly equal halves.
Average Case: $O(n \log n)$.
Worst Case: $O(n^2)$. Occurs when the array is already sorted (ascending or
descending) and the pivot is always the smallest or largest element, creating highly
unbalanced partitions.
Answer:
1. Introduction:
In hashing, a Collision occurs when the hash function maps two distinct keys to the same
index in the hash table (i.e., $h(k_1) = h(k_2)$). Collision resolution techniques are methods
to handle this scenario.
Concept: Each slot (index) in the hash table contains a pointer to a Linked List. All
keys that hash to the same index are stored in that linked list.
Operation:
o Insert: Calculate hash index. Add the key to the linked list at that index.
o Search: Calculate hash index. Traverse the linked list at that index to find the
key.
Advantage: Simple to implement; table never fills up (lists just get longer).
Disadvantage: Requires extra memory for pointers; search time increases if chains
become long ($O(n)$ in worst case).
Concept: All elements are stored within the hash table array itself. If a collision
occurs, we probe (search) for the next empty slot using a specific rule.
Types of Probing:
o A. Linear Probing:
We linearly search for the next empty slot.
Function: $Index = (h(k) + i) \pmod{size}$, where $i = 0, 1, 2...$
Drawback: Primary Clustering (clusters of occupied slots merge,
increasing search time).
o B. Quadratic Probing:
We search for slots based on a quadratic equation to reduce clustering.
Function: $Index = (h(k) + c_1 i + c_2 i^2) \pmod{size}$
Drawback: Secondary Clustering (keys hashing to the same initial
position follow the same probe sequence).
o C. Double Hashing:
Uses a second independent hash function to determine the step size.
Function: $Index = (h_1(k) + i \times h_2(k)) \pmod{size}$
Advantage: Drastically reduces clustering; considered one of the best
open addressing methods.
Q3. Explain Merge Sort algorithm. Show the trace of Merge Sort for the data: 38, 27,
43, 3, 9, 82, 10.
Answer:
1. Algorithm:
Merge Sort is a stable sorting algorithm that uses the Divide and Conquer strategy.
1. Divide: Find the middle point of the array to divide it into two halves.
2. Conquer: Recursively call Merge Sort for the first half and the second half.
3. Combine (Merge): Merge the two sorted halves into a single sorted array.
3. Complexity:
Time Complexity: $O(n \log n)$ in all cases (Best, Average, Worst).
Space Complexity: $O(n)$ (Requires auxiliary array for merging).
Q4. Explain Binary Search Algorithm with code/pseudocode and analyze its efficiency.
Answer:
1. Introduction:
2. Algorithm Logic:
1. Compare the target value x with the middle element of the array.
2. If x matches the middle element, return the index.
3. If x is greater than the middle element, ignore the left half and recurse on the right
half.
4. If x is smaller, ignore the right half and recurse on the left half.
3. Pseudocode (Iterative):
Python
Function BinarySearch(arr, target):
low = 0
high = length(arr) - 1
if arr[mid] == target:
return mid # Element found
else:
high = mid - 1 # Search left half
4. Efficiency Analysis:
The search terminates when the array size becomes 1 ($n / 2^k = 1$), which implies $n =
2^k$.
Therefore, the Time Complexity is $O(\log n)$. This is significantly faster than Linear
Search ($O(n)$) for large datasets.
Answer:
1. Introduction
2. Comparison Criteria
To compare these algorithms effectively, we look at:
Time Complexity: How the time taken grows as input size ($n$) increases.
Space Complexity: How much extra memory is needed.
Stability: Whether duplicate elements retain their original relative order.
In-Place: Whether the sorting happens within the original array or requires a new
one.
A. Bubble Sort
Mechanism: Repeatedly swaps adjacent elements if they are in the wrong order.
Large elements "bubble" to the top.
Best Case: $O(n)$ (When the array is already sorted).
Worst Case: $O(n^2)$ (When the array is reverse sorted).
Space: $O(1)$ (In-place).
Stability: Stable.
Usage: Rarely used in real-world applications due to inefficiency. Good for teaching
concepts.
B. Insertion Sort
Mechanism: Builds the sorted array one item at a time. It picks an element and
inserts it into its correct position among the previously sorted elements.
Best Case: $O(n)$ (Nearly sorted data).
Worst Case: $O(n^2)$.
Space: $O(1)$.
Stability: Stable.
Usage: efficient for small datasets ($n < 50$) or data that is already partially sorted.
It is often used as the base case for recursive algorithms like Quick Sort or Merge
Sort.
C. Selection Sort
Mechanism: Repeatedly finds the minimum element from the unsorted part and puts
it at the beginning.
Best/Worst Case: $O(n^2)$ (It always scans the remaining list, even if sorted).
Space: $O(1)$.
Stability: Unstable (Swapping long distances can disrupt order).
Usage: Useful when memory writes are very expensive (it makes the minimum
number of swaps, $O(n)$).
D. Merge Sort
E. Quick Sort
Mechanism: Divide-and-Conquer. Picks a "pivot" and partitions the array such that
smaller elements are on the left and larger on the right.
Best/Avg Case: $O(n \log n)$.
Worst Case: $O(n^2)$ (Rare, happens with poor pivot choice).
Space: $O(\log n)$ (Stack space for recursion).
Stability: Unstable.
Usage: The de-facto standard for sorting arrays. It is generally faster than Merge
Sort in practice because it works in-place and has good cache locality.
F. Heap Sort
Mechanism: Uses a Binary Heap data structure (Max-Heap). It builds a heap, then
repeatedly extracts the maximum element and moves it to the end.
Time Complexity: $O(n \log n)$ in all cases.
Space: $O(1)$ (In-place).
Stability: Unstable.
Usage: Great when you need guaranteed $O(n \log n)$ performance without the extra
memory overhead of Merge Sort. Used in systems with limited memory (embedded
systems).
4. Summary Table
Algorithm Best Time Avg Time Worst Time Space Stable? In-Place?
Merge $O(n \log n)$ $O(n \log n)$ $O(n \log n)$ $O(n)$ Yes No
Quick $O(n \log n)$ $O(n \log n)$ $O(n^2)$ $O(\log n)$ No Yes
Heap $O(n \log n)$ $O(n \log n)$ $O(n \log n)$ $O(1)$ No Yes
5. Conclusion
For small arrays, Insertion Sort is fastest.
For large general-purpose arrays, Quick Sort is preferred.
If memory is tight, Heap Sort is the best $O(n \log n)$ option.
If stability is required (e.g., sorting by name, then by grade), Merge Sort is the
choice
Question:
Insert the following keys into a Hash Table of size 10 using the Hash Function $h(k) = k \
mod 10$.Keys: 12, 18, 13, 2, 3, 23, 5, 15.
Answer:
1. Linear Probing
Concept: In Linear Probing, if a collision occurs at index i, we check the next index (i+1) %
size. We repeat this until an empty slot is [Link] Function: $h(k) = k \% 10$Table
Size: 10 (Indices 0 to 9)
Step-by-Step Trace:
5. Insert 3: $3 \% 10 = 3$.
o Slot 3 occupied (by 13). Collision!
| Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| Value | - | - | 12 | 13 | 2 | 3 | 23 | 5 | 18 | 15 |
2. Separate Chaining
Concept: Each slot in the hash table points to a Linked List. When a collision occurs, the
new key is simply appended to the list at that index.
Step-by-Step Trace:
1. Conceptual Logic
1. Sorted Sub-list: Initially contains only the first element (index 0).
2. Unsorted Sub-list: Contains the rest of the elements.
The Process: In each pass, we pick the first element (called the Key) from the unsorted part
and compare it backward into the sorted part. We shift larger elements one position to the
right to make space for the Key.
2. Step-by-Step Trace
Initial State:
Goal: Place 6 into the sorted sub-list [5, 11, 12, 13].
Comparison 1: Compare 6 with 13.
o $6 < 13$? Yes. Shift 13 right.
Comparison 2: Compare 6 with 12.
o $6 < 12$? Yes. Shift 12 right.
Comparison 3: Compare 6 with 11.
o $6 < 11$? Yes. Shift 11 right.
Comparison 4: Compare 6 with 5.
o $6 < 5$? No. Stop.
Insertion: Place 6 at the position immediately after 5 (Index 1).
Array State: [ 5, 6, 11, 12, 13 ]
Metrics: Comparisons: 4, Shifts: 3
Pass No. Key Value Array State After Pass Comparisons Shifts
Total 9 7
Conclusion:
Question:
Consider a scenario where you have a database of student IDs (integers) and you need to
retrieve records based on ID very frequently. The IDs are not sequential (e.g., 1001, 5092,
1024).
Answer:
1. Introduction
Based on these parameters, we analyze the best Searching and Sorting techniques below.
Detailed Justification:
Linear Search: Checking every ID one by one takes $O(n)$ time. For 1 million
students, this is too slow.
Binary Search: Requires sorted data. It takes $O(\log n)$ time. For 1 million records
($2^{20}$), it takes approx 20 comparisons. While fast, it is not "instant."
Hashing: Hashing maps a key directly to an address in memory.
o Average Case: $O(1)$ (Constant Time).
o Impact: Whether you have 100 students or 10 million students, retrieving a
record takes roughly 1 calculation. For high-frequency systems, this
difference is massive.
B. Suitability for Non-Sequential Data
Since IDs are integers like 1001, 5092, etc., we cannot use an array index directly
(Direct Addressing) because we would need an array size equal to the largest ID (e.g.,
if ID is 999999, we need 1 million slots even if we only have 5 students).
Hashing solves this by using a Hash Function (e.g., $h(x) = x \mod \text{TableSize}
$) to map these large, scattered integers into a compact table.
C. Comparison Summary
Suitability Best for exact match Best for range queries Small data only
Conclusion for Part 1: Hashing is selected because $O(1)$ access speed is ideal for
"frequent retrieval," and it efficiently handles non-sequential keys.
Context: The problem states that if we must sort the data (perhaps to enable Binary Search or
print a class list) and Memory is Limited, which algorithm fits best?
To choose the right algorithm, we look at Space Complexity (memory usage) and Worst-
Case Performance.
Mechanism: Merge sort divides the array and then merges sorted halves.
Memory Issue: To merge two arrays, Merge Sort requires an auxiliary array of size
$n$.
Space Complexity: $O(n)$.
Verdict: Since the problem explicitly states "Memory is Limited," allocating double
the memory (for the auxiliary array) is unacceptable.
Merge Sort $O(n \log n)$ $O(n)$ Yes Rejected (High Memory)
Heap Sort $O(n \log n)$ $O(1)$ No Selected (Efficient & Safe)
Final Conclusion
For a frequent retrieval system with non-sequential IDs and limited memory:
1. Storage/Search Strategy: Use Hashing with Open Addressing (to save space on
pointers) for $O(1)$ fast access.
2. Maintenance/Sorting Strategy: Use Heap Sort to sort the data when needed, as it
guarantees efficient sorting ($n \log n$) without consuming extra RAM ($O(1)$
space).
UNIT – 5
PART A (2 Marks) - Short Answer Questions
BFS (Breadth-First Search): Traverses the graph level by level using a Queue. It
finds the shortest path in unweighted graphs.
DFS (Depth-First Search): Traverses as deep as possible along each branch before
backtracking, using a Stack (or recursion).
Topological ordering of a DAG is a linear ordering of its vertices such that for every directed
edge $uv$ from vertex $u$ to vertex $v$, vertex $u$ comes before $v$ in the ordering.
An MST of a connected, undirected, weighted graph is a subgraph that connects all vertices
together with the minimum possible total edge weight and contains no cycles.
Greedy algorithms make the locally optimal choice at each step with the hope of finding a
global optimum. Examples: Prim's and Kruskal's algorithms for MST, Dijkstra's for shortest
path.
Dynamic Programming (DP) solves complex problems by breaking them down into simpler
overlapping subproblems and storing the results (memoization) to avoid redundant
computations. unlike Greedy, DP considers all possible options to find the global optimum.
1: Explain Graph Traversals (BFS and DFS) with suitable algorithms and examples.
Answer:
Graph traversal is the process of visiting (checking and/or updating) each vertex in a graph
exactly once. Unlike trees, graphs contain cycles, so we must keep track of "Visited" nodes to
avoid infinite loops. The two most common traversal techniques are Breadth-First Search
(BFS) and Depth-First Search (DFS).
Definition:
BFS is a traversal algorithm that starts at a selected node (source) and explores all of its
immediate neighbors at the present depth before moving on to nodes at the next depth level.
It explores the graph "layer by layer."
Algorithm:
Pseudocode:
Python
BFS(Graph, StartVertex):
Queue Q
Set Visited = {}
[Link](StartVertex)
[Link](StartVertex)
Example Trace:
Nodes: A, B, C, D, E, F
Edges: (A-B), (A-C), (B-D), (B-E), (C-F)
{A, B, C, D, E,
4 Dequeue C. Enqueue neighbor F. [D, E, F] C
F}
Dequeue D. No unvisited
5 [E, F] {All} D
neighbors.
Dequeue E. No unvisited
6 [F] {All} E
neighbors.
Dequeue F. No unvisited
7 [] (Empty) {All} F
neighbors.
Definition:
DFS is a traversal algorithm that starts at the root node and explores as far as possible along
each branch before backtracking. It goes "deep" into the graph structure.
Data Structure Used: Stack (LIFO - Last In, First Out) or Recursion (Implicit
Stack).
Principle: Backtracking.
Algorithm:
Python
DFS(Graph, Vertex, Visited):
Mark Vertex as Visited
print(Vertex)
Example Trace:
4. Complexity Analysis
Time Complexity:
o Adjacency List: $O(V + E)$ (Each vertex and edge is visited once).
o Adjacency Matrix: $O(V^2)$ (Scanning the entire row for each vertex).
Space Complexity: $O(V)$ (For the Queue/Stack and Visited array).
Behavior Explores neighbors level by level. Explores path deeply, then backtracks.
Answer:
1. Introduction
Shortest Path algorithms are designed to find the path with the minimum total edge weight
between two nodes in a graph. The two most fundamental algorithms for the "Single-Source
Shortest Path" problem (finding shortest paths from one source node to all other nodes) are
Dijkstra's Algorithm and Bellman-Ford Algorithm.
2. Dijkstra’s Algorithm
Concept:
Dijkstra's algorithm is a Greedy Algorithm. It maintains a set of visited vertices and a set of
unvisited vertices. At every step, it selects the unvisited vertex with the smallest known
distance from the source, visits it, and updates the distances of its neighbors.
Key Characteristics:
Algorithm Steps:
1. Initialization: Set distance to the source node as 0 and infinity (∞) for all other nodes.
Mark all nodes as unvisited.
2. Selection: Pick the unvisited node u with the smallest distance.
3. Relaxation: For every neighbor v of u, check if the current path to v through u is
shorter than the previously known distance to v.
o Condition: If dist[u] + weight(u, v) < dist[v]
o Update: dist[v] = dist[u] + weight(u, v)
4. Repeat: Repeat steps 2 and 3 until all nodes are visited or the destination is reached.
Pseudocode:
Python
function Dijkstra(Graph, Source):
dist[] = {infinity, infinity, ...}
dist[Source] = 0
PriorityQueue Q
[Link](Source, 0)
Example Trace:
3. Bellman-Ford Algorithm
Concept:
Key Characteristics:
Algorithm Steps:
Pseudocode:
Python
function BellmanFord(Graph, Source):
dist[] = {infinity, infinity, ...}
dist[Source] = 0
4. Detailed Comparison
Feature Dijkstra’s Algorithm Bellman-Ford Algorithm
Relaxes edges of the current specific Relaxes every single edge in the
Relaxation
node only. graph V-1 times.
Used in GPS systems, network routing Used in routing protocols like RIP,
Usage
(OSPF). and finance (arbitrage detection).
5. Conclusion
If edge weights are non-negative and speed is crucial, Dijkstra’s is the best choice.
If the graph contains negative weights or if you need to detect negative cycles,
Bellman-Ford is mandatory despite its slower performance.
Q3. Explain the construction of a Minimum Spanning Tree using Prim’s Algorithm.
Answer:
1. Introduction
Prim’s Algorithm is a greedy algorithm used to find the MST. It operates by building the tree
one vertex at a time, always adding the cheapest connection from the tree to a node outside
the tree.
2. Algorithm Steps
1. Initialize: Start with an arbitrary node (source). Maintain three values for every
vertex:
o Key (Minimum weight edge connecting it to the MST, initialized to $\infty$).
o Parent (The node in the MST it is connected to, initialized to Null).
o Visited (Boolean status, initialized to False).
2. Start: Set the Key of the source vertex to 0.
3. Iterate: Repeat until all vertices are included in the MST:
o Select the unvisited vertex u with the smallest Key value.
o Mark u as Visited.
o For every adjacent vertex v of u:
If v is not visited and the weight of edge (u, v) is smaller than the
current Key of v:
Update Key[v] = weight(u, v).
Update Parent[v] = u.
3. Illustrative Trace
Graph:
Vertices: A, B, C, D, E
Edges: (A-B: 2), (A-C: 3), (B-C: 1), (B-D: 4), (B-E: 5), (C-E: 6), (D-E: 2)
Step-by-Step Table:
2 Pick min (B, wt 2). Visited Visited 1 (P: B) 4 (P: B) 5 (P: B) {A, B}
Vertex Vertex Vertex Vertex Vertex MST Set
Step Operation
A B C D E (Visited)
Relax C, D, E.
Note: In Step 3, connecting C to E (wt 6) doesn't update E because its current key (5 from B)
is smaller. In Step 4, D connects to E with weight 2, which is better than 5, so E updates.
1. (A, B) - Weight 2
2. (B, C) - Weight 1
3. (B, D) - Weight 4
4. (D, E) - Weight 2
4. Complexity Analysis
Answer:
1. Introduction
Topological Sort is a linear ordering of vertices in a Directed Acyclic Graph (DAG) such
that for every directed edge $U \rightarrow V$, vertex $U$ appears before vertex $V$ in the
ordering.
1. Calculate In-Degree: Compute the in-degree for every vertex in the graph.
2. Initialize Queue: Enqueue all vertices with In-Degree == 0 (nodes with no
dependencies).
3. Process Queue: While the queue is not empty:
o Dequeue a vertex u and add it to the Topological Order list.
o For every neighbor v of u:
Decrease the in-degree of v by 1 (simulating the removal of u).
If In-Degree[v] becomes 0, enqueue v.
4. Check: If the Topological Order list contains fewer vertices than the graph, a cycle
exists.
3. Example Trace
Step-by-Step Execution:
Queue Output
Step In-Degree Array [1, 2, 3, 4, 5] Action
Status List
Dequeue 1. Reduce
1 [] [-, 0, 0, 2, 1] [1] neighbors 2 & 3. Both
become 0. Enqueue 2, 3.
Dequeue 2. Reduce
2 [3] [-, -, 0, 1, 1] [1, 2]
neighbor 4 (2 $\to$ 1).
Dequeue 3. Reduce
3 [] [-, -, -, 0, 1] [1, 2, 3] neighbor 4 (1 $\to$ 0).
Enqueue 4.
[1, 2, 3,
5 [] [-, -, -, -, -] Dequeue 5. Done.
4, 5]
4. Complexity
Time Complexity: $O(V + E)$ (Each vertex and edge is processed once).
Space Complexity: $O(V)$ (For storing in-degrees and the queue).
Q5. What is Dynamic Programming? Explain the Floyd-Warshall algorithm for All-
Pairs Shortest Paths.
Answer:
2. Floyd-Warshall Algorithm
This is an All-Pairs Shortest Path algorithm. It finds the shortest distances between every
pair of vertices in a weighted graph. It works with positive and negative edge weights (but no
negative cycles).
Core Concept:
Let $D[i][j]$ be the shortest distance from vertex $i$ to vertex $j$. The algorithm iteratively
improves the estimate of the shortest path between two vertices ($i, j$) by checking if a path
through an intermediate vertex $k$ is shorter than the direct path.
Recurrence Relation:
Graph: Nodes 1, 2, 3. Edges: $1 \to 2$ (4), $2 \to 1$ (3), $2 \to 3$ (1), $3 \to 1$ (6).
||1|2|3|
|---|---|---|---|
| 1 | 0 | 4 | $\infty$ |
|2|3|0|1|
| 3 | 6 | $\infty$ | 0 |
Update $D[1][3]$? Direct $\infty$. Via 2: $1 \to 2 \to 3$ ($4 + 1 = 5$). Update to 5.
Update $D[3][1]$? Direct 6. Via 2: $3 \to 2 \to 1$ ($10 + 3 = 13$). No change (6 is
better).
Final Matrix:
||1|2|3|
|---|---|---|---|
|1|0|4|5|
|2|3|0|1|
| 3 | 6 | 10 | 0 |
5. Complexity
Question:
Consider a network of cities connected by roads with specific lengths. Apply Dijkstra’s
Algorithm to find the shortest path from a starting city (A) to all other cities.
Graph Edges:
$A \rightarrow B (4)$
$A \rightarrow C (2)$
$B \rightarrow C (5)$
$B \rightarrow D (10)$
$C \rightarrow E (3)$
$E \rightarrow D (4)$
$D \rightarrow F (11)$
Answer:
Dijkstra’s Algorithm is a greedy algorithm that finds the shortest path from a single source
vertex to all other vertices in a weighted graph with non-negative edge weights.
2. Initialization
We maintain a table to track the Shortest Distance found so far and the Predecessor
(Parent) for path reconstruction.
| A | 0 | - | Unvisited |
| B | $\infty$ | - | Unvisited |
| C | $\infty$ | - | Unvisited |
| D | $\infty$ | - | Unvisited |
| E | $\infty$ | - | Unvisited |
| F | $\infty$ | - | Unvisited |
|A|0|-|
|B|4|A|
|C|2|A|
| D, E, F | $\infty$ | - |
|A|0|-|
|C|2|A|
|B|4|A|
|E|5|C|
| D, F | $\infty$ | - |
| A, B, C | Visited | |
|E|5|C|
| D | 14 | B |
| F | $\infty$ | - |
| A, B, C, E | Visited | |
|D|9|E|
| F | $\infty$ | - |
| A...E | Visited | |
| F | 20 | D |
A 0 A (Start)
B 4 A $\rightarrow$ B
C 2 A $\rightarrow$ C
E 5 A $\rightarrow$ C $\rightarrow$ E
5. Conclusion
Note specifically that for City D, the path through B ($A \rightarrow B \rightarrow
D$) cost 14, but the algorithm correctly identified the path through E ($A \rightarrow
C \rightarrow E \rightarrow D$) which only cost 9. This demonstrates the
"Relaxation" property of the algorithm.
Question:
For the given graph, construct the Minimum Spanning Tree (MST) using (a) Prim’s
Algorithm and (b) Kruskal’s Algorithm. Calculate the total cost.
Solution:
Assumption: Since a specific graph was not provided in the prompt, let us consider the
following weighted undirected graph with 5 Vertices (Nodes 1 to 5) and 7 Edges.
Concept: Prim's algorithm is a greedy algorithm. It grows the MST from a starting vertex
(arbitrarily chosen as Node 1) by always adding the cheapest edge that connects a vertex in
the tree to a vertex outside the tree.
Step-by-Step Construction:
Initialization:
o Visited Set: $\{1\}$
o MST Edges: $\{\}$
o Total Cost: $0$
Step 1:
Step 2:
Step 3:
Step 4:
Concept: Kruskal's algorithm is also a greedy algorithm but works differently. It treats the
graph as a forest of trees. It sorts all edges by weight and adds the smallest edge to the MST,
provided it does not form a cycle.
Step-by-Step Construction:
Edge Weight
(2, 3) 1
(2, 4) 1
(4, 5) 1
(1, 2) 2
(1, 3) 3
(2, 5) 4
(3, 5) 5
Edge Weight
2. Selection Process:
Iteration 1:
o Select Edge (2, 3) (Weight 1).
o Does it form a cycle? No.
o Action: Accept.
Iteration 2:
o Select Edge (2, 4) (Weight 1).
o Does it form a cycle? No.
o Action: Accept.
Iteration 3:
o Select Edge (4, 5) (Weight 1).
o Does it form a cycle? No.
o Action: Accept.
Iteration 4:
o Select Edge (1, 2) (Weight 2).
o Does it form a cycle? No. (Connects Node 1 to the cluster $\{2,3,4,5\}$).
o Action: Accept.
Iteration 5:
o Select Edge (1, 3) (Weight 3).
o Does it form a cycle? Yes. (Nodes 1 and 3 are already connected via 1-2-3).
o Action: Reject.
Iteration 6:
o Select Edge (2, 5) (Weight 4).
o Does it form a cycle? Yes. (Nodes 2 and 5 are connected via 2-4-5).
o Action: Reject.
Iteration 7:
o Select Edge (3, 5) (Weight 5).
o Does it form a cycle? Yes.
o Action: Reject.
Conclusion