0% found this document useful (0 votes)
2 views68 pages

04 Recursion Tdd.pptx

This document covers recursion and test-driven development (TDD) in data structures and algorithms. It explains the concept of recursion, including stopping criteria, reduced input, and combining results, along with examples of recursive functions for summing arrays and sorting algorithms like Merge Sort and Quick Sort. The document highlights the advantages and considerations of using recursion compared to iterative approaches.

Uploaded by

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

04 Recursion Tdd.pptx

This document covers recursion and test-driven development (TDD) in data structures and algorithms. It explains the concept of recursion, including stopping criteria, reduced input, and combining results, along with examples of recursive functions for summing arrays and sorting algorithms like Merge Sort and Quick Sort. The document highlights the advantages and considerations of using recursion compared to iterative approaches.

Uploaded by

legendnoman61
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 68

Data Structures And Algorithms

Lesson 04:
Recursion and
Test Driven Development (TDD)

Nasir Ali Kalmati


Recursion
Wikipedia Definition

Recursion in computer science is a method of


solving a problem where the solution depends
on solutions to smaller instances of the same
problem (as opposed to iteration)… Most
computer programming languages support
recursion by allowing a function to call itself
from within its own code.
General Definition

Recursion: see definition of Recursion


A tricky concept first time you see it… as our brains are wired to
think more in an iterative way… but maybe memes help…
Let’s Try With Math…


Recursive Sum
• Idea: if we can sum up to n-1, then, to sum up to n, we can
just add n

• f(n) = n + f(n-1)

• eg, f(5) = 5 + f(4)


• f(4) = 4 + f(3)
• f(3) = 3 + f(2)
• f(2) = 2 + f(1)
• …
Stopping/Exit
Criterion
• f(n) = n + f(n-1)

• f(5) = 5 + f(4)
• f(4) = 4 + f(3)
• f(3) = 3 + f(2)
• f(2) = 2 + f(1)
• f(1) = 1 + f(0)
• f(0) = 0 + f(-1)
• f(-1) = -1 + f(-2)
• f(-2) = -2 + f(-3)
• etc.
What If No
Stopping?
• Before f(n) is completed, f(n-1) must be completed

• But before f(n-1) is completed, f(n-2) must be completed

• And so on…

• At each function call, we push a frame on the function call stack

• If no stopping criterion, we will push so many frames that we will


run out of memory eventually
• leading so to a stack overflow exception
Number of Recursive Calls
• Considering f(n) = n + f(n-1), how many frames will be
pushed?
• assume stopping condition for n<=1

• We would have O(n) frames

• Even if stopping condition, we can get a stack overflow for


large enough n
• eg, n = 100,000
Reduced Input

• In each recursive call, we must reduce the input

• How much input is reduced has huge impact on viability of


the recursive algorithm
• we must avoid stack overflows

• If the decrease is linear, then we will have a linear number of


frames on stack
• which will likely lead to a stack overflow for non-small inputs
Sum Array Example
• Let’s sum all values in an array a, from start index s to end
index e
• where sum(a) = sum(a,0,a.length-1)

• Considering 2 different recursive versions

• sumX(a,s,e) = a[s] + sumX(a,s+1,e)

• sumY(a,s,e) = sumY(a,s,middle) + sumY(a,middle+1,e)

• Stopping: s==e -> sum(a,s,e)=a[s]

• Both give the right answer, but which version is better?


