Data Structures Using Python
II-I semester, Lab Manual
Department of ECE,
TECNRT (Autonomous)
1. Write a Python program for the class, Flower, that has three instance variables of type str,
int, and float that respectively represent the name of the flower, its number of petals, and
its price. Your class must include a constructor method that initializes each variable to an
appropriate value, and your class should include methods for setting the value of each type
and retrieving the value of each type.
Aim: Write a constructor, setter, and getter methods in a class Flower that has three instance variables
with its execution.
Theoretical Analysis:
The Python class Flower has three instance variables:
● name (type: str)
● num_petals (type: int)
● price (type: float)
It also has appropriate constructor, setter, and getter methods for each instance variable.
Program:
class Flower:
def __init__(self, name, num_petals, price):
self._name = str(name)
self._num_petals = int(num_petals)
self._price = float(price)
# Getter methods
def get_name(self):
return self._name
def get_num_petals(self):
return self._num_petals
def get_price(self):
return self._price
# Setter methods
def set_name(self, name):
self._name = str(name)
def set_num_petals(self, num_petals):
self._num_petals = int(num_petals)
def set_price(self, price):
self._price = float(price)
# Example usage:
if __name__ == "__main__":
flower = Flower("Rose", 32, 2.5)
print(flower.get_name()) # Output: Rose
print(flower.get_num_petals()) # Output: 32
print(flower.get_price()) # Output: 2.5
flower.set_name("Tulip")
flower.set_num_petals(6)
flower.set_price(1.2)
print(flower.get_name()) # Output: Tulip
print(flower.get_num_petals()) # Output: 6
print(flower.get_price()) # Output: 1.2
Output: …….. write python execution output observed…….
Result: Hence the Constructor, Setter, Getter method has been implemented using Python
with an example flower and its attributes.
2. Develop an inheritance hierarchy based upon a Polygon class that has abstract methods
area( ) and perimeter( ). Implement classes Triangle, Quadrilateral, Pentagon, that extend
this base class, with the obvious meanings for the area( ) and perimeter( ) methods. Write a
simple program that allows users to create polygons of the various types and input their
geometric dimensions, and the program then outputs their area and perimeter.
Aim: Develop a simple text menu for users to create the desired polygon, input
dimensions, and see the computed area and perimeter.
Theoretical Description:
Python's abc (Abstract Base Class) module define an abstract Polygon class with abstract
methods area() and perimeter(), along with concrete subclasses for Triangle, Quadrilateral
(assume it's a Rectangle for simplicity), and Pentagon (regular).
The program includes a simple text menu for users to create the desired polygon, input
dimensions, and see the computed area and perimeter.
Program:
from abc import ABC, abstractmethod
import math
# Abstract base Polygon class
class Polygon(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
# Triangle class (using Heron's formula)
class Triangle(Polygon):
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c
def perimeter(self):
return self.a + self.b + self.c
def area(self):
s = [Link]() / 2
return [Link](s * (s - self.a) * (s - self.b) * (s - self.c))
# Quadrilateral class (assuming rectangle for simplicity)
class Quadrilateral(Polygon):
def __init__(self, length, width):
[Link] = length
[Link] = width
def perimeter(self):
return 2 * ([Link] + [Link])
def area(self):
return [Link] * [Link]
# Pentagon class (regular pentagon)
class Pentagon(Polygon):
def __init__(self, side):
[Link] = side
def perimeter(self):
return 5 * [Link]
def area(self):
return (5 * [Link] ** 2) / (4 * [Link]([Link] / 5))
def main():
while True:
print("\nPolygon Calculator")
print("1. Triangle")
print("2. Quadrilateral (Rectangle)")
print("3. Pentagon (Regular)")
print("4. Exit")
choice = input("Select polygon type (1-4): ")
if choice == '1':
a = float(input("Enter side a: "))
b = float(input("Enter side b: "))
c = float(input("Enter side c: "))
poly = Triangle(a, b, c)
elif choice == '2':
length = float(input("Enter length: "))
width = float(input("Enter width: "))
poly = Quadrilateral(length, width)
elif choice == '3':
side = float(input("Enter side: "))
poly = Pentagon(side)
elif choice == '4':
print("Goodbye!")
break
else:
print("Invalid choice. Try again.")
continue
print(f"Perimeter: {[Link]():.2f}")
print(f"Area: {[Link]():.2f}")
if __name__ == "__main__":
main()
Output: ……..Write any two cases along with Invalid choice……….
Result: Hence, the text menu is created for polygon cases with dimensions using python to
compute area and perimeter.
Observation:
1. A basic abstract class is used from the ‘abc’ module.
2. Abstract methods of user-defined programs are built in subclasses like Triangle,
Quadrilateral, and Pentagonal.
3. Each subclass defines its user-defined function in an abstract method, set as an interface.
4. A text menu is created.
3. Write a Python program to implement method overloading and method overriding.
Aim: Write a Python program to implement method overloading and overriding.
Theoretical Description:
1. Method Overloading: Python does not support true method overloading as seen in languages like
Java or C++. However, similar behavior can be achieved by using default arguments or
variable-length arguments.
2. Method Overriding: A subclass defines a method with the same name as a method in its
superclass, thereby providing a specific implementation.
Program 1.
Case I. Using default arguments
class Calc:
def add(self, x, y, z=0):
if z:
return x + y + z
else:
return x + y
calc = Calc()
print([Link](1, 2)) # Output: 3
print([Link](1, 2, 3)) # Output: 6
Case II. Using Variable Length Argument.
class Example:
def add(self, *args):
return sum(args)
ex = Example()
print([Link](1, 2)) # Output: 3
print([Link](1, 2, 3, 4)) # Output: 10
Program 2.
class Animal:
def sound(self):
print("This animal makes a sound")
class Dog(Animal):
# Overriding the sound() method of Animal
def sound(self):
print("Dog barks")
# Usage
a = Animal()
[Link]() # Output: This animal makes a sound
d = Dog()
[Link]() # Output: Dog barks
Result: hence method overloading and Method over riding has been implemented
Observations:
1. Method overriding is constructed with variable-length arguments
2. Method Overloading has been done using subclass and its superclass with define methods
with same name
4. Write a Python program to illustrate the following comprehensions: a) List Comprehensions
b) Dictionary Comprehensions c) Set Comprehensions d) Generator Comprehensions
Aim: Write a program that illustrates list, dictionary, set, and generator comprehensions.
Theoretical description:
a) List Comprehensions
● What it is: A concise way to create lists by applying an expression to each item in an
iterable.
● Why use it: More readable and often faster than traditional for loops.
b) Dictionary Comprehensions
● What it is: A concise way to create dictionaries by deriving keys and values from
iterables.
● Why use it: Lets you quickly transform or filter data into key-value pairs.
c) Set Comprehensions
● What it is: Similar to a list comprehension, but creates a set (i.e., only unique items).
● Why use it: Useful for filtering duplicates automatically.
d) Generator Comprehensions
● What it is: Similar to a list comprehension but produces one value at a time and uses less
memory.
● Why use it: Good when dealing with large data streams or infinite sequences.
Program:
# List Comprehension
squares = [x**2 for x in range(10)]
print("List Comprehension (Squares 0-9):", squares)
# Dictionary Comprehension
squares_dict = {x: x**2 for x in range(10)}
print("Dictionary Comprehension (Number: Square):", squares_dict)
# Set Comprehension
even_squares_set = {x**2 for x in range(10) if x % 2 == 0}
print("Set Comprehension (Even x, Squares 0-9):", even_squares_set)
# Generator Comprehension
squares_gen = (x**2 for x in range(10))
print("Generator Comprehension (list from generator):", list(squares_gen))
Result: Hence, the various comprehensions has been illustrated using python.
Sample Case with C++
#include <iostream>
using namespace std;
// Overloaded functions: same name, different parameters
int add(int a, int b) { return a + b; }
int add(int a, int b, int c) { return a + b + c; }
double add(double a, double b) { return a + b; }
int main() {
cout << add(2, 3) << endl; // Calls first version: 5
cout << add(2, 3, 4) << endl; // Calls second version: 9
cout << add(2.5, 3.5) << endl; // Calls third version: 6.0
return 0;
}
5. Write a Python program to generate the combinations of n distinct objects taken from
the elements of a given list. Example: Original list: [1, 2, 3, 4, 5, 6, 7, 8, 9] Combinations of
2 distinct objects: [1, 2] [1, 3] [1, 4] [1, 5] [7, 8] [7, 9] [8, 9].
Aim: Generate the combinations of n distinct numbers.
Theoretical Description:
A combination is a way of selecting items from a collection such that (unlike permutations)
the order of selection does not matter.
Given a set of n distinct objects, the number of ways to choose r objects (without regard to
order) is C(n, r) = n! / (r! * (n - r)!)
In Python, the [Link](iterable, r) function produces all possible r-length tuples of
elements from the input iterable, where each tuple is a unique combination, and order within
each tuple is not relevant.
● The elements are considered unique based on their position.
● No element is repeated within a single combination.
● The returned combinations are in lexicographical order according to the input sequence.
Program:
import itertools
def combinations_of_list(lst, n):
return list([Link](lst, n))
# Example usage
original_list = [1, 2, 3, 4, 5, 6, 7, 8, 9]
n=2
combinations = combinations_of_list(original_list, n)
print(f"Combinations of {n} distinct objects:")
for combo in combinations:
print(list(combo))
Output:
Combinations of 2 distinct objects:
[1, 2], [1, 3], [1, 4], [1, 5], [1, 6], [1, 7], [1, 8], [1, 9],
[2, 3], [2, 4], [2, 5], [2, 6], [2, 7], [2, 8], [2, 9],
[3, 4], [3, 5], [3, 6], [3, 7], [3, 8], [3, 9],
[4, 5], [4, 6], [4, 7], [4, 8], [4, 9],
[5, 6], [5, 7], [5, 8], [5, 9],
[6, 7], [6, 8], [6, 9],
[7, 8], [7, 9],
[8, 9].
Result: Therefore, a unique combination of pairs of elements is generated from the itertools
library.
6. Write a program for Linear Search and Binary search
Aim: Write a program for linear and binary search in the given arrays.
Theoretical Description:
Linear Search: Linear Search is a simple searching algorithm. It sequentially checks each
element in a list until the target element is found or the list ends.
● Time Complexity: O(n), where ‘n’ is the number of elements in the list.
● Use Case: Suitable for unsorted or small datasets.
Program1:
def linear_search(arr, target):
for i in range(len(arr)):
if arr[i] == target:
return i
return -1
# Example usage:
arr = [4, 2, 7, 1, 9, 3]
target = 9
result = linear_search(arr, target)
if result != -1:
print(f"Linear Search: Element {target} found at index {result}.")
else:
print(f"Linear Search: Element {target} not found in the list.")
Output: Linear Search: Element 9 found at index 4
Binary Search: Binary Search is an efficient algorithm for finding an item from a sorted list. It
repeatedly divides the search interval in half to locate the target value.
● Precondition: List must be sorted.
● Time Complexity: O(log n)
● Use Case: Suitable for large, sorted datasets.
Program2:
def binary_search(arr, target):
left, right = 0, len(arr) - 1
while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
# Example usage:
arr = [1, 2, 3, 4, 7, 9]
target = 7
result = binary_search(arr, target)
if result != -1:
print(f"Binary Search: Element {target} found at index {result}.")
else:
print(f"Binary Search: Element {target} not found in the list.")
Result: Binary Search is generally more efficient but requires sorted input, while Linear
Search does not have such a prerequisite but can be slower on large lists.
Binary Search: Element 7 found at index 4
7. Write a program to implement Bubble Sort and Selection Sort.
Aim: Write a Python program implementing bubble and selection sort.
Theoretical Description:
a. Bubble Sort
Concept / Theory
● Bubble Sort is one of the simplest comparison-based sorting algorithms.
● It works by repeatedly swapping adjacent elements if they are in the wrong order.
● After each pass, the largest element “bubbles up” to its correct position (end of the list).
● The process continues until the array is sorted.
Characteristics
● Best case: O(n) (when already sorted, with optimization).
● Worst/average case: O(n²).
● Space complexity: O(1) (in-place sorting).
● Stability: Stable (relative order of equal elements is preserved).
b. Selection Sort
Concept / Theory
● Selection Sort works by repeatedly finding the minimum element from the unsorted
portion of the list and placing it in its correct position.
● Unlike Bubble Sort, it minimizes the number of swaps (at most n-1 swaps).
● However, it still requires O(n²) comparisons in all cases.
Characteristics
● Best, worst, and average case: O(n²).
● Space complexity: O(1).
● Stability: Not stable (relative order may change).
● Efficient for small datasets, but not practical for large ones.
Program:
# Bubble Sort Implementation
def bubble_sort(arr):
n = len(arr)
for i in range(n):
swapped = False
for j in range(0, n-i-1): # Last i elements are already sorted
if arr[j] > arr[j+1]:
arr[j], arr[j+1] = arr[j+1], arr[j] # Swap
swapped = True
if not swapped: # Optimization: stop if no swaps occurred
break
return arr
# Selection Sort Implementation
def selection_sort(arr):
n = len(arr)
for i in range(n):
min_idx = i
for j in range(i+1, n):
if arr[j] < arr[min_idx]:
min_idx = j
arr[i], arr[min_idx] = arr[min_idx], arr[i] # Swap
return arr
# Test the sorting algorithms
if __name__ == "__main__":
arr1 = [64, 25, 12, 22, 11]
arr2 = [Link]()
print("Original array:", arr1)
print("Bubble Sort:", bubble_sort(arr1))
print("Selection Sort:", selection_sort(arr2))
Result: Hence bubble and selection sort is implemented using IDLE Python
Original array: [64, 25, 12, 22, 11]
Bubble Sort: [11, 12, 22, 25, 64]
Selection Sort: [11, 12, 22, 25, 64]
8. Write a program to implement Merge sort and Quick sort.
Aim:
Theoretical Description:
1. Merge Sort:
Concept / Theory
● Merge Sort is a divide-and-conquer sorting algorithm.
● It works by:
1. Dividing the array into two halves.
2. Recursively sorting the two halves.
3. Merging the two sorted halves.
● The merge step ensures that the final result is sorted.
● Since merging two sorted lists takes linear time, the total runtime is efficient.
Characteristics
● Best/Average/Worst Case: O(n log n) (always efficient).
● Space Complexity: O(n) (extra memory needed for merging).
● Stable: Yes (retains relative order of equal elements).
● Preferred for: Linked lists and large datasets where stability is important.
2. Quick Sort
Concept / Theory
● Quick Sort is another divide-and-conquer algorithm.
● It works by:
1. Choosing a pivot element (commonly first, last, or median).
2. Partitioning the array into two parts: elements smaller than pivot and elements
greater than pivot.
3. Recursively applying Quick Sort on the sub-arrays.
● The partitioning ensures that the pivot is placed in its final sorted position.
Characteristics
● Best/Average Case: O(n log n).
● Worst Case: O(n²) (when pivot choice is poor, e.g., always picking smallest/largest in
already sorted array).
● Space Complexity: O(log n) (due to recursion stack).
● Stable: Not stable in basic form.
● Usually faster in practice compared to Merge Sort due to less memory overhead.
Program:
# Merge Sort Implementation
def merge_sort(arr):
if len(arr) > 1:
mid = len(arr) // 2 # Finding the middle
left = arr[:mid]
right = arr[mid:]
# Recursive sorting
merge_sort(left)
merge_sort(right)
i=j=k=0
# Merge process
while i < len(left) and j < len(right):
if left[i] < right[j]:
arr[k] = left[i]
i += 1
else:
arr[k] = right[j]
j += 1
k += 1
# Copy remaining elements
while i < len(left):
arr[k] = left[i]
i += 1
k += 1
while j < len(right):
arr[k] = right[j]
j += 1
k += 1
return arr
# Quick Sort Implementation
def quick_sort(arr):
def partition(low, high):
pivot = arr[high] # Choose last element as pivot
i = low - 1
for j in range(low, high):
if arr[j] < =pivot:
i += 1
arr[i], arr[j] = arr[j], arr[i]
arr[i+1], arr[high] = arr[high], arr[i+1]
return i+1
def quicksort_recursive(low, high):
if low < high:
pi = partition(low, high)
quicksort_recursive(low, pi-1)
quicksort_recursive(pi+1, high)
quicksort_recursive(0, len(arr)-1)
return arr
# Test the sorting algorithms
if __name__ == "__main__":
arr1 = [38, 27, 43, 3, 9, 82, 10]
arr2 = [Link]()
print("Original array:", arr1)
print("Merge Sort:", merge_sort(arr1))
print("Quick Sort:", quick_sort(arr2))
Result: Hence merge and quick sort are implemented for given array
Original array: [38, 27, 43, 3, 9, 82, 10]
Merge Sort: [3, 9, 10, 27, 38, 43, 82]
Quick Sort: [3, 9, 10, 27, 38, 43, 82]
9. Write a program to implement Stacks and Queues.
Aim: A python program to implement stacks and queues.
Stack: Theoretical Description
A stack is a linear data structure that follows the LIFO (Last In, First Out) principle: the last item
added is the first to be removed. All insertions and deletions happen at one end, called the "top"
of the stack. Basic stack operations are:
● Push: Add an item to the top.
● Pop: Remove and return the top item.
● Peek/Top: Get (without removing) the top item.
● IsEmpty: Check if the stack is empty.
Program:
class Stack:
def __init__(self):
[Link] = []
def push(self, value):
[Link](value)
def pop(self):
if self.is_empty():
print("Stack is empty!")
return None
return [Link]()
def peek(self):
if self.is_empty():
print("Stack is empty!")
return None
return [Link][-1]
def is_empty(self):
return len([Link]) == 0
def size(self):
return len([Link])
def display(self):
return print([Link])
if __name__ == "__main__":
print("### STACK OPERATIONS ###")
s = Stack()
[Link](10)
[Link](20)
[Link](30)
[Link]()
print("Popped:", [Link]())
print("Top of stack:", [Link]())
[Link]()
Queue: Theoretical Description
A Queue is a linear data structure that follows the FIFO (First In, First Out) principle: the first
item added is the first one to be removed. Additions are made at the "rear" and removals from the
"front." Basic queue operations are:
● Enqueue: Add an item at the rear.
● Dequeue: Remove and return the front item.
● Peek/Front: Get (without removing) the front item.
● IsEmpty: Check if the queue is empty.
Program:
class Queue:
def __init__(self):
[Link] = []
def enqueue(self, value):
[Link](value)
def dequeue(self):
if self.is_empty():
print("Queue is empty!")
return None
return [Link](0)
def peek(self):
if self.is_empty():
print("Queue is empty!")
return None
return [Link][-1]
def is_empty(self):
return len([Link]) == 0
def size(self):
return len([Link])
def display(self):
return print([Link])
# Driver code to test
if __name__ == "__main__":
print("\n### QUEUE OPERATIONS ###")
q = Queue()
[Link](10)
[Link](20)
[Link](30)
[Link]()
print("Dequeued:", [Link]())
print("End of queue:", [Link]())
[Link]()
Result: Stacks and queues are implemented in Python with instances.
Output:
### STACK OPERATIONS ###
[10,20,30]
Popped: 30
Top of stack: 20
[10. 20]
### QUEUE OPERATIONS ###
[10,20,30]
Dequeued: 10
End of queue: 30
[20,30]
10. Write a program to implement Singly Linked List.
Aim: A python program to implement a single linked list
Theoretical Description:
A Singly Linked List is a fundamental linear data structure made up of nodes, where each node
contains two fields:
● Data: Holds the value or information.
● Next: A reference (or pointer) to the next node in the sequence.
The list begins with a special node called the head, which points to the first node in the list; the
end of the list is indicated by the last node’s next field being set to None (or NULL).
Unlike arrays, singly linked lists do not store their elements in contiguous memory
locations—each node may be stored anywhere in memory and managed via explicit links.
Traversal proceeds forward from the head node, and access to any element requires traversing
the list from the head.
Insertion and deletion are efficient, especially at the start of the list, compared to arrays where
shifting is necessary.
Main Operations:
● Insertion (at beginning, end, or after a specified node)
● Deletion (of specified node)
● Traversal (iterating, searching)
● Displaying contents
Applications: Used to implement stacks, queues, and serve as the basis for more complex data
structures such as hash tables and adjacency lists.
Program:
# Node class to represent each element in the linked list
class Node:
def __init__(self, data):
[Link] = data
[Link] = None
# SinglyLinkedList class to manage the linked list
class SinglyLinkedList:
def __init__(self):
[Link] = None
# Insert new node at the beginning
def insert_at_beginning(self, data):
new_node = Node(data)
new_node.next = [Link]
[Link] = new_node
# Insert new node at the end
def insert_at_end(self, data):
new_node = Node(data)
if not [Link]:
[Link] = new_node
return
last = [Link]
while [Link]:
last = [Link]
[Link] = new_node
# Display the entire list
def display(self):
current = [Link]
while current:
print([Link], end=" -> ")
current = [Link]
print("None")
# Example usage:
ll = SinglyLinkedList()
ll.insert_at_beginning(3)
ll.insert_at_beginning(2)
ll.insert_at_beginning(1)
ll.insert_at_end(4)
ll.insert_at_end(5)
[Link]() # Output: 1 -> 2 -> 3 -> 4 -> 5 -> None
Result: A singly linked list with basic operations—insertion at the beginning, insertion at
the end, and displaying the contents—is implemented using Python.
11. Write a program to implement Doubly Linked list.
Aim: write a program to implement double linked list
Theoretical Description: A Doubly Linked List (DLL) is a data structure consisting of nodes
where each node contains three fields: a data field and two pointers, one pointing to the next
node and one to the previous node. This allows traversal of the list in both forward and backward
directions, a key advantage over singly linked lists, which can only be traversed in one direction.
● Node Structure: Each node contains:
● Data: Stores the value or information.
● Prev: Pointer/reference to the previous node.
● Next: Pointer/reference to the next node.
● Head: Points to the first node.
● Tail: Points to the last node (useful for reverse traversal).
Program:
class Node:
def __init__(self, data):
[Link] = data
[Link] = None
[Link] = None
class DoublyLinkedList:
def __init__(self):
[Link] = None
def append(self, data):
new_node = Node(data)
if [Link] is None:
[Link] = new_node
return
last = [Link]
while [Link]:
last = [Link]
[Link] = new_node
new_node.prev = last
def display_forward(self):
curr = [Link]
while curr:
print([Link], end=" ")
last = curr
curr = [Link]
print()
def display_backward(self):
curr = [Link]
if not curr:
return
while [Link]:
curr = [Link]
while curr:
print([Link], end=" ")
curr = [Link]
print()
dll = DoublyLinkedList()
[Link](10)
[Link](20)
[Link](30)
dll.display_forward() # Output: 10 20 30
dll.display_backward() # Output: 30 20 10
Result: Hence, forward and backward linked list is implemented.
12. Write a program to implement a binary search tree.
Aim: Write a program to implement BST.
Theoretical Description
● Definition: In a BST, for any given node:
● All values in the left subtree are strictly less than the node’s value.
● All values in the right subtree are strictly greater than the node’s value.
● Operations:
● Search: Quickly locate a value using binary search principles.
● Insert: Place new values in the correct position to maintain order.
● Delete: Remove nodes while preserving the BST property.
● Performance:
● Average-case time for search, insert, and delete:
● O(logn)
● O(logn) when balanced.
● Worst-case time:
● O(n)
● O(n) if the tree degenerates into a list.
● Applications: Efficient storage, indexing for databases, dynamic sets, sorting (tree sort),
and range queries
Program:
class Node:
def __init__(self, key):
[Link] = key
[Link] = None
[Link] = None
class BST:
def __init__(self):
[Link] = None
def insert(self, key):
[Link] = self._insert([Link], key)
def _insert(self, node, key):
if node is None:
return Node(key)
if key < [Link]:
[Link] = self._insert([Link], key)
elif key > [Link]:
[Link] = self._insert([Link], key)
return node
def search(self, key):
return self._search([Link], key)
def _search(self, node, key):
if node is None or [Link] == key:
return node
if key < [Link]:
return self._search([Link], key)
return self._search([Link], key)
def inorder(self):
self._inorder([Link])
print()
def _inorder(self, node):
if node:
self._inorder([Link])
print([Link], end=' ')
self._inorder([Link])
bst = BST()
for key in [50, 30, 20, 40, 70, 60, 80]:
[Link](key)
[Link]() # Output: 20 30 40 50 60 70 80
found = [Link](40) # Returns Node with key 40 if found, else None
Result: Hence, the binary search tree program is implemented with key insertion, ordering,
and then searching.