0% found this document useful (0 votes)
7 views

Data Structure Through C (Bcac302)

Uploaded by

Sangha Modak
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
7 views

Data Structure Through C (Bcac302)

Uploaded by

Sangha Modak
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 56

#### Module 1: Structure and Union

Certainly! Let's go through each of the topics with detailed


explanations and examples.

### 1. Explain the concept of structures in C/C++ with an example.

**Concept:**
A structure in C/C++ is a user-defined data type that allows the
grouping of variables of different types under a single name.
Structures help in organizing complex data in a more
understandable and manageable way.

**Example:**
```c
#include <stdio.h>

// Define a structure named 'Person'


struct Person {
char name[50];
int age;
float height;
};

int main() {
// Declare a variable of type 'Person'
struct Person person1;

// Assign values to the members of 'person1'


strcpy(person1.name, "John Doe");
person1.age = 30;
person1.height = 5.9;

// Print the values of the members of 'person1'


printf("Name: %s\n", person1.name);
printf("Age: %d\n", person1.age);
printf("Height: %.1f\n", person1.height);
return 0;
}
```

### 2. Differentiate between structure and union.

**Structure:**
- Each member has its own memory location.
- The size of the structure is at least the sum of the sizes of its
members.
- Members can be accessed independently.

**Union:**
- All members share the same memory location.
- The size of the union is equal to the size of its largest member.
- Only one member can be accessed at a time.

**Example of Structure vs. Union:**


```c
#include <stdio.h>

struct MyStruct {
int integer;
float floating;
char character;
};

union MyUnion {
int integer;
float floating;
char character;
};

int main() {
struct MyStruct s;
union MyUnion u;

// Assigning values to structure members


s.integer = 5;
s.floating = 5.5;
s.character = 'A';

// Assigning values to union members


u.integer = 5;

printf("Structure:\n");
printf("Integer: %d\n", s.integer);
printf("Float: %.1f\n", s.floating);
printf("Character: %c\n", s.character);

printf("\nUnion:\n");
printf("Integer: %d\n", u.integer);

// Now assign a float to the union


u.floating = 5.5;
printf("Float: %.1f\n", u.floating);

// Now assign a character to the union


u.character = 'A';
printf("Character: %c\n", u.character);

return 0;
}
```

### 3. How do you access members of a structure using pointers?

**Concept:**
To access members of a structure using a pointer, use the arrow
operator (`->`).

**Example:**
```c
#include <stdio.h>

// Define a structure
struct Person {
char name[50];
int age;
};

int main() {
// Declare and initialize a variable of type 'Person'
struct Person person1 = {"Alice", 28};

// Declare a pointer to 'Person' and assign the address of 'person1'


to it
struct Person *ptr = &person1;

// Access and modify members using the pointer


printf("Name: %s\n", ptr->name);
printf("Age: %d\n", ptr->age);

// Modify the age using the pointer


ptr->age = 29;
printf("Updated Age: %d\n", ptr->age);

return 0;
}
```

### 4. Discuss the concept of nested structures with an example.

**Concept:**
A nested structure is a structure that contains another structure as
a member.

**Example:**
```c
#include <stdio.h>

// Define a structure for Address


struct Address {
char city[50];
int zip;
};

// Define a structure for Person that contains an Address structure


struct Person {
char name[50];
int age;
struct Address address;
};

int main() {
// Declare and initialize a variable of type 'Person'
struct Person person1 = {"Bob", 32, {"New York", 10001}};

// Access members of the nested structure


printf("Name: %s\n", person1.name);
printf("Age: %d\n", person1.age);
printf("City: %s\n", person1.address.city);
printf("Zip: %d\n", person1.address.zip);

return 0;
}
```

### 5. How do you pass structures to functions?

**Concept:**
Structures can be passed to functions by value or by reference
(using pointers).

**Passing by Value Example:**


```c
#include <stdio.h>

// Define a structure
struct Point {
int x;
int y;
};

// Function to print point details


void printPoint(struct Point p) {
printf("Point: (%d, %d)\n", p.x, p.y);
}

int main() {
struct Point p1 = {10, 20};
printPoint(p1); // Pass by value
return 0;
}
```

**Passing by Reference Example:**


```c
#include <stdio.h>

// Define a structure
struct Point {
int x;
int y;
};

// Function to modify point details


void modifyPoint(struct Point *p) {
p->x = 30;
p->y = 40;
}

int main() {
struct Point p1 = {10, 20};
modifyPoint(&p1); // Pass by reference
printf("Modified Point: (%d, %d)\n", p1.x, p1.y);
return 0;
}
```
These explanations and examples cover the key concepts related to
structures, unions, pointers to structures, nested structures, and
passing structures to functions.

#### Module 2: Arrays, Sparse Matrices, Stack, Queue

Sure! Let's dive into each topic with detailed explanations,


implementations, and examples.

### 1. Explain how you would implement a stack using arrays.

**Concept:**
A stack is a linear data structure that follows the Last In First Out
(LIFO) principle. It can be implemented using arrays.

**Implementation:**
```c
#include <stdio.h>
#include <stdlib.h>

#define MAX 100 // Maximum size of the stack

// Stack structure
struct Stack {
int arr[MAX];
int top;
};

// Function to initialize the stack


void initialize(struct Stack *stack) {
stack->top = -1;
}

// Function to check if the stack is empty


int isEmpty(struct Stack *stack) {
return stack->top == -1;
}
// Function to check if the stack is full
int isFull(struct Stack *stack) {
return stack->top == MAX - 1;
}

// Function to push an element onto the stack


void push(struct Stack *stack, int value) {
if (isFull(stack)) {
printf("Stack overflow\n");
return;
}
stack->arr[++stack->top] = value;
}

// Function to pop an element from the stack


int pop(struct Stack *stack) {
if (isEmpty(stack)) {
printf("Stack underflow\n");
return -1;
}
return stack->arr[stack->top--];
}

// Function to peek the top element of the stack


int peek(struct Stack *stack) {
if (isEmpty(stack)) {
printf("Stack is empty\n");
return -1;
}
return stack->arr[stack->top];
}

// Main function to demonstrate stack operations


int main() {
struct Stack stack;
initialize(&stack);

push(&stack, 10);
push(&stack, 20);
push(&stack, 30);

printf("Top element is %d\n", peek(&stack));

printf("Elements: \n");
while (!isEmpty(&stack)) {
printf("%d\n", pop(&stack));
}

return 0;
}
```