sumX(a,s,e) = a[s] + sumX(a,s+1,e)
// a = {5,6,7,8}
sumX(a,s,e){ 0 recursion = ?
current = a[s] 0 current = 8
s==e ? return current 0 e=3
recursion = sumX(a,s+1,e) 0 s=3
return current + recursion 0 a=…
} recursion = ? recursion = ?
current = 7 current = 7
e=3 e=3
0 0 s=2 s=2
0 0 a=… a=…
0 recursion = ? recursion = ? recursion = ?
0 current = 6 current = 6 current = 6
0 e=3 e=3 e=3
0 s=1 s=1 s=1
0 a=… a=… a=…
recursion = ? recursion = ? recursion = ? recursion = ?
current = 5 current = 5 current = 5 current = 5
e=3 e=3 e=3 e=3
s=0 s=0 s=0 s=0
a=… a=… a=… a=…
• Until we reach stopping condition s==e, we keep on making
recursive calls, and push new frames on stack
• Recall a frame is popped only when its function is terminated
recursion = ?
current = 8
e=3
s=3
a=…
recursion = ? recursion = 8
current = 7 current = 7
e=3 e=3
s=2 s=2
a=… a=…
recursion = ? recursion = ? recursion =15
current = 6 current = 6 current = 6
e=3 e=3 e=3
s=1 s=1 s=1
a=… a=… a=…
recursion = ? recursion = ? recursion = ? recursion= 21
current = 5 current = 5 current = 5 current = 5
e=3 e=3 e=3 e=3
s=0 s=0 s=0 s=0
a=… a=… a=… a=…
public class SumArrayRecursive {

public static int sumX(int[] a, int s, int e) {


if (s == e) {
return a[s];
} else {
int current = a[s];
int recursionResult = sumX(a, s + 1, e);
return current + recursionResult;
}
}

public static void main(String[] args) {


int[] array = {5, 6, 7, 8};
int startIndex = 0;
int endIndex = array.length - 1;

int sum = sumX(array, startIndex, endIndex);


System.out.println("The sum of the array from index " + startIndex + " to " + endIndex +
" is: " + sum);
}
}
sumY(a,s,e) = sumY(a,s,middle) +
sumY(a,middle+1,e)
// a = {5,6,7,8}
sumY(a,s,e){ 0 left =?
s==e ? return a[s] 0 right = ?
middle = (s+e)/2 0 middle = ?
left = sumY(a,s,middle) 0 e=0
right = sumY(a,middle+1,e) 0 s=0
return left + right 0 a=…
} 0 left = ? left = ?
0 right = ? right = ?
0 middle = 0 middle = 0
0 e=1 e=1
0 s=0 s=0
0 a=… a=…
left = ? left = ? left = ?
right = ? right = ? right = ?
middle = 1 middle = 1 middle = 1
e=3 e=3 e=3
s=0 s=0 s=0
a=… a=… a=…
• Before we compute the right half, we wholly compute the left
half, including all its internal recursive calls (left and right)

left =? end = ?
right = ? right = ?
middle = ? middle =?
e=0 e=1
s=0 s=1
a=… a=…
left = ? left = 5 left = 5 left = 5
right = ? right = ? right = ? right = 6
middle = 0 middle = 0 middle = 0 middle = 0
e=1 e=1 e=1 e=1
s=0 s=0 s=0 s=0
a=… a=… a=… a=…
left = ? left = ? left = ? left = ? left = 11
right = ? right = ? right = ? right = ? right = ?
middle = 1 middle = 1 middle = 1 middle = 1 middle = 1
e=3 e=3 e=3 e=3 e=3
s=0 s=0 s=0 s=0 s=0
a=… a=… a=… a=… a=…
• Once we compute left=11, we enter in the call
right=sumY(middle+1,e)

end = ? left = ?
right = ? right =?
middle =? middle = ?
e=2 e=3
s=2 s=3
a=… a=…
left = ? left = ? left = 7 left = 7
right = ? right = ? right = ? right = ?
middle = 2 middle = 2 middle = 2 middle = 2
e=3 e=3 e=3 e=3
s=2 s=2 s=2 s=2
a=… a=… a=… a=…
left = 11 left = 11 left = 11 left = 11 left = 11
right = ? right = ? right = ? right = ? right = ?
middle = 1 middle = 1 middle = 1 middle = 1 middle = 1
e=3 e=3 e=3 e=3 e=3
s=0 s=0 s=0 s=0 s=0
a=… a=… a=… a=… a=…
• Once both left and right are recursively computed,
the final returned result is 11+15 = 26

left = ?
right =?
middle = ?
e=3
s=3
a=…
left = 7 left = 7
right = ? right = 8
middle = 2 middle = 2
e=3 e=3
s=2 s=2
a=… a=…
left = 11 left = 11 left = 11
right = ? right = ? right = 15
26
middle = 1 middle = 1 middle = 1
e=3 e=3 e=3
s=0 s=0 s=0
a=… a=… a=…
Space Complexity


Important to Remember

• 3 main aspects in recursive functions

• Stopping Criterion: otherwise no end, until a stack overflow

• Reduced Input: aim at least at halving it

