Ads Combined
Ads Combined
Master of Technology
In Software
Engineering
Submitted by
ADITYA PANDEY
24/SWE/10
I SEM, I YEAR
Submitted to
Ms. Shweta Meena
Assistant Professor
Department of Software Engineering
THEORY:
Linear Search:
In Linear Search, we iterate over all the elements of the array and check if the current element is equal
to the target element. If we find any element to be equal to the target element, then return the index of
the current element.
Binary Search:
A binary search is a search in which the middle element is calculated to check whether it is smaller or
larger than the element which is to be searched. The main advantage of using binary search is that it
does not scan each element in the list. Instead of scanning each element, it performs the searching to
the half of the list. So, the binary search takes less time to search an element as compared to a linear
search. The one pre-requisite of binary search is that an array should be in sorted order.
SOURCE CODE:
#include <bits/stdc++.h>
using namespace std;
int main() {
cout << "Name: ADITYA PANDEY\nRoll NO.: 24/SWE/10\n" << endl;
int n;
cout << "Enter the number of elements in the array: ";
cin >> n;
vector<int> arr(n);
ADITYA PANDEY 24/SWE/
4 10
cout << "Enter the elements of the array: ";
for (int i = 0; i < n; i++) {
cin >> arr[i];
}
int key;
cout << "Enter the key to search: ";
cin >> key;
return 0;
}
OUTPUT:
1. Linear Search is simple but less efficient for large datasets, as it checks each element sequentially.
2. Binary Search is more efficient with sorted arrays, dividing the search space with each comparison.
3. Recursive implementations use the call stack for iteration, which may lead to higher memory
usage but can be more concise.
4. Non-recursive methods avoid the overhead of recursion, which can be beneficial for larger data sets.
THEORY:
STACK:
A Stack is a linear data structure that follows the Last In, First Out (LIFO) principle. This means that
the last element added to the stack is the first one to be removed. Think of a stack of plates – you add
new plates on top, and you take plates from the top.
Operations on Stack
Push: Add an element to the top of the stack.
Pop: Remove the element from the top of the stack.
Peek (or Top): Retrieve the element at the top of the stack without removing it.
isEmpty: Check if the stack is empty.
isFull: Check if the stack is full (only relevant for a fixed-size stack implemented using an array).
QUEUE:
A Queue is a linear data structure that follows the First In, First Out (FIFO) principle. This means that
the first element added to the queue is the first one to be removed. A queue is similar to a line of
people; people join from the back and leave from the front.
Operations on Queue:
Enqueue: Add an element to the end (rear) of the queue.
Dequeue: Remove an element from the front of the queue.
Front: Retrieve the element at the front without removing
it. isEmpty: Check if the queue is empty.
isFull: Check if the queue is full (only relevant for a fixed-size queue implemented using an array).
SOURCE CODE:
#include <iostream>
using namespace std;
class Stack {
int top;
int arr[MAX];
public:
Stack() { top = -1; }
void push(int x) {
ADITYA PANDEY 24/SWE/
7 10
if (isFull()) {
cout << "Stack Overflow\n";
return;
}
arr[++top] = x;
}
int pop() {
if (isEmpty()) {
cout << "Stack Underflow\n";
return -1;
}
return arr[top--];
}
int peek() {
if (isEmpty()) {
cout << "Stack is Empty\n";
return -1;
}
return arr[top];
}
};
class Queue {
int front, rear;
int arr[MAX];
public:
Queue() { front = rear = -1; }
void enqueue(int x) {
if (isFull()) {
cout << "Queue Overflow\n";
return;
}
if (front == -1) front = 0;
arr[++rear] = x;
}
int dequeue() {
if (isEmpty()) {
cout << "Queue Underflow\n";
return -1;
}
return arr[front++];
}
int main() {
Stack s;
Queue q;
int choice, value;
while (true) {
cout << "\nChoose an operation:\n";
cout << "1. Push to Stack\n";
cout << "2. Pop from Stack\n";
cout << "3. Peek Stack\n";
cout << "4. Enqueue to Queue\n";
cout << "5. Dequeue from Queue\n";
cout << "6. Peek Queue\n";
cout << "0. Exit\n";
cout << "Enter your choice: ";
cin >> choice;
switch (choice) {
case 1:
cout << "Enter value to push onto the stack: ";
cin >> value;
s.push(value);
break;
case 2:
cout << "Popped from Stack: " << s.pop() << endl;
break;
case 3:
cout << "Top of Stack: " << s.peek() << endl;
break;
case 4:
cout << "Enter value to enqueue to the queue: ";
cin >> value;
q.enqueue(value);
break;
case 5:
cout << "Dequeued from Queue: " << q.dequeue() << endl;
break;
case 6:
cout << "Front of Queue: " << q.peek() << endl;
break;
case 0:
cout << "Exiting program.\n";
ADITYA PANDEY 24/SWE/
9 10
return 0;
default:
cout << "Invalid choice. Try again.\n";
}
}
return 0;
}
OUTPUT:
LEARNINGS:
1. Stack ADT is useful in scenarios where LIFO access is required, such as function call
stacks or undo mechanisms in software.
2. Queue ADT is effective for FIFO access, suitable for task scheduling, buffering, and realtime
processing applications.
3. Implementing these ADTs using arrays (lists in Python) is straightforward, though for largescale
applications, circular buffers or linked lists may be more efficient for queues.
4. Understanding ADTs helps in choosing the right data structure for specific
problem requirements in software development.
THEORY:
In arithmetic expressions, infix notation is the standard way of writing operations where operators are
placed between operands (A + B). However, to evaluate expressions more easily, they are often
converted to postfix notation (Reverse Polish Notation), where operators are written after the operands
(A B +). Postfix expressions eliminate the need for parentheses and operator precedence rules.
SOURCE CODE:
#include <iostream>
#include <stack>
#include <string>
#include <cctype>
bool isOperator(char c) {
return (c == '+' || c == '-' || c == '*' || c == '/' || c == '^');
}
if (isalnum(c)) {
postfix += c;
}
ADITYA PANDEY 24/SWE/
11 10
else if (c == '(') {
s.push(c);
}
else if (c == ')') {
while (!s.empty() && s.top() != '(') {
postfix += ' ';
postfix += s.top();
s.pop();
}
s.pop();
}
else if (isOperator(c)) {
postfix += ' ';
while (!s.empty() && precedence(s.top()) >= precedence(c)) {
postfix += s.top();
s.pop();
postfix += ' ';
}
s.push(c);
}
}
while (!s.empty()) {
postfix += ' ';
postfix += s.top();
s.pop();
}
return postfix;
}
int main() {
cout << "Name: ADITYA PANDEY\nRoll NO.: 24/SWE/10\n" << endl;
string infix;
return 0;
}
LEARNINGS:
THEORY:
STACK:
A Stack is a linear data structure that follows the Last In, First Out (LIFO) principle. This means that
the last element added to the stack is the first one to be removed. Think of a stack of plates – you add
new plates on top, and you take plates from the top.
Operations on Stack
Push: Add an element to the top of the stack.
Pop: Remove the element from the top of the stack.
Peek (or Top): Retrieve the element at the top of the stack without removing it.
isEmpty: Check if the stack is empty.
isFull: Check if the stack is full (only relevant for a fixed-size stack implemented using an array).
QUEUE:
A Queue is a linear data structure that follows the First In, First Out (FIFO) principle. This means that
the first element added to the queue is the first one to be removed. A queue is similar to a line of
people; people join from the back and leave from the front.
Operations on Queue
Enqueue: Add an element to the end (rear) of the queue.
Dequeue: Remove an element from the front of the queue.
Front: Retrieve the element at the front without removing
it. isEmpty: Check if the queue is empty.
isFull: Check if the queue is full (only relevant for a fixed-size queue implemented using an array).
SOURCE CODE:
#include <iostream>
using namespace std;
struct Node {
int data;
Node* next;
ADITYA PANDEY 24/SWE/
14 10
Node(int value) : data(value), next(nullptr) {}
};
public:
Stack() : top(nullptr) {}
int pop() {
if (isEmpty()) {
cout << "Stack is empty." << endl;
return -1; // Indicate empty stack
}
Node* temp = top;
int poppedValue = top->data;
top = top->next;
delete temp;
cout << poppedValue << " popped from stack." << endl;
return poppedValue;
}
int peek() {
if (isEmpty()) {
cout << "Stack is empty." << endl;
return -1; // Indicate empty stack
}
return top->data;
}
bool isEmpty() {
return top == nullptr;
}
};
public:
Queue() : front(nullptr), rear(nullptr) {}
int dequeue() {
if (isEmpty()) {
cout << "Queue is empty." << endl;
return -1; // Indicate empty queue
}
Node* temp = front;
int dequeuedValue = front->data;
front = front->next;
if (!front) rear = nullptr; // Queue is now empty
delete temp;
cout << dequeuedValue << " dequeued from queue." << endl;
return dequeuedValue;
}
int peek() {
if (isEmpty()) {
cout << "Queue is empty." << endl;
return -1; // Indicate empty queue
}
return front->data;
}
bool isEmpty() {
return front == nullptr;
}
};
int main() {
cout << "Name: ADITYA PANDEY\nRoll NO.: 24/SWE/10\n" << endl;
Stack stack;
Queue queue;
int choice, value;
while (true) {
cout << "\nChoose an operation:\n";
cout << "1. Push to Stack\n";
cout << "2. Pop from Stack\n";
cout << "3. Peek Stack\n";
cout << "4. Enqueue to Queue\n";
cout << "5. Dequeue from Queue\n";
cout << "6. Peek Queue\n";
cout << "0. Exit\n";
cout << "Enter your choice: ";
cin >> choice;
switch (choice) {
case 1:
cout << "Enter value to push onto the stack: ";
cin >> value;
stack.push(value);
break;
case 2:
stack.pop();
break;
case 3:
cout << "Top element: " << stack.peek() << endl;
break;
case 4:
ADITYA PANDEY 24/SWE/
16 10
cout << "Enter value to enqueue to the queue: ";
cin >> value;
queue.enqueue(value);
break;
case 5:
queue.dequeue();
break;
case 6:
cout << "Front element: " << queue.peek() << endl;
break;
case 0:
cout << "Exiting program.\n";
return 0;
default:
cout << "Invalid choice. Try again.\n";
}
}
return 0;
}
OUTPUT:
LEARNINGS:
1. Stack ADT using linked lists is efficient in terms of memory, as it dynamically grows
and shrinks, making it suitable for applications requiring LIFO access.
2. Queue ADT using linked lists also benefits from dynamic memory allocation and is
efficient for FIFO access, useful for tasks like scheduling and buffering.
3. Using Linked Lists to implement these ADTs provides flexibility and overcomes
the limitations of static array sizes.
4. Understanding linked lists in data structure implementation enhances efficiency and
memory management, which is crucial in system-level programming and application
development.
THEORY:
This structure enables efficient search, insertion, and deletion operations, making it useful for
applications that require dynamic data management.
SOURCE CODE:
#include <iostream>
using namespace std;
struct Node {
int data;
Node* left;
Node* right;
Node(int value) : data(value), left(nullptr), right(nullptr) {}
};
class BST {
ADITYA PANDEY 24/SWE/
18 10
private:
Node* root;
public:
BST() : root(nullptr) {}
void inorderTraversal() {
cout << "Inorder traversal of the BST: ";
inorder(root);
cout << endl;
}
};
int main() {
cout << "Name: ADITYA PANDEY\nRoll NO.: 24/SWE/10\n" << endl;
BST bst;
int choice, value;
cout << "\nChoose an operation:\n";
cout << "1. Insert into BST\n";
cout << "2. Search in BST\n";
cout << "3. Delete from BST\n";
cout << "4. Display Inorder Traversal\n";
cout << "0. Exit\n";
while (true) {
switch (choice) {
case 1:
cout << "Enter value to insert into the BST: ";
cin >> value;
bst.insert(value);
break;
case 2:
cout << "Enter value to search in the BST: ";
cin >> value;
cout << (bst.search(value) ? "Key " + to_string(value) + " found in the BST." : "Key " + to_string(value) + " not
found in the BST.") << endl;
break;
case 3:
cout << "Enter value to delete from the BST: ";
cin >> value;
bst.deleteNode(value);
break;
case 4:
bst.inorderTraversal();
break;
case 0:
cout << "Exiting program.\n";
return 0;
default:
cout << "Invalid choice. Try again.\n";
}
}
OUTPUT:
LEARNINGS:
1. Binary Search Trees (BST) provide an efficient structure for ordered data, enabling O(log
n) time complexity for search, insert, and delete operations in balanced cases.
2. In-order traversal of a BST retrieves elements in sorted order.
3. Search Operation in a BST starts from the root and traverses either left or right
depending on the key value, making it efficient for ordered data.
THEORY:
Dictionary ADT:
A Dictionary Abstract Data Type (ADT) is a collection of key-value pairs where each key is unique.
The dictionary allows for efficient insertion, deletion, and lookup operations. Hashing is a technique
used to implement dictionary ADT due to its efficient data retrieval capability.
SOURCE CODE:
#include <iostream>
#include <list>
#include <string>
using namespace std;
struct DictionaryEntry {
string key;
string value;
};
class HashTable {
private:
static const int hashSize = 10; // Size of the hash table
list<DictionaryEntry> table[hashSize]; // Hash table using separate chaining
public:
// Insert a key-value pair into the dictionary
void insert(const string& key, const string& value) {
int index = hashFunction(key);
// Check if the key already exists and update the value if it does
for (auto& entry : table[index]) {
if (entry.key == key) {
entry.value = value; // Update existing value
return;
}
}
// If key does not exist, insert a new entry
table[index].push_back({key, value});
cout << "Inserted (" << key << ", " << value << ") into the dictionary." << endl;
}
void display() {
cout << "Dictionary contents:" << endl;
for (int i = 0; i < hashSize; i++) {
if (!table[i].empty()) {
for (const auto& entry : table[i]) {
cout << "Key: " << entry.key << ", Value: " << entry.value << endl;
}
}
}
}
};
int main() {
cout << "Name: ADITYA PANDEY\nRoll NO.: 24/SWE/10\n" << endl;
HashTable dictionary;
ADITYA PANDEY 24/SWE/
23 10
int choice;
string key, value;
cout << "\nChoose an operation:\n";
cout << "1. Insert a key-value pair\n";
cout << "2. Search for a key\n";
cout << "3. Delete a key\n";
cout << "4. Display dictionary contents\n";
cout << "0. Exit\n";
while (true) {
switch (choice) {
case 1:
cout << "Enter key to insert: ";
cin >> key;
cout << "Enter value for " << key << ": ";
cin.ignore();
getline(cin, value);
dictionary.insert(key, value);
break;
case 2:
cout << "Enter key to search: ";
cin >> key;
value = dictionary.search(key);
if (!value.empty()) {
cout << "Value for key '" << key << "': " << value << endl;
} else {
cout << "Key not found: " << key << endl;
}
break;
case 3:
cout << "Enter key to delete: ";
cin >> key;
dictionary.del(key);
break;
case 4:
dictionary.display();
break;
case 0:
cout << "Exiting program.\n";
return 0;
default:
cout << "Invalid choice. Try again.\n";
}
}
return 0;
}
LEARNINGS:
1. Hash Tables provide efficient key-value storage, allowing constant time average-
case complexity for insert, search, and delete operations.
2. Hash Functions are crucial to mapping keys to table indices. Choosing a good hash
function minimizes collisions, improving the efficiency of the hash table.
3. Collision Handling with Chaining allows multiple elements to occupy the same index,
with each index containing a list of key-value pairs.
4. Understanding hash tables is essential for implementing dictionaries and optimizing
data retrieval, which are foundational in database indexing and memory management.
THEORY:
In a binary tree, each node has at most two children, called the left and right child. Traversing a binary
tree involves visiting each node in a specific order, and the main traversal methods are Preorder,
Inorder, and Postorder.
Inorder Traversal:
Inorder traversal visits the node in the order: Left -> Root -> Right
Preorder Traversal:
Preorder traversal visits the node in the order: Root -> Left -> Right
Postorder Traversal:
Postorder traversal visits the node in the order: Left -> Right -> Root
SOURCE CODE:
#include <iostream>
#include <stack>
using namespace std;
struct Node {
int data;
Node* left;
Node* right;
Node(int value) : data(value), left(nullptr), right(nullptr) {}
};
class BinaryTree {
public:
Node* root;
BinaryTree() : root(nullptr) {}
void iterativePreorder() {
if (!root) return;
while (!stack.empty()) {
Node* node = stack.top();
stack.pop();
cout << node->data << " ";
if (node->right) stack.push(node->right);
if (node->left) stack.push(node->left);
}
}
void iterativeInorder() {
stack<Node*> stack;
Node* current = root;
current = stack.top();
stack.pop();
cout << current->data << " ";
current = current->right;
}
}
void iterativePostorder() {
if (!root) return;
if (node->left) stack1.push(node->left);
if (node->right) stack1.push(node->right);
}
while (!stack2.empty()) {
cout << stack2.top()->data << " ";
stack2.pop();
}
}
void preorderTraversal() {
cout << "Preorder (Recursive): ";
recursivePreorder(root);
cout << endl;
void inorderTraversal() {
cout << "Inorder (Recursive): ";
recursiveInorder(root);
ADITYA PANDEY 24/SWE/
28 10
cout << endl;
void postorderTraversal() {
cout << "Postorder (Recursive): ";
recursivePostorder(root);
cout << endl;
int main() {
cout << "Name: ADITYA PANDEY\nRoll NO.: 24/SWE/10\n" << endl;
BinaryTree tree;
int choice, value;
cout << "\nChoose an operation:\n";
cout << "1. Insert a node\n";
cout << "2. Display Preorder traversal\n";
cout << "3. Display Inorder traversal\n";
cout << "4. Display Postorder traversal\n";
cout << "0. Exit\n";
while (true) {
switch (choice) {
case 1:
cout << "Enter value to insert: ";
cin >> value;
tree.insert(value);
cout << value << " inserted into the binary tree.\n";
break;
case 2:
tree.preorderTraversal();
break;
case 3:
tree.inorderTraversal();
break;
case 4:
tree.postorderTraversal();
break;
case 0:
cout << "Exiting program.\n";
return 0;
ADITYA PANDEY 24/SWE/
29 10
default:
cout << "Invalid choice. Try again.\n";
}
}
return 0;
}
OUTPUT:
LEARNINGS:
1. Recursive traversals provide an intuitive approach to tree operations, while non-recursive
traversals offer more control over memory usage.
2. Non-Recursive Traversals simulate recursion using explicit stacks, which can handle
large trees more efficiently in terms of memory, as they avoid potential stack overflow
issues.
3. Preorder Traversal is useful when the root node needs to be processed first, Inorder
Traversal is ideal for sorting (especially in binary search trees), and Postorder Traversal is
useful when child nodes need to be processed before the parent.
THEORY:
Graph: A graph is a collection of nodes (vertices) and edges connecting pairs of nodes. Graphs can be
represented in various ways, the most common being:
Adjacency Matrix: A 2D array where the cell at (i, j) indicates the presence or absence of an edge
between vertex i and vertex j.
Adjacency List: A collection of lists where each list corresponds to a vertex and contains the vertices
that are adjacent to it.
Breadth-First Search (BFS): BFS is an algorithm for traversing or searching tree or graph data
structures. It starts at a selected node (source) and explores all its neighbors at the present depth before
moving on to nodes at the next depth level.
Depth-First Search (DFS): DFS is another algorithm for traversing or searching through a graph. It
starts at a root node and explores as far as possible along each branch before backtracking.
SOURCE CODE:
#include <iostream>
#include <list>
#include <queue>
#include <stack>
#include <vector>
using namespace std;
class Graph {
private:
int vertices;
list<int>* adjList;
public:
Graph(int V) {
vertices = V;
adjList = new list<int>[V];
}
~Graph() {
delete[] adjList;
}
cout << "BFS starting from vertex " << start << ": ";
while (!q.empty()) {
int current = q.front();
q.pop();
cout << current << " ";
for (int neighbor : adjList[current]) {
if (!visited[neighbor]) {
visited[neighbor] = true;
q.push(neighbor);
}
}
}
cout << endl;
}
int main() {
cout << "Name: ADITYA PANDEY\nRoll NO.: 24/SWE/10\n" << endl;
int V, choice, v, w;
cout << "Enter the number of vertices: ";
cin >> V;
Graph g(V);
cout << "\nMenu:\n";
cout << "1. Add an edge\n";
cout << "2. Perform BFS\n";
cout << "3. Perform DFS\n";
cout << "0. Exit\n";
ADITYA PANDEY 24/SWE/
32 10
while (true) {
switch (choice) {
case 1:
cout << "Enter two vertices to add an edge between (v w): ";
cin >> v >> w;
if (v >= V || w >= V || v < 0 || w < 0) {
cout << "Invalid vertices. Try again.\n";
} else {
g.addEdge(v, w);
cout << "Edge added between " << v << " and " << w << ".\n";
}
break;
case 2:
cout << "Enter starting vertex for BFS: ";
cin >> v;
if (v >= V || v < 0) {
cout << "Invalid vertex. Try again.\n";
} else {
g.BFS(v);
}
break;
case 3:
cout << "Enter starting vertex for DFS: ";
cin >> v;
if (v >= V || v < 0) {
cout << "Invalid vertex. Try again.\n";
} else {
g.DFS(v);
}
break;
case 0:
cout << "Exiting program.\n";
return 0;
default:
cout << "Invalid choice. Try again.\n";
}
}
return 0;
}
LEARNINGS:
1. BFS (Breadth-First Search) is useful for exploring a graph level by level and is
commonly used to find the shortest path in unweighted graphs. BFS requires a queue data
structure to track the nodes to be visited.
2. DFS (Depth-First Search) explores the graph by diving deep into one branch before
backtracking. DFS can be implemented recursively or iteratively using a stack. Recursive
DFS utilizes the call stack, while iterative DFS explicitly uses a stack.
3. Analyzed the time and space complexity of both algorithms, understanding that both BFS and DFS
run in O(V + E) time, where V is the number of vertices and E This understanding aids in evaluating
the efficiency of algorithms.
THEORY:
Sorting algorithms are fundamental algorithms that arrange the elements of a list or array in a
particular order (usually ascending or descending).
1. Bubble Sort: A simple comparison-based sorting algorithm. It repeatedly steps through the list,
compares adjacent elements, and swaps them if they are in the wrong order. This process continues
until no swaps are needed.
Complexity: O(n^2) in the worst and average cases; O(n) in the best case
2. Insertion Sort: Builds a sorted array one element at a time. It takes each element from the unsorted
portion and inserts it into its correct position in the sorted portion of the array.
Complexity: O(n^2) in the worst and average cases; O(n) in the best case (when the array is already
sorted).
3. Quick Sort: A divide-and-conquer algorithm. It selects a 'pivot' element, partitions the other
elements into two sub-arrays according to whether they are less than or greater than the pivot, and
recursively sorts the sub-arrays.
Complexity: O(n log n) on average; O(n^2) in the worst case (when the pivot is the smallest or largest
element).
4. Merge Sort: Another divide-and-conquer algorithm. It divides the array into two halves,
recursively sorts both halves, and then merges them back together in sorted order.
Complexity: O(n log n) for all cases, making it more predictable than Quick Sort.
5. Heap Sort: Utilizes a binary heap data structure. It builds a max heap from the input data and
repeatedly extracts the maximum element from the heap to build the sorted array.
Complexity: O(n log n) for all cases, and it is an in-place sorting algorithm.
6. Radix Sort: A non-comparative sorting algorithm that sorts numbers by processing individual
digits. It sorts the input numbers by their least significant digit to the most significant digit using a
stable sub-sorting algorithm (like counting sort).
Complexity: O(nk), where n is the number of elements and k is the number of digits in the largest
number.
7. Binary Tree Sort: Involves inserting elements into a binary search tree (BST) and then performing
an in-order traversal of the tree to retrieve the sorted elements. This relies on the properties of BSTs,
where left children are smaller and right children are larger.
Complexity: O(n log n) on average for balanced trees; O(n^2) in the worst case for unbalanced trees.
// Bubble Sort
void bubbleSort(vector<int>& arr) {
int n = arr.size();
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr[j], arr[j + 1]);
}
}
}
}
// Insertion Sort
void insertionSort(vector<int>& arr) {
int n = arr.size();
for (int i = 1; i < n; i++) {
int key = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
// Quick Sort
int partition(vector<int>& arr, int low, int high) {
int pivot = arr[high];
int i = low - 1;
for (int j = low; j < high; j++) {
if (arr[j] < pivot) {
i++;
swap(arr[i], arr[j]);
ADITYA PANDEY 24/SWE/
36 10
}
}
swap(arr[i + 1], arr[high]);
return i + 1;
}
// Merge Sort
void merge(vector<int>& arr, int left, int mid, int right) {
int n1 = mid - left + 1;
int n2 = right - mid;
vector<int> L(n1), R(n2);
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
arr[k++] = L[i++];
} else {
arr[k++] = R[j++];
}
}
while (i < n1) arr[k++] = L[i++];
while (j < n2) arr[k++] = R[j++];
}
// Heap Sort
void heapify(vector<int>& arr, int n, int i) {
int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;
if (largest != i) {
swap(arr[i], arr[largest]);
heapify(arr, n, largest);
}
}
// Radix Sort
void countingSort(vector<int>& arr, int exp) {
int n = arr.size();
vector<int> output(n);
int count[10] = {0};
int main() {
string name;
string rollNo;
vector<int> arr;
return 0;
}
OUTPUT:
THEORY:
B-Tree: A B-tree is a self-balancing tree data structure that maintains sorted data and allows for
efficient insertion, deletion, and search operations. It is designed to work well on disk storage systems,
making it suitable for databases and filesystems.
Node Structure: Each node can contain multiple keys and child pointers. A B-tree of order 𝑚 can
B-trees are characterized by the following properties:
SOURCE CODE:
#include <iostream>
using namespace std;
class BTreeNode {
int *keys; // Array of keys in the node
int t; // Minimum degree (defines the range for the number of keys)
BTreeNode **C; // Array of child pointers
int n; // Current number of keys
bool leaf; // True if leaf node
public:
BTreeNode(int _t, bool _leaf);
// Fill the child C[idx] which has less than t-1 keys
void fill(int idx);
class BTree {
BTreeNode *root; // Pointer to root node
int t; // Minimum degree
public:
BTree(int _t) { root = nullptr; t = _t; }
void traverse() {
if (root != nullptr) root->traverse();
}
BTreeNode* search(int k) {
return (root == nullptr) ? nullptr : root->search(k);
}
void BTreeNode::insertNonFull(int k) {
int i = n - 1;
if (leaf) {
while (i >= 0 && keys[i] > k) {
keys[i + 1] = keys[i];
i--;
}
keys[i + 1] = k;
n = n + 1;
} else {
while (i >= 0 && keys[i] > k) i--;
if (C[i + 1]->n == 2 * t - 1) {
splitChild(i + 1, C[i + 1]);
if (keys[i + 1] < k) i++;
}
C[i + 1]->insertNonFull(k);
}
}
int main() {
cout << "Name: ADITYA PANDEY\nRoll NO.: 24/SWE/10\n" << endl;
BTree t(3); // A B-Tree with minimum degree 3
int n, val;
cout << "Enter the number of elements to insert: ";
cin >> n;
cout << "Enter " << n << " elements:\n";
for (int i = 0; i < n; i++) {
cin >> val;
t.insert(val);
}
return 0;
}
OUTPUT:
THEORY:
Min-Max Heap: A Min-Max heap is a specialized tree-based data structure that supports both
minimum and maximum heap properties.
It is a complete binary tree where:
Min Level: The nodes at even levels (starting from the root at level 0) maintain the min-heap property,
meaning that the value of each node is less than or equal to the values of its children.
Max Level: The nodes at odd levels maintain the max-heap property, meaning that the value of each
node is greater than or equal to the values of its children.
This structure allows efficient access to both the minimum and maximum elements, making it suitable
for applications requiring both operations.
SOURCE CODE:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class MinMaxHeap {
private:
vector<int> heap;
if (smallest != index) {
swap(heap[index], heap[smallest]);
trickleDownMin(smallest);
}
}
if (largest != index) {
swap(heap[index], heap[largest]);
trickleDownMax(largest);
}
}
public:
void insert(int value) {
heap.push_back(value);
trickleUp(heap.size() - 1);
}
void deleteMin() {
if (heap.empty()) {
cout << "Heap is empty" << endl;
return;
}
heap[0] = heap.back();
heap.pop_back();
if (!heap.empty()) trickleDown(0);
}
void display() {
for (int val : heap) {
cout << val << " ";
}
cout << endl;
}
};
int main() {
cout << "Name: ADITYA PANDEY\nRoll NO.: 24/SWE/10\n" << endl;
MinMaxHeap heap;
return 0;
}
OUTPUT:
LEARNINGS:
1. Min-Max Heaps provide an efficient way to manage and retrieve both minimum
and maximum values.
2. The Min-Max Heap is useful for applications requiring frequent access to both the
minimum and maximum values.
3. Implementing Min-Max Heap operations requires careful handling of elements to
maintain the structure’s alternating min-max levels.
THEORY:
AVL Tree: An AVL tree is a self-balancing binary search tree (BST) that maintains its balance
through height balancing. Named after its inventors Adelson-Velsky and Landis, an AVL tree ensures
that the heights of the two child subtrees of any node differ by no more than one. This property
guarantees logarithmic height, which leads to efficient operations.
Key Properties:
Balance Factor: For any node in the tree, the balance factor is defined as the height of the left
subtree minus the height of the right subtree. The balance factor can be -1, 0, or +1 for a balanced
AVL tree.
Self-Balancing: After insertions and deletions, the tree automatically rebalances itself through
rotations (single or double) to maintain the AVL property.
SOURCE CODE:
#include <iostream>
using namespace std;
class Node {
public:
int key;
Node* left;
Node* right;
int height;
int height(Node* N) {
return N ? N->height : 0;
}
int getBalance(Node* N) {
return N ? height(N->left) - height(N->right) : 0;
}
void updateHeight(Node* N) {
N->height = 1 + max(height(N->left), height(N->right));
}
Node* rightRotate(Node* y) {
updateHeight(y);
updateHeight(x);
return x;
}
Node* leftRotate(Node* x) {
Node* y = x->right;
Node* T2 = y->left;
y->left = x;
x->right = T2;
updateHeight(x);
updateHeight(y);
return y;
}
updateHeight(node);
int balance = getBalance(node);
updateHeight(root);
int balance = getBalance(root);
int main() {
cout << "Name: Aditya Pandey\nRoll NO.: 24/SWE/10\n" << endl;
switch (choice) {
case 1:
cout << "Enter value to insert: ";
cin >> value;
root = insert(root, value);
cout << "Node inserted successfully." << endl;
break;
case 2:
cout << "Enter value to delete: ";
cin >> value;
root = deleteNode(root, value);
cout << "Node deleted successfully." << endl;
break;
case 3:
cout << "In-order traversal of the AVL tree: ";
inOrder(root);
cout << endl;
break;
case 4:
cout << "Exiting program.\n";
break;
default:
cout << "Invalid choice. Please try again." << endl;
}
return 0;
}
LEARNINGS:
1. AVL Trees maintain balance by performing rotations after insertions and deletions,
ensuring O(log n) time complexity for these operations.
2. The balance factor helps in determining the type of rotation needed to maintain the
AVL property.
3. Implementing AVL Tree operations requires careful handling of node heights
and rebalancing conditions.