Data Structures and Algorithms - Unit 3 & 4 Solutions
Data Structures and Algorithms - Unit 3 & 4 Solutions
Memory
Dynamic and scattered (non-contiguous) Static and contiguous
allocation
Can grow or shrink dynamically during Fixed size in many languages (except dynamic
Size flexibility
execution arrays)
Element access O(n) time complexity (sequential access) O(1) time complexity (random access)
Memory overhead Extra memory for storing references/pointers No overhead for references
Cache locality Poor (elements scattered in memory) Excellent (elements adjacent in memory)
class Graph:
def __init__(self, vertices):
self.vertices = vertices
self.graph = [[] for _ in range(vertices)]
def print_adjacency_list(self):
for i in range(self.vertices):
print(f"Adjacency list of vertex {i}:", end=" ")
for neighbor in self.graph[i]:
print(f"{neighbor}", end=" ")
print()
python
class GraphMatrix:
def __init__(self, vertices):
self.vertices = vertices
# Initialize adjacency matrix with zeros
self.graph = [[0 for _ in range(vertices)] for _ in range(vertices)]
def print_adjacency_matrix(self):
print("Adjacency Matrix:")
for i in range(self.vertices):
for j in range(self.vertices):
print(f"{self.graph[i][j]}", end=" ")
print()
Example Usage
python
Elements are inserted at the rear and removed from the front
Basic operations:
Enqueue: Add element to the rear
Applications:
Print job scheduling
Circular Queue
A circular queue is a variation of a simple queue where the last position is connected to the first
It overcomes the problem of unutilized space in a simple queue implementation using arrays
When the queue is full and elements are dequeued, the freed positions can be reused
Improves memory utilization
Applications:
Traffic signal control
Memory management
CPU scheduling
Priority Queue
In a priority queue, elements are dequeued based on their priority rather than the order of insertion
Higher priority elements are processed before lower priority ones
Can be implemented using various data structures like arrays, linked lists, or heaps (binary heaps
provide efficient operations)
Basic operations:
Insert with priority
Applications:
Dijkstra's algorithm
Huffman coding
Event-driven simulation
4. Implement BFS traversal on a graph using Python. Print the order in which
nodes are visited.
python
from collections import defaultdict, deque
class Graph:
def __init__(self):
# Default dictionary to store graph
self.graph = defaultdict(list)
while queue:
# Dequeue a vertex from queue
current_node = queue.popleft()
traversal_order.append(current_node)
return traversal_order
# Example usage
g = Graph()
g.add_edge(0, 1)
g.add_edge(0, 2)
g.add_edge(1, 2)
g.add_edge(2, 0)
g.add_edge(2, 3)
g.add_edge(3, 3)
class Node:
def __init__(self, data):
self.data = data
self.next = None
class SinglyLinkedList:
def __init__(self):
self.head = None
def print_list(self):
if self.head is None:
print("Linked list is empty")
return
current = self.head
while current:
print(current.data, end=" -> ")
current = current.next
print("None")
# Example usage
# Initialize an empty linked list
linked_list = SinglyLinkedList()
class Graph:
def __init__(self):
self.graph = defaultdict(list)
# Recursive DFS
def dfs_recursive(self, start_node):
# Track visited nodes
visited = set()
# List to store traversal order
traversal_order = []
def dfs_util(node):
# Mark current node as visited
visited.add(node)
traversal_order.append(node)
# Iterative DFS
def dfs_iterative(self, start_node):
# Track visited nodes
visited = set()
# Stack for DFS
stack = [start_node]
# List to store traversal order
traversal_order = []
while stack:
# Pop a vertex from stack
current = stack.pop()
return traversal_order
return False
return False
1. Node Creation:
When a new node is created, memory is allocated for it dynamically at runtime
In low-level languages like C, functions like malloc() or calloc() are used to allocate memory
from the heap
In higher-level languages like Python or Java, memory allocation is handled by the garbage
collector
2. Non-contiguous Allocation:
Unlike arrays where memory is allocated in one contiguous block, linked list nodes can be
scattered throughout memory
Each node allocation is independent of others, allowing for efficient memory usage
4. Memory Structure:
Each node typically contains:
Data section: Stores the actual value
Reference section: Stores the address of the next node (and previous node in doubly linked
lists)
6. Memory Management:
In languages without garbage collection, memory must be explicitly freed when nodes are
deleted
This dynamic memory allocation is what gives linked lists their flexibility compared to static data
structures like arrays.
8. For an unweighted graph, find the shortest path between two nodes using
BFS. Binary Tree Construction and Traversals.
python
from collections import defaultdict, deque
while queue:
current, path = queue.popleft()
# No path found
return None
class BinaryTree:
def __init__(self):
self.root = None
def insert(self, key):
if not self.root:
self.root = TreeNode(key)
return
queue = deque([self.root])
while queue:
node = queue.popleft()
if not node.left:
node.left = TreeNode(key)
return
else:
queue.append(node.left)
if not node.right:
node.right = TreeNode(key)
return
else:
queue.append(node.right)
# Recursive traversals
def inorder_recursive(self, node, result=None):
if result is None:
result = []
if node:
self.inorder_recursive(node.left, result)
result.append(node.key)
self.inorder_recursive(node.right, result)
return result
if node:
result.append(node.key)
self.preorder_recursive(node.left, result)
self.preorder_recursive(node.right, result)
return result
if node:
self.postorder_recursive(node.left, result)
self.postorder_recursive(node.right, result)
result.append(node.key)
return result
Structure:
Characteristics:
Example operations:
python
# Insert at beginning
def insert_beginning(self, data):
new_node = Node(data)
new_node.next = self.head
self.head = new_node
Structure:
Node: [Prev | Data | Next]
List: [Head] <-> [Prev | Data1 | Next] <-> [Prev | Data2 | Next] <-> ... <-> [Prev | DataN |
NULL]
Characteristics:
Each node contains data, a reference to the next node, and a reference to the previous node
Example operations:
python
new_node = Node(data)
new_node.next = prev_node.next
new_node.prev = prev_node
if prev_node.next:
prev_node.next.prev = new_node
prev_node.next = new_node
Characteristics:
The last node points back to the first node (no NULL pointers)
Allows continuous traversal around the list
Efficient for operations that need to cycle through all elements
Example operations:
python
if self.head is None:
self.head = new_node
new_node.next = self.head
return
current = self.head
while current.next != self.head:
current = current.next
current.next = new_node
new_node.next = self.head
Comparison
Feature Singly Linked List Doubly Linked List Circular Linked List
Traversal Forward only Both forward and backward Continuous (no end)
class BinarySearchTree:
def __init__(self):
self.root = None
# Insert operation
def insert(self, key):
self.root = self._insert_recursive(self.root, key)
# Search operation
def search(self, key):
return self._search_recursive(self.root, key)
# Inorder traversal
def inorder(self):
result = []
self._inorder_recursive(self.root, result)
return result
return current
# Delete operation
def delete(self, key):
self.root = self._delete_recursive(self.root, key)
return root
11. What is a stack? Explain its operations (push, pop, peek) with examples.
A stack is a linear data structure that follows the Last-In-First-Out (LIFO) principle, meaning that the last
element inserted is the first one to be removed.
2. Pop: Removes and returns the topmost element from the stack
3. Undo Functionality: Applications like text editors use stacks to implement undo features
5. Syntax Parsing: Compilers use stacks for parsing programming language syntax
6. Browser History: The back button in web browsers uses a stack to track visited pages
Implementation Approaches
Stacks can be implemented using:
Each approach has different trade-offs in terms of simplicity, memory usage, and performance.
12. Create an AVL tree in Python and implement binary tree traversals.
python
class TreeNode:
def __init__(self, key):
self.key = key
self.left = None
self.right = None
self.height = 1 # Height of the node (for AVL)
class AVLTree:
# Get height of a node
def height(self, node):
if not node:
return 0
return node.height
# Right rotation
def right_rotate(self, y):
x = y.left
T2 = x.right
# Perform rotation
x.right = y
y.left = T2
# Update heights
y.height = max(self.height(y.left), self.height(y.right)) + 1
x.height = max(self.height(x.left), self.height(x.right)) + 1
# Left rotation
def left_rotate(self, x):
y = x.right
T2 = y.left
# Perform rotation
y.left = x
x.right = T2
# Update heights
x.height = max(self.height(x.left), self.height(x.right)) + 1
y.height = max(self.height(y.left), self.height(y.right)) + 1
# Insert a node
def insert(self, root, key):
# Perform normal BST insertion
if not root:
return TreeNode(key)
@staticmethod
def preorder_recursive(root, result=None):
if result is None:
result = []
if root:
result.append(root.key)
BinaryTreeTraversal.preorder_recursive(root.left, result)
BinaryTreeTraversal.preorder_recursive(root.right, result)
return result
@staticmethod
def postorder_recursive(root, result=None):
if result is None:
result = []
if root:
BinaryTreeTraversal.postorder_recursive(root.left, result)
BinaryTreeTraversal.postorder_recursive(root.right, result)
result.append(root.key)
return result
@staticmethod
def inorder_iterative(root):
result = []
stack = []
current = root
return result
@staticmethod
def preorder_iterative(root):
if not root:
return []
result = []
stack = [root]
while stack:
current = stack.pop()
result.append(current.key)
return result
@staticmethod
def postorder_iterative(root):
if not root:
return []
result = []
stack1 = [root]
stack2 = []
if current.left:
stack1.append(current.left)
if current.right:
stack1.append(current.right)
@staticmethod
def level_order_traversal(root):
if not root:
return []
result = []
queue = [root]
while queue:
current = queue.pop(0)
result.append(current.key)
if current.left:
queue.append(current.left)
if current.right:
queue.append(current.right)
return