• Combine Results: once recursive calls finished, combine


their outputs together for the final result
• often it is not as trivial as doing a +
Why Using Recursion???
Recursion vs. Iterative

• Iterative versions of algorithms are “usually” better


• recall that each recursive call has to create and push a new function call
frame

• However, there are many algorithms that are easier and


more efficient to write in a recursive form
• this will become more evident when we will start to work with Trees

• Recursive sorting: Merge Sort and Quick Sort


Merge Sort

• Divide and Conquer 4 5 1 3 2 6

• Recursive implementation

• Split the array in two 4 5 1 3 2 6

• Sort the two parts 1 4 5 2 3 6

• Merge the two parts once sorted 1 2 3 4 5 6


Recursion
• Writing mergeSort(T[] array)

• How to sort the 2 halves if my goal was to write a sort


algorithm???

• Recursion: reuse function you are writing, but on smaller data


mergeSort(T[] array){
mergeSort(array, 0, array.length / 2)
mergeSort(array, array.length /2 , array.length)
mergeHalves(array)
}
Recursion End

• To avoid infinite loop, you need to define a stopping


condition

• When do I know for certain that an array is sorted?

• Answer: when its size is at most 1

• So, stop recursion when reaching an half of size 1 or less


Merge of Halves
• By recursion, I can sort the 2 halves, but array with 2 sorted halves is not sorted

• Scan the 2 halves, and copy min to a new array

• Between N/2 and N comparisons: once reached end of one half, copy over the other

1 4 5 1 4 5

2 3 6 2 3 6

1 1 2

1 4 5 1 4 5

2 3 6 2 3 6

1 2 3 1 2 3 4
Cost


Considerations
• Asymptotically, does not exist comparison-based
sorting better than O(n log n)

• Merge-sort is therefore asymptotically optimal


• Ie, no instance for which get worse than O(n log n)

• But… more memory, need extra array buffer

• … might be not best on average


Wikipedia
• Besides book, Wikipedia is good source to read about
algorithms and data structures in layman terms
• https://en.wikipedia.org/wiki/Merge_sort
Quick Sort


Recursion
• Still Divide and Conquer algorithm, like Merge Sort

• Choose a value X (pivot)

• Move values <X before X, and >X after it

• After one step X is in the correct position

• Apply recursion on subarrays before and after X


6 3 7 5 4 2 1
• Choose a pivot, eg value 5

1 3 7 5 4 2 6
• Scan from left till > 5, from right till < 5

• Swap (eg 6 with 1), and continue


1 3 7 5 4 2 6

• Note how 3 is not touched, as < 5


1 3 2 5 4 7 6

• At the end, the pivot 5 is in the right


1 3 2 5 4 7 6
position

• On left side, all values < 5


1 3 2 4 5 7 6

• On right side, all values > 5


1 3 2 4 5 7 6

• Apply recursively on left and right of


pivot
Choice of Pivot
• Choice of pivot is crucial for
performance of Quick Sort 1 (and 7) is worst choice
• pivot = array[i] 4 3 7 1 5 2 6

• For performance, would like a pivot


value that gives the 2 partitions of
equal size 4 is best choice
4 3 7 1 5 2 6
• How to choose “i”?
• An option is to take “i” at random
• Another option is to take middle, which is
good when array nearly sorted
Which Sorting Algorithm to
Use?

• Unless you have very specific, advanced cases, you use the
default of what is given by the standard library of the
language/framework you use

• Most of the time, it will be a variant of Quick or Merge Sort


• eg in Java (and Python) the default is TimSort, which is an hybrid algorithm
based on Merge and Insertion Sort.
★ you can look at code directly at java.util.TimSort
Test Driven
Development (TDD)
TDD in One Slide

First write the test cases, which


will fail, and then write code to
make the test cases pass
What TDD is Not
• It is not a fancy tool you can buy/download

• No cutting edge novel technology

• No silver bullet solving all your software development


problems

• It is just a (relatively simple) design methodology to


improve productivity
Why TDD???

• Bugs in the field can be extremely expensive


• Ariane 5 explosion, Toyota braking system (100k recalled cars), etc

• Aim at improving quality (“testing”) within acceptable


cost

• Lot of hype on TDD as possible solution


