BULE HORA UNIVERSITY
COLLEGE OF COMPUTING AND INFORMATICS
DEPARTMENT OF SOFTWARE ENGINEERING
COURSE TITLE :-DATA STRUCTURE AND ALGORITHM
COURSE CODE:-SENG2122
INDIVIDUAL ASSIGNMENT
PREPARED BY:- NAME ID
WONDIMAGN TEREFA------------------------0701/16
Submitted to:- [Link]
Submitted data: 13/917 E.C
Bule Hora Ethiopia
1. Recursion Function and types of recursion function
What is Recursion?
Recursion is a programming paradigm where a function solves a problem by calling
itself as a subroutine. This technique is often used when the problem can be broken
down into smaller, self-similar subproblems. Imagine a set of Russian nesting dolls,
where each doll is a smaller version of the previous one, until you reach the smallest
doll which cannot contain any more.
In programming, a recursive function effectively delegates the solution of a smaller
instance of the same problem to itself.
Key Components of a Recursive Function:
1. Base Case: This is the most critical part. The base case is a condition that stops the
recursion. Without a base case, the function would call itself indefinitely, leading to an
infinite loop and eventually a stack overflow error (as each call consumes memory on
the call stack). The base case provides a direct solution for the simplest form of the
problem.
2. Recursive Step (or Recursive Call): This is where the function calls itself with a
modified input, usually moving closer to the base case. The idea is that the current
problem's solution depends on the solution of a smaller instance of the same problem.
How Recursion Works (Call Stack Analogy)
When a function is called, an activation record (or stack frame) is pushed onto the call
stack. This record contains essential information for that function call, including:
• Parameters passed to the function.
• Local variables declared within the function.
• The return address (where execution should resume after the function completes).
When a recursive function calls itself, a new activation record is pushed onto the stack for each call.
The execution flow proceeds as follows:
1. An initial call to function(n) is made. Its activation record is pushed.
2. Inside function(n), if n is the base case, the function returns a value, and its activation
record is popped.
3. If n is not the base case, function(n) calls function(n-1) (or some other smaller
subproblem). A new activation record for function(n-1) is pushed onto the stack.
4. This process continues until the base case is reached.
Paga 1
5. Once the base case returns a value, the stack begins to unwind. Each function call returns its
result to its caller, and its activation record is popped from the stack. This continues until the
initial call returns its final result.
Example 1: Factorial Calculation in C++
The factorial of a non-negative integer n, denoted as n, is the product of all positive
integers less than or equal to n.
n=n
times(n−1)
times(n−2)
times
dots
times1
By definition, 0=1.
#include <iostream>
// Function to calculate factorial recursively
long long factorial(int n) {
// Base Case: When n is 0, recursion stops and returns 1
if (n == 0) {
return 1;
}
// Recursive Step: n * factorial of (n-1)
else {
// This line shows the recursive call:
// factorial(n) depends on factorial(n-1)
return n * factorial(n - 1);
}
Paga 2
}
int main() {
int num = 5;
long long result = factorial(num);
std::cout << "Factorial of " << num << " is: " << result << std::endl;
// Expected output: Factorial of 5 is: 120
return 0;
}
Pros and Cons of Recursion
Pros:
Elegance and Readability: For problems that are naturally defined
recursively (e.g., tree traversals, certain mathematical sequences),
recursive solutions can be much cleaner, more intuitive, and easier to
understand than their iterative counterparts.
Reduced Code Size: Can sometimes lead to more concise code.
Problem Mapping: Directly maps to mathematical definitions of
problems like factorial or Fibonacci.
Cons:
. Performance Overhead: Each recursive call adds a new stack frame,
consuming memory and leading to performance overhead compared
to iteration (function call overhead)..
Stack Overflow: If the recursion goes too deep without reaching a base
case (or if the problem size is too large for the available stack
memory), it can exhaust the call stack, leading to a "stack overflow"
error.
. Debugging Difficulty: Tracing recursive calls can be more complex
than tracing iterative loops, especially with multiple branches.
Paga 3
. Redundant Calculations: As seen in the naive Fibonacci example,
without optimization techniques (like memoization or dynamic
programming), recursive functions can re-calculate the same
subproblems many times, leading to inefficiency.
Types of Recursion Functions
[Link] Recursion
A function calls itself directly from within its own body. This is the
most common type and what we saw in the factorial and Fibonacci
examples.
// Example: Direct recursion for sum of numbers
int sum_recursive(int n) {
if (n <= 0) {
return 0; }
return n + sum_recursive(n - 1); // Direct call to sum_recursive
// sum_recursive(3) -> 3 + sum_recursive(2) -> 3 + (2 +
sum_recursive(1)) -> 3 + (2 + (1 + sum_recursive(0))) -> 3 + (2 + (1
+ 0)) = 6
2. Indirect Recursion (or Mutual Recursion)
Two or more functions call each other in a cycle. Function A calls
Function B, and Function B, in turn, calls Function A (possibly
through other functions).
#include <iostream>
// Forward declarations are necessary for mutually recursive
functions
bool isEven(int n);
bool isOdd(int n);
bool isEven(int n) {
if (n == 0) {
Paga 4
return true;
// Calls isOdd
return isOdd(n - 1);
bool isOdd(int n) {
if (n == 0) {
return false;}
// Calls isEven
return isEven(n - 1);}
// int main() {
// std::cout << "Is 4 even? " << (isEven(4) ? "Yes" : "No") <<
std::endl; // Output: Yes
// std::cout << "Is 5 odd? " << (isOdd(5) ? "Yes" : "No") <<
std::endl; // Output: Yes
// return 0;
// }
2. Tail Recursion
A function is tail-recursive if the recursive call is the last operation
performed in the function before returning. This means there's no
further computation needed after the recursive call returns. Some
compilers (especially in functional languages) can optimize tail
recursion into an iterative loop, preventing stack overflow errors.
However, C++ compilers are not required to perform tail call
optimization (TCO) and many popular compilers (like GCC/Clang)
might not do it by default or consistently across all scenarios,
especially in debug builds.
#include <iostream>
Paga 5
// Tail-recursive function for factorial using an accumulator
long long factorial_tail_recursive(int n, long long accumulator = 1) {
// Base Case
if (n == 0) {
return accumulator;
// Recursive Step: The recursive call is the very last operation.
// The result of this call is directly returned.
return factorial_tail_recursive(n - 1, accumulator * n);
// int main() {
// std::cout << "Factorial (tail-recursive) of 5: " <<
factorial_tail_recursive(5) << std::endl;
// // Output: Factorial (tail-recursive) of 5: 120
// return 0;
// }
4. Non-Tail Recursion
If the recursive call is not the last operation, or if there's any
computation left to do after the recursive call returns, it's non-tail
recursion. Most recursive functions you write will be non-tail.
#include <iostream>
// Non-tail recursive function for sum of numbers
int non_tail_recursive_sum(int n) {
Paga 6
if (n == 0) {
return 0;
// The addition `n +` happens after the recursive call returns its
value.
return n + non_tail_recursive_sum(n - 1);
// int main() {
// std::cout << "Sum (non-tail-recursive) up to 5: " <<
non_tail_recursive_sum(5) << std::endl;
// // Output: Sum (non-tail-recursive) up to 5: 15
// return 0;
// }
5. Linear Recursion
A function that makes at most one recursive call each time it's
invoked. The call structure forms a straight line or a single chain.
Examples: factorial, sum of numbers, simple list traversals.
// Factorial is a prime example of linear recursion
long long factorial(int n) { /* as defined previously */ }
6. Tree Recursion
A function that makes multiple recursive calls within its body for
each invocation. The computation branches out, resembling a tree
structure. The classic example is the naive Fibonacci sequence.
// Fibonacci is a prime example of tree recursion
int fibonacci(int n) { /* as defined previously */ }
Paga 7
2. Self-Adjusting Trees (Splay
Trees)
What are Self-Adjusting Trees?
Self-adjusting trees are a class of binary search trees (BSTs) that
automatically restructure themselves to improve the performance of
future operations, especially for frequently accessed elements. Unlike
strictly balanced trees (like AVL trees or Red-Black trees) that
guarantee O(logn) worst-case performance for every operation, self-
adjusting trees aim for good amortized performance. This means that
a sequence of m operations, on average, performs well, even if a
single operation might occasionally take longer (O(n) in the worst
case for a single operation).The most prominent and widely studied
example of a self-adjusting tree is the Splay Tree.
Splay Trees:
A Splay Tree is a self-adjusting binary search tree with the additional
property that recently accessed elements are quickly accessible
again. It achieves this by moving the accessed node to the root of the
tree using a series of tree rotations.
Key Idea:
When a node X is accessed (searched, inserted, or deleted), a "splay"
operation is performed. This operation involves a sequence of
rotations that move X to the root of the tree. The path from the
original root to X is effectively flattened and restructured, making
future accesses to X or its nearby nodes faster.
Advantages:
Amortized O(logn) Time Complexity: While a single operation might
take O(n) in the worst case (e.g., accessing the deepest node in an
unbalanced tree), a sequence of m operations takes O(mlogn) time on
average. This makes them very efficient for real-world scenarios
where data access often exhibits locality of reference (i.e., recently
accessed items are likely to be accessed again soon).
Simpler to Implement: Compared to AVL or Red-Black trees, splay
trees can be simpler to implement as they don't require storing
balance factors or colors within each node.
Paga 8
No Explicit Balance Information: They don't need to store extra
information (like height or color) in each node, saving memory.
Disadvantages:
Worst-Case Single Operation Performance: A single operation can
take O(n) time, which might be undesirable for applications requiring
strict real-time guarantees.
Unpredictable Structure: The tree's structure changes frequently due
to splay operations, which can make debugging or understanding
specific snapshots of the tree more challenging.
Splay Operations (Rotations)
The core of splaying involves performing pairs of rotations (zig, zig-
zig, zig-zag) along the path from the accessed node to the root. Let X
be the accessed node, P its parent, and G its grandparent.
Node Structure:
struct SplayNode {
int key;
SplayNode* left;
SplayNode* right;
SplayNode* parent; // Parent pointer is helpful for splaying
SplayNode(int k) : key(k), left(nullptr), right(nullptr), parent(nullptr)
{}
Basic Rotation Helper Functions (Conceptual):
These are fundamental to any self-balancing BST.;
// Right rotation around 'p' (p becomes right child of p->left)
SplayNode* rightRotate(SplayNode* p) {
SplayNode* x = p->left;
p->left = x→right;
Paga 9
if (x->right) x->right->parent = p;
x->right = p;
x->parent = p->parent; // Update x's parent
p->parent = x; // Update p's parent
return x; // Return new root of the subtree
// Left rotation around 'p' (p becomes left child of p->right)
SplayNode* leftRotate(SplayNode* p) {
SplayNode* x = p->right;
p->right = x->left;
if (x->left) x->left->parent = p;
x->left = p;
x->parent = p->parent; // Update x's parent
p->parent = x; // Update p's parent
return x; // Return new root of the subtree
[Link] Rotation (When X is a child of the root or P has no
grandparent):
• If X is the left child of P, perform a right rotation around P.
• If X is the right child of P, perform a left rotation around P. This
brings X up one level.
<!-- end list -->
P X
/ \ -> /\
Paga 10
X R L P
/ \ / \
L C C R
2. Zig-Zig Rotation (When X, P, and G are all on the same side - linear
path):
• Left-Left (X and P are left children): Perform two right rotations.
First rotate P around G, then rotate X around P (the new P).
• Right-Right (X and P are right children): Perform two left
rotations. First rotate P around G, then rotate X around P (the
new P). This flattens the path and brings X up two levels.
<!-- end list -->
G X
/ /\
P -> L P
/\ /\
X C1 C2 G
/\ /\
L C2 C3 R
3. Zig-Zag Rotation (When X, P, and G are on alternating sides - bent
path):
• Left-Right (X is right child of P, P is left child of G): Rotate left
around P (brings X above P), then rotate right around G (brings
X above G).
• Right-Left (X is left child of P, P is right child of G): Rotate right
around P (brings X above P), then rotate left around G (brings X
above G). This also flattens the path and brings X up two levels.
<!-- end list -->
Paga 11
G X
/\ /\
P R -> P G
/\ /\/\
L X L C1 C2 R
/\
C1 C2
The splay operation consists of repeatedly applying these rotations
until X becomes the root.
Conceptual splay function (high-level logic):
// This is a simplified conceptual outline. A full implementation is
complex.
SplayNode* splay(SplayNode* root, SplayNode* x) {
if (x == nullptr) return root;
while (x->parent != nullptr) {
SplayNode* p = x->parent;
SplayNode* g = p->parent;
if (g == nullptr) { // Zig rotation (X is child of root)
if (x == p->left) {
root = rightRotate(p);
} else {
root = leftRotate(p);
} else if (x == p->left && p == g->left) { // Zig-Zig (left-left)
root = rightRotate(g); // Rotate P around G
Paga 12
root = rightRotate(p); // Then X around P (new P)
} else if (x == p->right && p == g->right) { // Zig-Zig (right-
right)
root = leftRotate(g); // Rotate P around G
root = leftRotate(p); // Then X around P (new P)
} else if (x == p->right && p == g->left) { // Zig-Zag (left-right)
root = leftRotate(p); // Rotate X around P
root = rightRotate(g); // Then X around G (new G is now X's
parent)
} else { // Zig-Zag (right-left)
root = rightRotate(p); // Rotate X around P
root = leftRotate(g); // Then X around G (new G is now X's
parent)
// Need to update root if rotations affect it, and ensure correct
parent pointers are established
// The actual `splay` implementation modifies direct parent
pointers of the tree.
}
return x; // X is now the new root
Operations in Splay Trees (High-Level):
Search/Access (Find): Perform a standard BST search for the node.
After finding it, perform a splay(root, found_node) operation to
•
bring it to the root. If the node is not found, splay the last
accessed node along the search path (e.g., the node where the
search terminates).
Insert: Insert the new node as in a standard BST. Then, splay the
newly inserted node to the root.
•
Delete:
Paga 13
•
1. Search for the node X to be deleted. Splay X to the root. Now, X
is the root, and it has no parent.
2. The tree is now split into two subtrees: X->left and X->right.
3. Find the maximum element in X->left (which will be its
rightmost node). Splay this maximum element to the root of the
X->left subtree. This new root will have no right child.
4. Make X->right the right child of this new root (which was the
maximum element from X->left). This merges the two subtrees
into a single splay tree, with the former maximum element from
the left subtree becoming the new overall root.
Splay trees are commonly used in caches, garbage collectors, and
other applications where locality of reference is important due to
their good amortized performance.
3. Heaps and Functions
What is a Heap?
A heap is a specialized tree-based data structure that satisfies the
heap property. It is typically implemented as a binary heap, which is
a complete binary tree.
Key Properties of a Binary Heap:
1. Heap Order Property:
Min-Heap: For every node N (except the root), the value of N is
less than or equal to the value of its parent. Consequently, the
•
smallest element is always at the root.
Max-Heap: For every node N (except the root), the value of N is
greater than or equal to the value of its parent. Consequently,
•
the largest element is always at the root.
2. Shape Property (Complete Binary Tree):
• All levels of the tree are fully filled, except possibly the last
level.
Paga 14
• Nodes on the last level are as far left as possible. This property
is crucial because it allows a binary heap to be efficiently
represented using a contiguous array (or std::vector in C++)
without explicit pointers.
Array Representation of a Binary Heap
Because of the complete binary tree property, a heap can be stored in
a simple array (or std::vector) without explicit pointers.
• The root is at index 0.
• For any node at index i:
Its left child is at index 2
timesi+1.
Its right child is at index 2
timesi+2.
Its parent is at index
lfloor(i−1)/2
rfloor.
Example:
A min-heap:
10
/ \
20 30
/\ /\
40 50 60 70
Array representation: [10, 20, 30, 40, 50, 60, 70]
Basic Heap Functions (Operations) in C++
Paga 15
All heap operations aim to maintain the heap property after an
insertion or deletion. We'll implement a MinHeap class using
std::vector.
#include <iostream>
#include <vector>
#include <algorithm> // For std::swap
class MinHeap {
private:
std::vector<int> heap_array;
// Helper function to maintain the min-heap property by bubbling
down a node
void heapifyDown(int index) {
int smallest = index;
int left_child = 2 * index + 1;
int right_child = 2 * index + 2;
int n = heap_array.size();
// If left child exists and is smaller than current smallest
if (left_child < n && heap_array[left_child] <
heap_array[smallest]) {
smallest = left_child;
// If right child exists and is smaller than current smallest
if (right_child < n && heap_array[right_child] <
heap_array[smallest]) {
smallest = right_child;
// If smallest is not the current node, swap and continue
heapifying down
if (smallest != index) {
Paga 16
std::swap(heap_array[index], heap_array[smallest]);
heapifyDown(smallest); // Recursively heapify the affected
subtree
}
// Helper function to maintain the min-heap property by bubbling
up a node
void heapifyUp(int index) {
// Continue bubbling up as long as the current node is not the
root
// and it's smaller than its parent
while (index > 0) {
int parent_index = (index - 1) / 2;
if (heap_array[index] < heap_array[parent_index]) {
std::swap(heap_array[index], heap_array[parent_index]);
index = parent_index; // Move to the parent's position
} else {
break; // Heap property satisfied
public:
MinHeap() {} // Default constructor
// Constructor to build a heap from an initial vector
MinHeap(const std::vector<int>& initial_data) {
heap_array = initial_data;
Paga 17
buildHeap();
// 1. buildHeap(): Builds a heap from an unsorted array
// Time Complexity: O(n)
void buildHeap() {
int n = heap_array.size();
// Start from the last non-leaf node and go up to the root
// The last non-leaf node is at index (n/2 - 1)
for (int i = n / 2 - 1; i >= 0; --i) {
heapifyDown(i);
// 2. insert(value): Inserts a new element into the heap
// Time Complexity: O(log n)
void insert(int value) {
heap_array.push_back(value); // Add to the end
heapifyUp(heap_array.size() - 1); // Bubble up the new element
// 3. extractMin(): Removes and returns the minimum element
(root)
// Time Complexity: O(log n)
int extractMin() {
if (heap_array.empty()) {
throw std::runtime_error("Heap is empty!");
Paga 18
int min_val = heap_array[0];
heap_array[0] = heap_array.back(); // Move last element to root
heap_array.pop_back(); // Remove the last element
if (!heap_array.empty()) {
heapifyDown(0); // Restore heap property from the root }
return min_val; }
// 4. peek(): Returns the minimum element without removing it
// Time Complexity: O(1)
int peek() const {
if (heap_array.empty()) {
throw std::runtime_error("Heap is empty!") }
return heap_array[0];
// Check if the heap is empty
bool isEmpty() const {
return heap_array.empty();
// Get the size of the heap
int size() const {
return heap_array.size();
}
Paga 19
// For debugging/demonstration: print heap array
void printHeap() const {
std::cout << "Heap: [";
for (size_t i = 0; i < heap_array.size(); ++i) {
std::cout << heap_array[i] << (i == heap_array.size() - 1 ?
"" : ", ");
std::cout << "]" << std::endl;
};
int main() {
// Demonstrate MinHeap operations
MinHeap myMinHeap;
[Link](30);
[Link](10);
[Link](50);
[Link](20);
[Link](5);
[Link](); // Expected: [5, 10, 30, 20, 50] (order of
20, 50 might vary)
std::cout << "Min element: " << [Link]() <<
std::endl; // Output: 5
int extracted = [Link]();
std::cout << "Extracted min: " << extracted << std::endl; //
Output: 5
[Link](); // Expected: [10, 20, 30, 50] (order
might vary)
Paga 20
std::cout << "Is heap empty? " << ([Link]() ? "Yes" :
"No") << std::endl; // Output: No
// Demonstrate building from an array
std::vector<int> data = {40, 10, 30, 50, 20, 15};
MinHeap builtHeap(data);
std::cout << "Built heap from array: ";
[Link](); // Expected: [10, 20, 15, 50, 40, 30]
return 0;
Applications of Heaps:
Priority Queues: Heaps are the most common and efficient
underlying data structure for implementing priority queues,
•
where elements are served based on their priority (e.g., job
scheduling in operating systems, event management in
simulations).
#include <queue> // For std::priority_queue
// int main() {
// // By default, std::priority_queue is a max-heap
// std::priority_queue<int> max_pq;
// max_pq.push(10);
// max_pq.push(30);
// max_pq.push(20);
// std::cout << "Max PQ top: " << max_pq.top() << std::endl; //
Output: 30
// max_pq.pop();
// std::cout << "Max PQ top after pop: " << max_pq.top() <<
std::endl; // Output: 20
Paga 21
// // To create a min-priority queue:
// std::priority_queue<int, std::vector<int>, std::greater<int>>
min_pq;
// min_pq.push(10);
// min_pq.push(30);
// min_pq.push(20);
// std::cout << "Min PQ top: " << min_pq.top() << std::endl; //
Output: 10
// return 0;
// }
Heap Sort: An efficient, in-place sorting algorithm with O(nlogn)
time complexity. It involves building a max-heap from the array
•
and then repeatedly extracting the maximum element (root) and
placing it at the end of the array.
• Graph Algorithms:
• Dijkstra's Shortest Path Algorithm (often uses a min-priority
queue to efficiently select the next vertex to visit).
• Prim's Minimum Spanning Tree Algorithm (also uses a min-
priority queue).
. Finding Kth Largest/Smallest Elements: You can use a min-heap (to
find the K largest) or a max-heap (to find the K smallest) to efficiently
find the Kth largest/smallest element in a collection in O(nlogk) time.
. Median Finding: Can be done efficiently using two heaps (one min-
heap for the larger half and one max-heap for the smaller half of
elements).
Paga 22
Summary
Heaps and hash functions are two foundational structures and techniques in
computer science. Heaps provide a way to manage and prioritize data efficiently,
especially in sorting and scheduling. Their binary tree structure allows for quick
access to the highest or lowest element, which is essential in algorithms like
Dijkstra’s and applications like job scheduling.
Hash functions, on the other hand, offer a mechanism for mapping large data into
smaller, manageable indices, enabling constant-time access in hash tables. The
effectiveness of hashing depends heavily on the quality of the hash function and
the strategy used to resolve collisions.
Paga 23