1 / 41

Recursion

Learn about recursion in CS courses, solving numerical problems, recursive methods for linked lists, base cases, pitfalls, and implementations like factorial and binary search.

mcbroom
Download Presentation

Recursion

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. Recursion ITI 1121 N. El Kadri

  2. Reminders about recursion • In your 1st CS course (or its equivalent), you have seen how to use recursion to solve numerical problems, such as calculating factorials, or locating a target value into a sorted array. • We now consider recursive methods applied to linked lists.

  3. The general idea behind recursion is to divide a problem into two or more (smaller) sub-problems and to combine the solutions of the sub-problems to solve the initial problem. The solution to the sub-problems can be obtained by the same strategy. • For example, the following algorithm calculates the sum of the elements from position 0 to k of an array, t, where k is the size of t minus 1.

  4. 1. Let’s say that you can obtain the sum of the values from positions 1 to k by some mean and let’s call this result s. • 2. How would you obtain the final result? s+t[0]. • 3. How to obtain s? The sum of all the elements of the sub-interval? Similarly, except this time the interval is smaller, 1 to (length - 1).

  5. 4. How should the initial call look like? sum(t, 0) • It’s important that the size of the problem gets smaller with each successive call, otherwise this would create an infinite recursion (similar to infinite loops). • To stop the recursion, there must be a size of problem such that the result can be computed directly (no recursive call). • Let’s call those special cases where the result can be obtained directly the base cases, there is at least one but there can be more than one base case. • What is the base case for the sum? • Eventually, the interval contains only one value, the last position of this array, the sum simply returns this element.

  6. Recursive Methods • A recursive method therefore always obeys the following pattern: <type> method( parameters ) { <type> result; if ( test parameters for base case ) { // base case // calculate the result directly // i.e. no recursive call, recursion stop here } else { // general case // pre-processing; partitioning the data for example result = method( sub-set of the data ); // recursive call // post-processing; combining the results for example } return result; }

  7. Base Case  The base case must be tested first, otherwise, this would cause an infinite recursion.  Certain programming languages (such as Lisp, Prolog or Haskell) have no loop control-structures and therefore recursion is the only way to create iterations.

  8. Pitfall • Never mix iterative control structures and recursion. • Recursion is used in place of iterative control structures. If you find yourself writing something like: void foo( Node p ) { while ( ... ) { ... } foo(p.next) } something is wrong.

  9. Factorial public static int factorial( int n ) { if ( n<=1 ) return 1; else return n * factorial( n-1 ); }

  10. The factorial method corresponds to the general pattern, the base case is tested first, and the result is computed directly without the need for a recursive call (recursion stops here!), the general case is always making the value of the parameter smaller so that eventually the base case will be applied.

  11. Binary Search public static int binarySearch( int value, int[] array ) { return binSearch( value, array, 0, array.length-1 ); } private static int binSearch( int value, int[] array, int lo, int hi ) { if ( lo > hi ) return -1; int middle = ( lo + hi ) / 2; if ( value == array [middle ] ) return middle; if ( value < array[ middle ] ) return binarySearch( value, array, lo, middle - 1 ); else return binarySearch( value, array, middle+1, hi ); }

  12. A binarySearch can be applied to search for a target value in a sorted array. This method is efficient because the size of the intervals of positions to search decreases by a factor of two with every successive call. • Base case: an interval of size zero, no recursive call. • General case: creates smaller and smaller intervals of values to look.

  13. Recursive list processing - head + tail strategy • Let’s develop a general strategy to solve list processing problems recursively. • To develop this strategy, let’s consider calculating the size (length) of a list. • Let’s divide the initial list in two parts, its head (the first element) and its tail (the rest of the list). • By analogy with the method sum let’s say that we can obtain the size of the tail by some mean, let’s call this value s. • If you knew s, what would be the size of the entire list? Answer: s+1 • How to obtain s? Well, s is the size of a list, and, furthermore, it’s a shorter one, we could apply (recursively) the method that we are developing.

  14. The total length of the list will be s + 1.

  15. The length of the list designated by head will be length of s + 1.

  16. The length of the list designated by head will be length of s + 1.

  17. The length of the list designated by head is 0.

  18. Implementation • The methods presented in this lecture are instance methods of a class defined as follows: public class OrderedList { private static class Node { private Comparable value; private Node next; Node ( Comparable value, Node next ) { this.value = value; this.next = next; } } private Node first; public OrderedList () { first = null; } // other methods ... }

  19. “head + tail” strategy • Given a list l, apply the method recursively to the rest of the list (tail). • In general, the result obtained by the recursive call is used in combination with the head to calculate a final • result. • This strategy does not solve all recursive list processing problems but it’s the first strategy you should try. • For the method size, what would be the base case? • For each recursive call, the list gets smaller and smaller, what is then the smallest list. The empty list! • The empty list is a valid case, its length is zero, however, it has to be a base case because it has no tail! • Because of that, for all methods that apply the “head + tail” strategy , the empty list cannot be part of the general case.

  20. int size( Node p ) { if ( p == null ) return 0; else return 1 + size( p.next ); }  Can this method be called from outside of the class? How is it called initially?

  21. No, it cannot be called from outside of the class because it needs to be given a reference to a node (which is an implementation detail and should be private to the class). • We need an auxiliary method that initiates the process starting with the first element of the list: public int size() { return size( first ); }

  22. Because, the recursive method cannot and should not be used from outside of the class, it should be declared private: public int size() { return size( head ); } private static int size( Node p ) { if ( p == null ) return 0; else return 1 + size( p.next ); } • All our recursive methods will obey this pattern. They will all have a public part that call the recursive method, which is private. The public method initiates the first call to the recursive method with a reference to the first node.

  23. The “head + tail” strategy is not the only way to solve this problem, we could have chosen to divide the list into two sublists of approximately the same size and sum their lengths. • In fact, for a list of length n, there are n − 1 ways to separate this list into two sublists. • The “head + tail” strategy is just a special case; however, no recursive call is made for the head. • Given our list implementation, the “head + tail” strategy is simpler to apply. • The strategy that consists in dividing the list into two sublists of approximately the same size can lead to more efficient algorithms, but that’s a subject left for CSI 2114 (data-structures), here, we’ll use the “head + tail” strategy.

  24. findMax() • Write a recursive method that finds the maximum value for a list of Comparable objects. The smallest valid list contains one element. • The auxiliary method that initiates the search starting with the first value will be: public Object findMax() { if ( first == null ) throw new IllegalStateException(); return findMax( first ); } • Let’s apply the “head+tail” strategy and consider the general case first. There will be a recursive call for the tail: Comparable result = (Comparable) findMax( p.next ); • What does the result represent? Well, it’s the maximum value for the rest of the list.

  25. What should be done next? Compare this value to the value of the head. if ( result.compareTo( p.value ) > 0 ) return result; else return p.value; • What should be the base case? • This process makes the list smaller and smaller, what is the smallest list to handle? • It is not the empty list, we said the smallest valid list contains one element.

  26. What value should be returned in that case? The value found at that node. if ( p.next == null ) return p.value; Let’s put everything together: public Object findMax() { if ( first == null ) throw new IllegalStateException(); return findMax( first ); } private Object findMax( Node p ) { if ( p.next == null ) return p.value; Comparable result = (Comparable) findMax( p.next ); if ( result.compareTo( p.value ) > 0 ) return result; else return p.value; }

  27. indexOf ( Object obj ) • The method indexOf returns the position of the first occurrence of obj in the list, the first element of the list is the position 0, the method returns -1 if the element is not found in the list. • Following the “head + tail” strategy, the general case will involve a recursive call for the tail of the list: int result = indexOf( o, p.next ); • What does the result represent? It’s the position of o in p.next.

  28. What is the position of o with respect to p? if ( p.value.equals( o ) ) return 0; else if ( result == -1 ) return result; else return result + 1; }

  29. What is the base case? • The smallest list is the empty list, it cannot contain the value we’re looking for, we should return the special value -1, to indicate that a match could not be found. if ( p == null ) return -1;

  30. Putting everything together: private int indexOf( Object o, Node p ) { if ( p == null ) return -1; int result = indexOf( o, p.next ); if ( p.value.equals( o ) ) return 0; if ( result == -1 ) return result; else return result + 1; }  Does this work? Yes, but it’s inefficient. Why?

  31. The recursion should stop as soon as the first occurrence has been found. private int indexOf( Object o, Node p ) { if ( p == null ) return -1; if ( p.value.equals( o ) ) return 0; int result = indexOf( o, p.next ); if ( result == -1 ) return result; else return result + 1; }

  32. contains( Object o ) • The method contains returns true if the list contains the element o, i.e. there is a node such that value.equals( o ). • The auxiliary will initiate the search from the first node. public boolean contains( Object o ) { return contains( o, first ); }

  33. The signature of the recursive methods will be: private boolean contains( Object o, Node p ) { ... } Let’s apply the head and tail strategy. • The empty list has to be part of the base case, if list is empty it cannot contain the object, contains should return false: if ( p == null ) return false; • The strategy suggests to call contains for the tail: boolean result = contains( o, p.next );

  34. Contains is similar to indexOf, the method should stop as soon as the first occurrence has been found: private boolean contains( Object o, Node p ) { if ( p == null ) return false; else if ( p.value.equals( o ) ) return true; else return contains( o, p.next ); }

  35. The methods we looked at considered only one element at a time but this does not need to be. • Let’s consider the method isIncreasing. It returns true if each element of the list is equal to or greater than its predecessor. • To solve this problem, we can scan the list and return false as soon a consecutive pair of elements has been found such that the predecessor is greater than its successor, if the end is reached this means the list is increasing.

  36. public boolean isIncreasing() { }

  37. Exercises • For a singly linked list implement the following methods recursively: • int indexOfLast( Object o ); returns the position of the last occurrence of o and -1 if the element cannot be found in the list. • void addLast( Object o ); • boolean equals( OrderedList other ); compares all the elements of this list to the elements of the other list; the lists are not necessarily of the same length.

  38. The methods considered so far do not modify the structure of the list. • In the case of methods that do not modify the list, such as indexOf and contains, the only consequence of unnecessary recursive calls is inefficiency. • However, when the methods are allowed to change the structure of the list, such as remove below, the consequences of unnecessary recursive calls are severe. • Let’s implement the method remove, that removes the first occurrence of an object.

  39. Another difference is that the reference in the header of the list can be modified, this will be done by the helper method. • Consider the method that initiates the removal of the object starting from the head of the list: public void remove( Object o ) { if ( head != null ) if ( head.value.equals( o ) ) head = head.next; else remove( o, head ); }

  40. If the first node equals to the parameter o this element is removed and nothing else needs to be done. Otherwise, we proceed with the rest of the list. • This is convenient, since the node to be removed is always the next one. This process will work because the auxiliary function has checked the first node and therefore the recursive method remove knows that the current element has been checked.

  41. public void remove( Object o ) { if ( head != null ) if ( head.value.equals( o ) ) head = head.next; else remove( o, head ); } private void remove( Object o, Node p ) { if ( p.next != null ) if ( p.next.value.equals( o ) ) p.next = p.next.next; else remove( o, p.next ); }

More Related