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)
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])