CS303E Homework 12

Instructor: Dr. Bill Young
Due Date: Friday, November 15, 2024 at 11:59pm

Copyright © William D. Young. All rights reserved.

Practice with Recursive Functions

Recursion is a very useful programming skill. You may not use it very often in most languages, but the ability to think recursively is a valuable skill to acquire. There are programming languages (e.g., Lisp) in which most programs are recursive. In this assignment, you'll get a chance to practice writing recursive functions.

For each of the following functions, you should fill in the body, with semantics as indicated by the comment. I have done the first two for you. You do not have to validate the inputs. If it says the input is a string or integer, your function doesn't have to check that it is or work if it's not.

Many of these can be solved easily using functions and class methods you've learned already this semester. But that would defeat the purpose of this homework. Your solutions must be recursive. That is, it must call itself, except for the final problem where the helper function is the recursive function. I've done the first couple for you. All of these functions return their answer; none of them print it.

Note also that none of these functions have explicit error cases. That is, none of them print an error message if the parameters are not reasonable. You can assume that the parameters are OK.

def sumItemsInList( L ):
    """ Given a list of numbers, return the sum. """
    if L == []:
        return 0
    else:
        return L[0] + sumItemsInList( L[1:] )

def countOccurrencesInList( key, L ):
    """ Return the number of times key occurs in list L. """
    if not L:                 # same as L == []:
        return 0
    elif key == L[0]:
        return 1 + countOccurrencesInList( key, L[1:] )
    else:
        return countOccurrencesInList( key, L[1:] )

def addToN ( n ):
   """ Return the sum of the non-negative integers to n.
    E.g., addToN( 5 ) = 0 + 1 + 2 + 3 + 4 + 5. """

def findSumOfDigits( n ):
   """ Return the sum of the digits in a non-negative integer. """

def integerToBinary( n ):
   """ Given a nonnegative integer n, return the 
   binary representation as a string. """

def integerToList( n ):
   """ Given a nonnegative integer, return a list of the 
   digits (as strings). """

def isPalindrome( s ):
   """ Return True if string s is a palindrome and False
   otherwise. Count the empty string as a palindrome. """

def findFirstUppercase( s ):
   """ Return the first uppercase letter in 
   string s, if any. Return None if there
   is none. """

# for this one, don't reverse the string.
def findLastUppercase( s ):
   """ Return the last uppercase letter in 
   string s, if any. Return None if there
   is none. """

def negateItems( lst ):
   """Assume lst is a list of numbers.  Return a list
   of the negations of those numbers."""

# I posted a video on the class webpage explaining 
# helper functions.  If you're stuck on this one, view
# that video and see if it helps.

def findFirstUppercaseIndexHelper( s, index ):
   """ Helper function for findFirstUppercaseIndex.
   Return the offset of the first uppercase letter;
   assume you are starting at index. Return -1 
   if there is none."""

# The following function is already completed for you. But 
# make sure you understand what it's doing. 

def findFirstUppercaseIndex( s ):
   """ Return the index of the first uppercase letter in 
   string s, if any. Return -1 if there is none. This one 
   requires a helper function, which is the recursive 
   function. """
   return findFirstUppercaseIndexHelper( s, 0 )

Expected output

Below is the output from the functions I wrote for this assignment, running them in interactive mode. If you wanted to run this in batch mode you'd have to write test code in a main() function and print the results.
>>> from RecursiveFunctions import *
>>> sumItemsInList.__doc__                        # shows using docstring
' Given a list of numbers, return the sum. '
>>> countOccurrencesInList.__doc__
' Return the number of times key occurs in list L. '
>>> addToN( 10 )
55
>>> addToN( 100 )
5050
>>> addToN( 0 )
0
>>> findSumOfDigits( 0 )
0
>>> findSumOfDigits( 12345 )
15
>>> integerToBinary( 25 )
'11001'
>>> integerToBinary( 100 )
'1100100'
>>> integerToBinary( 0 )
'0'
>>> integerToList( 123 )
['1', '2', '3']
>>> integerToList( 348914 )
['3', '4', '8', '9', '1', '4']
>>> integerToList( 0 )
['0']                        # this function is easier if you return
                             # this for 0