Some Facts on TDD
• There isn’t much scientific evidence that it helps on
large scale systems
• Small improvements in quality, but also small decrease in
productivity

• But many anecdotal, success stories

• My opinion might be different from what you might


hear/read on TDD
TDD and Algorithms

• Algorithms is a good a place to start with TDD, as having


clear functions with clear expected outputs

• Lot of hype on TDD

• However, here we deal with small functions and classes,


and not whole applications
Do You Practice What You
Preach?
• TDD can be useful, especially for students / junior developers

• But to be honest, I am not using TDD...


• TDD is a bottom-up approach to software design, whereas I prefer top-down

• As any technique, its success depends on the context


• eg, type of software, stage in which TDD is introduced (at the beginning or in a
mature project), etc

• Can test software even without TDD

• Success of TDD depends also on management


General Principles of
TDD
- Empty code stub
- Test case that fails

- Integrate - Minimal coding to


changes (eg pass test case
commit to Git)

- Refactor to
improve
code
Example: A List

//Start from an “empty” stub that compiles

class List{

int size(){return -1; //wrong value}

}
First Unit Test

void testEmptyList(){

List list = new List();

Assert.equals( 0 , list.size());

// expected empty list


This will fail!
}
Fix The Code

class List{

int size(){return 0;}

Now the test case does not fail


Refactor Step
Improve code

class List{

int counter = 0;

int size(){return counter;}

}
Integration Step
• As all test cases do pass, make changes permanent

• Example: commit to repository


• Git, CVS, SVN, Mercurial, etc.

• Do you really need to at each TDD step??? I do not


think so...
• overhead, tedious, side effects if email generated for each
commit, etc.
Back to First Step
class List{

int counter = 0;

void insert(int value){ /* do nothing */}

int size(){return counter; }

}
Write A Second Test
void testInsertOneElement(){

List list = new List();

list.insert(42);

Assert.equals( 1 , list.size());

}
This will fail!
Minimal Fix
Does not work:
class List{ - testInsertOneElement does pass
- but testEmptyList will fail
int counter = 1;

void insert(int value){ }

int size(){return counter; }

}
Still need all passing tests
class List{

int counter = 0;

void insert(int value){ counter++;}

int size(){return counter; }

}
Refactoring Step
Do not do that!
class List{
You have no test
int[] data = new int[16]; case checking for
int counter = 0;
internal state!

void insert(int value){ data[counter]=value;

counter++;}

int size(){return counter; }

}
New Empty Stub
class List{

int counter = 0;

void insert(int value){ counter++;}

int size(){return counter; }

int getLast(){return 0;}

}
Write A Third Test
void testInsertAndGetOneElement(){

List list = new List();


Note: we are not
int value = 42; testing for “size”
here
list.insert(value);

Assert.equals( value , list.getLast());

}
This will fail!
Minimal Fix Before
Refactoring
class List{

int counter = 0;

void insert(int value){ counter++;}

int size(){return counter; }

int getLast(){return 42;}

}
And so on... keep
following the cycle

Test

Integrate Code

Refactor
Measuring Test
Effectiveness

• Big issue, regardless of TDD

• #bugs found in QA is a tricky measure

• Code coverage is often used


Set Of Test 54% Statement
TDD
Cases Coverage

“Regular” Set Of Test 54% Statement


Development Cases Coverage
TDD and Coverage

• TDD is not about more code coverage

• Managers can just set a target (eg >50%) and let


engineers decide how to achieve it

• TDD is about design


Coverage Does Not Tell
You Much
void testInsertOneElement(){

List list = new List();


Let’s say 15%
list.insert(42); coverage

int size = list.size();


Coverage
/
Assert.equals( 1 , size ); does not
/
change!!!
}
How Good Are Your Test
Assertions?

• Current tools cannot tell you that


• However, “Mutation Testing” is starting to get usable

• TDD helps you because force you to write effective


assertions
• tests first need to fail, and then pass
TDD is A Design
Methodology
• You can write tests even without TDD

• Incremental, small steps, each one verified

• Can be useful for complex code

• Can apply TDD only on subset of a project

• TDD might lead to write very different code


• impact on architecture
Homework

• Study Book Chapter 2.2 and 2.3

• Study code in the org.pg4200.les04 package

• Do exercises in exercises/ex04

• Extra: do exercises in the book

You might also like