### 2. Compare and contrast infix, postfix, and prefix notations with
examples.

**Infix Notation:**
- Operators are placed between operands.
- Common in arithmetic expressions.
- Requires parentheses for precedence.
- Example: \(A + B\)

**Postfix Notation (Reverse Polish Notation):**


- Operators follow their operands.
- No need for parentheses as the order of operations is
unambiguous.
- Easier for computers to evaluate.
- Example: \(AB+\)

**Prefix Notation (Polish Notation):**


- Operators precede their operands.
- No need for parentheses as the order of operations is
unambiguous.
- Less common but useful in certain computational contexts.
- Example: \(+AB\)

**Examples and Comparisons:**


- Infix: \((A + B) \times C\)
- Postfix: \(AB+C\times\)
- Prefix: \(\times +ABC\)

### 3. Discuss the concept of a priority queue. Provide an example


scenario where it would be useful.

**Concept:**
A priority queue is an abstract data type similar to a regular queue
or stack, but where each element has a priority associated with it.
Elements are dequeued in order of their priority.

**Example Implementation:**
```c
#include <stdio.h>
#include <stdlib.h>

#define MAX 100

// Structure for priority queue


struct PriorityQueue {
int data[MAX];
int priority[MAX];
int size;
};

// Function to initialize the priority queue


void initialize(struct PriorityQueue *pq) {
pq->size = 0;
}

// Function to insert an element with a priority


void enqueue(struct PriorityQueue *pq, int value, int priority) {
if (pq->size == MAX) {
printf("Priority Queue is full\n");
return;
}
pq->data[pq->size] = value;
pq->priority[pq->size] = priority;
pq->size++;
}

// Function to get the element with the highest priority


int dequeue(struct PriorityQueue *pq) {
if (pq->size == 0) {
printf("Priority Queue is empty\n");
return -1;
}
int highestPriority = pq->priority[0];
int index = 0;
for (int i = 1; i < pq->size; i++) {
if (pq->priority[i] > highestPriority) {
highestPriority = pq->priority[i];
index = i;
}
}
int value = pq->data[index];
for (int i = index; i < pq->size - 1; i++) {
pq->data[i] = pq->data[i + 1];
pq->priority[i] = pq->priority[i + 1];
}
pq->size--;
return value;
}

// Main function to demonstrate priority queue operations


int main() {
struct PriorityQueue pq;
initialize(&pq);

enqueue(&pq, 10, 2);


enqueue(&pq, 20, 3);
enqueue(&pq, 30, 1);

printf("Dequeued element: %d\n", dequeue(&pq)); // Should return 20


enqueue(&pq, 40, 4);

printf("Dequeued element: %d\n", dequeue(&pq)); // Should return 40

return 0;
}
```

**Example Scenario:**
A priority queue can be useful in a scenario like a hospital's
emergency room, where patients are treated based on the severity
of their condition rather than their arrival time. High-priority
patients (with more severe conditions) are treated before low-
priority patients.

These explanations, implementations, and examples cover the key


concepts related to stack implementation, notations (infix, postfix,
prefix), and priority queues.

#### Module 3: Linked Lists

Certainly! Let's go through each topic with detailed explanations


and code snippets where applicable.

### 1. Implement insertion and deletion operations in a doubly


linked list.

**Concept:**
A doubly linked list is a type of linked list in which each node
contains a data part and two pointers, one pointing to the next node
and one pointing to the previous node.

**Insertion Operation:**
```c
#include <stdio.h>
#include <stdlib.h>

// Definition of a node in the doubly linked list


struct Node {
int data;
struct Node *prev;
struct Node *next;
};

// Function to create a new node


struct Node* createNode(int data) {
struct Node *newNode = (struct Node*)malloc(sizeof(struct Node));
newNode->data = data;
newNode->prev = NULL;
newNode->next = NULL;
return newNode;
}

// Function to insert a node at the front of the list


void insertAtFront(struct Node **head, int data) {
struct Node *newNode = createNode(data);
if (*head == NULL) {
*head = newNode;
return;
}
newNode->next = *head;
(*head)->prev = newNode;
*head = newNode;
}

// Function to insert a node at the end of the list


void insertAtEnd(struct Node **head, int data) {
struct Node *newNode = createNode(data);
if (*head == NULL) {
*head = newNode;
return;
}
struct Node *temp = *head;
while (temp->next != NULL) {
temp = temp->next;
}
temp->next = newNode;
newNode->prev = temp;
}

// Function to display the list


void displayList(struct Node *node) {
while (node != NULL) {
printf("%d ", node->data);
node = node->next;
}
printf("\n");
}

// Main function to demonstrate insertion


int main() {
struct Node *head = NULL;

insertAtEnd(&head, 10);
insertAtEnd(&head, 20);
insertAtFront(&head, 5);

printf("Doubly Linked List: ");


displayList(head);

return 0;
}
```

**Deletion Operation:**
```c
#include <stdio.h>
#include <stdlib.h>

// Function to delete a node from the front of the list


void deleteFromFront(struct Node **head) {
if (*head == NULL) {
printf("List is empty\n");
return;
}
struct Node *temp = *head;
*head = (*head)->next;
if (*head != NULL) {
(*head)->prev = NULL;
}
free(temp);
}

// Function to delete a node from the end of the list


void deleteFromEnd(struct Node **head) {
if (*head == NULL) {
printf("List is empty\n");
return;
}
struct Node *temp = *head;
while (temp->next != NULL) {
temp = temp->next;
}
if (temp->prev != NULL) {
temp->prev->next = NULL;
} else {
*head = NULL;
}
free(temp);
}

// Main function to demonstrate deletion


int main() {
struct Node *head = NULL;

insertAtEnd(&head, 10);
insertAtEnd(&head, 20);
insertAtFront(&head, 5);

printf("Doubly Linked List before deletion: ");


displayList(head);
deleteFromFront(&head);
printf("Doubly Linked List after deleting from front: ");
displayList(head);

deleteFromEnd(&head);
printf("Doubly Linked List after deleting from end: ");
displayList(head);

return 0;
}
```