>>> isPalindrome( "abcDcba" )
True
>>> isPalindrome( "abcDcbaF" )
False
>>> isPalindrome( "" )
True
>>> isPalindrome( "X" )
True
>>> findFirstUppercase( "ab c  dEFg hi" )
'E'
>>> findFirstUppercase( "ab c  defg hi" )       # None doesn't print in interactive mode
>>> findLastUppercase("ABcdE Fghi")
'F'
>>> findLastUppercase("")
>>> findLastUppercase("abcdefghi")
>>> negateItems( [12, -3, 1.45, -18, 0] )
[-12, 3, -1.45, 18, 0]
>>> negateItems( [] )
[]
>>> findFirstUppercaseIndexHelper("abCd", 7)
9
>>> findFirstUppercaseIndexHelper("abCd", 10)
12
>>> findFirstUppercaseIndexHelper("abcd", 10)
-1
>>> findFirstUppercaseIndexHelper("abCd", 0)
2
>>> findFirstUppercaseIndex("abCd")
2
>>> findFirstUppercaseIndex( "ab c  dEFg hi" )
7
>>> findFirstUppercaseIndex( "ab c  defg hi" )
-1
>>> findFirstUppercaseIndex( "" )
-1
Students often have the most trouble thinking about findFirstUppercaseIndexHelper. Here's how to consider this. Suppose the string were "abcdEfGHijk". Suppose you wrote a function like this:
def findFirstUppercaseBad (s):
   if not s:
      return -1
   elif s[0].isupper():
      return 0
   else:
      return findFirstUppercaseBad( s[1:] )
The problem with this is that it would always return either 0 or -1. Somehow, you need to keep track of the number of characters at the start of the string that you've considered and discarded before the current recursive call. For example, you'd eventually have a call:
findFirstUppercaseBad( "EfGHijk" )
At this point, you'd need to know that you've considered and discarded 4 characters before you got to this point. That's the reason for the Helper function. It has an additional parameter that keeps track of that. So your recursive call would look more like:
findFirstUppercaseIndexHelper( "EfGHijk", 4 )
Then the non-Helper function would always pass 0 in that argument position because the non-Helper (non-recursive) function is called from the start of the original string.

Turning In the Assignment:

The program should be in a file named RecursiveFunctions.py. Submit the file via Canvas before the deadline shown at the top of this page. Submit it to the assignment hw12 under the assignments sections by uploading your Python file.

Your file must compile and run before submission. It must also contain a header with the following format:

# File: RecursiveFunctions.py
# Student: 
# UT EID:
# Course Name: CS303E
# 
# Date:
# Description of Program: 

Programming Tips:

Thinking recursively: Programming a recursive function may seem difficult at first, but doesn't have to be if you cultivate the habit of thinking recursively. The trick is seeing the problem in the right way so that:

Think about the problem of adding up all of the elements in a list of integers. What's the base case? That is, what's the simplest list of integers possible? It's just the empty list. So what's the sum? It's 0.

If we're not in the base case, we know there's at least one number in the list. Any such list will always have form: [first-element] + rest-of-the-list. Summing rest-of-list is a "smaller" instance of the same problem. So assume we could solve that problem! If so, we're almost done. We just have to add first-element to that sum. This naturally suggests the form of the recursive solution:

def sumList( lst ):
    if not lst:                             # lst is empty
       return 0
    else:                                   # lst is not empty
       return   lst[0] \                    # add first-element
              + sumList( lst[1:] )          # to the sum of the rest
If you develop the knack of thinking recursively, you can solve a huge variety of problems with recursive programs. And many problems in the real world are very naturally recursive.

But consider efficiency: Recursion is a very powerful programming technique, but it's not without cost. Consider the Fibonacci function, described in Slideset 12. Recall that the nth Fibonacci number is defined by the equations:

  F(0) = 0
  F(1) = 1
  F(n) = F(n-2) + F(n-1), for n > 1
This is trivial to translate into Python:
def fib(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fib(n-1) + fib(n-2)
This is easy to program, but horribly inefficient. Computing fib(100), for example, requires 1,146,295,688,027,634,168,201 recursive calls. Even on the fastest computer, this would take centuries to compute. Our iterative solution fibBetter(100) takes much less than a second to compute. So don't use recursion just because it's easier, without thinking about the efficiency of the resulting code.

Tail-recursion: Any recursive function can be computed iteratively, but sometimes the translation is hard. However, many recursive functions are tail-recursive. This means that the recursive call is the last statement that is executed by the function. This implies that no further computation is required on the value returned from the recursive call.

For example, the function sumList above is tail-recursive because, once the recursive call sumList( lst[1:] ) returns, no further manipulation of that value occurs. That is, the recursive call is the "last" thing done. So once the value is computed, it can be stored in a variable and there's no need to maintain a stack frame for the call. Most programming language compilers automatically translate tail-recursive functions into iterative ones, which are typically much more efficient. The function fib above is not tail recursive, since you need both of the recursive calls to compute the value of the function.

Stack overflow: For any function call the computer has to keep track of certain information, like the location and local variables of the caller, so it can return control to the caller after the call completes. That information is kept in a call frame on a stack. There's a limit to how much memory is allocated on the stack for these call frames. Your program will crash with a "stack overflow" if your recursion doesn't terminate or if your call chain is too deep. You can tell your system to increase the stack size, which means that you can make more recursive calls. But often the problem is that your recursion doesn't terminate; no finite stack will solve that problem.

By the way, the number of calls made is not equal to the depth of the call chain. For example, our recursive version of fib(100) has trillions of calls, but the recursion depth is never more than 100, so it could run for centuries, but probably would never overflow the stack; though it might exhaust other system resources.