Advanced Data Structure Lab Manual-r23
Advanced Data Structure Lab Manual-r23
Aim:
To construct an AVL tree from a given set of elements, perform insertion and deletion operations,
and display the tree's in-order traversal.
Program (Python):
class Node:
self.key = key
self.left = None
self.right = None
self.height = 1
class AVLTree:
if not root:
return 0
return root.height
x = y.left
T2 = x.right
x.right = y
y.left = T2
return x
# Function to left rotate
y = x.right
T2 = y.left
y.left = x
x.right = T2
return y
if not root:
return Node(key)
else:
return self.right_rotate(root)
return self.left_rotate(root)
root.left = self.left_rotate(root.left)
return self.right_rotate(root)
if balance < -1 and key < root.right.key:
root.right = self.right_rotate(root.right)
return self.left_rotate(root)
return root
# In-order traversal
if not root:
return
self.inorder(root.left)
self.inorder(root.right)
# Driver code
avl = AVLTree()
root = None
for el in elements:
avl.inorder(root)
Result:
The AVL tree is constructed, and the in-order traversal of the tree is displayed.
Output:
10 20 25 30 40 50
Experiment 2: B-Tree Construction and Operations
Aim:
To construct a B-Tree of order 5 with a set of 100 random elements stored in an array, and to
implement insertion, deletion, and searching operations.
Introduction to B-Tree:
A B-Tree is a self-balancing search tree where each node can contain multiple keys and children. It is
used in databases and file systems to store large amounts of data.
Program (Python):
import random
class BTreeNode:
i = len(self.keys) - 1
if self.leaf:
self.keys.append(0)
self.keys[i + 1] = self.keys[i]
i -= 1
self.keys[i + 1] = key
else:
i += 1
if len(self.children[i].keys) == 2 * self.t - 1:
self.split_child(i, self.children[i])
i += 1
self.children[i].insert_non_full(key)
t = self.t
z = BTreeNode(t, y.leaf)
self.children.insert(i + 1, z)
y.keys = y.keys[:t - 1]
if not y.leaf:
y.children = y.children[:t]
# B-Tree class
class BTree:
self.root = None
if not self.root:
else:
if len(self.root.keys) == 2 * self.t - 1:
new_node = BTreeNode(self.t)
new_node.children.append(self.root)
new_node.split_child(0, self.root)
i=0
i += 1
new_node.children[i].insert_non_full(key)
self.root = new_node
else:
self.root.insert_non_full(key)
i=0
i += 1
return node
if node.leaf:
return None
i=0
for i in range(len(node.keys)):
if not node.leaf:
self.inorder(node.children[i])
print(node.keys[i], end=" ")
if not node.leaf:
self.inorder(node.children[i + 1])
if __name__ == "__main__":
btree = BTree(5)
btree.insert(element)
btree.inorder(btree.root)
key_to_search = elements[10]
if result:
else:
Result:
The B-Tree of order 5 is constructed with 100 random elements, and the in-order traversal is
displayed. A search operation is performed to find a specific key.
Output:
Aim:
To construct a Min Heap and Max Heap using arrays, perform deletion of any element, and display
the content of the heaps.
Introduction:
Min Heap: The parent node is always less than or equal to its children.
Max Heap: The parent node is always greater than or equal to its children.
Program (Python):
python
smallest = i
left = 2 * i + 1
right = 2 * i + 2
smallest = left
smallest = right
if smallest != i:
min_heapify(arr, n, smallest)
def build_min_heap(arr):
n = len(arr)
min_heapify(arr, n, i)
left = 2 * i + 1
right = 2 * i + 2
largest = left
largest = right
if largest != i:
max_heapify(arr, n, largest)
def build_max_heap(arr):
n = len(arr)
max_heapify(arr, n, i)
n = len(arr)
try:
index = arr.index(value)
return True
except ValueError:
return False
# Driver code
if __name__ == "__main__":
# Example array
min_heap = arr.copy()
build_min_heap(min_heap)
max_heap = arr.copy()
build_max_heap(max_heap)
element_to_delete = 25
if delete_element(min_heap, element_to_delete):
build_min_heap(min_heap)
else:
if delete_element(max_heap, element_to_delete):
build_max_heap(max_heap)
else:
Result:
The Min Heap and Max Heap are successfully constructed using arrays.
An element is deleted from both heaps, and the contents are displayed.
Output:
Min Heap: [5, 10, 25, 35, 40, 50]
Min Heap after deleting 25: [5, 10, 50, 35, 40]
Aim:
To implement Breadth-First Traversal (BFT) and Depth-First Traversal (DFT) for a given graph,
represented by:
a) Adjacency Matrix
b) Adjacency Lists
Program (Python):
class GraphMatrix:
self.V = vertices
self.adj_matrix[u][v] = 1
self.adj_matrix[v][u] = 1
queue = deque([start])
visited[start] = True
while queue:
vertex = queue.popleft()
for i in range(self.V):
queue.append(i)
visited[i] = True
visited[vertex] = True
for i in range(self.V):
self.DFT_matrix(i, visited)
class GraphList:
def __init__(self):
self.graph = defaultdict(list)
self.graph[u].append(v)
self.graph[v].append(u)
queue = deque([start])
visited.add(start)
while queue:
vertex = queue.popleft()
queue.append(neighbor)
visited.add(neighbor)
visited.add(vertex)
self.DFT_list(neighbor, visited)
# Driver code
if __name__ == "__main__":
vertices = 5
graph_matrix = GraphMatrix(vertices)
graph_matrix.add_edge(0, 1)
graph_matrix.add_edge(0, 2)
graph_matrix.add_edge(1, 3)
graph_matrix.add_edge(2, 4)
graph_matrix.BFT_matrix(0)
graph_matrix.DFT_matrix(0, visited)
graph_list = GraphList()
graph_list.add_edge(0, 1)
graph_list.add_edge(0, 2)
graph_list.add_edge(1, 3)
graph_list.add_edge(2, 4)
graph_list.BFT_list(0)
visited = set()
graph_list.DFT_list(0, visited)
Result:
The graph is represented using both adjacency matrix and adjacency lists.
Breadth-First Traversal (BFT) and Depth-First Traversal (DFT) are implemented for both
representations.
Output:
Copy code
Adjacency Matrix Representation:
01234
01324
01234
01324
Aim:
Introduction:
A bi-connected component is a maximal biconnected subgraph where there are at least two disjoint
paths between any two vertices, meaning the removal of any single vertex does not disconnect the
graph.
We use Depth-First Search (DFS) along with tracking discovery and low values to find articulation
points and bi-connected components.
Program (Python):
class Graph:
self.graph[u].append(v)
self.graph[v].append(u)
children = 0
visited[u] = True
self.time += 1
for v in self.graph[u]:
if not visited[v]:
children += 1
# Check if the subtree rooted at v has a connection back to one of u's ancestors
if parent is None and children > 1 or parent is not None and low[v] >= disc[u]:
bcc = []
bcc.append(st.pop())
bcc.append(st.pop())
st.append((u, v))
def BCC(self):
parent = None
st = []
for i in range(self.V):
if not visited[i]:
# If stack is not empty, pop remaining edges for the last BCC
if st:
bcc = []
while st:
bcc.append(st.pop())
# Driver code
if __name__ == "__main__":
g = Graph(5)
g.add_edge(0, 1)
g.add_edge(0, 2)
g.add_edge(1, 2)
g.add_edge(1, 3)
g.add_edge(3, 4)
g.BCC()
Result:
The bi-connected components of the given graph are successfully identified and printed.
Output:
Copy code
Experiment 6: Quick Sort and Merge Sort with Execution Time Observation
Aim:
To implement Quick Sort and Merge Sort algorithms and observe their execution time for various
input sizes.
Program (Python):
import time
import random
pivot = arr[high]
i = low - 1
return i + 1
quick_sort(arr, low, pi - 1)
quick_sort(arr, pi + 1, high)
n1 = m - l + 1
n2 = r - m
L = arr[l:m + 1]
R = arr[m + 1:r + 1]
i=j=0
k=l
arr[k] = L[i]
i += 1
else:
arr[k] = R[j]
j += 1
k += 1
arr[k] = L[i]
i += 1
k += 1
arr[k] = R[j]
j += 1
k += 1
if l < r:
m = l + (r - l) // 2
merge_sort(arr, l, m)
merge_sort(arr, m + 1, r)
merge(arr, l, m, r)
# Function to observe execution time for Quick Sort and Merge Sort
start_time = time.time()
sort_func(arr.copy(), 0, len(arr) - 1)
end_time = time.time()
if __name__ == "__main__":
Result:
The program successfully implements Quick Sort and Merge Sort, and the execution time is observed
for various input sizes.
Sample Output:
In this example, the execution time of both Quick Sort and Merge Sort is measured for input sizes of
100, 1000, 5000, and 10000. The times may vary based on the system running the program.
Experiment 7: Performance Comparison of Single Source Shortest Paths Using Greedy Method
Aim:
To compare the performance of the Single Source Shortest Paths (SSSP) using the Greedy method
(Dijkstra’s algorithm) when the graph is represented by:
a) Adjacency Matrix
b) Adjacency List
Introduction:
Dijkstra’s Algorithm is a popular greedy algorithm for finding the shortest paths from a
source vertex to all other vertices in a graph.
We compare its performance on two different graph representations: Adjacency Matrix and
Adjacency List.
Program (Python):
import heapq
import sys
import time
V = len(graph)
dist = [sys.maxsize] * V
dist[src] = 0
visited = [False] * V
for _ in range(V):
min_dist = sys.maxsize
min_index = -1
for v in range(V):
min_dist = dist[v]
min_index = v
u = min_index
visited[u] = True
# Update dist[] of adjacent vertices of the picked vertex
for v in range(V):
return dist
V = len(graph)
dist = [sys.maxsize] * V
dist[src] = 0
while priority_queue:
current_dist, u = heapq.heappop(priority_queue)
continue
# Check neighbors
return dist
end_time = time.time()
if __name__ == "__main__":
# Graph with 5 vertices using adjacency list (list of pairs: (neighbor, weight))
adj_list = {
src_vertex = 0
Result:
The program successfully implements Dijkstra's algorithm for both graph representations (adjacency
matrix and adjacency list), and the execution time for each representation is observed.
Sample Output:
Analysis:
Adjacency List is often faster for sparse graphs (graphs with fewer edges) because it only
processes the edges connected to a vertex.
Adjacency Matrix may be slower for sparse graphs since it checks every vertex for
connections even if no edge exists.
Aim:
To implement the Job Sequencing problem using a greedy strategy to maximize the total profit while
considering job deadlines.
Introduction:
The Job Sequencing problem involves a set of jobs, each with a deadline and profit. The objective is
to schedule the jobs to maximize the total profit, ensuring that no job is executed after its deadline.
Greedy Strategy:
Maximize profit by selecting jobs that can be completed within their deadlines.
Program (Python):
class Job:
self.job_id = job_id
self.profit = profit
self.deadline = deadline
total_profit = 0
# Find a free slot for this job (starting from the last possible slot)
if result[slot] == -1:
result[slot] = job.job_id
total_profit += job.profit
break
# Driver code
if __name__ == "__main__":
jobs = [Job('J1', 100, 2), Job('J2', 19, 1), Job('J3', 27, 2), Job('J4', 25, 1), Job('J5', 15, 3)]
job_sequencing(jobs, max_deadline)
Result:
The program successfully schedules the jobs based on the greedy strategy and calculates the total
profit.
Output:
Explanation:
The algorithm finds the nearest available slot for each job based on its deadline.
Aim:
To implement the 0/1 Knapsack problem using dynamic programming to maximize the total value of
items that can be carried within a given weight limit.
Introduction:
The 0/1 Knapsack problem is a combinatorial optimization problem where you have to determine
the maximum value that can be obtained by selecting a subset of items, each with a weight and a
value, such that the total weight does not exceed the given limit.
1. Create a 2D array dp where dp[i][w] represents the maximum value that can be obtained
with the first i items and a maximum weight w.
2. If the weight of the current item is less than or equal to w, you can either include it or
exclude it.
Program (Python):
n = len(values)
if weights[i - 1] <= w:
else:
# Driver code
if __name__ == "__main__":
weights = [1, 2, 3, 2]
Result:
The program successfully calculates the maximum value that can be obtained within the specified
weight limit.
Output:
Explanation:
The weights and values of the items are defined, and the maximum weight capacity of the
knapsack is set.
The knapsack function uses dynamic programming to fill the dp table and determine the
maximum value that can be obtained.
Experiment 10: N-Queens Problem Using Backtracking
Aim:
To solve the N-Queens problem using the backtracking algorithm, placing N queens on an N×N
chessboard such that no two queens threaten each other.
Introduction:
The N-Queens problem involves placing N queens on an N×N chessboard so that no two queens
share the same row, column, or diagonal.
Backtracking Approach:
1. Place queens one by one in different columns, starting from the leftmost column.
3. If placing the queen leads to a solution, return true; if not, backtrack and try the next
position.
Program (Python):
for i in range(col):
if board[row][i] == 1:
return False
if board[i][j] == 1:
return False
if board[i][j] == 1:
return False
return True
if col >= N:
return True
for i in range(N):
return True
board[i][col] = 0 # Backtrack
return False
def solve_n_queens(N):
return
# Driver code
if __name__ == "__main__":
solve_n_queens(N)
Result:
The program successfully finds a solution for the N-Queens problem and prints the chessboard with
queens placed.
Output:
Q...
..Q.
.Q..
...Q
Explanation:
The board is initialized, and the backtracking function attempts to place queens in valid
positions.
If a valid configuration is found, the program prints the board, showing queens represented
as 'Q' and empty squares as
Aim:
To solve the 0/1 Knapsack problem using the backtracking strategy to find the maximum value of
items that can be carried within a given weight limit.
Introduction:
The 0/1 Knapsack problem is a combinatorial optimization problem where you have to maximize the
total value of items that can fit into a knapsack of a specified weight capacity. Each item can either be
included in the knapsack or excluded (hence 0/1).
Backtracking Approach:
1. Recursively explore the two options for each item: include it or exclude it.
2. Keep track of the total weight and total value at each step.
3. Return the maximum value found that respects the weight limit.
Program (Python):
if n == 0 or capacity == 0:
return 0
# Driver code
if __name__ == "__main__":
weights = [1, 2, 3, 2]
values = [20, 5, 10, 40]
# Number of items
n = len(values)
Result:
The program successfully calculates the maximum value that can be obtained within the specified
weight limit using backtracking.
Output:
Explanation:
The weights and values of the items are defined, along with the maximum weight capacity of
the knapsack.
The maximum value that can be carried in the knapsack is calculated and displayed.
Aim:
To solve the Travelling Salesman Problem (TSP) using the Branch and Bound approach to find the
shortest possible route that visits each city exactly once and returns to the origin city.
Introduction:
The Travelling Salesman Problem is an NP-hard problem in combinatorial optimization. Given a list of
cities and the distances between each pair of cities, the objective is to find the shortest route that
visits every city exactly once and returns to the starting city.
1. Use a priority queue to explore routes based on their lower bound cost.
Program (Python):
import math
class Node:
cost = 0
return cost
def branch_and_bound_tsp(matrix):
n = len(matrix)
initial_path = [0]
pq = PriorityQueue()
initial_cost = 0
best_cost = math.inf
best_path = []
current_node = pq.get()
if current_node.level == n:
best_cost = total_cost
continue
# If the new cost is less than the best found, push it to the queue
if __name__ == "__main__":
distance_matrix = [
Result:
The program successfully calculates the optimal path and minimum cost for the Travelling Salesman
Problem using the Branch and Bound approach.
Output:
Minimum cost: 60
Explanation:
The branch_and_bound_tsp function explores possible paths and prunes those that exceed
the best found cost.
Finally, it prints the optimal path and the corresponding minimum cost, ensuring that each
city is visited exactly once before returning to the start.