DSAP-Lecture 4 - Recursion

Download as pdf or txt
Download as pdf or txt
You are on page 1of 40

Recursion

Data Structures & Algorithms with Python

Lecture 4
Outline
● Introduction
● Recursion Examples
● Analyzing the time-complexity of recursions
● Types of Recursion
● Designing Recursive Algorithms
● Tower of Hanoi Problem
● Summary
Introduction
Two ways to define repetition:

● Using loops: for-loop, while-loop etc.


● Using recursion.

Recursion: It is a technique by which a function makes one or more calls to itself


during execution, or by which a data structure relies upon smaller instances of the
very same type of structure in its presentation.

Few real examples of recursion in nature:

➔ Fractal patterns
➔ Russian Matryoshka Dolls
Illustrative Examples of recursion
● Factorial function
● English Ruler
● Binary Search
● File System
The Factorial Function
The number of ways in which n distinct items
can be arranged into a sequence = n!. In other
Definition: words, permutation of n items = n!.

For example, the characters a, b and c can be


arranged in 3! = 3.2.1 = 6 ways: abc, acb, bac,
Example: bca, cab and cba.

Recursive Definition:

A recursive definition contains one non-recursive


base case and one or more recursive cases.
● Each time a function is called, a structure known
as an activation record or frame is created to
store information about the progress of that
invocation of the function.

● Activation record includes a namespace for


storing the function call parameters and local
variables and information about which command
in the body of the function is currently executing.

● When the execution of a function leads to a


nested function call, the execution of the former
function call is suspended and its activation
record stores the place in the source code at
which the flow of control should continue upon
return of the nested call.

● There is a different activation record for each


active call.
Drawing an English Ruler
draw_ruler(2, 3)
center_length = 1 > 0
draw_interval(0)
center_length = 2 > 0 draw_line(1)
major length = 3 draw_interval(1) draw_interval(0)
num_inches =2 draw_line(2)
draw_line(3,’0’) draw_interval(1) Activation Record 3

J = 1, Activation Record 2
draw_interval(2)
center_length = 1 > 0
draw_line(3, ‘1’)
draw_interval(0)
draw_line(1)
J=2 Center_length = 1>0
draw_interval(0)
draw_interval(1) draw_interval(0)
draw_line(3, ‘2’) draw_line(1)
Activation Record 4
draw_interval(0)
Activation Record 1
Activation Record 5
Binary Search

● Sequential search is used for unsorted


sequence - Loop over each and every
element to find the target
low(0) High (n-1)

● Binary search is used to efficiently locate


a target value within a sorted sequence of
n elements Consider three cases:
➔ If target == data[mid], target is found,
search ends.
● For any index j in a sorted array, all the
values stored at indices 0, 1, … j-1 are ➔ If target < data[mid], recur the search
less than or equal to the value at index j. in the first half of the sequence.

● In BS, the sequence is partitioned into two ➔ If target > data[mid], recur the search
in the second half of the sequence.
sub-sequences and then the search is
performed only in one of the ➔ Repeat these steps until the interval
sub-sequence. [low, high] is empty or low > high.
Search ‘22’ in the array
File Systems

● In modern operating system,


the file-system directories (or
folders) are defined in a
recursive way.

● Many OS functions such as


copying or deleting implement
recursive algorithms.

● Example: cumulative disk


usage for all files and
directories within a particular
directory.
Following functions from Python’s os module will be used:

● os.path.getsize(path):
○ Return the immediate disk usage (measured in
bytes) for the file or directory that is identified by
the string path

● os.path.isdir(path):
○ Return True if entry designated by string path is
a directory; False otherwise

● os.listdir(path):
○ Return a list of strings that are the names of all
entries within a directory designated by string
path

● os.path.join(path, filename):
○ Compose the path string and filename string
using an appropriate operating system separator
between the two (‘/’ in Linux)

~ 56.8 MB
Analyzing Recursive Algorithms

● Computing Factorials
○ Each individual activation of factorial() executes a constant number of
operations.
○ To compute factorial(n), there are n+1 activations. The main function that
calls the factorial() function has its own activation record.
○ Time complexity ~ O(n)
Analyzing the English Ruler Example