### 2. Discuss the advantages of using circular linked lists over


linear linked lists.

**Concept:**
A circular linked list is a type of linked list where the last node
points back to the first node, creating a circular structure. This can
be applied to both singly and doubly linked lists.

**Advantages:**
1. **Efficient Traversal:** In circular linked lists, it is easy to
traverse the entire list starting from any node. You can start from
any node and reach back to the same node, making it useful for
applications that require continuous looping through the list.
2. **No NULL Handling:** Since the list is circular, there is no need
to handle `NULL` pointers, which can simplify the implementation.
3. **Efficient Insertion and Deletion:** Inserting and deleting nodes
at the beginning, end, or between nodes can be more efficient since
you don’t need to handle the special case of `NULL` pointers for the
last node.
4. **Applications:** Circular linked lists are useful in implementing
data structures like round-robin schedulers, circular queues, and in
applications where the list needs to be accessed in a cyclic manner.

### 3. Explain how skip lists are implemented and their advantages
in search operations.
**Concept:**
Skip lists are a data structure that allows fast search, insertion, and
deletion operations. They use a hierarchy of linked lists, where
each higher-level list skips over several elements, allowing faster
traversal.

**Implementation:**
```c
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

#define MAX_LEVEL 4

// Node structure for skip list


struct Node {
int data;
struct Node *forward[MAX_LEVEL + 1];
};

// Skip list structure


struct SkipList {
struct Node *header;
int level;
};

// Function to create a new node


struct Node* createNode(int level, int data) {
struct Node *newNode = (struct Node*)malloc(sizeof(struct Node));
newNode->data = data;
for (int i = 0; i <= level; i++) {
newNode->forward[i] = NULL;
}
return newNode;
}

// Function to create a skip list


struct SkipList* createSkipList() {
struct SkipList *skipList = (struct SkipList*)malloc(sizeof(struct
SkipList));
skipList->header = createNode(MAX_LEVEL, INT_MIN);
skipList->level = 0;
return skipList;
}

// Function to generate a random level for node


int randomLevel() {
int level = 0;
while (rand() % 2 && level < MAX_LEVEL) {
level++;
}
return level;
}

// Function to insert a node into skip list


void insertNode(struct SkipList *skipList, int data) {
struct Node *update[MAX_LEVEL + 1];
struct Node *current = skipList->header;

for (int i = skipList->level; i >= 0; i--) {


while (current->forward[i] != NULL && current->forward[i]->data <
data) {
current = current->forward[i];
}
update[i] = current;
}

current = current->forward[0];

if (current == NULL || current->data != data) {


int newLevel = randomLevel();
if (newLevel > skipList->level) {
for (int i = skipList->level + 1; i <= newLevel; i++) {
update[i] = skipList->header;
}
skipList->level = newLevel;
}
struct Node *newNode = createNode(newLevel, data);
for (int i = 0; i <= newLevel; i++) {
newNode->forward[i] = update[i]->forward[i];
update[i]->forward[i] = newNode;
}
}
}

// Function to search for a node in skip list


struct Node* searchNode(struct SkipList *skipList, int data) {
struct Node *current = skipList->header;
for (int i = skipList->level; i >= 0; i--) {
while (current->forward[i] != NULL && current->forward[i]->data <
data) {
current = current->forward[i];
}
}
current = current->forward[0];
if (current != NULL && current->data == data) {
return current;
} else {
return NULL;
}
}

// Main function to demonstrate skip list operations


int main() {
struct SkipList *skipList = createSkipList();

insertNode(skipList, 3);
insertNode(skipList, 6);
insertNode(skipList, 7);
insertNode(skipList, 9);
insertNode(skipList, 12);
insertNode(skipList, 19);

struct Node *result = searchNode(skipList, 7);


if (result != NULL) {
printf("Found: %d\n", result->data);
} else {
printf("Not found\n");
}

return 0;
}
```

**Advantages:**
1. **Fast Search:** Skip lists provide \(O(\log n)\) average time
complexity for search, insertion, and deletion operations due to
multiple levels of linked lists that skip over elements.
2. **Simplicity:** Skip lists are relatively simple to implement
compared to balanced trees like AVL or Red-Black trees.
3. **Flexibility:** Skip lists are dynamic and can easily adapt to
changes (insertions and deletions) without the need for complex
balancing operations.

These explanations and code snippets cover the key concepts


related to doubly linked lists (insertion and deletion), circular linked
lists, and skip lists, including their advantages and implementation
details.

#### Module 4: Recursion

Sure! Let's go through each topic with detailed explanations, code


examples, and complexity analysis.

### 1. Write a recursive function to calculate the factorial of a


number.

**Concept:**
The factorial of a non-negative integer \(n\) is the product of all
positive integers less than or equal to \(n\). It is denoted as \(n!\).
The recursive definition of factorial is:
- \(0! = 1\)
- \(n! = n \times (n-1)!\) for \(n > 0\)

**Recursive Function:**
```c
#include <stdio.h>

// Recursive function to calculate factorial


int factorial(int n) {
if (n == 0) {
return 1; // Base case: 0! = 1
}
return n * factorial(n - 1); // Recursive call
}

int main() {
int number = 5;
printf("Factorial of %d is %d\n", number, factorial(number));
return 0;
}
```

**Complexity Analysis:**
- **Time Complexity:** \(O(n)\) - The function makes \(n\) recursive
calls.
- **Space Complexity:** \(O(n)\) - The function uses \(n\) stack
frames due to recursion.

### 2. Explain the recursive approach to solving the Tower of Hanoi


problem.

**Concept:**
The Tower of Hanoi is a classic problem involving three rods and \
(n\) disks of different sizes. The goal is to move all the disks from
the source rod to the destination rod, following these rules:
1. Only one disk can be moved at a time.
2. A disk can only be placed on top of a larger disk.
3. All disks must be moved using an auxiliary rod.
**Recursive Approach:**
1. Move \(n-1\) disks from the source rod to the auxiliary rod.
2. Move the \(n\)th disk from the source rod to the destination rod.
3. Move \(n-1\) disks from the auxiliary rod to the destination rod.

