Data Structure Through C (Bcac302)
Data Structure Through C (Bcac302)
**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>
int main() {
// Declare a variable of type 'Person'
struct Person person1;
**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.
struct MyStruct {
int integer;
float floating;
char character;
};
union MyUnion {
int integer;
float floating;
char character;
};
int main() {
struct MyStruct s;
union MyUnion u;
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);
return 0;
}
```
**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};
return 0;
}
```
**Concept:**
A nested structure is a structure that contains another structure as
a member.
**Example:**
```c
#include <stdio.h>
int main() {
// Declare and initialize a variable of type 'Person'
struct Person person1 = {"Bob", 32, {"New York", 10001}};
return 0;
}
```
**Concept:**
Structures can be passed to functions by value or by reference
(using pointers).
// Define a structure
struct Point {
int x;
int y;
};
int main() {
struct Point p1 = {10, 20};
printPoint(p1); // Pass by value
return 0;
}
```
// Define a structure
struct Point {
int x;
int y;
};
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.
**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>
// Stack structure
struct Stack {
int arr[MAX];
int top;
};
push(&stack, 10);
push(&stack, 20);
push(&stack, 30);
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\)
**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>
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.
**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>
insertAtEnd(&head, 10);
insertAtEnd(&head, 20);
insertAtFront(&head, 5);
return 0;
}
```
**Deletion Operation:**
```c
#include <stdio.h>
#include <stdlib.h>
insertAtEnd(&head, 10);
insertAtEnd(&head, 20);
insertAtFront(&head, 5);
deleteFromEnd(&head);
printf("Doubly Linked List after deleting from end: ");
displayList(head);
return 0;
}
```
**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
current = current->forward[0];
insertNode(skipList, 3);
insertNode(skipList, 6);
insertNode(skipList, 7);
insertNode(skipList, 9);
insertNode(skipList, 12);
insertNode(skipList, 19);
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.
**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>
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.
**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>
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.
**Recursive Function:**
```c
#include <stdio.h>
int main() {
int number = 10;
printf("Fibonacci of %d is %d\n", number, fibonacci(number));
return 0;
}
```
**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;
}
```
**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.
Sure! Let's delve into each topic with detailed explanations, code
examples, and traversal techniques.
**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.
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);
return 0;
}
```
// Stack structure
struct Stack {
struct Node *data;
struct Stack *next;
};
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);
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>
// 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;
// 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;
int main() {
struct Node *root = NULL;
return
**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.
**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>
int main() {
int arr[] = {12, 11, 13, 5, 6, 7};
int arr_size = sizeof(arr) / sizeof(arr[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.
**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.
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
**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.
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.
#define TABLE_SIZE 10
int hashTable[TABLE_SIZE];
// Hash function
int hashFunction(int key) {
return key % TABLE_SIZE;
}
int main() {
initTable();
insert(12);
insert(22);
insert(32);
insert(42);
displayTable();
return 0;
}
```
**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>
// Hash function
int hashFunction(int key) {
return key % TABLE_SIZE;
}
int main() {
initTable();
insert(12);
insert(22);
insert(32);
insert(42);
displayTable();
return 0;
}
```
**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.
**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.
#define TABLE_SIZE 11
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
#### 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.**
---
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;
return 0;
}
```
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;
}
```
struct Person {
char name[50];
int age;
float salary;
};
int main() {
struct Person p1 = {"John", 30, 50000.50};
struct Person *ptr = &p1;
return 0;
}
```
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}};
return 0;
}
```
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;
}
```
struct Node {
int data;
struct Node* prev;
struct Node* next;
};
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;
}
```
#### 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;
}
```
.THANK YOU.