● Drawing an English Ruler


○ For c >= 0, a call to draw_interval(c) results in precisely lines of output.

Proof by Induction:
Base case: For c = 0, (no lines are drawn)

Assume that above proposition holds true for . Hence,


draw_interval(c-1) prints lines of output

Inductive step: Each call to draw_interval(c) for c>0 spawns two calls to
draw_interval(c-1) and a single call to draw_line().
For any general c > 0, the number of lines printed:
Analyzing binary search algorithm

Proposition: The binary search algorithm runs in O(log n) time for a sorted sequence with
n elements.

Proof: The number of candidates to be search with each recursive call = high - low +1

The number of candidates to be searched is reduced by at least one half with each
recursive call. In general, after the jth call in a binary search, the number of candidates
remaining is at most

The maximum number of recursive calls to be performed is the smallest integer r such that

This implies that binary search runs in O(log n) time.


Analyzing disk space usage algorithm
Nesting Level 0

● Total number of file-system entries in a Nesting Level 1


given part of the file system is n (n = 19, in
this case).
● It can be proved by induction that there is Nesting Level 2

exactly one recursive invocation of


disk_usage() upon each entry at the
Nesting Level 3
nesting level k.
● In the worst case, it has running time of
order Nesting Level 4

● A tighter bound will be where n


is the total number of entries in the
file-system.
● Amortization: We can sometimes get a
tighter bound on a series of operations by
considering the cumulative effect, rather
than assuming that each achieves a worst
case.
# calls to disk_usage() function: 4
# calls to disk_usage() function: 4

No of entries in the file system is


n= 4. Case II: The folder contains 3
Case I: The folder contains 3 files. empty directories.

O(n) is a tighter bound on complexity for disk_usage() function.


Bad use of Recursion - Revisiting Element uniqueness problem

Let n = stop - start

If n = 1, running time of unique3() is O(1).

If general, a single call to unique3 for problem


size n may result in two recursive calls on
problems of size n-1.

Those two calls with size n-1 will lead to four


calls with a size of n-2 leading to 8 calls in turn
with size n-3 and so on …
Run time of function unique3 is
Total number of function calls is given by the
geometric summation:
unique(S, 0, 5) Each function call leads to two more function
calls to itself leading to exponential time
complexity.

unique(S, 0, 3)
0 1 2 3 4

S[0] != s[4]

0 1 2 3 4

unique(S, 1, 4)
Another bad use of Recursion: Bad_fibonacci

● Let be the number of calls performed in the


execution of bad_fibonacci(n) .

● The number of calls more than doubles for every


two consecutive indices. In other words,

● This implies that the function bad_fibonacci has a


time complexity of
An Efficient implementation of Fibonacci algorithm using recursion

● We modify the requirement by making it


return two consecutive numbers instead of
one.
● Each call leads to only one recursive call
with problem size reducing by 1 at each
call.
● So for n elements, n recursive calls are
made.
● So the time-complexity of
good_fibonacci(n) is O(n).
Maximum Recursive Depth in Python

● Each recursive call makes another recursive call without ever


reaching a base case - infinite recursion.

Example:

● Python limits the total number of function activations to some


fixed value (default = 1000). If this number is exceeded, a
RuntimeError or RecursionError exception is raised by the
interpreter.

● The default recursion limit can be changed:


Further Examples of Recursion

● Linear Recursion - one recursive call starts at most one other.

● Binary recursion - one recursive call may start two others.

● Multiple recursion - one recursive call may start three or more others.
Linear Recursion

Linear Recursion - one recursive call starts at most one other.

Examples:

➔ Factorial
➔ Good_fibonacci
➔ Binary search

Linear recursion terminology reflects the structure of the recursion trace, not
the asymptotic analysis of the running time.
Summing the elements of a sequence

n=4 linear_sum(S,4) S[3] + linear_sum(S,3) 20 + 30 = 50

n=3 linear_sum(S,3) S[2] + linear_sum(S,2) 15 + 15 = 30

n=2 linear_sum(S,2) S[1] + linear_sum(S,1) 10 + 5 = 15