**Recursive Function:**
```c
#include <stdio.h>

// Recursive function to solve Tower of Hanoi


void towerOfHanoi(int n, char source, char destination, char
auxiliary) {
if (n == 1) {
printf("Move disk 1 from %c to %c\n", source, destination);
return;
}
towerOfHanoi(n - 1, source, auxiliary, destination);
printf("Move disk %d from %c to %c\n", n, source, destination);
towerOfHanoi(n - 1, auxiliary, destination, source);
}

int main() {
int n = 3; // Number of disks
towerOfHanoi(n, 'A', 'C', 'B'); // A = source, B = auxiliary, C =
destination
return 0;
}
```

**Complexity Analysis:**
- **Time Complexity:** \(O(2^n)\) - The function makes \(2^n - 1\)
moves.
- **Space Complexity:** \(O(n)\) - The function uses \(n\) stack
frames due to recursion.

### 3. Discuss the efficiency of recursion compared to iterative


solutions for calculating Fibonacci numbers.
**Concept:**
The Fibonacci sequence is defined as:
- \(F(0) = 0\)
- \(F(1) = 1\)
- \(F(n) = F(n-1) + F(n-2)\) for \(n > 1\)

**Recursive Function:**
```c
#include <stdio.h>

// Recursive function to calculate Fibonacci number


int fibonacci(int n) {
if (n == 0) {
return 0; // Base case
}
if (n == 1) {
return 1; // Base case
}
return fibonacci(n - 1) + fibonacci(n - 2); // Recursive call
}

int main() {
int number = 10;
printf("Fibonacci of %d is %d\n", number, fibonacci(number));
return 0;
}
```

**Complexity Analysis of Recursive Approach:**


- **Time Complexity:** \(O(2^n)\) - Each call splits into two more
calls.
- **Space Complexity:** \(O(n)\) - The function uses \(n\) stack
frames due to recursion.

**Iterative Function:**
```c
#include <stdio.h>
// Iterative function to calculate Fibonacci number
int fibonacciIterative(int n) {
if (n == 0) {
return 0;
}
if (n == 1) {
return 1;
}
int a = 0, b = 1, c;
for (int i = 2; i <= n; i++) {
c = a + b;
a = b;
b = c;
}
return c;
}

int main() {
int number = 10;
printf("Fibonacci of %d is %d\n", number,
fibonacciIterative(number));
return 0;
}
```

**Complexity Analysis of Iterative Approach:**


- **Time Complexity:** \(O(n)\) - The function uses a single loop to
calculate the Fibonacci number.
- **Space Complexity:** \(O(1)\) - The function uses a constant
amount of space.

**Efficiency Comparison:**
- The recursive approach for calculating Fibonacci numbers is highly
inefficient due to its exponential time complexity. It recalculates
the same values multiple times, leading to redundant computations.
- The iterative approach is much more efficient, with linear time
complexity and constant space complexity, making it the preferred
method for calculating Fibonacci numbers in practice.
These explanations, code examples, and complexity analyses cover
the key concepts related to recursion, factorial calculation, the
Tower of Hanoi problem, and the efficiency comparison between
recursive and iterative solutions for calculating Fibonacci numbers.

#### Module 5: Trees

Sure! Let's delve into each topic with detailed explanations, code
examples, and traversal techniques.

### 1. Implement recursive and iterative methods for inorder


traversal of a binary search tree (BST).

**Concept:**
Inorder traversal of a BST visits nodes in ascending order. The left
subtree is visited first, followed by the root, and then the right
subtree.

**Recursive Inorder Traversal:**


```c
#include <stdio.h>
#include <stdlib.h>

// Definition of a node in the binary search tree


struct Node {
int data;
struct Node *left;
struct Node *right;
};

// Function to create a new node


struct Node* createNode(int data) {
struct Node *newNode = (struct Node*)malloc(sizeof(struct Node));
newNode->data = data;
newNode->left = NULL;
newNode->right = NULL;
return newNode;
}

// Recursive inorder traversal


void inorderRecursive(struct Node *root) {
if (root != NULL) {
inorderRecursive(root->left);
printf("%d ", root->data);
inorderRecursive(root->right);
}
}

int main() {
struct Node *root = createNode(4);
root->left = createNode(2);
root->right = createNode(6);
root->left->left = createNode(1);
root->left->right = createNode(3);
root->right->left = createNode(5);
root->right->right = createNode(7);

printf("Recursive Inorder Traversal: ");


inorderRecursive(root);
printf("\n");

return 0;
}
```

**Iterative Inorder Traversal:**


```c
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

// Definition of a node in the binary search tree


struct Node {
int data;
struct Node *left;
struct Node *right;
};

// Function to create a new node


struct Node* createNode(int data) {
struct Node *newNode = (struct Node*)malloc(sizeof(struct Node));
newNode->data = data;
newNode->left = NULL;
newNode->right = NULL;
return newNode;
}

// Stack structure
struct Stack {
struct Node *data;
struct Stack *next;
};

// Function to push a node onto the stack


void push(struct Stack **top, struct Node *node) {
struct Stack *newNode = (struct Stack*)malloc(sizeof(struct
Stack));
newNode->data = node;
newNode->next = *top;
*top = newNode;
}

// Function to pop a node from the stack


struct Node* pop(struct Stack **top) {
if (*top == NULL) {
return NULL;
}
struct Stack *temp = *top;
*top = (*top)->next;
struct Node *node = temp->data;
free(temp);
return node;
}
// Function to check if the stack is empty
bool isEmpty(struct Stack *top) {
return top == NULL;
}

// Iterative inorder traversal


void inorderIterative(struct Node *root) {
struct Stack *stack = NULL;
struct Node *current = root;

while (!isEmpty(stack) || current != NULL) {


while (current != NULL) {
push(&stack, current);
current = current->left;
}
current = pop(&stack);
printf("%d ", current->data);
current = current->right;
}
}

int main() {
struct Node *root = createNode(4);
root->left = createNode(2);
root->right = createNode(6);
root->left->left = createNode(1);
root->left->right = createNode(3);
root->right->left = createNode(5);
root->right->right = createNode(7);

printf("Iterative Inorder Traversal: ");


inorderIterative(root);
printf("\n");

return 0;
}
```
### 2. Explain how you would insert and delete nodes in an AVL
tree, ensuring it remains balanced.

**Concept:**
An AVL tree is a self-balancing binary search tree where the
difference in heights between the left and right subtrees of any
node (the balance factor) is at most 1. When nodes are inserted or
deleted, rotations are used to restore balance.

