610 likes | 697 Views
CS203 Lecture 2 . John Hurley Cal State LA. Recursion. Recursion is the technique of breaking down a problem into smaller instances of the same problem. In programming, a recursive algorithm is one that directly or indirectly invokes itself direct recursion: methodA calls methodA
E N D
CS203 Lecture 2 John Hurley Cal State LA
Recursion • Recursion is the technique of breaking down a problem into smaller instances of the same problem. • In programming, a recursive algorithm is one that directly or indirectly invokes itself • direct recursion: methodA calls methodA • indirect recursion: methodA calls methodB and methodB calls methodA
Recursion • Recursion is used in many common algorithms. It is also extremely important in functional programming, a long-established paradigm (way of thinking about and practicing programming) which is increasingly important today. • As you will learn in CS312, recursion is equivalent to iteration. In other words, any problem you can solve with either of these two techniques, you can also solve with the other. However: • a) many problems can be solved with simpler algorithms using recursion, but • b) most programmers find iteration easier to code
Stack Overflow • Each method call requires additional space on the call stack to track the data values for the current instance of the method • If the stack exceeds the memory available to it, a stack overflow occurs public class StackOverflowDemo { int x; public static void main(String[] args){ int x = 1; StackOverflowDemo s = new StackOverflowDemo(); s.recurseToOverflow(x); } public void recurseToOverflow(int x){ System.out.println("instance " + x); x = x + 1; recurseToOverflow(x); } }
Stack Overflow • If you get a stack overflow error while doing you work in this class, you have a bug in your code. Think until you figure it out and fix it. • However, note that we run our programs inside Eclipse, which decides how much space to allocate to the call stack.
Recursion • Avoid infinite regression (and stack overflows) by defining a condition that indicates that the recursion has extended as far as it can, and that therefore causes termination of the recursion • Recursion usually involves performing some operation on a sequence of values (for example, a list). The termination condition occurs when there are no values left to process.
"Where shall I begin, please your majesty?" he asked."Begin at the beginning," the King said gravely, "and go on till you come to the end; then stop. King of Hearts’ Algorithm
doSomething() • If you are finished, stop. • Otherwise • Solve part of the problem • Run this algorithm Recursive Algorithm
For example, we can define the operation goHome() as: • If you are at home, stop. • Else • Take one step toward home. • goHome() Recursion
function factorial is:input: integer n such that n >= 0output: [n × (n-1) × (n-2) × … × 1] Iterative algorithm • create new variable called running_total with a value of 1 • begin loop • if n is 0, exit loop • set running_total to (running_total × n) • decrement n • repeat loop • return running_totalend Iterative (not Recursive) Factorial
Calculating the factorial of (a positive integer) n can be reduce to multiplying n times the factorial of n-1: function factorial:input: integer n such that n >= 0output: [n × (n-1) × (n-2) × … × 1] if n == 0, return 1 otherwise, return [ n × factorial(n-1) ] Recursive Factorial
public class Demo { public static int factorialRecursive(int intIn){ if(intIn ==0) return 1; return intIn * factorialRecursive(intIn -1); } public static void main(String[] args) { int num = Integer.parseInt(JOptionPane.showInputDialog(null, "Please " + "enter the number whose factorial you would like to compute")); JOptionPane.showMessageDialog(null, "The factorial of " + num + " is " + factorialRecursive(num)); } } Recursive Factorial
Euclid's Algorithm • Find the GCD of two positive integers this way: Input Two positive integers, a and b. Output The greatest common divisor, g, of a and b. If a<b, exchange a and b. Divide a by b and get the remainder, r. If r=0, report b as the GCD of a and b. Replace a by b and replace b by r. Return to the previous step. http://www.math.rutgers.edu/~greenfie/gs2004/euclid.html
Euclid's Algorithm // by Robert Sedgewick and Levin Wayne : http://introcs.cs.princeton.edu/java/23recursion/Euclid.java.html // assumes p > q for simplicity public class Euclid { public static int gcd(int p, int q) {// recursive implementation if (q == 0) return p; else return gcd(q, p % q); } public static int gcd2(int p, int q) {// non-recursive implementation while (q != 0) { int temp = q; q = p % q; p = temp; } return p; } public static void main(String[] args) { int p = Integer.parseInt(JOptionPane.showInputDialog(null, "Please enter the first integer")); int q = Integer.parseInt(JOptionPane.showInputDialog(null, "Please enter the second integer")); int d = gcd(p, q); int d2 = gcd2(p, q); JOptionPane.showMessageDialog(null, "gcd(" + p + ", " + q + ") = " + d); JOptionPane.showMessageDialog(null, "gcd(" + p + ", " + q + ") = " + d2); } }
Multiple Recursion A recursive problem may have more than one base case and/or more than one recursive call. The definition of the Fibonacci numbers is recursive, and the nth Fibonacci number can be found using a recursive function. Fibonacci numbers: f(n) = We will write a method that has both multiple base cases and multiple recursive calls
Multiple Recursion public class FibonacciCalculator { public static void main(String[] args){ FibonacciCalculator f = new FibonacciCalculator(); int n = 7; long fib = f.fibonacci(n); System.out.println("Fibonacci number No. " + n + " = " + fib); } public static long fibonacci(long n) { // https://www.inf.unibz.it/~calvanese/teaching/04-05-ip/lecture-notes/uni10/node23.html if (n < 0) return -1; // F(n) is not defined when n is negative if (n == 0) return 0; else if (n == 1) return 1; else return fibonacci(n-1) + fibonacci(n-2); } }
Recursion can be used to process a list (or array of) values. Recursion
package demos; import java.util.ArrayList; import java.util.List; //https://stackoverflow.com/questions/126756/examples-of-recursive-functions public class RecursionExample { public static void main(String[] args) { String[] sleeplessArray = { "ant", "frog", "goose", "weasel", "child" }; List<String> sleeplessList = new ArrayList<String>(); for (String s : sleeplessArray) sleeplessList.add(s); RecursionExample r = new RecursionExample(sleeplessList); } public RecursionExample(List<String> animals){ System.out.print("There was a "); tellStory(animals); } private void tellStory(List<String> sleeplessList) { int last=sleeplessList.size() -1; String animal = sleeplessList.get(last); if(sleeplessList.size() == 1) System.out.println("little " + animal +" who went to sleep"); else { System.out.println("little " + animal + " who couldn't go to sleep, so his mother read him a story about a "); sleeplessList.remove(last); tellStory(sleeplessList); } System.out.println("so the little " + animal + " went to sleep" ); } } Recursion
Recursion can also be used to process a list of values and produce a new list of processed values. • This is at the heart of functional programming, as you will learn in CS332F. In the FP paradigm, neither the old list nor the objects in the list change. We produce a new list containing new values. • Functional languages like LISP, Haskell, and Scala are designed to make this easy. • It is less straightforward in OOP, but the next version of Java will have more support for this Recursion
package demos; import java.util.ArrayList; import java.util.List; public class ListEx { public static void main(String[] args) { List<Integer> origList = new ArrayList<Integer>(); for (int counter = 1; counter < 10; counter++) origList.add(counter); ListEx r = new ListEx(); List<Integer> newList = r.squareList(origList, null); for(Integer i: newList) System.out.print(i + " "); } private List<Integer> squareList(List<Integer> oldList, List<Integer> newList) { int lastIndex = oldList.size() -1; int base = oldList.get(lastIndex); oldList.remove(lastIndex); if(oldList.size() == 0) { newList = new ArrayList<Integer>(); } else newList = squareList(oldList, newList); newList.add(base * base); return newList; } } Recursion
Here is the last example in Haskell: Function definition: squarelist:: Num a => [a] -> [a] squarelist [] = [] squarelist (x:xs) = (x*x): squarelistxs Function call: squarelist[1,2,3,4,5,6,7,8,9] Recursion
Some algorithms involve following one path as far as it can go, then backing up to the last point at which a different path could have been chosen and then following that path Depth-First Search (DFS) is a classic example. Start at the root of a tree or graph and explore as far as possible along each branch before backtracking. Backtracking
Depth-First Search Source of picture: https://upload.wikimedia.org/wikipedia/commons/thumb/1/1f/Depth-first-tree.svg/450px-Depth-first-tree.svg.png
File Listing • Consider the problem of listing all the files in a directory, including those in all its subdirectories, etc. until every path has been exhausted. • The natural way to approach this problem is to do it recursively using Depth First Search: • List files in the current directory • Run this algorithm on each subdirectory, continuing as far as we can go
File Listing package demos; import java.io.File; import javax.swing.JOptionPane; //https://stackoverflow.com/questions/105838/real-world-examples-of-recursion public class FileLister { private static StringBuilder indentation = new StringBuilder(); public static void main (String args [] ){ String start = JOptionPane.showInputDialog(null, "Please enter the starting directory"); getDirectoryContent(start); } private static void getDirectoryContent(String filePath) { File currentDirOrFile = new File(filePath); if ( !currentDirOrFile.exists() ){ return; } else if ( currentDirOrFile.isFile() ){ System.out.println(indentation + currentDirOrFile.getName()); return; } else{ System.out.println("\n" + indentation + "|_" +currentDirOrFile.getName()); indentation.append(" "); for ( String currentFileOrDirName : currentDirOrFile.list()){ getDirectoryContent(currentDirOrFile + "\\" + currentFileOrDirName); } if (indentation.length() - 3 > 3 ){ indentation.delete(indentation.length() - 3, indentation.length()); } } } }
The Towers of Hanoi problem is said to originate in various Asian countries; it is actually an 19th Century European mathematician's idea, made to sound more mysterious by setting it in a place his readers thought of as exotic • Not unlike conceptualizing a maze solver as a zombie attack • The story is that, at the beginning of time, a group of monks in Hanoi were tasked with moving a set of 64 disks between three pegs, according to these rules: • No disk may ever be places above a smaller disk • The starting position has all the disks, in descending order of size, stacked on the first peg • The ending position has all the disks in the same order, stacked on the third peg. • An optimal solution for n disks requires 2n-1 moves 264= 18,446,744,073,709,551,616 • There is a nice animation of a 4-disk version of the problem at https://en.wikipedia.org/wiki/File:Tower_of_Hanoi_4.gif Towers Of Hanoi
Recursive solution: • For any n > 1, the problem can be solved optimally in this way: • Solve the problem for n -1 disks, starting at the start post and ending at the "extra" post. • The remaining disk will be the largest one. Move it to the finish post. • Then solve the problem for the n-1 posts, moving from the "extra" post to the finish post • The above recursive procedure is applied recursively until n = 1 • Try this out online at http://www.softschools.com/games/logic_games/tower_of_hanoi/ Before you try to understand the code from the book at http://bcs.wiley.com/he-bcs/Books?action=mininav&bcsId=7872&itemId=1118431111&assetId=310564&resourceId=30501&newwindow=true Towers Of Hanoi
A tail-recursive method is one in which there is one recursive call at the very end of the method, e.g. the examples above of recursive factorial calculations and Euclid's algorithm. In many programming languages, compilers can convert tail recursion into iterative code, which is more efficient since it does not create new instances on the call stack and since, in many cases, it can avoid redundant calculations. This is called Tail-Call Optimization, or TCO. Java compilers may have this feature in the future, but not at the current time. Tail Recursion
To understand recursion, read this sentence. A truly greedy man is one who writes his will and names himself as heir -- from Philogelos, the oldest known book of jokes, compiled about 400 AD. Function recurse() • If you are done with your program, stop • Otherwise, A. Try to find the problem B. Utter an obscenity • recurse() Recursive Jokes
Testing Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it. --Brian Kernighan
Testing • Finding and correcting errors is the most time-consuming part of programming • Testing and debugging always takes much more time than the first pass at writing code • You can make this process easier if you are systematic about testing. Test early and often, and test the smallest increments of code that you can in order to isolate errors. • This is one more reason to break your code down into many simple methods instead of a few complicated ones.
Testing • Professional software development organizations have full-time testers whose job is to find the faults in your code • They will always find some, but the better your code is when they get it, the longer you will keep your job • The earlier errors are caught, the easier it is to correct them and the fewer person-hours are expended on testing and debugging. • The first line of testing is done by the programmers themselves.
Testing • Besides providing plenty of output that you can check, test various conditions that should be true or false. package monsters; public class MonsterAttackDriver { public static void main(String[] args) { Monster dracula; String dName = "Dracula", dNewName = "Bob", dHome = "Transylvania"; dracula = new Vampire(dName, dHome); if(dracula.getName() != dName) System.out.println("Name error"); dracula.setName(dNewName); if(dracula.getName() != dNewName) System.out.println("Name error"); if(dracula.getOriginStory() == null) System.out.println("Origin story error"); // etc. Test all public methods } }
Unit Testing • We have used driver and tester classes to test our classes • We have also written test methods that look for likely errors • Unit testing is a systematic way to test parts of your applications • Test for every likely error you can think of • Each test asserts that some condition is true or false; the assertions will fail if particular errors occur
Unit Testing • Run all the tests periodically to find errors caused by later code breaking things that worked before or by implementation changes in one part of a system • This is the simplest instance of the concept of regression testing. • Regression means "going back". Regression testing "goes back" to test code again to see if it still works correctly.
JUnit • Eclipse includes a unit testing framework called JUnit • A test case is a class that contains one or more tests, usually all testing the same target class. • Create one or more separate packages in each project for test cases. • Tests use assertions of various kinds • assertNull(Object o), assertNotNull(Object o) • assertEquals(Object o, Object p), assertFalse(boolean) • Many others listed here: https://github.com/junit-team/junit/wiki/Assertions • A test succeeds if the assertion(s) are true when the test is run and fails if one or more are false • The object of a test is to assert something that will be true if the class is working correctly but false if some plausible error occurs
JUnit • Let's start by writing unit tests for the Vampire class, which implements the Monster interface, and for the Crypt class, which is used by Vampire. • Test Crypt first, because it can work without Vampire, but Vampire will not work if Crypt is broken
Monster package monsters; public interface Monster { public void setName(String name); public String getName(); public void setLocation(String location); public String getLocation(); public void rampage(); public String getOriginStory(); } • getLocation() is new in this version of Monster
Crypt package monsters; public class Crypt { private String location; public Crypt(String location) { this.location = location; } public void setLocation(String location) { this.location = location; } public String getLocation() { return location; } public String toString(){ return "a mysterious crypt in " + location; } }
Vampire package monsters; public class Vampire implements Monster, Cloneable { private String name; private Crypt crypt; public Vampire(String name, String location) { this.name = name; crypt = new Crypt(location); } @Override public void setName(String name) { this.name = name; } @Override public String getName() { return name; } @Override public void setLocation(String location) { crypt.setLocation(location);// TODO Auto-generated method stub } @Override public String getLocation(){ return crypt.getLocation(); }
@Override public String getOriginStory() { return "undead creature which lives by sucking the blood of living humans"; } @Override public void rampage() { StringBuilder sb = new StringBuilder(name + " arises from " + crypt.toString() + " and "); if (crypt.getLocation() == "Transylvania") sb.append("sucks people's blood all night, then returns to a coffin to hide from sunlight"); else if (crypt.getLocation() == "Burbank") sb.append("takes over the entire television industry"); else { System.out.println("wreaks unknown havoc in fresh Vampire territory"); return; } System.out.println(sb); } @Override public Object clone() { Vampire newV; try { /* Object clone() returns an Object. It will be a Vampire, but in order to get to anything specific to Vampires, we need to cast it to a Vampire and use a Vampire reference variable */ newV = (Vampire) super.clone(); newV.crypt= new Crypt(crypt.getLocation()); } catch (CloneNotSupportedException e) { e.printStackTrace(); return null; } return newV; } }
Unit Testing • JUnit tests are identified with the annotation @Test: @Test public void testCryptCreated(){ String location = "Transylvania"; Crypt c = new Crypt(location); assertNotNull(c); } @Test public void testToString(){ String location = "Transylvania"; Crypt c = new Crypt(location); assertNotNull(c.toString()); }
JUnit • JUnit starts us off with a test that is directed to fail:
Unit Testing JUnit Assertions require imports JUnit Assertions require imports