n=1 linear_sum(S,1) S[0] + linear_sum(S, 0) 5+0 =5

n=0 linear_sum(S,0) 0 0

Run time
Reversing a Sequence with Recursion

● The code is guaranteed to terminate after


a total of recursive calls.

● Runtime
Recursive algorithms for computing Powers

● Trivial recursion definition

● Runtime
Let

Improved Recursion algorithm:


Binary Recursion

● One functions make two recursive calls.


● Examples:
○ English ruler
○ Bad_fibonacci
○ Binary_sum

Recursion trace of binary_sum(0,8)

● For n elements the depth of recursion is

● Binary sum uses of additional


space - activation frames
● However, the running time is as
there are function calls.
Multiple recursion

● Multiple recursion is a process in which a


function may take more than two recursive
calls.
● Examples:
○ Disk space usage of a file system.
○ Solving combinatorial puzzles.
Designing Recursive Algorithms

Recursive algorithms usually have the following Parameterizing a Recursion:


form:
● Sometimes it becomes necessary to
● Test the base case. introduce parameters to define a recursive
○ Begin by testing for a set of base cases algorithm.
(there should be at least one)
○ These base cases are defined so that
every possible chain of recursive calls will ● Examples:
eventually reach a base case, and the
base case should not use recursion. binary_search(data, target, low, high)
binary_sum(S, start, stop)
linear_sum(S,n)
● Recur: If not a base case, we perform one
reverse(S, start, stop)
or more recursive calls.
Advantages and Disadvantages of Recursion

● Advantages of Recursion
○ provides a succinct way of achieve repetition by avoiding nested loops.
○ Makes the code readable and efficient.

● The usefulness of recursion comes at a modest cost.


○ Python interpreter must maintain activation records that keep track of the
state of each nested call.
○ Such calls should be avoided where memory is at a premium.
○ Some forms of recursion can be eliminated without any use of auxiliary
memory. e.g - tail recursions.
Eliminating Tail Recursion

● A recursion is a tail recursion if any recursive call that is made from one context is the
very last operation in that context, with the return value of the recursive call immediately
returned by the enclosing recursion.

● By necessity, a tail recursion must be a linear recursion.

● Examples:

binary_search(data, target)
reverse(S, start, stop)

● Following are not valid examples of tail recursions (Why?):

factorial(n)
linear_sum(S,n)
● Any tail recursion can be
re-implemented non recursively by
enclosing the body in a loop for
repetition, and by replacing a
recursive call with new parameters
by a reassignment of the existing
parameters to those values.
Few More Examples of Recursion
Tower of Hanoi

● It consists of 3 rods or pegs or towers a number of


disks of different sizes which can slide onto any
rod.

● The puzzle starts with the disks on one rod in


ascending order of its size, smallest at the top,
thus making a conical tower shape.
Algorithm:
● The objective is to move the entire stack to ● Move top n-1 disks from source to Auxiliary tower.
another rod, satisfying the following rules:
● Move the nth disk from source to destination tower.
○ Only one disk may be moved at a time.
● Move the n-1 disks from auxiliary tower to the
○ Each move consists of taking the uppermost destination tower.
disk in one rod and sliding onto another rod
that may or may not have disks in them. ● Transferring the top n-1 disks from source to
auxiliary tower can again be considered as a fresh
problem which can be solved by the above 3 steps.
○ No disk may be placed on the top of a
smaller disk.
● Runtime complexity
● Binary recursion - one call leads to
two recursive calls.
● 4 disks requires 15 steps to solve
● 5 disks require 31 steps to solve
Move 0 Move 1 Move 2 Move 3

Move 4 Move 5
Move 6 Move 7

Move 8 Move 11
Move 9 Move 10

Move 12 Move 15
Move 13 Move 14
Check for a sorted array

● Given an array, check whether the array is


in sorted order
● Linear Recursion
● Runtime complexity is O(n)
Summary
We cover the following topics in this lecture:

● What is recursion and why is it needed?


● Some examples of recursive algorithm
● Analyzing their runtime complexity &
memory complexity
● Different types of recursion
● Downside of recursion

You might also like