**Insertion:**
1. **Perform standard BST insertion.**
2. **Update the height of the ancestor nodes.**
3. **Check the balance factor of each node.**
4. **Perform rotations if needed:**
- **Left-Left (LL) Rotation:** Single right rotation.
- **Right-Right (RR) Rotation:** Single left rotation.
- **Left-Right (LR) Rotation:** Left rotation followed by a right
rotation.
- **Right-Left (RL) Rotation:** Right rotation followed by a left
rotation.

**Deletion:**
1. **Perform standard BST deletion.**
2. **Update the height of the ancestor nodes.**
3. **Check the balance factor of each node.**
4. **Perform rotations if needed (similar to insertion).**

**Insertion Example:**
```c
#include <stdio.h>
#include <stdlib.h>

// Definition of a node in the AVL tree


struct Node {
int data;
struct Node *left;
struct Node *right;
int height;
};

// Function to get the height of a node


int height(struct Node *node) {
if (node == NULL) {
return 0;
}
return node->height;
}

// Function to create a new node


struct Node* createNode(int data) {
struct Node *newNode = (struct Node*)malloc(sizeof(struct Node));
newNode->data = data;
newNode->left = NULL;
newNode->right = NULL;
newNode->height = 1; // New node is initially added at leaf
return newNode;
}

// Function to get the balance factor of a node


int getBalance(struct Node *node) {
if (node == NULL) {
return 0;
}
return height(node->left) - height(node->right);
}

// Right rotate subtree rooted with y


struct Node* rightRotate(struct Node *y) {
struct Node *x = y->left;
struct Node *T2 = x->right;

// Perform rotation
x->right = y;
y->left = T2;
// Update heights
y->height = max(height(y->left), height(y->right)) + 1;
x->height = max(height(x->left), height(x->right)) + 1;

// Return new root


return x;
}

// Left rotate subtree rooted with x


struct Node* leftRotate(struct Node *x) {
struct Node *y = x->right;
struct Node *T2 = y->left;

// Perform rotation
y->left = x;
x->right = T2;

// Update heights
x->height = max(height(x->left), height(x->right)) + 1;
y->height = max(height(y->left), height(y->right)) + 1;

// Return new root


return y;
}

// Utility function to get the maximum of two integers


int max(int a, int b) {
return (a > b) ? a : b;
}

// Recursive function to insert a node in the AVL tree


struct Node* insert(struct Node* node, int data) {
// Perform the normal BST insertion
if (node == NULL) {
return createNode(data);
}

if (data < node->data) {


node->left = insert(node->left, data);
} else if (data > node->data) {
node->right = insert(node->right, data);
} else {
return node; // Equal keys are not allowed in BST
}

// Update height of this ancestor node


node->height = 1 + max(height(node->left), height(node->right));

// Get the balance factor of this ancestor node


int balance = getBalance(node);

// If this node becomes unbalanced, then there are 4 cases

// Left Left Case


if (balance > 1 && data < node->left->data) {
return rightRotate(node);
}

// Right Right Case


if (balance < -1 && data > node->right->data) {
return leftRotate(node);
}

// Left Right Case


if (balance > 1 && data > node->left->data) {
node->left = leftRotate(node->left);
return rightRotate(node);
}

// Right Left Case


if (balance < -1 && data < node->right->data) {
node->right = rightRotate(node->right);
return leftRotate(node);
}

// Return the (unchanged) node pointer


return node;
}

// Function to print inorder traversal of the AVL tree


void inorder(struct Node *root) {
if (root != NULL) {
inorder(root->left);
printf("%d ", root->data);
inorder(root->right);
}
}

int main() {
struct Node *root = NULL;

root = insert(root, 10);


root = insert(root, 20);
root = insert(root, 30);
root = insert(root, 40);
root = insert(root, 50);
root = insert(root, 25);

printf("Inorder traversal of the constructed AVL tree is: ");


inorder(root);
printf("\n");

return

#### Module 6: Searching and Sorting

Sure! Let's discuss each topic in detail, including algorithms, time


complexities, and scenarios for choosing specific sorting and
searching techniques.

### 1. Compare the time complexity of linear search and binary


search.

**Linear Search:**
- **Algorithm:** Linear search involves checking each element of
the array sequentially until the desired element is found or the end
of the array is reached.
- **Time Complexity:**
- Best case: \(O(1)\) - The element is found at the first position.
- Average case: \(O(n)\) - The element is found in the middle of the
array.
- Worst case: \(O(n)\) - The element is found at the last position or
not present at all.
- **Space Complexity:** \(O(1)\) - Only a few extra variables are
used.

**Binary Search:**
- **Algorithm:** Binary search requires the array to be sorted. It
repeatedly divides the search interval in half. If the value of the
search key is less than the item in the middle of the interval, the
interval is narrowed to the lower half. Otherwise, it is narrowed to
the upper half. This process continues until the value is found or the
interval is empty.
- **Time Complexity:**
- Best case: \(O(1)\) - The element is found at the middle position.
- Average case: \(O(\log n)\) - The search space is halved in each
step.
- Worst case: \(O(\log n)\) - The element is found at the last position
after halving the array multiple times.
- **Space Complexity:** \(O(1)\) - Iterative version uses a few extra
variables.
- Recursive version: \(O(\log n)\) due to the recursion stack.

**Comparison:**
- Linear search is simpler and does not require the array to be
sorted.
- Binary search is much faster for large, sorted arrays due to its
logarithmic time complexity.
- Linear search is preferred when dealing with small or unsorted
arrays.
- Binary search is preferred for searching in large, sorted arrays due
to its efficiency.
### 2. Implement the merge sort algorithm with a detailed step-by-
step explanation.

**Merge Sort Algorithm:**


Merge sort is a divide-and-conquer algorithm that splits the array
into halves, recursively sorts each half, and then merges the sorted
halves.

**Steps:**
1. Divide the array into two halves.
2. Recursively sort each half.
3. Merge the two sorted halves.

**Code Implementation:**
```c
#include <stdio.h>
#include <stdlib.h>

// Function to merge two halves


