307 Shyam
307 Shyam
Problem Statement
Identify two applications for each of the following approaches and implement any one of the applications for
each of the approaches.
Approach Application
Pre-order traversal 1. Expression Parsing:
of tree data
structure
2. Binary Expression Trees
In-order traversal of 1. Validation of Binary Search Tree
tree data structure
2. Expression Evaluation
Post-order traversal 1. Memory Management (Garbage Collection)
of tree data
structure
2. Print Hierarchical Structure
2. Symbol Tables
Pre-order traversal of a tree data structure is a specific approach used in some of these
applications due to its particular advantages. One prominent application where pre-order traversal
is beneficial is expression evaluation.
How It Works:
*Build Expression Tree: First, we build an expression tree using the input infix expression. This
expression tree represents the expression's structure and follows operator precedence rules.
*Pre-order Traversal: Next, we traverse the expression tree using pre-order traversal, which means
visiting the root node first, then the left subtree, and finally the right subtree.
*Evaluation of Nodes: While performing pre-order traversal, when we visit an operator node, we
apply the corresponding operation to its left and right children (operands). The result of this
operation becomes the new value for the current node.
*Recursive Evaluation: The process continues recursively until we traverse the entire expression
tree, effectively evaluating the entire expression.
Code
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
struct TreeNode {
char data;
};
newNode->data = data;
return newNode;
if (!isOperator(currentChar)) {
// If the current character is an operand, create a node and push it to the stack
stack[++top] = newNode;
} else {
// If the current character is an operator, pop two nodes from the stack and make them its
children
newNode->right = stack[top--];
newNode->left = stack[top--];
stack[++top] = newNode;
// The final element in the stack will be the root of the expression tree
return stack[top];
if (root) {
preOrderTraversal(root->left);
preOrderTraversal(root->right);
int main() {
preOrderTraversal(root);
return 0;
Sample Input:
Enter a postfix expression: ab+cd-*
Sample Output:
Expression Tree (Pre-order): * + a b - c d
Note: Replicate the table for 7 more times (for each application- 1 table)
“You are given a binary tree represented as a set of linked nodes. Your task is to determine
whether the given binary tree is a Binary Search Tree (BST).”
Validation of a Binary Search Tree (BST) using in-order traversal is a common application of in-
order traversal in binary tree problems. The in-order traversal approach is particularly well-suited
for this task due to its properties related to BSTs.
Why In-order Traversal?: In-order traversal visits the nodes of a binary tree in ascending order
when applied to a valid BST. This property makes it ideal for validating whether a given binary tree
is a valid BST or not.
How It Works:
*In-order Traversal: In-order traversal is performed on the given binary tree. This process visits
each node in ascending order when applied to a valid BST.
*Check Order: During the in-order traversal, compare each node's value with the previous node's
value. If all the nodes are in ascending order, the binary tree is a valid BST. If any node violates the
ascending order, the tree is not a valid BST.
*Ordered Visit: In-order traversal ensures that nodes are visited in ascending order, which is
essential for validating the BST property. It allows us to check whether the binary tree follows the
rules of a BST.
Code
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
if (data == -1)
return NULL;
return newNode;
}
// Check if the current node's data is greater than the previous node's data
if (root->data <= *prev)
return false;
int main() {
// Construct the binary tree from user input
struct TreeNode* root = constructBinaryTree();
return 0;
}
Sample Input:
Enter the node data (Enter -1 for no node): 5
Enter the left subtree of 5:
Enter the node data (Enter -1 for no node): 3
Enter the left subtree of 3:
Enter the node data (Enter -1 for no node): 1
Enter the left subtree of 1:
Enter the node data (Enter -1 for no node): -1
Enter the right subtree of 1:
Enter the node data (Enter -1 for no node): -1
Enter the right subtree of 3:
Enter the node data (Enter -1 for no node): 4
Enter the left subtree of 4:
Enter the node data (Enter -1 for no node): -1
Enter the right subtree of 4:
Enter the node data (Enter -1 for no node): -1
Enter the right subtree of 5:
Enter the node data (Enter -1 for no node): 7
Enter the left subtree of 7:
Enter the node data (Enter -1 for no node): -1
Enter the right subtree of 7:
Enter the node data (Enter -1 for no node): 9
Enter the left subtree of 9:
Enter the node data (Enter -1 for no node): -1
Enter the right subtree of 9:
Enter the node data (Enter -1 for no node): -1
Sample Output:
The binary tree is a Binary Search Tree (BST).
You are required to implement a C program that allows the user to construct a binary tree by
entering the node values. The program will then perform garbage collection using post-order
traversal to mark and deallocate memory for unreachable nodes.
Why Post-order Traversal? Post-order traversal is a natural fit for garbage collection in tree-based
data structures because it ensures that child nodes are processed before their parent nodes. In the
context of memory management, post-order traversal helps identify and deallocate memory for
objects that are no longer reachable, ensuring efficient and safe memory usage.
How It Works:
*Tree Representation: The dynamic memory allocation for objects in many applications is often
represented using tree-like data structures (e.g., linked data structures, graphs, etc.).
*Memory Allocation and Deallocation: As the application creates objects during runtime, memory
is allocated dynamically. However, when objects are no longer needed or accessible, deallocating
their memory becomes crucial to avoid memory leaks and efficient memory management.
*Garbage Collection Process: Garbage collection involves identifying and reclaiming memory
occupied by objects that are no longer reachable or in use by the application.
*Marking Phase: During post-order traversal, the garbage collector traverses the tree, starting
from the leaves (bottom) and marking objects that are still reachable from the root as "live" or
"marked." Objects that are not marked are considered "unreachable" or "garbage."
*Sweep Phase: After the marking phase, the garbage collector goes through the entire memory
space, deallocating memory for the unmarked (unreachable) objects. This process involves
reclaiming memory and making it available for future dynamic memory allocations.
*Correctness: Post-order traversal ensures that child nodes are processed before parent nodes.
This property is essential for garbage collection because it helps identify all unreachable objects
before deallocating their memory.
*Efficiency: The post-order traversal algorithm allows the garbage collector to identify and free
unreachable objects in a bottom-up manner, minimizing the risk of dangling pointers and potential
segmentation faults.
Code
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
if (data == -1)
return NULL;
return newNode;
}
markUnreachableNodes(root->left);
markUnreachableNodes(root->right);
if (root->marked) {
if (root->left != NULL)
root->left->marked = true;
if (root->right != NULL)
root->right->marked = true;
}
}
deallocateMemory(root->left);
deallocateMemory(root->right);
if (!root->marked) {
free(root);
}
}
int main() {
// Construct the binary tree from user input
struct TreeNode* root = constructBinaryTree();
return 0;
}
Sample Input:
Enter the node data (Enter -1 for no node): 5
Enter the left subtree of 5:
Enter the node data (Enter -1 for no node): 3
Enter the left subtree of 3:
Enter the node data (Enter -1 for no node): 1
Enter the left subtree of 1:
Enter the node data (Enter -1 for no node): -1
Enter the right subtree of 1:
Enter the node data (Enter -1 for no node): -1
Enter the right subtree of 3:
Enter the node data (Enter -1 for no node): 4
Enter the left subtree of 4:
Enter the node data (Enter -1 for no node): -1
Enter the right subtree of 4:
Enter the node data (Enter -1 for no node): -1
Enter the right subtree of 5:
*Task Dependency Management: Topological sorting allows project managers to visualize and
manage task dependencies efficiently. It provides a clear understanding of which tasks can start
first and which tasks must wait for their prerequisites to be completed.
*Resource Allocation: With the task order determined by topological sorting, managers can
allocate resources effectively, knowing that tasks with dependencies will be executed in the
correct order.
Code
#include <stdio.h>
#include <stdlib.h>
topologicalSort(graph);
return 0;
}
Sample Input:
Enter the number of vertices in the graph: 6
Enter the number of edges: 6
Enter the edges (src dest) separated by space:
52
50
40
41
23
31
Sample Output:
Topological Sort: 5 4 2 3 1 0
*Navigation and Route Planning: BFS is extensively used in GPS systems and map applications to
find the shortest route between two locations. It helps in determining the quickest path for
reaching a destination while considering various roads or streets.
*Network Routing: BFS plays a crucial role in computer networks to find the shortest path for data
packets from the source to the destination. It helps in efficient routing and minimizes the time
taken for data transmission.
*Social Networking: BFS is used in social networks to determine the shortest path or the minimum
number of connections between two users. It aids in identifying mutual friends, social circles, and
influencers.
Code
#include <stdio.h>
#include <stdlib.h>
newNode = createNode(src);
newNode->next = graph->adjacencyList[dest];
graph->adjacencyList[dest] = newNode;
}
// Function to perform BFS and find the shortest path from 'start' to 'end'
int shortestPathBFS(Graph* graph, int start, int end) {
int visited[MAX_VERTICES] = {0};
int distance[MAX_VERTICES];
for (int i = 0; i < MAX_VERTICES; i++)
distance[i] = INFINITY;
visited[start] = 1;
distance[start] = 0;
return distance[end];
}
if (shortestDistance == INFINITY)
printf("No path found from %d to %d.\n", start, end);
else
printf("Shortest distance from %d to %d is: %d\n", start, end, shortestDistance);
return 0;
}
Sample Input:
Enter the number of vertices in the graph: 6
Enter the number of edges: 7
Enter the edges (src dest) separated by space:
01
02
13
14
24
25
35
Enter the source and destination vertices for shortest path: 0 5
Sample Output:
Shortest distance from 0 to 5 is: 2
*Fast Lookup: Linear probing provides relatively fast lookup time for searching words in the hash
table. As the hash table uses an array, the search time for each word is generally O(1) on average if
the load factor is kept low and collisions are handled efficiently.
*Memory Efficiency: Hash tables with linear probing require less memory compared to other data
structures like trie-based spell checkers. This is because the hash table only needs to store the
words and their hash values, whereas trie-based structures need to store individual characters at
each node, leading to higher memory overhead.
*Collision Handling: Linear probing handles collisions by placing the next word in the next available
slot, thereby ensuring that all words are eventually stored in the hash table. This approach avoids
the need for complex data structures to handle collisions.
Code
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
hashTable[index] = newNode;
}
int main() {
// Input dictionary of correct words
int numWords;
printf("Enter the number of words in the dictionary: ");
scanf("%d", &numWords);
// Spell checking
char word[WORD_LENGTH];
printf("Enter a word to spell check (enter 'q' to quit):\n");
while (1) {
scanf("%s", word);
if (strcmp(word, "q") == 0) {
break;
}
if (searchWord(word)) {
printf("%s is spelled correctly.\n", word);
} else {
printf("%s is misspelled.\n", word);
}
}
return 0;
}
Sample Input:
Enter the number of words in the dictionary: 5
Enter 5 words (one word per line):
apple
banana
cat
elephant
jaguar
Enter a word to spell check (enter 'q' to quit):
banana
horse
jaguar
zebra
q
Sample Output:
banana is spelled correctly.
horse is misspelled.
zebra is misspelled.
Here are the reasons why the quadratic probing approach is suitable for this application:
*Hash Table for Fast Word Lookup: The Spell Checkers application requires quick word lookup to
verify if a given word exists in the dictionary. Hash tables offer constant-time average lookup
complexity (O(1)) for search operations, making them an efficient choice for handling large
dictionaries.
*Handling Collisions: In real-world scenarios, words with different hash values can sometimes
collide, meaning they are assigned the same index in the hash table. Quadratic probing is a
collision resolution technique that effectively addresses this issue. It enables finding an alternative
position within the hash table to store a collided word, minimizing clustering and improving the
overall performance of the spell checker.
*Space Efficiency: Quadratic probing, unlike other collision resolution methods like chaining, does
not require additional data structures to store collided elements. This makes the spell checker
more memory-efficient, which is essential when handling large dictionaries.
Code
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Maximum length for a word in the dictionary and for spell checking
#define WORD_LENGTH 100
// Function to calculate the hash value for a given word using quadratic probing
unsigned int hashFunction(const char* word, int i) {
// ... (same as before) ...
}
// Function to insert a word into the hash table using quadratic probing
void insertWord(const char* word) {
// ... (same as before) ...
}
int main() {
// Input dictionary of correct words
int numWords;
printf("Enter the number of words in the dictionary: ");
scanf("%d", &numWords);
// Spell checking
char word[WORD_LENGTH];
printf("Enter a word to spell check (enter 'q' to quit):\n");
while (1) {
scanf("%s", word);
if (strcmp(word, "q") == 0) {
break;
}
if (searchWord(word)) {
printf("%s is spelled correctly.\n", word);
} else {
printf("%s is misspelled.\n", word);
}
}
return 0;
}
Sample Input:
jaguar is misspelled.
*Caches serve as a fast-access storage layer between the CPU and main memory, storing a subset
of data from a larger dataset.
*The choice of cache replacement policy is crucial for efficient cache utilization and minimizing
cache misses.
*Double hashing is a collision resolution technique used to implement caches with a large number
of cache entries.
*Double hashing involves using two hash functions to determine the position of an item in the
cache.
*The use of two hash functions helps distribute items evenly across the cache, reducing collisions
and improving cache performance.
*The double hashing technique, combined with a cache replacement policy like LRU (Least
Recently Used), ensures efficient cache utilization and fast access to frequently used data.
*Double hashing minimizes the collision rate, reducing the likelihood of cache evictions and cache
misses.
Code
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
CacheNode* cache[CACHE_SIZE];
CacheNode* head = NULL;
CacheNode* tail = NULL;
node->prev->next = node->next;
if (node->next != NULL) {
node->next->prev = node->prev;
}
node->next = head;
node->prev = NULL;
head->prev = node;
head = node;
}
return node->value;
}
node = node->next;
}
int main() {
// Input cache items
int numItems;
printf("Enter the number of items in the cache: ");
scanf("%d", &numItems);
// Spell checking
char word[KEY_LENGTH];
printf("Enter a word to spell check (enter 'q' to quit):\n");
while (1) {
scanf("%s", word);
if (strcmp(word, "q") == 0) {
break;
}
return 0;
}
Sample Input:
Enter the number of items in the cache: 3
Enter 3 cache items (key value pairs):
key1 value1
key2 value2
key3 value3
Enter a word to spell check (enter 'q' to quit):
key2
key3
key4
q
Sample Output:
key2 is spelled correctly. Value: value2
key4 is misspelled.