Daa Programs
Daa Programs
Linear Search checks each element of a list one by one until the desired element is found or the
list ends. Given an array, arr of n elements, and an element x, find whether element x is present
in the array. Return the index of the first occurrence of x in the array, or -1 if it doesn’t exist.
Examples:
Input: arr[] = [10, 50, 30, 70, 80, 20, 90, 40], x = 30
Output : 2
Explanation: For array [10, 50, 30, 70, 80, 20, 90, 40], the element to be searched is 30 and it is at index
2. So, the output is 2.
The iterative method of binary search has a time complexity of O(logn n). This logarithmic performance occurs
because the algorithm repeatedly divides the search space in half during each iteration of the loop until the
target is found or the range is exhausted.
1. Write a program to implement binary search algorithm.
Binary Search is an efficient searching algorithm used to find an element in a sorted array by repeatedly dividing
the search interval in half. It reduces the time complexity to O(log N), making it much faster than linear search.
Here is working of Binary Search
1. Find the middle element of the array.
2. Compare the middle element with the target key. If equal: return index.
3. If the key is smaller: search the left half.
4. If the key is larger: search the right half.
5. Repeat until the element is found or the search space is empty.
Explanation:
Using a while loop to implement iterative binary search .
Initialize low and high pointers to define the search range.
Calculates the middle index mid = low + (high - low) // 2.
If arr[mid] is less than x, moves search to the right half.
If arr[mid] is greater than x, moves search to the left half.
If arr[mid] == x, returns the index.
Returns -1 if the element is not found.
A factorial is positive integer n, and denoted by n!. Then the product of all positive integers less than or equal
to n.
n !=n∗(n−1)∗(n−2)∗(n−3)∗....∗1
For example:
calculate the factorial of a number using recursion.
Examples:
Input: 5
Output: 120
Input: 6
Output: 720
Implementation:
If fact(5) is called, it will call fact(4), fact(3), fact(2) and fact(1). So it means keeps calling itself by reducing value
by one till it reaches 1.
Given two matrices, the task is to multiply them together to form a new matrix. Each element in the result is
obtained by multiplying the corresponding elements of a row from the first matrix and a column from the
second matrix.
For Example:
Input: A = [[1, 2], [3, 4]], B = [[5, 6], [7, 8]]
Output: [[19, 22], [43, 50]]
Explanation: 1×5 + 2×7 = 19,
1×6 + 2×8 = 22,
3×5 + 4×7 = 43,
3×6 + 4×8 = 50
Using NumPy
NumPy handles matrix multiplication internally using optimized C-based operations. It takes the rows of matrix
A and the columns of matrix B, performs vectorized dot-products, and produces the result efficiently without
manual loops.
import numpy as np
A = [[12, 7, 3],
[4, 5, 6],
[7, 8, 9]]
B = [[5, 8, 1, 2],
[6, 7, 3, 0],
[4, 5, 9, 1]]
r = [Link](A, B)
for row in r:
print(row)
Output
[114 160 60 27]
[74 97 73 14]
[119 157 112 23]
Explanation: [Link](A, B) computes all row × column dot-products internally using vectorized, compiled code.
A = [[12, 7, 3],
[4, 5, 6],
[7, 8, 9]]
B = [[5, 8, 1, 2],
[6, 7, 3, 0],
[4, 5, 9, 1]]
r = [[0]*len(B[0]) for _ in range(len(A))]
for i in range(len(A)):
for j in range(len(B[0])):
for k in range(len(B)):
r[i][j] += A[i][k] * B[k][j]
for row in r:
print(row)
Output
[114, 160, 60, 27]
[74, 97, 73, 14]
[89, 109, 106, 11]
Explanation:
r = [[0]*len(B[0]) for _ in range(len(A))] initializes a result matrix with zeros.
Triple loop: outer i -> selects row of A, middle j -> selects column index of B and inner k -> multiplies
A[i][k] * B[k][j] and accumulates into r[i][j].
4. Write a program to sort a given set of numbers using bubble sort algorithm
Bubble Sort is one of the simplest sorting algorithms. It repeatedly compares adjacent elements in the
list and swaps them if they are in the wrong order.
How Bubble Sort Works
1. Compare each pair of adjacent elements.
2. If the first element is greater than the second, swap them.
3. After each full pass, the largest unsorted element "bubbles up" to its correct position at the end.
4. Repeat this process for the remaining unsorted portion of the list.
5. The algorithm stops early if no swaps occur in a pass, meaning the array is already sorted.
Python Implementation
def bubbleSort(arr):
n = len(arr)
for i in range(n):
swapped = False
for j in range(0, n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
swapped = True
if not swapped:
break
arr = [64, 34, 25, 12, 22, 11, 90]
bubbleSort(arr)
print(arr)
Explanation:
for i in range(n): Runs multiple passes; each pass positions the next largest element at the end.
swapped = False: Flag to detect if any swap occurs in the current pass.
for j in range(0, n - i - 1): Iterates through unsorted elements; range shrinks as the end becomes sorted.
if arr[j] > arr[j + 1]: Checks for inversion between adjacent elements.
arr[j], arr[j + 1] = arr[j + 1], arr[j]: Swaps elements in place using tuple unpacking.
if not swapped: break, Stops early if no swaps occur, indicating the array is already sorted.
6. Write a program to sort a given set of numbers using Merge sort algorithm.
Merge Sort
The Merge Sort algorithm is a divide-and-conquer algorithm that sorts an array by first breaking it down into
smaller arrays, and then building the array back together the correct way so that it is sorted.
Divide: The algorithm starts with breaking up the array into smaller and smaller pieces until one such sub-array
only consists of one element.
Conquer: The algorithm merges the small pieces of the array back together by putting the lowest values first,
resulting in a sorted array.
The breaking down and building up of the array to sort the array is done recursively.
In the animation above, each time the bars are pushed down represents a recursive call, splitting the array into
smaller pieces. When the bars are lifted up, it means that two sub-arrays have been merged together.
The Merge Sort algorithm can be described like this:
How it works:
1. Divide the unsorted array into two sub-arrays, half the size of the original.
2. Continue to divide the sub-arrays as long as the current piece of the array has more than one element.
3. Merge two sub-arrays together by always putting the lowest value first.
4. Keep merging until there are no sub-arrays left.
Take a look at the drawing below to see how Merge Sort works from a different perspective. As you can see, the
array is split into smaller and smaller pieces until it is merged back together. And as the merging happens,
values from each sub-array are compared so that the lowest value comes first.
Step 2: The splitting of the first sub-array is finished, and now it is time to merge. 8 and 9 are the first two
elements to be merged. 8 is the lowest value, so that comes before 9 in the first merged sub-array.
[ 12] [ 8, 9] [ 3, 11, 5, 4]
Step 3: The next sub-arrays to be merged is [ 12] and [ 8, 9]. Values in both arrays are compared from the start.
8 is lower than 12, so 8 comes first, and 9 is also lower than 12.
[ 8, 9, 12] [ 3, 11, 5, 4]
Step 5: 3 and 11 are merged back together in the same order as they are shown because 3 is lower than 11.
[ 8, 9, 12] [ 3, 11] [ 5, 4]
Step 6: Sub-array with values 5 and 4 is split, then merged so that 4 comes before 5.
[ 8, 9, 12] [ 3, 11] [ 5] [ 4]
[ 8, 9, 12] [ 3, 11] [ 4, 5]
Step 7: The two sub-arrays on the right are merged. Comparisons are done to create elements in the new
merged array:
1. 3 is lower than 4
2. 4 is lower than 11
3. 5 is lower than 11
4. 11 is the last remaining value
[ 8, 9, 12] [ 3, 4, 5, 11]
Step 8: The two last remaining sub-arrays are merged. Let's look at how the comparisons are done in more
detail to create the new merged and finished sorted array:
3 is lower than 8:
Before [ 8, 9, 12] [ 3, 4, 5, 11]
After: [ 3, 8, 9, 12] [ 4, 5, 11]
----------------------------------or------------------------------------------------------------------------------------------------------------
Merge Sort is one of the most efficient and stable sorting algorithms based on the Divide and Conquer
technique. It divides an input array into two halves, recursively sorts them, and then merges the two sorted
halves using a function called merge().
The key function, merge(arr, l, m, r), assumes that both halves arr[l..m] (left half) and arr[m+1..r] (right half) are
already sorted and merges them into one sorted array.
Python Implementation
def merge(arr, l, m, r):
n1 = m - l + 1
n2 = r - m
L = [0] * n1
R = [0] * n2
for i in range(n1):
L[i] = arr[l + i]
for j in range(n2):
R[j] = arr[m + 1 + j]
i=j=0
k=l
while i < n1 and j < n2:
if L[i] <= R[j]:
arr[k] = L[i]
i += 1
else:
arr[k] = R[j]
j += 1
k += 1
while i < n1:
arr[k] = L[i]
i += 1
k += 1
while j < n2:
arr[k] = R[j]
j += 1
k += 1
def mergeSort(arr, l, r):
if l < r:
m = l + (r - l) // 2
mergeSort(arr, l, m)
mergeSort(arr, m + 1, r)
merge(arr, l, m, r)
arr = [12, 11, 13, 5, 6, 7]
print("Given array is:", arr)
mergeSort(arr, 0, len(arr) - 1)
print("Sorted array is:", arr)
Output
Given array is: [12, 11, 13, 5, 6, 7]
Sorted array is: [5, 6, 7, 11, 12, 13]
Explanation:
Divide:
In mergeSort(arr, l, r), the condition if l < r: ensures the array is divided only when it has more than one
element.
The midpoint is calculated as m = l + (r - l) // 2.
The array is divided into two halves by recursive calls:
mergeSort(arr, l, m) → sorts the left half.
mergeSort(arr, m + 1, r) → sorts the right half.
Conquer:
Each recursive call continues dividing until the subarrays have a single element (automatically sorted).
Once divided, recursion starts returning — at each level, the two sorted halves are ready to be merged.
Merge:
The merge(arr, l, m, r) function merges the two sorted halves.
n1 = m - l + 1 and n2 = r - m calculate the sizes of the left and right subarrays.
Temporary arrays L and R store these elements using loops.
Variables i, j, and k are initialized for left array, right array, and merged array indices.
The loop while i < n1 and j < n2: compares L[i] and R[j], placing the smaller into arr[k].
After one list is exhausted, remaining elements from L or R are copied back into arr.
This merging continues recursively until the full array becomes sorted.
Advantages of Merge Sort
Efficient for large datasets.
Guarantees O(n log n) performance.
Stable sorting (preserves order of equal elements).
Works well with linked lists.
Disadvantages
Requires additional space (O(n)).
Slower for small datasets compared to simpler algorithms like Insertion Sort.
7. Write a program to sort a given set of numbers using Insertion Sort algorithm.
Insertion sort is a simple sorting algorithm that works by iteratively inserting each element of an
unsorted list into its correct position in a sorted portion of the list. It is like sorting playing cards in your
hands. You split the cards into two groups: the sorted cards and the unsorted cards. Then, you pick a
card from the unsorted group and put it in the right place in the sorted group.
Start with the second element as the first element is assumed to be sorted.
Compare the second element with the first if the second is smaller then swap them.
Move to the third element, compare it with the first two, and put it in its correct position
Repeat until the entire array is sorted.
# Python program for implementation of Insertion Sort
# Function to sort array using insertion sort
def insertionSort(arr):
for i in range(1, len(arr)):
key = arr[i]
j=i-1
# Move elements of arr[0..i-1], that are
# greater than key, to one position ahead
# of their current position
while j >= 0 and key < arr[j]:
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = key
# A utility function to print array of size n
def printArray(arr):
for i in range(len(arr)):
print(arr[i], end=" ")
print()
# Driver method
if __name__ == "__main__":
arr = [12, 11, 13, 5, 6]
insertionSort(arr)
printArray(arr)
Output
5 6 11 12 13
arr = {23, 1, 10, 5, 2}
Initial:
Current element is 23
The first element in the array is assumed to be sorted.
The sorted part until 0th index is : [23]
First Pass:
Compare 1 with 23 (current element with the sorted part).
Since 1 is smaller, insert 1 before 23 .
The sorted part until 1st index is: [1, 23]
Second Pass:
Compare 10 with 1 and 23 (current element with the sorted part).
Since 10 is greater than 1 and smaller than 23 , insert 10 between 1 and 23 .
The sorted part until 2nd index is: [1, 10, 23]
Third Pass:
Compare 5 with 1 , 10 , and 23 (current element with the sorted part).
Since 5 is greater than 1 and smaller than 10 , insert 5 between 1 and 10
The sorted part until 3rd index is : [1, 5, 10, 23]
Fourth Pass:
Compare 2 with 1, 5, 10 , and 23 (current element with the sorted part).
Since 2 is greater than 1 and smaller than 5 insert 2 between 1 and 5 .
The sorted part until 4th index is: [1, 2, 5, 10, 23]
Final Array:
The sorted array is: [1, 2, 5, 10, 23]
Advantages
Simple and easy to implement.
Stable sorting algorithm.
Efficient for small lists and nearly sorted lists.
Space-efficient as it is an in-place algorithm.
Adoptive. the number of inversions is directly proportional to number of swaps. For example, no
swapping happens for a sorted array and it takes O(n) time only.
Disadvantages
Inefficient for large lists.
Not as efficient as other sorting algorithms (e.g., merge sort, quick sort) for most cases.
Applications of Insertion Sort
Insertion sort is commonly used in situations where:
The list is small or nearly sorted.
Simplicity and stability are important.
Used as a subroutine in Bucket Sort
Can be useful when array is already almost sorted (very few inversions)
Since Insertion sort is suitable for small sized arrays, it is used in Hybrid Sorting algorithms along with
other efficient algorithms like Quick Sort and Merge Sort. When the subarray size becomes small, we
switch to insertion sort in these recursive algorithms. For example IntroSort and TimSort use insertions
sort.
8. Write a program to sort a given set of numbers using Selection sort algorithm.
Selection Sort is one of the simplest comparison-based sorting algorithms. It sorts an array by repeatedly
finding the smallest (or largest) element from the unsorted portion and placing it in its correct position.
Working of Selection Sort
1. Start from the first element and find the smallest element in the entire array by iterating over it.
2. Swap this smallest element with the first element.
3. Now, move to the second element find the next smallest in the remaining unsorted portion and swap it
with the second position.
4. Repeat this process until the entire array becomes sorted.
Python Implementation
def selectionSort(array, size):
for ind in range(size):
min_index = ind
for j in range(ind + 1, size):
if array[j] < array[min_index]:
min_index = j
array[ind], array[min_index] = array[min_index], array[ind]
arr = [-2, 45, 0, 11, -9, 88, -97, -202, 747]
size = len(arr)
selectionSort(arr, size)
print(arr)
Output
[-202, -97, -9, -2, 0, 11, 45, 88, 747]
Explanation:
for ind in range(size) (Outer loop): Iterates through each index, treating it as the start of the unsorted
portion.
min_index = ind: Assumes the current index holds the smallest element.
for j in range(ind + 1, size) (Inner loop): Finds the index of the minimum element in the unsorted part of
the array.
if array[j] < array[min_index]: Updates min_index whenever a smaller element is found.
Swap step: Exchanges the smallest found element with the first element of the unsorted section.
9. Write a program to find the minimum spanning tree of a given graph using Prim’s algorithm
Prim’s algorithm is a Greedy algorithm like Kruskal's algorithm. This algorithm always starts with a single
node and moves through several adjacent nodes, in order to explore all of the connected edges along
the way.
The algorithm starts with an empty spanning tree.
The idea is to maintain two sets of vertices. The first set contains the vertices already included in the
MST, and the other set contains the vertices not yet included.
At every step, it considers all the edges that connect the two sets and picks the minimum weight edge
from these edges. After picking the edge, it moves the other endpoint of the edge to the set containing
MST.
Working of the Prim's Algorithm
1. Determine an arbitrary vertex as the starting vertex of the MST. We pick 0 in the above diagram.
2. Follow steps 3 to 5 till there are vertices that are not included in the MST (known as fringe vertex).
3. Find edges connecting any tree vertex with the fringe vertices.
4. Find the minimum among these edges.
5. Add the chosen edge to the MST. Since we consider only the edges that connect fringe vertices with the
rest, we never get a cycle.
6. Return the MST and exit
import sys
def primMST(graph):
V = len(graph)
key = [[Link]] * V
parent = [-1] * V
mstSet = [False] * V
key[0] = 0
for _ in range(V):
# Find min key vertex
min_val = [Link]
u = -1
for v in range(V):
if key[v] < min_val and not mstSet[v]:
min_val = key[v]
u=v
mstSet[u] = True
# Update adjacent vertices
for v in range(V):
if 0 < graph[u][v] < key[v] and not mstSet[v]:
key[v] = graph[u][v]
parent[v] = u
print("Edge \tWeight")
for i in range(1, V):
print(f"{parent[i]} - {i} \t{graph[i][parent[i]]}")
# Example Usage
graph = [
[0, 2, 0, 6, 0],
[2, 0, 3, 8, 5],
[0, 3, 0, 0, 7],
[6, 8, 0, 0, 9],
[0, 5, 7, 9, 0]
]
primMST(graph)
Prim's algorithm is a greedy method used to find the Minimum Spanning Tree (MST) of a connected, weighted,
and undirected graph. It grows the MST one vertex at a time, starting from an arbitrary initial vertex, by adding
the cheapest possible edge connecting the growing tree to a vertex not yet in the tree.
Here is an example program implemented in Python using an adjacency matrix representation.
# Prim's Algorithm in Python
INF = 9999999
# number of vertices in graph
V=5
# create a 2d array of size 5x5
# for adjacency matrix to represent graph
G = [[0, 9, 75, 0, 0],
[9, 0, 95, 19, 42],
[75, 95, 0, 51, 66],
[0, 19, 51, 0, 31],
[0, 42, 66, 31, 0]]
# create a array to track selected vertex
# selected will become true otherwise false
selected = [0, 0, 0, 0, 0]
# set number of edge to 0
no_edge = 0
# the number of egde in minimum spanning tree will be
# always less than(V - 1), where V is number of vertices in
# graph
# choose 0th vertex and make it true
selected[0] = True
# print for edge and weight
print("Edge : Weight\n")
while (no_edge < V - 1):
# For every vertex in the set S, find the all adjacent vertices
#, calculate the distance from the vertex selected at step 1.
# if the vertex is already in the set S, discard it otherwise
# choose another vertex nearest to selected vertex at step 1.
minimum = INF
x=0
y=0
for i in range(V):
if selected[i]:
for j in range(V):
if ((not selected[j]) and G[i][j]):
# not in selected and there is an edge
if minimum > G[i][j]:
minimum = G[i][j]
x=i
y=j
print(str(x) + "-" + str(y) + ":" + str(G[x][y]))
selected[y] = True
no_edge += 1
Output
Edge : Weight
0-1:9
1-3:19
3-4:31
3-2:51
Although adjacency matrix representation of graphs is used, this algorithm can also be implemented
using Adjacency List to improve its efficiency.
Adjacency Matrix
An adjacency matrix is a way of representing a graph as a matrix of booleans (0's and 1's). A finite graph can be
represented in the form of a square matrix on a computer, where the boolean value of the matrix indicates if
there is a direct path between two vertices.
For example, we have a graph below.
An undirected graph
If there is a path from i to j, then the value of Aij is 1 otherwise its 0. For instance, there is a path from vertex 1
to vertex 2, so A12 is 1 and there is no path from vertex 1 to 3, so A13 is 0.
In case of undirected graphs, the matrix is symmetric about the diagonal because of every edge (i,j), there is
also an edge (j,i).
Adjacency List
An adjacency list represents a graph as an array of linked lists. The index of the array represents a vertex and
each element in its linked list represents the other vertices that form an edge with the vertex.
For example, we have a graph below.
An undirected graph
We can represent this graph in the form of a linked list on a computer as shown below.
10. Write a program to find the minimum spanning tree of a given graph using Kruskal’s algorithm.
class Graph:
def __init__(self, vertices):
self.V = vertices
[Link] = []
def add_edge(self, u, v, w):
[Link]([u, v, w])
def kruskal_mst(self):
def find(parent, i):
if parent[i] == i:
return i
return find(parent, parent[i])
def union(parent, rank, x, y):
x_root = find(parent, x)
y_root = find(parent, y)
if rank[x_root] < rank[y_root]:
parent[x_root] = y_root
elif rank[x_root] > rank[y_root]:
parent[y_root] = x_root
else:
parent[y_root] = x_root
rank[x_root] += 1
result = []
i=0
e=0
[Link] = sorted([Link], key=lambda item: item[2])
parent = [i for i in range(self.V)]
rank = [0] * self.V
while e < self.V - 1:
u, v, w = [Link][i]
i += 1
x = find(parent, u)
y = find(parent, v)
if x != y:
e += 1
[Link]([u, v, w])
union(parent, rank, x, y)
return result
# Example usage:
g = Graph(4)
g.add_edge(0, 1, 10)
g.add_edge(0, 2, 6)
g.add_edge(0, 3, 5)
g.add_edge(1, 3, 15)
g.add_edge(2, 3, 4)
mst = g.kruskal_mst()
for u, v, weight in mst:
print(f"Edge: {u} - {v}, Weight: {weight}")
Output
Edge: 2 - 3, Weight: 4
Edge: 0 - 3, Weight: 5
Edge: 0 - 1, Weight: 10
In this example, we use Kruskal's algorithm to find the Minimum Spanning Tree of a graph represented
by an adjacency list. The algorithm starts with an empty MST and iteratively adds the smallest available
edges while ensuring that no cycles are formed.
Kruskal's algorithm is valuable for optimizing network design, cluster analysis, and various applications
involving connected graphs. It provides an efficient way to find the minimum spanning tree in a graph.
Kruskal's Algorithm
Kruskal's algorithm is a minimum spanning tree algorithm that takes a graph as input and finds the subset of
the edges of that graph which
form a tree that includes every vertex
has the minimum sum of weights among all the trees that can be formed from the graph
Choose the next shortest edge that doesn't create a cycle and add it
Choose the next shortest edge that doesn't create a cycle and add it
11. Write a program to find the binomial co-efficient C(n,k) [Where n and k are integers and n>k] using
brute force based algorithm and also dynamic programming based algorithm.
Output
Brute Force C(5, 2) is: 10
Output
Dynamic Programming C(5, 2) is: 10
[Link] a program to implement BFS Traversal algorithm.
Output
Following is Breadth First Traversal (starting from vertex 2)
2031
Output
Graph representation: {'A': ['B', 'C'], 'B': ['A', 'D', 'E'], 'C': ['A', 'F'], 'D': ['B'], 'E': ['B', 'F'], 'F': ['C', 'E']}
Starting node for BFS: B
BFS Traversal Order: ['B', 'A', 'D', 'E', 'C', 'F']
Depth First Traversal (or DFS) for a graph is similar to Depth First Traversal of a tree. The only catch here is, that,
unlike trees, graphs may contain cycles (a node may be visited twice). To avoid processing a node more than
once, use a Boolean visited array. A graph can have more than one DFS traversal.
Depth First Search in Python
Python Depth First Search Algorithm is used for traversing or searching tree or graph data structures. The
algorithm starts at the root node (selecting some arbitrary node as the root node in the case of a graph) and
explores as far as possible along each branch before backtracking.
Let us understand the working of Depth First Search with the help of the following illustration: for the source as
0.
#Python program implementations
from collections import defaultdict
# This class represents a directed graph using
# adjacency list representation
class Graph:
# Constructor
def __init__(self):
# Default dictionary to store graph
[Link] = defaultdict(list)
# Function to add an edge to graph
def addEdge(self, u, v):
[Link][u].append(v)
# A function used by DFS
def DFSUtil(self, v, visited):
# Mark the current node as visited
# and print it
[Link](v)
print(v, end=' ')
# Recur for all the vertices
# adjacent to this vertex
for neighbour in [Link][v]:
if neighbour not in visited:
[Link](neighbour, visited)
# The function to do DFS traversal. It uses
# recursive DFSUtil()
def DFS(self, v):
# Create a set to store visited vertices
visited = set()
# Call the recursive helper function
# to print DFS traversal
[Link](v, visited)
# Driver's code
if __name__ == "__main__":
g = Graph()
[Link](0, 1)
[Link](0, 2)
[Link](1, 2)
[Link](2, 0)
[Link](2, 3)
[Link](3, 3)
print("Following is Depth First Traversal (starting from vertex 2)")
# Function call
[Link](2)
Output
Following is Depth First Traversal (starting from vertex 2):
2013
DFS Algorithm Time Complexity: O(V+E) where V is the number of vertices in the graph and E is the number of
edges
Auxiliary Space: O(V+E)