void merge(int arr[], int left, int mid, int right) {
int n1 = mid - left + 1;
int n2 = right - mid;

// Create temporary arrays


int L[n1], R[n2];

// Copy data to temporary arrays L[] and R[]


for (int i = 0; i < n1; i++) {
L[i] = arr[left + i];
}
for (int j = 0; j < n2; j++) {
R[j] = arr[mid + 1 + j];
}

// Merge the temporary arrays back into arr[l..r]

int i = 0; // Initial index of first subarray


int j = 0; // Initial index of second subarray
int k = left; // Initial index of merged subarray

while (i < n1 && j < n2) {


if (L[i] <= R[j]) {
arr[k] = L[i];
i++;
} else {
arr[k] = R[j];
j++;
}
k++;
}

// Copy the remaining elements of L[], if there are any


while (i < n1) {
arr[k] = L[i];
i++;
k++;
}

// Copy the remaining elements of R[], if there are any


while (j < n2) {
arr[k] = R[j];
j++;
k++;
}
}

// Function to implement merge sort


void mergeSort(int arr[], int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2;

// Sort first and second halves


mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
// Merge the sorted halves
merge(arr, left, mid, right);
}
}

// Function to print an array


void printArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}

int main() {
int arr[] = {12, 11, 13, 5, 6, 7};
int arr_size = sizeof(arr) / sizeof(arr[0]);

printf("Given array is \n");


printArray(arr, arr_size);

mergeSort(arr, 0, arr_size - 1);

printf("Sorted array is \n");


printArray(arr, arr_size);
return 0;
}
```

**Complexity Analysis:**
- **Time Complexity:** \(O(n \log n)\) - The array is divided in half (\
(O(\log n)\)) and then merged (\(O(n)\)).
- **Space Complexity:** \(O(n)\) - Additional arrays are used for
merging.

### 3. Discuss the scenarios where selection sort is preferred over


quick sort.

**Selection Sort:**
- **Algorithm:** Selection sort repeatedly selects the smallest (or
largest) element from the unsorted portion and swaps it with the
first unsorted element.
- **Time Complexity:**
- Best case: \(O(n^2)\)
- Average case: \(O(n^2)\)
- Worst case: \(O(n^2)\)
- **Space Complexity:** \(O(1)\) - No additional memory is needed.

**Quick Sort:**
- **Algorithm:** Quick sort selects a 'pivot' element and partitions
the array around the pivot. It then recursively sorts the partitions.
- **Time Complexity:**
- Best case: \(O(n \log n)\)
- Average case: \(O(n \log n)\)
- Worst case: \(O(n^2)\) - Occurs when the pivot selection is poor.
- **Space Complexity:** \(O(\log n)\) for the recursive stack.

**Scenarios Where Selection Sort is Preferred:**


1. **Small Data Sets:** For very small arrays, the simplicity of
selection sort makes it faster due to lower constant factors.
2. **Memory Constraints:** Selection sort requires \(O(1)\)
additional space, making it suitable for environments with strict
memory limits.
3. **Nearly Sorted Data:** When the array is nearly sorted,
selection sort performs fewer swaps compared to quick sort.
4. **Deterministic Performance:** Selection sort has predictable
performance, whereas quick sort's performance can degrade to \
(O(n^2)\) with poor pivot choices.

**Example of Selection Sort Implementation:**


```c
#include <stdio.h>

// Function to perform selection sort


void selectionSort(int arr[], int n) {
for (int i = 0; i < n-1; i++) {
int min_idx = i;
for (int j = i+1; j < n; j++) {
if (arr[j] < arr[min_idx]) {
min_idx = j;
}
}
// Swap the found minimum element with the first element
int temp = arr[min_idx];
arr[min_idx] = arr[i];
arr[i] = temp;
}
}

// Function to print an array


void printArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}

int main() {
int arr[] = {64, 25, 12, 22, 11};
int n = sizeof(arr)/sizeof(arr[0]);
selectionSort(arr, n);
printf("Sorted array: \n");
printArray(arr, n);
return 0;
}
```

**Conclusion:**
- Selection sort is easy to implement and performs well on small
datasets or when memory is a constraint.
- Quick sort is preferred for larger datasets due to its average-case
efficiency but requires careful handling to avoid worst-case
scenarios.
- The choice of sorting algorithm depends on the specific
requirements and constraints of the problem at hand.
#### Module 7: Hashing

Sure! Let's explore hash tables and their associated concepts in


detail, including implementation details, collision resolution
strategies, and examples.

### 1. Explain the process of resolving collisions using open


addressing.

**Concept:**
Open addressing is a collision resolution strategy in hash tables
where all elements are stored within the hash table itself, avoiding
linked lists. When a collision occurs (i.e., two elements hash to the
same index), the algorithm probes for the next available slot
according to a predefined sequence.

**Common Open Addressing Methods:**

1. **Linear Probing:**
- Probes sequentially by moving one slot at a time until an empty
slot is found.
- Formula: \( \text{index} = (\text{hash} + i) \% \text{table size} \),
where \( i \) is the probe number.
- **Pros:** Simple and easy to implement.
- **Cons:** Primary clustering can occur, causing long probe
sequences.

2. **Quadratic Probing:**
- Probes using a quadratic function to reduce primary clustering.
- Formula: \( \text{index} = (\text{hash} + c1 \cdot i + c2 \cdot i^2) \%
\text{table size} \), where \( i \) is the probe number, and \( c1 \) and \(
c2 \) are constants.
- **Pros:** Reduces primary clustering compared to linear probing.
- **Cons:** Secondary clustering can still occur; more complex to
implement.

3. **Double Hashing:**
- Uses a second hash function to determine the probe sequence.
- Formula: \( \text{index} = (\text{hash1} + i \cdot \text{hash2}) \%
\text{table size} \), where \( i \) is the probe number.
- **Pros:** Minimizes clustering by using two independent hash
functions.
- **Cons:** Requires careful selection of the second hash function
to ensure it is independent and does not produce zero.

**Example Implementation of Linear Probing:**


```c
#include <stdio.h>
#include <stdlib.h>

#define TABLE_SIZE 10

int hashTable[TABLE_SIZE];

// Initialize hash table


void initTable() {
for (int i = 0; i < TABLE_SIZE; i++) {
hashTable[i] = -1; // Using -1 to indicate an empty slot
}
}

