MICROLINK INFORMATION AND BUSINESS COLLAGE
DATA STRUCTURE AND ALGORITHM
INDIVIDUAL ASSIGNMENT
Name soliana adhanom
Id no mdr/1634/19
Department SE(R)
add
1. Explain the differences between arrays and linked lists in terms of memory allocation,
insertion/deletion efficiency, and traversal. Provide examples where each data structure is more suitable
ARRAY LINKED LIST
-Size of array is fixed -Size of list is not fixed
-Memory is allocated from stack -memory is allocated from heap
-It is necessary to specify the number -it is not necessary to specify z number
of elements during declaration of elements during declaration (memory
-It occupies less memory tgan linked list for is allocated during run time)
The same elements
-Inserting new elements at the front is potentially - inserting a new element at any posit-
Expensive because existing elements need to be on can be carried out easily
Shifted over to make room
-Deleting an element froman array is not possible -deleting an element is possible
-Use an array when you need fast access by index -use a linked list when u expect freqent
and know the size ahead of time inseriton/deletion (e.g implementing
(e.g., storing fixed-size data) a queue
2. Compare recursion and iteration. Write a C++ function to calculate the Fibonacci sequence using both
approaches and discuss their time/space complexities.
• Recursion involves a function calling itself to solve smaller instances of the problem. It can be more
elegant but may lead to stack overflow if too deep.
• Iteration uses loops to repeat a block of code until a condition is met. It is usually more memory-
efficient.
• Fibonacci Sequence Example:
// Recursive Fibonacci
int fibonacci_recursive(int n) {
if (n <= 1) return n;
return fibonacci_recursive(n - 1) + fibonacci_recursive(n - 2);
}
// Iterative Fibonacci
int fibonacci_iterative(int n) {
if (n <= 1) return n;
int a = 0, b = 1;
for (int i = 2; i <= n; i++) {
int temp = a + b;
a = b;
b = temp;
return b;
• Time/Space Complexity:
• Recursive: O(2^n) time, O(n) space due to call stack.
• Iterative: O(n) time, O(1) space.
3. Define hash table collisions and explain two resolution strategies (e.g., chaining, open addressing).
Provide code snippets illustrating each method.
• A hash table collision occurs when two keys hash to the same index in the hash table.
• Resolution Strategies:
• Chaining: Each slot in the hash table contains a linked list of entries that hash to the same index.
struct Node {
int key;
Node* next;
};
class HashTable {
Node** table;
int size;
public:
HashTable(int s) : size(s) { table = new Node*[size](); }
void insert(int key) {
int index = key % size;
Node* newNode = new Node{key, table[index]};
table[index] = newNode;
};
• Open Addressing: All elements are stored in the array itself, and a probing sequence is used to find
an empty slot.
class HashTable {
int* table;
int size;
public:
HashTable(int s) : size(s) { table = new int[size](); }
void insert(int key) {
int index = key % size;
while (table[index] != 0) { // Assuming 0 means empty
index = (index + 1) % size; // Linear probing
table[index] = key;
};
4. Tree Traversals:
Describe in-order, pre-order, and post-order traversal algorithms for binary trees. Manually traverse the
following tree and list the nodes in each order:
A . In-order: Left, Root, Right .Post-order: Left, Right, Root
/\ . Manual Traversal of Given Tree: .Pre-order: Root, Left, Right
BC
/\\ In-order: D, B, E, A, C, F
DEF Pre-order: A, B, D, E, C, F
Post-order: D, E, B, F, C, A
5. Analyze the time complexity of the following code using Big-O notation:
for (int i = 0; i < n; i *= 2) {
for (int j = 0; j < i; j++) {
cout << j << endl;
• The outer loop runs as long as i is less than n. However, i starts at 0 and never changes due to i *= 2,
which means the loop never executes.
• Time complexity: O(1).
6. Implement a stack using a singly linked list in C++. Include push, pop, and peek operations. Handle
edge cases like underflow
struct Node
int data;
Node* next;
};
class Stack {
Node* top;
public:
Stack() : top(nullptr) {}
void push(int value) {
Node* newNode = new Node{value, top};
top = newNode;
void pop() {
if (top == nullptr) throw std::underflow_error("Stack Underflow");
Node* temp = top;
top = top->next;
delete temp;
int peek() {
if (top == nullptr) throw std::underflow_error("Stack Underflow");
return top->data;
};
7. Create a BST in C++ with functions to insert nodes, delete nodes, and search for a value. Demonstrate
inserting [15, 10, 20, 8, 12, 17, 25] and deleting the node with value 15.
struct TreeNode {
int value;
TreeNode* left;
TreeNode* right;
TreeNode(int val) : value(val), left(nullptr), right(nullptr) {}
};
class BST {
TreeNode* root;
void insert(TreeNode*& node, int value) {
if (!node) {
node = new TreeNode(value);
} else if (value < node->value) {
insert(node->left, value);
} else {
insert(node->right, value);
TreeNode* deleteNode(TreeNode* node, int value) {
if (!node) return nullptr;
if (value < node->value) {
node->left = deleteNode(node->left, value);
} else if (value > node->value) {
node->right = deleteNode(node->right, value);
} else {
// Node with one child or no child
if (!node->left) return node->right;
else if (!node->right) return node->left;
// Node with two children
TreeNode* minNode = node->right;
while (minNode && minNode->left) minNode = minNode->left;
node->value = minNode->value;
node->right = deleteNode(node->right, minNode->value);
return node;
}
public:
BST() : root(nullptr) {}
void insert(int value) { insert(root, value); }
void deleteValue(int value) { root = deleteNode(root, value); }
};
// Demonstration
BST tree;
int values[] = {15, 10, 20, 8, 12, 17, 25};
for (int val : values) tree.insert(val);
tree.deleteValue(15)
8. Implement a graph using an adjacency list in C++. Include functions to add edges and perform BFS
traversal starting from a given node.
#include <iostream>
#include <vector>
#include <queue>
class Graph {
std::vector<std::vector<int>> adjList;
public:
Graph(int vertices) : adjList(vertices) {}
void addEdge(int u, int v) {
adjList[u].push_back(v);
adjList[v].push_back(u); // For undirected graph
void BFS(int start) {
std::vector<bool> visited(adjList.size(), false);
std::queue<int> q;
visited[start] = true;
q.push(start);
while (!q.empty()) {
int node = q.front();
q.pop();
std::cout << node << " ";
for (int neighbor : adjList[node]) {
if (!visited[neighbor]) {
visited[neighbor] = true;
q.push(neighbor);
};
9. Write a C++ program to sort an array using the merge sort algorithm. Explain the divide-and-conquer
approach and provide a step-by-step breakdown for sorting [9, 3, 7, 5, 6, 4, 8, 2].
#include <iostream>
#include <vector>
void merge(std::vector<int>& arr, int left, int mid, int right) {
int n1 = mid - left + 1;
int n2 = right - mid;
std::vector<int> L(n1), R(n2)
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];
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++];
void mergeSort(std::vector<int>& arr, int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2;
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
merge(arr, left, mid, right);
// Example usage:
int main() {
std::vector<int> arr = {9, 3, 7, 5, 6, 4, 8, 2};
mergeSort(arr, 0, arr.size() - 1);
for (int num : arr)
std::cout << num << " ";
return 0;
• Divide-and-Conquer Approach: Merge sort divides the array into halves recursively until single
elements are reached and then merges them back together in sorted order.
10.Design a C++ program to dynamically create a 2D array, populate it with user input, and deallocate
memory properly. Explain pointer arithmetic in your code.
#include <iostream>
int main() {
int rows, cols;
std::cout << "Enter number of rows and columns: ";
std::cin >> rows >> cols;
// Dynamically allocate a 2D array
int** array = new int*[rows];
for (int i = 0; i < rows; ++i)
array[i] = new int[cols];
// Populate the array with user input
for (int i = 0; i < rows; ++i)
for (int j = 0; j < cols; ++j)
std::cin >> array[i][j];
// Display the array
std::cout << "The entered array is:\n";
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < cols; ++j)
std::cout << array[i][j] << " ";
std::cout << std::endl;
}
// Deallocate memory
for (int i = 0; i < rows; ++i)
delete[] array[i];
delete[] array;
return 0;
• Pointer Arithmetic: In C++, pointer arithmetic allows you to navigate through memory locations. For
example:
int *ptr = array[0]; // Points to the first row
11. Greedy Algorithm Application:
Solve the "Coin Change" problem using a greedy approach (e.g., coins = [1, 5, 10]). Explain why this
method works for the given denominations but fails for others like [1, 3, 4]
Problem Statement: Given a set of coin denominations, determine the minimum number of coins
needed to make a specific amount.
Example Denominations: coins = [1, 5, 10]
Greedy Approach:
1. Start with the largest denomination and use as many of those coins as possible without exceeding the
target amount.
2. Move to the next largest denomination and repeat until the amount is reached.
Why It Works for [1, 5, 10]:
• For any amount, you can always use the largest coin available first. This ensures that you minimize the
total number of coins used because larger denominations cover more value with fewer coins.
Example:
• To make 28:
• Use 2 coins of 10 (20)
• Use 1 coin of 5 (5)
• Use 3 coins of 1 (3)
• Total coins = 2 + 1 + 3 = 6
Why It Fails for [1, 3, 4]:
• Consider making 6 with denominations [1, 3, 4]: • Greedy would choose one 4 (leaving 2), then it
would need two 1s (total of 3 coins).
• The optimal solution is two 3s (total of 2 coins).
12. Backtracking: N-Queens Problem in C++
Implement the N-Queens problem using backtracking in C++. Print one valid configuration for a 4x4
chessboard and explain how backtracking avoids invalid placements Problem Statement: Place N queens
on an N x N chessboard such that no two queens threaten each other.
C++ Implementation:
#include <iostream>
#include <vector>
using namespace std;
bool isSafe(vector<vector<int>>& board, int row, int col, int N) {
for (int i = 0; i < col; i++) {
if (board[row][i] == 1) return false; // Check row
for (int i = row, j = col; i >= 0 && j >= 0; i--, j--) {
if (board[i][j] == 1) return false; // Check upper diagonal
for (int i = row, j = col; j >= 0 && i < N; i++, j--) {
if (board[i][j] == 1) return false; // Check lower diagonal
return true;
bool solveNQueensUtil(vector<vector<int>>& board, int col, int N) {
if (col >= N) return true;
for (int i = 0; i < N; i++) {
if (isSafe(board, i, col, N)) {
board[i][col] = 1; // Place queen
if (solveNQueensUtil(board, col + 1, N)) return true;
board[i][col] = 0; // Backtrack
return false;
void solveNQueens(int N) {
vector<vector<int>> board(N, vector<int>(N, 0));
if (!solveNQueensUtil(board, 0, N)) {
cout << "Solution does not exist" << endl;
return;
for (const auto& row : board) {
for (int val : row) cout << val << " ";
cout << endl;
int main() {
solveNQueens(4);
return 0;
Explanation:
• The function isSafe checks if placing a queen at a specific position is safe from attacks.
• The function solveNQueensUtil uses recursion to place queens in columns one by one.
• If placing a queen leads to a solution, it returns true; otherwise, it backtracks by removing the queen.
13. Dynamic Programming: Fibonacci Sequence
Use dynamic programming to solve the Fibonacci sequence problem. Compare the time complexity of
your solution with the recursive approach and explain memoization.
Problem Statement: Calculate the nth Fibonacci number.
#include <iostream>
#include <vector>
using namespace std;
int fibonacci(int n) {
vector<int> fib(n + 1);
fib[0] = 0;
fib[1] = 1;
for (int i = 2; i <= n; i++) {
fib[i] = fib[i - 1] + fib[i - 2];
return fib[n];
int main() {
int n = 10; // Example input
cout << "Fibonacci(" << n << ") = " << fibonacci(n) << endl;
return 0;
Comparison with Recursive Approach:
• Recursive Approach: O(2^n) time complexity due to repeated calculations.
• Dynamic Programming Approach: O(n) time complexity as it stores results of previous computations
(memoization).
Memoization Explanation:
• Memoization involves storing the results of expensive function calls and returning the cached result
when the same inputs occur again. This avoids redundant calculations and speeds up the execution
significantly.
14. Queue Using Stacks in C++
Design a queue using two stacks. Provide C++ code for enqueue and dequeue operations and analyze
the time complexity.
Designing a Queue Using Two Stacks:
#include <iostream>
#include <stack>
using namespace std;
class QueueUsingStacks {
private:
stack<int> stack1, stack2;
public:
void enqueue(int x) {
stack1.push(x);
int dequeue() {
if (stack2.empty()) {
while (!stack1.empty()) {
stack2.push(stack1.top());
stack1.pop();
}
if (stack2.empty()) throw runtime_error("Queue is empty");
int front = stack2.top();
stack2.pop();
return front;
};
int main() {
QueueUsingStacks q;
q.enqueue(1);
q.enqueue(2);
cout << q.dequeue() << endl; // Output: 1
q.enqueue(3);
cout << q.dequeue() << endl; // Output: 2
return 0;
Time Complexity Analysis:
• Enqueue Operation: O(1) since we just push onto stack1.
• Dequeue Operation: Amortized O(1). If stack2 is empty, we transfer elements from stack1 to stack2,
which takes O(n), but this happens infrequently.
15. Binary Search:
Write a C++ function to perform binary search on a sorted array. Test it with the input [2, 5, 8, 12, 16,
23, 38, 56] and search for 23. Explain why binary search requires a sorted dataset.
Binary Search Function:
#include <iostream>
using namespace std;
int binarySearch(int arr[], int size, int target) {
int left = 0, right = size - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) return mid;
else if (arr[mid] < target) left = mid + 1;
else right = mid - 1;
return -1; // Not found
int main() {
int arr[] = {2, 5, 8, 12, 16, 23, 38, 56};
int target = 23;
int result = binarySearch(arr, sizeof(arr)/sizeof(arr[0]), target);
if (result != -1)
cout << "Element found at index: " << result << endl;
else
cout << "Element not found" << endl;
return 0;