Reviewing Recursion

Exercise: write a recursive function convertFromBinary that accepts an a string of that number's representation in binary (base 2) and returns the base 10 int equivalent. e.g.,

convertFromBinary("111") returns 7
convertFromBinary("1100") returns 12
convertFromBinary("101010") returns 42
How is this problem self-similar? What is the smallest amount of work? When to stop?
# Returns the given int's binary representation. 
# Precondition: n >= 0 
def convertFromBinary(binary):
  length = len(binary)
  if length == 1:
    # base case: binary is same as base 10         
    return int(binary)
  # recursive case: break number apart 
  lastCharacter = binary[length - 1]
  beginning = binary[0:length - 1]
  return 2 * convertFromBinary(beginning) + convertFromBinary(lastCharacter)

Arm's length recursion or short-circuit recursion. Consider the following recursive implementation of power():

//Assumes exp >= 0
def power(base, exp):
  if exp == 0:
    return 1
  elif exp % 2 == 0:
    return power(base * base, exp/2) #recursive case 1
  elif:
    return base * power(base, exp - 1) #recursive case 2

Here is a short-circuit implementation of the same logic:

//Assumes exp >= 0
def power(base, exp):
  if exp == 0:
    return 1
  elif exp % 2 == 0:
    return power(base * base, exp/2) #recursive case 1
  elif:
    return base * power(base * base, (exp - 1)/2) #recursive case 2

Similarly, consider the following recursive implementation of fac():

def fac(n):
  if n <= 0:
    return 1
  else:
    return n*fac(n-1)
And below is a short-circuit implementation (also called arms-length recursion) of the fac() function:
def facHelper(n):
  if n == 2:
    return 2
  else:
    return n*facHelper(n-1)

def fac(n):
  if n <= 1:
    return 1
  else:
    return facHelper(n)

Exhaustive search

Explore every possible combination from a set of choices or values.

Applications

Often the search space consists of many decisions, each of which has several available choices

A general pseudo-code algorithm for exhaustive search:

Explore(decisions):
  -- if there are no more decisions to make: Stop

  -- else let's handle one decision ourselves, and the rest by recursion.
     For each available choice C for this decision:
     -- Choose C by modifying parameters
     -- Explore the remaining decisions that could follow C.
     -- Un-choose C by returning parameters to original state (if necessary)

Exhaustive search model.

Exercise: printAllBinary

Solution:

def printAllBinary(numDigits):
  printAllBinaryHelper(numDigits, "")

def printAllBinaryHelper(digits, soFar):
  if digits == 0:
    print soFar
  else:
    printAllBinaryHelper(digits - 1, soFar + "0")
    printAllBinaryHelper(digits - 1, soFar + "1")

Show the balanced tree of calls for printAllBinary(2) of depth 3, showing the digits and soFar arguments for each call. This kind of diagram is called a call tree or decision tree. Think of each call as a choice or decision made by the algorithm:

printDecimal: Write a recursive function printDecimal that accepts an integer number of digits and prints all base-10 numbers that have exactly that many digits, in ascending order, one per line.

printDecimal(2):
00
01
02
03
..
..
98
99

printDecimal(3):
000
001
...
998
999

Solution:

def printDecimal(numDigits):
  printDecimalHelper(numDigits, "")

def printDecimalHelper(digits, soFar):
  if digits == 0:
    print soFar
  else:
    for i in range(10):
      printAllBinaryHelper(digits - 1, soFar + str(i))
When the set of digit choices is large, using a loop to enumerate them makes the code succinct (this is okay!). Notice that we are looping over choices, and recursing over decisions.

All subsets

Solution:

def allSubsets(s):
  allSubsetsHelper(s, [])

def allSubsetsHelper(remaining, curChoices):
  if len(remaining) == 0:
    print curChoices
  else:
    remainingCopy = remaining[:]
    decisionOn = remainingCopy.pop()
    allSubsetsHelper(remainingCopy, curChoices)
    allSubsetsHelper(remainingCopy, [decisionOn] + curChoices)

allSubsets([1,2])

Why do we need to clone the list: remainingCopy = remaining[:]. Consider an implementation where we replace this cloning operation by a simple assignment that creates aliases: remainingCopy = remaining. Now consider the function activation allSubsetsHelper([1,2], []) which will in turn call allSubsetsHelper([1], []) which will in turn pop the list associated with remainingCopy. So, the second call from allSubsetsHelper([1,2], []) will now be to allSubsetsHelper([], [2]) (instead of the expected allSubsetsHelper([1], [2]). Thus, this program prints [0], [1], [2], instead of the desired [0], [1], [2], [1,2].

Instead of copying, we could alternatively, modify and unmodify the input parameter as follows:

def allSubsets(s):
  allSubsetsHelper(s, [])

def allSubsetsHelper(remaining, curChoices):
  if len(remaining) == 0:
    print curChoices
  else:
    decisionOn = remaining.pop()  #modify
    allSubsetsHelper(remaining, curChoices)
    allSubsetsHelper(remaining, [decisionOn] + curChoices)
    remaining.append(decisionOn)  #unmodify

allSubsets([1,2])