// Hash function
int hashFunction(int key) {
return key % TABLE_SIZE;
}

// Insert key into hash table using linear probing


void insert(int key) {
int index = hashFunction(key);
int i = 0;

while (hashTable[(index + i) % TABLE_SIZE] != -1) {


i++;
}
hashTable[(index + i) % TABLE_SIZE] = key;
}

// Display hash table


void displayTable() {
for (int i = 0; i < TABLE_SIZE; i++) {
printf("%d -> %d\n", i, hashTable[i]);
}
}

int main() {
initTable();
insert(12);
insert(22);
insert(32);
insert(42);

displayTable();
return 0;
}
```

### 2. Implement a hash table using separate chaining for collision


resolution.

**Concept:**
Separate chaining involves maintaining a linked list for each index
in the hash table. When a collision occurs, the new element is
added to the linked list at that index.

**Example Implementation:**
```c
#include <stdio.h>
#include <stdlib.h>

// Definition of a node in the linked list


struct Node {
int key;
struct Node* next;
};

// Hash table of linked lists


#define TABLE_SIZE 10
struct Node* hashTable[TABLE_SIZE];

// Initialize hash table


void initTable() {
for (int i = 0; i < TABLE_SIZE; i++) {
hashTable[i] = NULL;
}
}

// Hash function
int hashFunction(int key) {
return key % TABLE_SIZE;
}

// Insert key into hash table


void insert(int key) {
int index = hashFunction(key);
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
newNode->key = key;
newNode->next = hashTable[index];
hashTable[index] = newNode;
}

// Display hash table


void displayTable() {
for (int i = 0; i < TABLE_SIZE; i++) {
printf("%d -> ", i);
struct Node* temp = hashTable[i];
while (temp != NULL) {
printf("%d -> ", temp->key);
temp = temp->next;
}
printf("NULL\n");
}
}

int main() {
initTable();
insert(12);
insert(22);
insert(32);
insert(42);

displayTable();
return 0;
}
```

### 3. Discuss the concept of perfect hashing and its applications.

**Concept:**
Perfect hashing is a hashing technique used to create a hash
function that has no collisions for a given set of keys. It guarantees
constant-time complexity \( O(1) \) for search operations.

**Types of Perfect Hashing:**

1. **Static Perfect Hashing:**


- Used when the set of keys does not change (i.e., no insertions or
deletions after the initial setup).
- Consists of two-level hashing:
- **First Level:** Uses a primary hash function to hash keys into
buckets.
- **Second Level:** Each bucket has its own secondary hash
function to resolve collisions within that bucket.
- **Pros:** Ensures no collisions and provides constant-time
lookups.
- **Cons:** The set of keys must be known in advance and remain
static.

2. **Dynamic Perfect Hashing:**


- Allows for insertions and deletions, but is more complex to
implement and maintain.
- Typically involves rehashing when the hash table needs to be
resized.

**Applications:**
- **Compilers:** Used for symbol tables where the set of identifiers
is fixed.
- **Databases:** Efficient indexing for static sets of keys.
- **Network Routers:** For efficient packet routing tables.

**Example of Perfect Hashing:**


Let's consider a simple example of static perfect hashing for a fixed
set of keys:
```c
#include <stdio.h>
#include <stdlib.h>

#define TABLE_SIZE 11

// Define a key set and a simple primary hash function


int keys[] = {1, 3, 5, 7, 9, 11};
int n = sizeof(keys) / sizeof(keys[0]);

// Hash function for the first level


int primaryHash(int key) {
return key % TABLE_SIZE;
}

// Secondary hash function for perfect hashing


int secondaryHash(int key, int attempt) {
return (key + attempt) % TABLE_SIZE;
}

// Create a perfect hash table


void createPerfectHashTable(int table[]) {
for (int i = 0; i < TABLE_SIZE; i++) {
table[i] = -1; // Initialize table with -1 indicating empty slots
}

for (int i = 0; i < n; i++) {


int key = keys[i];
int index = primaryHash(key);

// Use secondary hash to resolve collisions


int attempt = 0;
while (table[secondaryHash(index, attempt)] != -1) {
attempt++;
}

table[secondaryHash(index, attempt)] = key;


}
}

// Display the perfect hash table


void displayTable(int table[]) {
for (int i = 0; i < TABLE_SIZE; i++) {
printf("%d -> %d\n", i, table[i]);
}
}

int main() {
int table[TABLE_SIZE];
createPerfectHashTable(table);
displayTable(table);
return 0;
}
```

**Conclusion:**
- **Open Addressing:** Effective for avoiding extra memory
allocation but can lead to clustering.
- **Separate Chaining:** Simple to implement and avoids clustering
but requires additional memory for linked lists.
- **Perfect Hashing:** Ideal for static sets of keys with no collisions
and constant-time complexity.
### Internal Examination

- **Topics Covered**: A cumulative exam covering all modules with


a higher weighting.
- **Sample Questions**: Typically a mix of questions from all
modules to assess
comprehensive understanding.

Sure! Let's create a cumulative exam covering all the previously


discussed modules. This exam will include questions that assess
understanding of data structures, algorithms, and their
implementations.

### Sample Cumulative Exam Questions:

#### 1. Structures and Unions


**a. Explain the concept of structures in C/C++ with an example.**
**b. Differentiate between structure and union.**
**c. How do you access members of a structure using pointers?**
**d. Discuss the concept of nested structures with an example.**

#### 2. Arrays, Stacks, and Queues


**a. Explain how you would implement a stack using arrays.**
**b. Compare and contrast infix, postfix, and prefix notations with
examples.**
**c. Discuss the concept of a priority queue. Provide an example
scenario where it would be useful.**

#### 3. Linked Lists


**a. Implement insertion and deletion operations in a doubly linked
list.**
**b. Discuss the advantages of using circular linked lists over linear
linked lists.**
**c. Explain how skip lists are implemented and their advantages in
search operations.**

#### 4. Recursion
**a. Write a recursive function to calculate the factorial of a
number.**
**b. Explain the recursive approach to solving the Tower of Hanoi
problem.**
**c. Discuss the efficiency of recursion compared to iterative
solutions for calculating Fibonacci numbers.**

#### 5. Trees
**a. Implement recursive and iterative methods for inorder traversal
of a binary search tree.**
**b. Explain how you would insert and delete nodes in an AVL tree,
ensuring it remains balanced.**
**c. Discuss the concept of threaded binary trees and their
advantages.**

#### 6. Searching and Sorting


**a. Compare the time complexity of linear search and binary
search.**
**b. Implement the merge sort algorithm with a detailed step-by-
step explanation.**
**c. Discuss the scenarios where selection sort is preferred over
quick sort.**

#### 7. Hash Tables


**a. Explain the process of resolving collisions using open
addressing.**
**b. Implement a hash table using separate chaining for collision
resolution.**
**c. Discuss the concept of perfect hashing and its applications.**

---

### Detailed Answers:

#### 1. Structures and Unions


**a. Structures in C/C++:**
A structure in C/C++ is a user-defined data type that allows grouping
variables of different types under a single name.
```c
#include <stdio.h>

