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 42How 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)
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
printAllBinary(2): 00 01 10 11 printAllBinary(3): 000 001 010 011 100 101 110 111
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
allSubsets([]): [] allSubsets([1]): [1] allSubsets(["a", "hello"]): [] ["a"] ["hello"] ["a", "hello"]
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])