Data Structures and Applications(BCS304) June - July 2024
Data Structures and Applications(BCS304) June - July 2024
Data structure is a representation of the logical relationships existing between individual elements of
data. A data structure is a way of organizing all data items that considers not only the elements stored but also
their relationship to each other.
A data structure is a way to organize, store, and manage data efficiently for use in algorithms and
computations. Examples include arrays, linked lists, stacks, queues, trees, and graphs, each designed to handle
specific types of data operations and access patterns.
1 b. Explain the classification of Data Structures with example.
Linear data structures organize data sequentially, where elements are arranged one after the other.
1. Array : A collection of elements of the same data type stored in contiguous memory locations.
2. Linked List: A collection of nodes, where each node contains data and a reference (or link) to the next
node in the sequence.
3. Stack: A collection of elements following the Last In First Out (LIFO) principle.
4. Queue: A collection of elements following the First In First Out (FIFO) principle.
1. Tree: A hierarchical structure where each node is connected to children nodes. A tree starts with a root
node.
Example: Representing file systems or organizational hierarchies.
3. Hash Table: A data structure that maps keys to values for efficient lookup using a hash function.
Example: Implementing dictionaries or caches.
Primitive Operations on Data Structures
1. Insertion : Adding a new element to the data structure.
array.append(5)
Output: [1, 2, 3, 4, 5]
linked_list.remove(3)
Output: [1, 2, 4]
print(element)
Output: 1 2 3 4
print(3 in array)
Output: True
array.sort()
Output: [1, 2, 3, 4]
print(array[1])
Output: 2
1 c. Explain all operations of Data Structures.
1. Traversal
Description: Traversal operations are used to visit each node in a data structure in a specific order.
Example (Array Traversal):
array = [1, 2, 3, 4]
print(element)
Output: 1 2 3 4
2. Insertion
print(array)
Output: [1, 2, 3, 4]
3. Deletion
array.remove(3) # Removes 3
print(array)
Output: [1, 2, 4]
4. Search
if 3 in array:
print("Found")
else:
print("Not Found")
Output: Found
5. Sorting
print(array)
Output: [1, 2, 3, 4]
2. a. Explain any five string handling functions supported by 'c' with syntax and 10 example.
1. strlen
Description: Determines the length of a string (excluding the null character \0).
Syntax:
size_t strlen(const char *str);
Example:
int main()
{
char str[] = "Hello";
printf("Length of the string: %lu\n", strlen(str));
return 0;
}
Output: 5
2. strcpy
Example:
int main()
{
char src[] = "World";
char dest[20];
strcpy(dest, src);
printf("Copied string: %s\n", dest);
return 0;
}
Output: World
3. strcat
Example:
int main()
4. strcmp
Example:
int main()
5. strrev (Not part of the standard library, but widely available in compilers like Turbo C)
Example:
int main()
Output: olleH
2 b. Convert the following infix expression to postfix expression using stack: A+ (B*C (D/E ^F) G)* H.
1. Wasted Space:
In a linear queue, when elements are dequeued, the freed space at the front of the queue cannot
be reused, even though it is no longer occupied.
2. Queue Overflow:
Even if there is unused space at the front, the queue may still show as full when the rear pointer
reaches the maximum size of the array.
3. Inefficient Memory Utilization:
The fixed size of the array in a linear queue can lead to underutilization of memory when
elements are dequeued.
How Circular Queue Solves These Issues
1. Reusability of Space:
In a circular queue, the rear pointer wraps around to the beginning of the array when it reaches
the end, reusing the freed space at the front.
2. Efficient Memory Utilization:
Circular queues use all available space efficiently by reusing the memory locations freed during
deletions.
3. No Overflow Until Full:
Overflow occurs only when all slots are occupied, regardless of the position of front and rear.
// Function to insert an item into circular queue // Function to delete an element from queue
void insert_rear (int item) void delete_front()
{ {
// Check for overflow of queue // Check for underflow of queue
if (count === Q_SIZE) if (count == 0)
{ {
printf ("Queue Overflow"); printf ("Queue Underflow");
return; return;
} }
// Increment rear by 1 // Delete the item from circular queue
rear = ( rear + 1) % Q_SIZE; printf ("Item deleted :%d", queue[front]);
// Insert an item into the queue // Increment front by 1
queue[ rear] = item; front (front + 1)% Q_SIZE;
// Update count by 1 // Update count by 1
count++; count = count - 1;
} }
3b. Explain in detail about multiple queue with relevant functions in 'C'.
A multiple queue is a data structure where two or more queues are implemented within a single memory array.
These queues may be used to manage multiple independent data streams or processes efficiently within shared
storage.
typedef struct
int front;
int rear;
int start;
int end;
} Queue;
typedef struct
int data[MAX];
Queue queues[2];
} MultiQueue;
Example Program
int main()
MultiQueue mq;
int numQueues = 2;
int segmentSize = MAX / numQueues;
// Queue 0 operations
enqueue(&mq, 0, 10);
enqueue(&mq, 0, 20);
enqueue(&mq, 0, 30);
printf("Queue 0: ");
displayQueue(&mq, 0);
// Queue 1 operations
enqueue(&mq, 1, 100);
enqueue(&mq, 1, 200);
printf("Queue 1: ");
displayQueue(&mq, 1);
// Dequeue operations
printf("Dequeue from Queue 0: %d\n", dequeue(&mq, 0));
printf("Queue 0 after dequeue: ");
displayQueue(&mq, 0);
A Singly Linked List (SLL) is a data structure where each element (node) points to the next node in the
sequence. Each node contains two parts:
1. Insertion
Insert at the beginning
Insert at the end
Insert after a given node
2. Deletion
Delete the first node
Delete the last node
Delete a node at a specific position
3. Traversal
Traverse the list to print all elements
4. Search
Search for an element in the list
5. Reversal
Reverse the list
Types of Linked Lists
Each node contains two pointers: one points to the next node, and the other points
to the previous node.
A variation where the last node points back to the first node, forming a circle.
Insert on element at the front end of SLL Insert on element at the rear end of SLL
NODE insert_front(int item, NODE first) NODE insert_rear(int item, NODE first)
{ {
NODE temp; NODE temp;
temp = getnode(); NODE cur;
temp->info = item; temp = getnode();
temp->link = first; temp->info = item;
return temp; temp->link = NULL;
}
if ( first == NULL ) return temp;
cur = first;
while ( cur->link != NULL )
{
cur = cur->link;
}
cur->link = temp;
return first;
}
Delete a node at the beginning of SLL. Delete a node at the end of SLL.
NODE delete_front(NODE first) NODE delete_rear(NODE first)
{ {
NODE temp; NODE cur, prev;
if (first == NULL)
if ( first == NULL ) {
{ printf(“List is empty cannot delete\n”);
printf("List is empty cannot delete\n"); return first;
return NULL; }
} if ( first ->link == NULL )
temp = first; {
temp = temp->link printf (“The item to deleted is
printf("Item deleted = %d\n",first->info); %d\n”,first->info);
free(first);
return temp; free(first);
} return NULL;
}
prev = NULL;
cur = first;
while( cur->link != NULL )
{
prev = cur;
cur = cur->link;
}
printf(“The item deleted is %d\n”,cur->info);
free(cur);
prev->link = NULL;
return first;
}
4b. Examine a node structure for linked representation of polynomial. Explain algorithm to add two
polynomial represented using linked list.
A polynomial can be represented as a sequence of terms where each term has two parts:
struct Node
int coefficient;
int exponent;
};
The linked list representation of a polynomial is ordered by exponents in decreasing order. For example,
the polynomial 5x^3 + 4x^2 + 3x + 2 will be represented as:
Head -> [5, 3] -> [4, 2] -> [3, 1] -> [2, 0] -> NULL
Here:
The process to add two polynomials involves iterating through both polynomial linked lists, comparing
the exponents, and combining terms with the same exponent. If the exponents are different, the term
with the higher exponent is added to the result list.
Steps:
1. Initialize two pointers: One for each polynomial (poly1 and poly2).
2. Iterate through the lists:
If the exponent of poly1 is greater than poly2, add the term of poly1 to the result list and
move poly1 to the next node.
If the exponent of poly1 is smaller than poly2, add the term of poly2 to the result list and
move poly2 to the next node.
If the exponents are equal, add the coefficients and insert the new term with the same
exponent.
3. Add remaining terms: Once one list is fully traversed, append the remaining terms from the
other list to the result list.
4. Return the resulting polynomial.
head1->next = nextPtr;
return head1;
head2->next = nextPtr;
return head2;
head1->coeff += head2->coeff;
head1->next = nextPtr;
return head1;
}
5a. Summarize Sparse Matrix. For the given sparse matrix, write the diagrammatic linked list
representation.
8 0 0 0
5 0 0 3
0 0 0 0
4 0 0 8
0 0 9 1
To represent the given sparse matrix as a linked list diagrammatically, you need to create a representation
where each non-zero element is stored along with its row and column indices. Here's how you can summarize
it:
Diagram Representation:
[8, 0, 0] -> [5, 1, 0] -> [3, 1, 3] -> [4, 3, 0] -> [8, 3, 3] -> [9, 4, 2] -> [1, 4, 3] -> NULL
This linked list representation is sequentially connected, storing only non-zero elements, saving space
compared to the full matrix. Let me know if you need further elaboration or a drawing for better visualization!
5b. Define Doubly linked list. Write the functions to perform the following operations on doubly linked
list.
Definition: A doubly-linked list is a linear collection of nodes where each node is divided into three parts:
void deleteAtRear()
{
if (head == NULL)
{
printf("List is empty.\n");
return;
}
Node* temp = head;
while (temp->next != NULL)
{
temp = temp->next;
}
if (temp->prev != NULL)
{
temp->prev->next = NULL;
}
else
{
head = NULL;
}
free(temp);
}
A Tree is a hierarchical data structure consisting of nodes, where each node may have a parent and zero or
more children. It is defined as a collection of nodes such that:
1. There is a distinguished node called the root.
2. Every other node is connected by an edge from exactly one parent node.
3. The structure has no cycles, making it a connected, acyclic graph.
Test for equality – check whether two tress are equal or not
// Function to get the exact copy of a tree // Function to get the exact copy of a tree
6 C. Draw a binary tree and find out the binary tree traversals for the following expression
3+4*(7-6)/4+3.
E E
+D3 D3+
++3C3 3C+3+
++3/B43 3B4/+3+
++3/*4A43 34A*4/+3+
++3/*4–7643 3476-*4/+3+
(Prefix Expression) (Postfix Expression)
7 a.. Construct binary search tree for the given set of values 14, 15, 4, 9, 7, 18, 3, 5, 16, 20. Also perform
inroder, preorder and post order traversals of the obtained tree.
The left subtree of a node contains only nodes with values less than the node's value.
The right subtree of a node contains only nodes with values greater than the node's value.
14
/ \
4 15
/ \ \
3 9 18
/ / \
5 16 20
\
7
Implementation Steps
1. Node Structure
Implementation in C
1. Base Case:
o If the current node (root) is NULL, the value is not found.
o If the value matches the current node's value, the node is returned.
2. Recursive Steps:
o If the value is smaller than the current node's value, the search continues in the left subtree.
o If the value is larger, the search continues in the right subtree.
The given graph represents a directed graph. To solve the problem, let's compute both the adjacency matrix
and the adjacency list representation for the graph.
Adjacency Matrix
The adjacency matrix is a 2D matrix where each entry M[i][j] represents whether there is an edge from
vertex ii to vertex jj (1 if there is an edge, 0 otherwise).
Adjacency List
The adjacency list representation associates each vertex with a list of its direct neighbors (vertices it has
outgoing edges to).
Vertices: A,B,C,D,E
Adjacency Matrix:
A B C D E
A [0, 1, 1, 1, 0]
B [0, 0, 0, 0, 1]
C [0, 0, 0, 1, 0]
D [0, 0, 0, 1, 1]
E [0, 0, 0, 0, 0]
Adjacency List:
A: B, C, D
B: E
C: D
D: D, E
E: (no neighbors)
8.b. Explain all methods used for traversing a graph with suitable example and write 'C' function for the
same.
Graph traversal refers to the process of visiting each vertex and edge in a graph. There are two primary methods
to traverse a graph:
Description:
DFS explores as far as possible along each branch before backtracking. It uses a stack (explicitly or via
recursion) to keep track of the vertices to visit.
Steps:
Example:
Graph:
A
/ \
B C
/ \
D E
Description:
BFS explores all the neighbors of a vertex before moving to the next level of vertices. It uses a queue to
keep track of the vertices to visit.
Steps:
Example:
Graph:
A
/ \
B C
/ \
D E
queue[rear++] = startVertex;
visited[startVertex] = 1;
9. a. Differentiate between static hashing and dynamic hashing in detail with operations.
1. Static Hashing
Definition:
In static hashing, the hash table has a fixed size. Once the table is created, the number of buckets remains
constant throughout its usage.
Characteristics:
1. Fixed Table Size: The number of buckets is predefined and does not change even if data grows beyond
capacity.
2. Overflow Handling: Collisions and overflow are managed using techniques like chaining (linked lists)
or overflow buckets.
3. Efficiency: Works well when the size of the data is known and does not change significantly.
4. Memory Usage: Can waste memory if the hash table is underutilized or cause performance degradation
if it's overfull.
5. Collisions: More frequent in static hashing as data grows.
Operations:
1. Insertion:
o Compute the hash value of the key using a hash function.
o Place the key-value pair in the corresponding bucket.
o If the bucket is full, manage overflow using chaining or other methods.
2. Search:
o Compute the hash value of the key.
o Search within the bucket to find the value.
3. Deletion:
o Compute the hash value of the key.
o Remove the key-value pair from the bucket.
Example:
Hash Table with 5 buckets (bucket[0] to bucket[4]).
Hash function: h(key)=key mod 5.
For keys 12, 22, 32:
Insertion: All map to bucket[2] causing overflow, managed via chaining.
2. Dynamic Hashing
Definition:
In dynamic hashing, the size of the hash table grows or shrinks dynamically based on the data. It uses a
directory structure to map keys to buckets.
Characteristics:
1. Variable Table Size: Buckets are split or merged dynamically as data increases or decreases.
2. Collision Reduction: Reduces collisions as new buckets are created when required.
3. Scalable: Ideal for applications where data size is unpredictable.
4. Memory Usage: Optimized as the structure adapts to data requirements.
5. Uses Directory: A directory level manages the mapping of keys to buckets, and it grows as buckets are
split.
Operations:
1. Insertion:
o Compute the hash value of the key using the hash function.
o Place the key-value pair in the corresponding bucket.
o If the bucket overflows, split the bucket and update the directory.
2. Search:
o Compute the hash value of the key.
o Use the directory to find the corresponding bucket and retrieve the value.
3. Deletion:
o Compute the hash value of the key.
o Remove the key-value pair from the bucket.
o Optionally merge buckets if utilization falls below a threshold.
Example:
Initial directory has 2 buckets (size doubles dynamically).
Insert keys: 5, 15, 25 (hash function h(key)=key mod 4).
After splitting due to overflow:
o Directory grows to accommodate more buckets.
o Keys are redistributed based on new hash values.
Comparison Table
Aspect Static Hashing Dynamic Hashing
Dynamic. Adjusts as data grows or
Table Size Fixed. Defined during initialization.
shrinks.
Handled by splitting buckets
Overflow Handling Managed via chaining or overflow buckets.
dynamically.
Can waste memory if underutilized or
Memory Usage Efficient as it adjusts to the data.
degrade performance when overfilled.
Limited. Not suitable for unpredictable or Highly scalable for dynamic or
Scalability
growing datasets. unpredictable datasets.
Collision
Higher, especially as data grows. Lower, as buckets split dynamically.
Frequency
Ease of Complex due to directory management
Simpler to implement and understand.
Implementation and bucket splitting logic.
Suitable for static or small datasets where Suitable for dynamic or large datasets
Use Case
size is predictable. with unpredictable size.
A Double-Ended Priority Queue (DEPQ), also known as a Two-ended Priority Queue, is a specialized data
structure that supports the following operations:
1. Insert: Insert an element into the queue.
2. Delete-Min: Remove and return the smallest element in the queue.
3. Delete-Max: Remove and return the largest element in the queue.
4. Peek-Min: Return the smallest element without removing it.
5. Peek-Max: Return the largest element without removing it.
Key Characteristics:
Two Ends
Efficient Operations
Operations in a Double-Ended Priority Queue (DEPQ)
1. Insert
2. Delete-Min
3. Delete-Max
4. Peek-Min
5. Peek-Max
Implementation Details
A typical implementation of a Double-Ended Priority Queue can use two heaps (min-heap and max-heap).
This structure allows both ends to be efficiently accessed:
1. Min-Heap
2. Max-Heap
Advantages of Double-Ended Priority Queue (DEPQ):
Efficient
Versatile
Applications:
Scheduling Systems
Data Streams
Game Engines
Hashing is a technique used to map data (keys) to fixed-size values, typically integers, called hash values or
hash codes. This mapping is performed by a hash function.
Different Hashing Functions
A hash function takes an input (key) and produces a fixed-size output (hash value). There are different types of
hash functions depending on the requirements, such as:
1. Division Method:
o The simplest and most commonly used method.
o The hash value is computed by taking the modulus of the key with a prime number mm (size of
the table).
Hash function:
h(k)=k mod m
where k is the key, and mm is the size of the table (preferably a prime number).
Example:
o Given a table size of 10, hash function h(k)=k mod 10
o For key 15, the hash value is h(15)=15 mod 10=5
2. Folding Method:
o In this method, the key is divided into several parts, which are then added together to produce
the hash value.
Hash function:
o Split the key into equal-sized parts, then sum the parts and take modulo mm.
Example:
o Given key 123456, split it into parts: 12, 34, 56.
o Sum the parts: 12+34+56=102.
o Take 102 mod 10=2.
o The hash value is 2.
3. Mid Square Method:
o In this method, the key is squared, and the middle digits of the result are extracted as the hash
value.
Hash function:
h(k)=middle digits of k2
Example:
o For key 23, square it: 232=529.
o Extract the middle digit(s): 5.
o The hash value is 5.
10 a. What is collision? Explain the method to resolve collision with suitable algorithm of linear probing.
Insert keys 72, 27, 36, 24, 63, 81, 92, 101 into % [size 10].
A collision in hashing occurs when two or more keys map to the same index in a hash table. Since each index
in a hash table can only store one element, when two keys hash to the same index, a collision happens. This can
occur when the hash function produces the same hash value for different keys.
1. Chaining: Store multiple elements at the same hash index using a linked list or another dynamic data
structure.
2. Open Addressing: All elements are stored within the hash table itself. When a collision occurs, the
algorithm searches for the next available slot based on a specific probing technique. Some common
open addressing methods include:
o Linear Probing
o Quadratic Probing
o Double Hashing
Linear Probing for Collision Resolution
In Linear Probing, when a collision occurs at a given index, the algorithm checks the next index (i.e., index +
1) in the hash table. If that index is occupied, it checks the next one (index + 2), and so on, until an empty slot is
found. This technique is simple but suffers from primary clustering, where groups of consecutive occupied
slots form, leading to performance degradation.
h(72)=72%10=2
h(27)=27%10=7
h(36)=36%10=6
h(24)=24%10=4
h(63)=63%10=3
h(81)=81%10=1
h(92)=92%10=2
Slot 2 is occupied (by 72), so use linear probing. Check the next index (index 3):
h(101)=101%10=1
Slot 1 is occupied (by 81), so use linear probing. Check the next index (index 2):
After inserting all the keys using linear probing, the hash table looks like this:
Index Value
0 Empty
1 81
2 72
3 63
4 24
5 92
6 36
7 27
8 101
9 Empty
10 b. Construct an optimal binary search tree for the following keys with the probabilities as
Keys A B C D E
Probability 0.25 0.2 0.05 0.2 0.3
To construct an Optimal Binary Search Tree (OBST) for the given keys and probabilities, we will use the
dynamic programming approach.
Given:
Keys: A, B, C, D, E
Probabilities:
o P(A) = 0.25
o P(B) = 0.20
o P(C) = 0.05
o P(D) = 0.20
o P(E) = 0.30
Objective:
We want to find an optimal binary search tree such that the expected search cost is minimized.
Steps:
1. Define Variables:
2. Initialize the Probability Table: For each pair of keys i and j, calculate W[i][j], the sum of
probabilities for keys i to j:
W[i][j]=P[i]+P[i+1]+⋯+P[j]]
3. Initialize the Cost Table: For a single key (when i=j), the cost is simply the probability of the key:
C[i][i]=P[i]
The table of costs for individual keys:
i/j 1 (A) 2 (B) 3 (C) 4 (D) 5 (E)
1 (A) 0.25
2 (B) 0.20
3 (C) 0.05
4 (D) 0.20
5 (E) 0.30
4. Fill the Table Using Dynamic Programming: We now fill the table for C[i][j], the minimum cost for
constructing a binary search tree from keys i to j, using the recurrence relation:
Now we continue this for larger ranges (for example, C[1][3],C[2][4], etc.), and continue applying the
recurrence relation.
Finally, the optimal binary search tree will be built by choosing the root that minimizes the cost at each stage.