Assignment 3
Assignment 3
MODULE – 5
| 0 | ---> | | ---> | |
| 1 | ---> | | ---> | |
| 2 | ---> | | ---> | |
| 3 | ---> | | ---> | |
| n | ---> | | ---> | |
8 MARKS
1. Define Graph and Explain the Concept of Graph with an Example?
In data structures, a graph is a non-linear data structure composed of a set of vertices (also
known as nodes) and a set of edges that connect pairs of vertices. Graphs are widely used
to model relationships between entities, making them a fundamental data structure in
computer science.
Here's a formal definition of a graph:
"A graph G consists of a non-empty set V of vertices (nodes) and a set E of edges. Each edge
in E is a pair (v, w) where v, w ∈ V. For directed graphs, the pair (v, w) indicates an edge from
vertex v to vertex w. For undirected graphs, the pair (v, w) indicates a bidirectional edge
between vertices v and w."
Now, let's explain the concept of a graph with an example:
Consider a social network where individuals (vertices) are connected by friendships (edges).
We can represent this social network as a graph:
Vertices: {Alice, Bob, Charlie, Dave, Eve} Edges: {(Alice, Bob), (Bob, Charlie), (Charlie, Dave),
(Charlie, Eve), (Dave, Eve)}
In this example:
Vertices represent individuals in the social network, such as Alice, Bob, Charlie, Dave,
and Eve.
Edges represent friendships between individuals. For instance, there is an edge
between Alice and Bob, indicating that they are friends.
This graph is undirected, meaning that friendships are mutual. If Alice is friends with
Bob, then Bob is also friends with Alice.
This example demonstrates how a graph can be used to model relationships between
entities in a social network. In this context, graphs are valuable for analyzing connectivity,
identifying communities, and studying the structure of the network.
In data structures, graphs can be implemented using various representations, such as
adjacency matrices, adjacency lists, or edge lists. These representations provide different
trade-offs in terms of memory usage and efficiency for different types of graph operations.
1. **Create Graph**: This operation involves initializing a graph data structure. The graph
can be represented using various data structures such as an adjacency matrix, adjacency
list, or an edge list.
// Example of creating a graph using an adjacency list representation
#include <stdio.h>
#include <stdlib.h>
// Graph structure
struct Graph {
int numVertices;
struct Node* adjLists[MAX_VERTICES];
};
return graph;
}
```
2. **Add Edge**: This operation involves adding an edge between two vertices of the
graph.
```c
// Example of adding an edge in an adjacency list representation
3. **Remove Edge**: This operation involves removing an edge between two vertices of
the graph.
```c
// Example of removing an edge in an adjacency list representation
if (current != NULL) {
if (prev != NULL) {
prev->next = current->next;
} else {
graph->adjLists[dest] = current->next;
}
free(current);
}
}
```
These are some of the basic graph operations that can be performed in C. Depending on the
requirements and the specific application, additional operations such as graph traversal
(DFS, BFS), finding shortest paths, and determining connected components can also be
implemented.
1. **Hashing Function**: Initially, each element is hashed to find its initial position in the
hash table.
3. **Probing**: Probing involves searching for an empty slot in the hash table to place the
collided element. There are different methods of probing, such as linear probing, quadratic
probing, and double hashing.
4. **Insertion**: Once an empty slot is found, the collided element is inserted into that
position.
5. **Search and Deletion**: During search and deletion operations, the same probing
technique is used to locate the element. If the element is found, it is either returned or
deleted, respectively.
And we want to insert the following elements into the hash table:
- 25
- 35
- 15
- 45
Initially, the hash table is empty. After inserting 25, it hashes to position 5.
```
Index: 0 1 2 3 4 5 6 7 8 9
Element: [ ] [ ] [ ] [ ] [ ] [25] [ ] [ ] [ ] [ ]
```
Then, when inserting 35, it also hashes to position 5, but it's already occupied. So, linear
probing would search for the next available slot, which is position 6.
```
Index: 0 1 2 3 4 5 6 7 8 9
Element: [ ] [ ] [ ] [ ] [ ] [25] [35] [ ] [ ] [ ]
```
Similarly, for 15, it hashes to position 5 (collision again), then probes linearly and finds an
empty slot at position 7.
```
Index: 0 1 2 3 4 5 6 7 8 9
Element: [ ] [ ] [ ] [ ] [ ] [25] [35] [15] [ ] [ ]
```
Finally, when inserting 45, it hashes to position 5 (collision again), then probes linearly and
finds an empty slot at position 8.
```
Index: 0 1 2 3 4 5 6 7 8 9
Element: [ ] [ ] [ ] [ ] [ ] [25] [35] [15] [45] [ ]
```
Now, the hash table is fully occupied. During searches or deletions, the same linear probing
technique would be used to locate elements.
return graph;
}
int main() {
struct Graph* graph = createGraph(5); // Create a graph with 5 vertices
return 0;
}
```
This C program defines a graph data structure using an adjacency list representation. It
includes functions to create a graph, add edges, and perform Depth-First Search (DFS)
traversal starting from a specified vertex. Finally, it demonstrates the usage of these
functions in the main function.
```
BFS(graph, start):
// Initialize a queue to keep track of vertices to visit
queue = new Queue()
Explanation:
- The BFS algorithm maintains a queue to keep track of vertices to visit. It starts by
enqueuing the starting vertex onto the queue and marking it as visited.
- In each iteration, it dequeues a vertex from the queue, processes it (e.g., prints it), and
then explores its neighbors.
- If a neighbor has not been visited yet, it is enqueued onto the queue and marked as
visited. This ensures that vertices are explored in a breadth-first manner.
This algorithm explores the entire graph reachable from the starting vertex in a breadth-first
manner.
Note: This algorithm assumes that the graph is represented using an adjacency list.
Additionally, it does not handle disconnected graphs. If the graph is disconnected, you may
need to modify the algorithm to handle multiple connected components.
```
4 1
(1) --- (2) --- (3)
| /| /
11 5 2 3
|/ |/
(4) --- (5)
6
```
```
4 1
(1) --- (2) --- (3)
|
3
|
(5)
```
Now, let's write a sample code in C to find the Minimum Spanning Tree using the Prim's
algorithm:
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <limits.h>
return min_index;
}
// Function to construct and print MST for a graph represented using adjacency matrix
representation
void primMST(int graph[V][V]) {
int parent[V]; // Array to store constructed MST
int key[V]; // Key values used to pick minimum weight edge in cut
bool mstSet[V]; // To represent set of vertices included in MST
// Always include the first vertex in MST. Make key 0 so that this vertex is picked as the
first vertex.
key[0] = 0;
parent[0] = -1; // First node is always root of MST
// Update key value and parent index of the adjacent vertices of the picked vertex.
// Consider only those vertices which are not yet included in MST
for (int v = 0; v < V; v++) {
// graph[u][v] is non-zero only for adjacent vertices of m
// mstSet[v] is false for vertices not yet included in MST
// Update the key only if graph[u][v] is smaller than key[v]
if (graph[u][v] && mstSet[v] == false && graph[u][v] < key[v]) {
parent[v] = u;
key[v] = graph[u][v];
}
}
}
// Driver code
int main() {
/* Let us create the following graph
2 3
(0)--(1)--(2)
| /\ |
6| 8/ \5 |7
|/ \|
(3)-------(4)
9 */
int graph[V][V] = {{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}};
return 0;
}
```
This code demonstrates the implementation of Prim's algorithm to find the Minimum
Spanning Tree (MST) of a graph represented using an adjacency matrix. It prints the edges
of the MST along with their weights.
10. What are the Logical Differences between BFS and DFS?
1. **Hash Table Initialization**: Initialize a hash table with a certain number of buckets.
Each bucket can be an array or a linked list.
2. **Hash Function**: Implement a hash function that maps keys to bucket indices. This
function should distribute keys evenly across the buckets to minimize collisions.
- Apply the hash function to the key to determine the bucket index.
- If the bucket at the determined index is empty, create a new linked list node with the key-
value pair and insert it into the bucket.
- If the bucket is not empty, traverse the linked list in the bucket to check if the key already
exists:
- If the key doesn't exist, append a new node with the key-value pair to the end of the
linked list.
- Apply the hash function to the key to determine the bucket index.
- Traverse the linked list in the bucket to find the node with the matching key:
- Apply the hash function to the key to determine the bucket index.
- Traverse the linked list in the bucket to find the node with the matching key:
Example:
Suppose we have a hash table with 5 buckets and the following hash function:
```
hash(key) = key % 5
```
Let's say we want to insert the following key-value pairs into the hash table:
- (2, "apple")
- (7, "banana")
- (12, "orange")
- (17, "grape")
- hash(2) = 2 % 5 = 2
- hash(17) = 17 % 5 = 2 (collision with (2, "apple"), (7, "banana"), and (12, "orange"))
After insertion, the hash table would look like this (using linked lists for separate chaining):
```
Bucket 0:
Bucket 1:
Bucket 2: (2, "apple") -> (7, "banana") -> (12, "orange") -> (17, "grape")
Bucket 3:
Bucket 4:
```
In this example, separate chaining resolved collisions by storing collided elements in linked
lists within the same buckets.
1. **Uniform Distribution**: A good hash function should distribute keys uniformly across
the hash table buckets. This ensures that each bucket has roughly the same number of keys,
reducing the likelihood of collisions.
2. **Deterministic**: The hash function should always produce the same hash value for the
same input key. This ensures consistency in hashing operations.
5. **Avalanche Effect**: A small change in the input key should produce a significant
change in the resulting hash value. This property ensures that similar keys are distributed
across different buckets, enhancing the uniformity of distribution.
7. **Deterministic Time Complexity**: The hash function should have a deterministic time
complexity, meaning its performance should be predictable and not dependent on the input
key size.
1. **Uniform Distribution**: A good hash function aims to distribute the hash values
uniformly across the output space. This helps in minimizing collisions, where two different
inputs produce the same hash value, and ensures efficient utilization of the data structure.
2. **Deterministic Mapping**: For the same input key, a hash function must produce the
same hash value every time. This ensures consistency and predictability in hash-based
operations.
4. **Avalanche Effect**: A small change in the input key should result in a significant
change in the resulting hash value. This property ensures that similar keys are distributed
across different hash values, promoting uniformity and reducing clustering.
6. **Security**: In cryptographic applications, hash functions are used for data integrity
verification, password hashing, digital signatures, etc. In such cases, the hash function should
be resistant to attacks, such as collision attacks and pre-image attacks, to ensure the security
of the system.
Overall, the objective of a hash function is to provide a fast and efficient way to map data to
hash values, ensuring uniformity, determinism, and security as per the requirements of the
application.
1. **Collision Handling**:
- **Open Addressing vs. Separate Chaining**: In open addressing, collisions are resolved
by finding alternative positions within the hash table, while in separate chaining, collided
elements are stored in linked lists within the same bucket. The efficiency of collision
handling depends on how well these techniques are implemented and how often collisions
occur.
2. **Distribution of Keys**:
- **Uniform Distribution**: A good hash function aims to distribute keys uniformly across
the hash table buckets. If keys are unevenly distributed, it can lead to clustering and poor
performance.
- **Avalanche Effect**: A small change in the input key should result in a significant
change in the hash value, ensuring that similar keys are distributed across different buckets.
This helps in achieving a more uniform distribution.
3. **Computational Overhead**:
4. **Memory Usage**:
- **Space Complexity**: The memory usage of a hash table depends on factors like the
number of buckets, the size of the hash table, and the average number of elements per
bucket. It's essential to balance memory usage with performance requirements.
- **Load Factor and Rehashing**: The load factor of a hash table (ratio of the number of
elements to the number of buckets) affects its performance. When the load factor exceeds
a certain threshold, rehashing may be required to maintain efficiency by increasing the
number of buckets.
5. **Collision Avoidance**:
- **Hash Function Quality**: The quality of the hash function plays a crucial role in
avoiding collisions. A good hash function minimizes the likelihood of collisions by
distributing keys uniformly across the hash table.
Overall, hashing efficiency is determined by how well the hash function distributes keys,
how collisions are handled, the computational overhead of hashing operations, and the
memory usage of the hash table. Balancing these factors is essential to ensure optimal
performance in hash-based data structures.
#include <stdio.h>
#include <stdlib.h>
#define MAX_VERTICES 10
// Structure for a node in the adjacency list
struct Node {
int vertex;
};
struct Graph {
int numVertices;
int* visited;
};
newNode->vertex = v;
newNode->next = NULL;
return newNode;
graph->numVertices = vertices;
graph->adjLists[i] = NULL;
graph->visited[i] = 0;
return graph;
newNode->next = graph->adjLists[src];
graph->adjLists[src] = newNode;
newNode = createNode(src);
newNode->next = graph->adjLists[dest];
graph->adjLists[dest] = newNode;
graph->visited[vertex] = 1;
if (graph->visited[adjVertex] == 0) {
DFS(graph, adjVertex);
adjList = adjList->next;
int main() {
addEdge(graph, 0, 2);
addEdge(graph, 1, 3);
addEdge(graph, 2, 4);
addEdge(graph, 3, 4);
return 0;
```
This C program defines a graph data structure using an adjacency list representation and
includes functions to create a graph, add edges, and perform Depth-First Search (DFS)
traversal starting from a specified vertex. Finally, it demonstrates the usage of these
functions in the main function.
Breadth-First Search (BFS) is a graph traversal algorithm that starts traversing the graph
from a chosen source vertex and explores all of its neighbor vertices at the present depth
before moving on to the vertices at the next depth level. It ensures that all vertices at a
given level are visited before moving to the vertices at the next level.
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
typedef struct {
int items[MAX_VERTICES];
int front;
int rear;
} Queue;
Queue* createQueue() {
queue->front = -1;
queue->rear = -1;
return queue;
printf("Queue overflow\n");
else {
if (queue->front == -1)
queue->front = 0;
queue->rear++;
queue->items[queue->rear] = value;
int item;
if (isEmpty(queue)) {
printf("Queue underflow\n");
exit(EXIT_FAILURE);
} else {
item = queue->items[queue->front];
queue->front++;
return item;
}
// Graph representation using adjacency list
int dest;
} Node;
typedef struct {
Node* head;
} AdjList;
typedef struct {
int num_vertices;
AdjList* array;
} Graph;
newNode->dest = dest;
newNode->next = NULL;
return newNode;
graph->num_vertices = num_vertices;
graph->array[i].head = NULL;
return graph;
newNode->next = graph->array[src].head;
graph->array[src].head = newNode;
visited[i] = false;
visited[start] = true;
enqueue(queue, start);
while (!isEmpty(queue)) {
while (temp) {
if (!visited[adj_vertex]) {
visited[adj_vertex] = true;
enqueue(queue, adj_vertex);
temp = temp->next;
int main() {
int num_vertices = 6;
addEdge(graph, 0, 1);
addEdge(graph, 0, 2);
addEdge(graph, 1, 3);
addEdge(graph, 1, 4);
addEdge(graph, 2, 4);
addEdge(graph, 3, 4);
addEdge(graph, 3, 5);
addEdge(graph, 4, 5);
bfs(graph, 0);
return 0;
```
This code defines a simple directed graph and performs a BFS traversal starting from a given
vertex. It utilizes an adjacency list representation for the graph and a queue for BFS
traversal.
1. **Algorithm Efficiency**: Using algorithms that have low time complexity, such as O(1),
O(log n), or O(n), can significantly speed up computations. For example, algorithms like
binary search, hashing, or dynamic programming can provide faster solutions compared to
brute-force methods.
7. **Approximation and Heuristics**: In some cases, trading off accuracy for speed by using
approximation algorithms or heuristic methods can lead to faster computation. These
techniques are often employed in optimization problems or real-time systems where quick
decisions are required.
- **Usage**: Random keys are typically used in symmetric encryption algorithms like AES
(Advanced Encryption Standard) or stream ciphers. These keys are required to be kept
secret and are shared between the sender and receiver of encrypted messages.
- **Strength**: Random keys are considered secure as long as they are sufficiently long
and truly random or generated by a cryptographically secure pseudorandom number
generator (CSPRNG). The strength of encryption directly depends on the randomness and
length of the key.
2. **Non-Random Keys**:
- **Example**: In RSA encryption, a user generates a key pair consisting of a public key
(which can be shared with anyone) and a private key (which must be kept secret). The
public key is derived from the product of two large prime numbers, while the private key is
derived from the prime factors of this product. Although the keys are not random, the
security of RSA relies on the computational complexity of factoring large numbers.
In summary, random keys are essential for symmetric encryption algorithms, while non-
random keys are commonly used in asymmetric encryption schemes. Both types of keys
play critical roles in ensuring the security and confidentiality of cryptographic
communications, with their strength depending on factors such as randomness, length, and
the underlying cryptographic algorithms used for key generation.
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
typedef struct {
int items[MAX_VERTICES];
int front;
int rear;
} Queue;
Queue* createQueue() {
queue->front = -1;
queue->rear = -1;
return queue;
}
bool isEmpty(Queue* queue) {
if (queue->rear == MAX_VERTICES - 1)
printf("Queue overflow\n");
else {
if (queue->front == -1)
queue->front = 0;
queue->rear++;
queue->items[queue->rear] = value;
int item;
if (isEmpty(queue)) {
printf("Queue underflow\n");
exit(EXIT_FAILURE);
} else {
item = queue->items[queue->front];
queue->front++;
if (queue->front > queue->rear) {
return item;
int dest;
} Node;
typedef struct {
Node* head;
} AdjList;
typedef struct {
int num_vertices;
AdjList* array;
} Graph;
newNode->next = NULL;
return newNode;
graph->num_vertices = num_vertices;
graph->array[i].head = NULL;
return graph;
newNode->next = graph->array[src].head;
graph->array[src].head = newNode;
visited[start] = true;
enqueue(queue, start);
while (!isEmpty(queue)) {
while (temp) {
if (!visited[adj_vertex]) {
visited[adj_vertex] = true;
enqueue(queue, adj_vertex);
temp = temp->next;
int main() {
int num_vertices = 6;
addEdge(graph, 0, 1);
addEdge(graph, 0, 2);
addEdge(graph, 1, 3);
addEdge(graph, 1, 4);
addEdge(graph, 2, 4);
addEdge(graph, 3, 4);
addEdge(graph, 3, 5);
addEdge(graph, 4, 5);
bfs(graph, 0);
return 0;
In this C program:
- We first define a `Queue` structure and its functions for enqueue, dequeue, and checking
if it's empty. This queue will be used in the BFS traversal.
- We then define a structure for the adjacency list representation of the graph, which
consists of nodes and linked lists.
- Functions like `createNode`, `createGraph`, and `addEdge` are used for creating the graph
and adding edges between vertices.
- The `bfs` function implements the Breadth-First Search algorithm. It starts from the given
start vertex, explores its adjacent vertices level by level, and prints them.
- In the `main` function, we create a graph, add some edges, and then perform BFS traversal
starting from vertex 0.
Compile and run this C program, and you'll get the Breadth-First Traversal of the given
graph.
20. Define Depth-First Search and Write the Sample Code ?
Depth-First Search (DFS) is a graph traversal algorithm that explores as far as
possible along each branch before backtracking. It traverses the depth of any
particular branch before moving on to explore the siblings.
```c
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
typedef struct {
Node* head;
} AdjList;
typedef struct {
int num_vertices;
AdjList* array;
} Graph;
int main() {
int num_vertices = 4;
Graph* graph = createGraph(num_vertices);
addEdge(graph, 0, 1);
addEdge(graph, 0, 2);
addEdge(graph, 1, 2);
addEdge(graph, 2, 0);
addEdge(graph, 2, 3);
addEdge(graph, 3, 3);
return 0;
}
In this C implementation:
- We define structures for representing a graph using an adjacency list.
- Functions like `createNode`, `createGraph`, and `addEdge` are used for creating
the graph and adding edges between vertices.
- The `dfsUtil` function performs the actual DFS traversal recursively. It marks the
current vertex as visited, prints it, and then recursively calls itself for all adjacent
vertices that have not been visited yet.
- The `dfs` function initializes the visited array and calls `dfsUtil` with the starting
vertex.
- In the `main` function, we create a graph, add some edges, and then perform DFS
traversal starting from vertex 2.
Compile and run this C program, and you'll get the Depth-First Traversal of the given
graph.
THANK YOU