More general kinds of recursion
[comp1110-7L] - Fabian Muehlboeck
More general kinds of recursion
[comp1110-7L]- Fabian Muehlboeck
The recursive functions we have seen so far directly matched the structure of a data type definition. Sometimes, such a structure is less obvious, and sometimes, we don't follow the structure of a data type that closely. But as long as you can argue that a recursive call is on "smaller" input (in the sense that we get close to a base case), recursion is a good option.
Example 1. Factorial
[comp1110-24] - Liam O'Connor
Example 1. Factorial
[comp1110-24]- Liam O'Connor
You may have seen this self-referential (recursive) definition of factorial before:
\[\begin {array}{lcl} 0! &=& 1 \\ n! &=& (n-1)! \cdot n \end {array}\]Try it out in the Java visualiser.
Warning 2. Factorial of Zero
[comp1110-7M] - Fabian Muehlboeck
Warning 2. Factorial of Zero
[comp1110-7M]- Fabian Muehlboeck
In the lecture, we used the following definition of factorial:
This is not the standard definition of factorial, which usually holds that 0! == 1.
Our definition above still matches the definition of factorial for every other valid input.
The main point here is that a function may have multiple base cases, some of which may never be hit coming from a recursive case.
Definition 3. Divide and Conquer
[comp1110-26] - Liam O'Connor
Definition 3. Divide and Conquer
[comp1110-26]- Liam O'Connor
Divide and conquer is a recursive problem solving strategy that solves a problem by following three distinct steps: Divide the problem into one or more subproblems that are smaller instances of the same problem; Conquer the subproblems by solving them recursively or directly if they are small enough; and Combine the subsolutions into the solution for the original problem. This approach is defined by reducing the problem size by a significant factor, such as halving the input in binary search, rather than just a constant amount (like in Factorial).
Example 4. Recursive binary search
[comp1110-27] - Liam O'Connor, Fabian Muehlboeck
Example 4. Recursive binary search
[comp1110-27]- Liam O'Connor, Fabian Muehlboeck
A recursive version of our iterative Binary Search. Note that we had to add additional parameters lower and upper to give us enough flexibility to use this function recursively. Such generalisation is often required when
working with recursive functions. Where needed, you can define another function without those parameters that simply calls the main one with suitable initial values.
Try it out in the Java visualiser.
5. Recursion and Loop Invariants
[comp1110-7N] - Fabian Muehlboeck
5. Recursion and Loop Invariants
[comp1110-7N]- Fabian Muehlboeck
Consider the iterative version of binary search, and specifically its loop invariant:
Example. Binary Search
[comp1110-1J] - Liam O'Connor
Example. Binary Search
[comp1110-1J]- Liam O'Connor
If we add a precondition to the spec of contains, that the input array is sorted,
we can use a trick similar to our solution algorithm for our guessing game, which as we saw,
can reduce the amount of iterations of the program from linear in the size of the input to logarithmic.
The idea is that we consistently pick the middle index from the range of possible indices. If the value at this index is our target, we have found what we're looking for, and we're done. If the value at this index is larger than our target, that means our desired index is to the left, and reduces the range of possible indices by half. If it is smaller, then our desired index is to the other half, on the right of the selected midpoint.
This narrows the range of possible indices, which permits us to repeat this technique until we reach single elements.
Contrast this with Recursive binary search. Notice how the loop invariant is essentially encoded in the preconditions for lower and upper.
The line of reasoning is the same - the loop invariant must be true at the end of each loop, just as the preconditions of the recursive function call have
to be true at each such recursive function call.
The precondition must be true at the first call of the recursive function, just as the loop invariant has to be true when entering the loop.
In exchange, we can assume that the loop invariant is true at the start of the loop and after it, just as we can assume the precondition holds at the start of the function, and (typically) in the base cases.
Example 6. Merge Sort
[comp1110-29] - Liam O'Connor, Fabian Muehlboeck
Example 6. Merge Sort
[comp1110-29]- Liam O'Connor, Fabian Muehlboeck
Merge sort is both an efficient sorting algorithm (although it is not in-place) and a classic example
of Divide and Conquer techniques. The algorithm works by dividing the input array in half, recursively sorting each half, and then merging the results. We can write an efficient merge function that combines two sorted lists into a single combined sorted lists. Note that we just loop through both arrays once, so this merge function is more efficient than simply appending the two arrays and then sorting them by other means.
This function consistutes the "combine" step in the divide and conquer strategy. Now all that is left is the "dividing" and "conquering":
Note: This functions efficiency can be slightly improved by only allocating one output array and passing it into each function, however this obscures the functionality of the algorithm.