struct Person {
char name[50];
int age;
float salary;
};

int main() {
struct Person p1;
strcpy(p1.name, "John");
p1.age = 30;
p1.salary = 50000.50;

printf("Name: %s\n", p1.name);


printf("Age: %d\n", p1.age);
printf("Salary: %.2f\n", p1.salary);

return 0;
}
```

**b. Structure vs. Union:**


- **Structure:** All members have their own storage space.
- **Union:** All members share the same memory location. Only one
member can hold a value at a time.
```c
#include <stdio.h>

struct MyStruct {
int intVal;
float floatVal;
};

union MyUnion {
int intVal;
float floatVal;
};

int main() {
struct MyStruct s;
union MyUnion u;

s.intVal = 5;
s.floatVal = 10.5;

u.intVal = 5;
printf("Union intVal: %d\n", u.intVal);
u.floatVal = 10.5;
printf("Union floatVal: %.2f\n", u.floatVal);

return 0;
}
```

**c. Accessing Structure Members Using Pointers:**


```c
#include <stdio.h>

struct Person {
char name[50];
int age;
float salary;
};

int main() {
struct Person p1 = {"John", 30, 50000.50};
struct Person *ptr = &p1;

printf("Name: %s\n", ptr->name);


printf("Age: %d\n", ptr->age);
printf("Salary: %.2f\n", ptr->salary);

return 0;
}
```

**d. Nested Structures:**


```c
#include <stdio.h>

struct Date {
int day;
int month;
int year;
};

struct Person {
char name[50];
struct Date birthdate;
};

int main() {
struct Person p1 = {"John", {15, 5, 1990}};

printf("Name: %s\n", p1.name);


printf("Birthdate: %d/%d/%d\n", p1.birthdate.day,
p1.birthdate.month, p1.birthdate.year);

return 0;
}
```

#### 2. Arrays, Stacks, and Queues


**a. Implementing Stack Using Arrays:**
```c
#include <stdio.h>
#include <stdlib.h>

#define MAX 100

int stack[MAX];
int top = -1;
void push(int val) {
if (top == MAX - 1) {
printf("Stack Overflow\n");
} else {
stack[++top] = val;
}
}

int pop() {
if (top == -1) {
printf("Stack Underflow\n");
return -1;
} else {
return stack[top--];
}
}

int peek() {
if (top == -1) {
printf("Stack is empty\n");
return -1;
} else {
return stack[top];
}
}

int main() {
push(10);
push(20);
push(30);
printf("Top element is %d\n", peek());
printf("Popped element is %d\n", pop());
printf("Popped element is %d\n", pop());
printf("Popped element is %d\n", pop());

return 0;
}
```

**b. Infix, Postfix, and Prefix Notations:**


- **Infix:** Operators are between operands (e.g., A + B).
- **Postfix:** Operators are after operands (e.g., A B +).
- **Prefix:** Operators are before operands (e.g., + A B).

**c. Priority Queue:**


A priority queue is a data structure where each element is
associated with a priority, and elements are served based on their
priority.
**Example Scenario:**
- Task scheduling in operating systems where tasks with higher
priority should be executed before those with lower priority.

#### 3. Linked Lists


**a. Insertion and Deletion in a Doubly Linked List:**
```c
#include <stdio.h>
#include <stdlib.h>

struct Node {
int data;
struct Node* prev;
struct Node* next;
};

struct Node* head = NULL;

void insert(int data) {


struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
newNode->data = data;
newNode->prev = NULL;
newNode->next = head;
if (head != NULL) {
head->prev = newNode;
}
head = newNode;
}

void delete(int data) {


struct Node* temp = head;
while (temp != NULL && temp->data != data) {
temp = temp->next;
}
if (temp == NULL) {
printf("Element not found\n");
return;
}
if (temp->prev != NULL) {
temp->prev->next = temp->next;
} else {
head = temp->next;
}
if (temp->next != NULL) {
temp->next->prev = temp->prev;
}
free(temp);
}

void display() {
struct Node* temp = head;
while (temp != NULL) {
printf("%d -> ", temp->data);
temp = temp->next;
}
printf("NULL\n");
}

int main() {
insert(10);
insert(20);
insert(30);
display();
delete(20);
display();
return 0;
}
```

**b. Advantages of Circular Linked Lists:**


- Efficient for applications that require circular traversal (e.g.,
round-robin scheduling).
- No need to traverse to the end to append elements.

**c. Skip Lists:**


Skip lists are layered linked lists that allow fast search within an
ordered sequence of elements.
- **Advantages:** Faster search operations than regular linked lists
(average case \(O(\log n)\)).

#### 4. Recursion
**a. Recursive Factorial Function:**
```c
#include <stdio.h>

int factorial(int n) {
if (n == 0) {
return 1;
} else {
return n * factorial(n - 1);
}
}

int main() {
int num = 5;
printf("Factorial of %d is %d\n", num, factorial(num));
return 0;
}
```

**b. Tower of Hanoi:**


- Recursive approach involves moving \(n-1\) disks to an auxiliary
rod, moving the nth disk to the target rod, and then moving \(n-1\)
disks from the auxiliary rod to the target rod.
```c
#include <stdio.h>

void towerOfHanoi(int n, char from_rod, char to_rod, char aux_rod) {


if (n == 1) {
printf("Move disk 1 from rod %c to rod %c\n", from_rod, to_rod);
return;
}
towerOfHanoi(n

.THANK YOU.

You might also like