0% found this document useful (0 votes)
15 views27 pages

Understanding Recursion in Programming

Uploaded by

wondete4
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as ODT, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
15 views27 pages

Understanding Recursion in Programming

Uploaded by

wondete4
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as ODT, PDF, TXT or read online on Scribd

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

You might also like