Compiler and design lab pdf
Compiler and design lab pdf
Dehradun-Uttarakhand
Session 2024-2025
Design and Analysis of Algorithm
Practical Lab file
(CSP-010)
Submitted By Submitted To
Shakir Ali Mr. Santosh Kumar Mishra 220530101071
Associate Prof. (CSE)
B.Tech (CSE)-B
INDEX
S.NO Practical Name Page Date Remark
no.
1. 1.Programming that uses recurrence relations to analyse 22/08/2024
recursive algorithms.
2. 2.Computing best, average, and worst-case time complexity of various 23/08/2024
sorting techniques.
3. 3.Performance analysis of different internal and external sorting 23/08/2024
algorithms with different type of data
set.
4. Use of divide and conquer technique to solve some problem that uses 06/09/2024
two different algorithms for
solving small problem.
5. Implementation of different basic computing algorithms like Hash 07/09/2024
tables, including collision-avoidance
strategies, Search trees (AVL and B-trees).
6. Consider the problem of eight queens on an (8x8) chessboard. Two 08/10/2024
queens are said to attack each
other if they are on the same row, column, or diagonal. Write a program
that implements
backtracking algorithm to solve the problem i.e. place eight non-
attacking queens on the board.
7. Write a program to find the strongly connected components in a 15/10/204
digraph.
8. Write a program to implement file compression (and un-compression) 11/11/2024
using Huffman’s algorithm.
9. Write a program to implement dynamic programming algorithm to solve 12/11/2024
the all pairs shortest path
problem.
10. Write a program to solve 0/1 knapsack problem using the following: 05/12/2024
a)
Greedy algorithm.
b) Dynamic programming algorithm.
c)
Backtracking algorithm.
d) Branch and bound algorithm.
11. Write a program that uses dynamic programming algorithm to solve the 06/12/2024
optimal binary search tree
problem.
12. Write a program for solving traveling salespersons problem using the 09/12/2024
following:
a)
Dynamic programming algorithm.
b) The back tracking algorithm.
c)
Branch and bound.
1.Programming that uses recurrence relations to analyses recursive
algorithms.
A recurrence relation is an equation that defines a sequence of values based on previous terms. In the context of
algorithms, it helps us determine the running time of a recursive function by expressing it as a function of the
input size.
python
Copy code
def factorial(n):
if n == 1:
return 1
else:
return n * factorial(n - 1)
• The algorithm calls itself with n−1n - 1n−1 until n=1n = 1n=1.
• Let T(n)T(n)T(n) represent the time complexity for input size nnn.
• The recurrence relation is: T(n)=T(n−1)+cT(n) = T(n-1) + cT(n)=T(n−1)+c Here, ccc is the constant time
for the multiplication and function call.
• When n=1n = 1n=1, the function does not recurse. So: T(1)=cT(1) = cT(1)=c
Step 3: Solve the Recurrence
Conclusion
• Using recurrence relations, we analyzed that the factorial algorithm has linear time complexity
O(n)O(n)O(n).
• This method is useful for evaluating recursive algorithms like merge sort, binary search, etc.
2.Computing best, average, and worst-case time complexity of various sorting
techniques.
1. Bubble Sort
• Description: Compares adjacent elements and swaps them if they're in the wrong order.
• Complexity:
o Best Case: O(n) (Already sorted, single pass)
o Average Case: O(n²) (Random order)
o Worst Case: O(n²) (Reversed order)
2. Selection Sort
• Description: Repeatedly finds the minimum element and places it in the sorted portion.
• Complexity:
o Best Case: O(n²) (No early stopping mechanism)
o Average Case: O(n²)
o Worst Case: O(n²)
3. Insertion Sort
• Description: Inserts elements into their correct position within a growing sorted portion.
• Complexity:
o Best Case: O(n) (Already sorted)
o Average Case: O(n²) (Random order)
o Worst Case: O(n²) (Reversed order)
4. Merge Sort
• Description: Divides the array into halves, sorts them, and then merges them back.
• Complexity:
o Best Case: O(n log n)
o Average Case: O(n log n)
o Worst Case: O(n log n)
5. Quick Sort
7. Counting Sort
• Description: Counts occurrences and uses them to place elements in sorted order (suitable for integers).
• Complexity:
o Best Case: O(n + k)
o Average Case: O(n + k)
o Worst Case: O(n + k)
(k is the range of input values.)
8. Radix Sort
• Description: Sorts numbers digit by digit, starting from the least significant digit.
• Complexity:
o Best Case: O(nk)
o Average Case: O(nk)
o Worst Case: O(nk)
(k is the number of digits in the largest number.)
9. Bucket Sort
• Description: Divides the array into buckets, sorts each bucket, and combines.
• Complexity:
o Best Case: O(n + k) (Uniform distribution of elements)
o Average Case: O(n + k)
o Worst Case: O(n²) (All elements in one bucket)
Summary Table:
This concise explanation ensures clarity and provides an easy reference for understanding sorting complexities!
3.Performance analysis of different internal and external sorting algorithms
with different type of data set.
Objective
To analyze and compare the performance of various internal and external sorting algorithms using different types of datasets, such as:
• Sorted data
• Reverse sorted data
• Random data
Procedure
1. Dataset Preparation:
a. Generate datasets:
i. Sorted order: [1, 2, 3, 4, ..., n]
ii. Reverse order: [n, n-1, ..., 3, 2, 1]
iii. Random order: [5, 2, 8, 1, 3, ..., ]
b. Use random generators or predefined arrays.
2. Implement Sorting Algorithms:
a. Write code for the algorithms listed above.
b. Ensure correctness by testing with small datasets first.
3. Record Execution Time:
a. Use built-in timing functions:
i. Python: time or timeit library
ii. C++: chrono library
iii. Java: System.nanoTime()
b. Measure time taken for each algorithm with different datasets.
4. Analyze Performance:
a. Compare the execution times for:
i. Different algorithms
ii. Different dataset types
b. Note the time complexity and behavior.
import time
def bubble_sort(arr):
n = len(arr)
for i in range(n):
for j in range(0, n-i-1):
if arr[j] > arr[j+1]:
arr[j], arr[j+1] = arr[j+1], arr[j]
# Example dataset
data = [64, 34, 25, 12, 22, 11, 90]
Sample Results
Observations
1. Internal Sorting:
a. Bubble Sort is inefficient for large datasets due to O(n2)O(n^2)O(n2) complexity.
b. Merge Sort performs well and has consistent O(nlogn)O(n \log n)O(nlogn) time.
c. Quick Sort is faster but may degrade to O(n2)O(n^2)O(n2) in the worst case.
2. External Sorting:
a. Effective for datasets larger than available memory.
b. Requires additional storage and I/O operations.
3. Impact of Dataset Types:
a. Sorted datasets improve performance in algorithms like Insertion Sort.
b. Random and reverse datasets highlight the efficiency of divide-and-conquer algorithms like Quick Sort and Merge Sort.
Conclusion
1. Divide:
a. Break the problem into smaller subproblems.
b. Here, the array is divided into smaller subarrays for sorting using Merge Sort.
2. Conquer:
a. Solve the subproblems.
b. Use Merge Sort to sort the array and Binary Search to efficiently find an element in the sorted array.
3. Combine:
a. Combine the results to form the final solution.
b. The sorted array and the search result provide the complete solution.
Algorithm Overview
Merge Sort divides the array into smaller parts, sorts them, and merges them back.
1. Divide the Array: Split the array into two halves until each part has one element.
2. Conquer: Sort each half recursively.
3. Combine: Merge the two sorted halves into a single sorted array.
Binary Search works on the sorted array to find a target element efficiently.
Example Implementation
def merge_sort(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
return merge(left, right)
def merge(left, right):
sorted_arr = []
i=j=0
while i < len(left) and j < len(right):
if left[i] < right[j]:
sorted_arr.append(left[i])
i += 1
else:
sorted_arr.append(right[j])
j += 1
sorted_arr.extend(left[i:])
sorted_arr.extend(right[j:])
return sorted_arr
# Example Usage
array = [38, 27, 43, 3, 9, 82, 10]
target = 43
1. Divide:
a. Merge Sort splits the array into smaller parts.
b. Binary Search divides the array to find the target.
2. Conquer:
a. Sorting and searching are handled using recursive or iterative methods.
3. Combine:
a. The sorted array and search result provide the complete solution.
This approach demonstrates how two different algorithms (Merge Sort and Binary Search) are used effectively in a Divide and Conquer
framework.
5. Implementation of different basic computing algorithms like Hash tables, including
collision-avoidance
strategies, Search trees (AVL and B-trees).
Here's a clear and simplified explanation of implementing basic computing algorithms like hash tables with collision-avoidance strategies and search trees (AVL
and B-trees). I'll provide a step-by-step approach for better understanding.
A hash table is a data structure that maps keys to values using a hash function. Collision avoidance is essential to ensure that two keys do not map to the same
index.
Key Concepts
def display(self):
for i, slot in enumerate(self.table):
print(f"Index {i}: {slot}")
# Example Usage
hash_table = HashTable(5)
hash_table.insert("Shakir", 25)
hash_table.insert("Ali", 30)
hash_table.insert("Data", 45)
hash_table.display()
2. AVL Trees
An AVL tree is a self-balancing binary search tree where the height difference between left and right subtrees (balance factor) is at most 1.
Key Operations
class AVLTree:
def get_height(self, node):
return node.height if node else 0
# LL Case
if balance > 1 and key < node.left.key:
return self.rotate_right(node)
# RR Case
if balance < -1 and key > node.right.key:
return self.rotate_left(node)
# LR Case
if balance > 1 and key > node.left.key:
node.left = self.rotate_left(node.left)
return self.rotate_right(node)
# RL Case
if balance < -1 and key < node.right.key:
node.right = self.rotate_right(node.right)
return self.rotate_left(node)
return node
# Example Usage
tree = AVLTree()
root = None
keys = [10, 20, 30, 40, 50, 25]
tree.pre_order(root)
3. B-Trees
B-Trees are general tree data structures used for storing large amounts of sorted data and allowing efficient insertion, deletion, and search operations.
Key Features
if self.leaf:
while i >= 0 and key < self.keys[i]:
i -= 1
self.keys.insert(i + 1, key)
else:
while i >= 0 and key < self.keys[i]:
i -= 1
i += 1
if len(self.children[i].keys) == 2 * self.t - 1:
self.split_child(i)
if key > self.keys[i]:
i += 1
self.children[i].insert_non_full(key)
new_child.keys = child.keys[t:]
child.keys = child.keys[:t - 1]
if not child.leaf:
new_child.children = child.children[t:]
child.children = child.children[:t]
class BTree:
def __init__(self, t):
self.root = BTreeNode(t, True)
self.t = t
# Example Usage
b_tree = BTree(3)
keys = [10, 20, 5, 6, 12, 30, 7, 17]
These implementations are beginner-friendly and focus on clarity for understanding basic algorithms. Let me know if you'd like detailed explanations for any
specific part!
6. Consider the problem of eight queens on an (8x8) chessboard. Two queens are said to attack
each
other if they are on the same row, column, or diagonal. Write a program that implements
backtracking algorithm to solve the problem i.e. place eight non-attacking queens on the board.
Python Program for 8-Queens Problem
# Function to print the chessboard
def print_board(board):
for row in board:
print(" ".join("Q" if col else "." for col in row))
print("\n")
return True
return False
# Main function
def eight_queens():
n = 8 # Chessboard size (8x8)
board = [[0] * n for _ in range(n)] # Initialize an empty board
1. print_board(board):
a. Displays the chessboard, showing "Q" for queens and "." for empty cells.
2. is_safe(board, row, col, n):
a. Checks whether placing a queen at (row, col) is safe. It ensures no queen exists in:
i. The same column.
ii. The upper-left diagonal.
iii. The upper-right diagonal.
3. solve_n_queens(board, row, n):
a. A recursive function that attempts to place queens row by row.
b. If placing a queen leads to a valid solution, it returns True. Otherwise, it backtracks and removes the queen.
4. eight_queens():
a. Initializes the chessboard and calls the solver function.
7.Write a program to find the strongly connected components in a digraph.
from collections import defaultdict
# Example usage
if __name__ == "__main__":
g = Digraph(5) # Create a graph with 5 vertices (0 to 4)
g.add_edge(0, 2)
g.add_edge(2, 1)
g.add_edge(1, 0)
g.add_edge(0, 3)
g.add_edge(3, 4)
g.find_sccs()
Explanation:
1. Graph Representation:
a. The graph is represented as an adjacency list using Python's defaultdict.
2. Kosaraju's Algorithm:
a. Step 1: Perform a DFS on the original graph and store vertices in a stack based on their finishing times.
b. Step 2: Transpose the graph (reverse all edges).
c. Step 3: Perform DFS on the transposed graph, starting with the vertices in the order defined by the stack
from Step 1. Each DFS traversal gives one SCC.
3. Input Example:
a. The program assumes a graph with 5 vertices. You can change edges or add more vertices as needed.
4. Outpu
t Example:
8.Write a program to implement file compression (and un-compression)
using Huffman’s algorithm.
import heapq
import os
class HuffmanNode:
def __init__(self, char, freq):
self.char = char # Character
self.freq = freq # Frequency
self.left = None # Left child
self.right = None # Right child
heapq.heappush(priority_queue, merged)
return huffman_codes
# Encode data
encoded_data = ''.join(huffman_codes[char] for char in data)
return ''.join(decoded_data)
# Main function
if __name__ == "__main__":
# Example: Compress and decompress a simple text
text = "shakir ali huffman coding example"
print("Original text:", text)
# Compress
encoded, tree = compress_file(text)
print("\nEncoded data:", encoded)
# Decompress
decoded = decompress_file(encoded, tree)
print("\nDecoded text:", decoded)
Input:
Output:
9.Write a program to implement dynamic programming algorithm to solve
the all pairs shortest path
Problem.
# Floyd-Warshall Algorithm to find All-Pairs Shortest Path
def floyd_warshall(graph):
"""
Function to find shortest distances between all pairs of vertices
using the Floyd-Warshall Algorithm.
Parameters:
graph (list of lists): Adjacency matrix representing the graph.
Use a large number (like float('inf')) for no direct edge.
Returns:
list of lists: Matrix with shortest distances between all pairs of vertices.
"""
# Number of vertices in the graph
V = len(graph)
return dist
# Example usage
if __name__ == "__main__":
# Adjacency matrix representation of the graph
# Replace float('inf') with a very large number for no direct edge
graph = [
[0, 3, float('inf'), 7],
[8, 0, 2, float('inf')],
[5, float('inf'), 0, 1],
[2, float('inf'), float('inf'), 0]
]
Explanation:
a) Greedy Algorithm
The Greedy algorithm doesn't guarantee the optimal solution for 0/1 Knapsack but can provide a quick approximation by maximizing the value-
to-weight ratio.
Dynamic Programming finds the optimal solution by storing the results of subproblems in a table. def knapsack_dp(weights,
values, capacity):
n = len(weights)
dp = [[0] * (capacity + 1) for _ in range(n + 1)]
return dp[n][capacity]
Branch and Bound uses a priority queue to keep track of promising nodes and calculates bounds to prune unnecessary branches.
class Node:
def __init__(self, level, value, weight, bound):
self.level = level
self.value = value
self.weight = weight
self.bound = bound
if j < n:
profit_bound += (capacity - tot_weight) * values[j] / weights[j]
return profit_bound
v.level = u.level + 1
v.weight = u.weight + weights[v.level]
v.value = u.value + values[v.level]
v.weight = u.weight
v.value = u.value
v.bound = bound(v, n, weights, values, capacity)
if v.bound > max_profit:
pq.put((-v.bound, v))
return max_profit
1. Input:
a. A sorted array of keys.
b. Probability arrays p[i] (search probabilities) and q[i] (dummy node probabilities).
2. Output:
a. The minimum cost of the optimal binary search tree.
b. The structure of the tree.
• Use a cost table cost[i][j] to store the minimal cost for a subtree spanning i to j.
• Use a root table root[i][j] to store the optimal root for each subtree.
• Compute the optimal cost for each possible subtree size iteratively.
Python Implementation:
python
Copy code
def optimal_bst(keys, p, q, n):
# Initialize cost and weight matrices
cost = [[0 for _ in range(n + 1)] for _ in range(n + 1)]
weight = [[0 for _ in range(n + 1)] for _ in range(n + 1)]
root = [[0 for _ in range(n)] for _ in range(n)]
# Example usage
if __name__ == "__main__":
keys = [10, 20, 30, 40] # Sorted keys
p = [0.1, 0.2, 0.4, 0.3] # Probabilities for actual keys
q = [0.05, 0.1, 0.05, 0.05, 0.05] # Probabilities for dummy keys
n = len(keys)
Example Output:
Explanation of Output:
The Traveling Salesperson Problem (TSP) is a classic problem in optimization. The goal is to find the shortest possible route that visits each city once and returns
to the starting city. Below are solutions for TSP using Dynamic Programming, Backtracking, and Branch and Bound in Python:
This solution uses the Held-Karp algorithm, a well-known dynamic programming approach for solving TSP.
python
Copy code
import sys
min_cost = sys.maxsize
for next_city in range(n):
if visited & (1 << next_city) == 0 and graph[city][next_city] > 0:
min_cost = min(
min_cost,
graph[city][next_city] + visit(next_city, visited | (1 << next_city))
)
dp[city][visited] = min_cost
return min_cost
# Example graph
graph = [
[0, 10, 15, 20],
[10, 0, 35, 25],
[15, 35, 0, 30],
[20, 25, 30, 0]
]
print("TSP cost using Dynamic Programming:", tsp_dynamic_programming(graph))
b) Backtracking Algorithm
Backtracking explores all possible routes and selects the optimal one.
python
Copy code
def tsp_backtracking(graph, start=0):
n = len(graph)
visited = [False] * n
min_cost = sys.maxsize
for i in range(n):
if not visited[i] and graph[curr_pos][i] > 0:
visited[i] = True
solve(i, count + 1, cost + graph[curr_pos][i])
visited[i] = False
visited[start] = True
solve(start, 1, 0)
return min_cost
# Example graph
graph = [
[0, 10, 15, 20],
[10, 0, 35, 25],
[15, 35, 0, 30],
[20, 25, 30, 0]
]
print("TSP cost using Backtracking:", tsp_backtracking(graph))
This method maintains a lower bound and prunes branches that exceed it.
python
Copy code
import heapq
return min_cost
# Example graph
graph = [
[0, 10, 15, 20],
[10, 0, 35, 25],
[15, 35, 0, 30],
[20, 25, 30, 0]
]
print("TSP cost using Branch and Bound:", tsp_branch_and_bound(graph))
Output Example