First, you need to decide whether to use an iterative or recursive process - in Python it doesn't matter much (since Python doesn't employ tail call optimization), but in languages it could. For a more in-depth explanation of iterative/recursive process see this question.
In either case, it's usually best to start from the ending condition and work your way from there. In this case that's n == 1
.
Recursive process
Let's start with a recursive process. In this case, state is maintained in the call chain, and the result is computed once we have reached the innermost call (and we're returning up the call chain).
def hailstone(n):
if n == 1:
return [n], 1
Alright, that's the end condition - now we've made sure the function will return once n
is 1. Let's continue and add the rest of the conditions:
def hailstone_rec(n):
if n == 1:
return (n,), 1
if n % 2 == 0: # Even
rest = hailstone_rec(n//2)
else: # Odd
rest = hailstone_rec(n*3+1)
return (n,) + rest[0], rest[1] + 1
What happens here when n
is not 1, is that we first recurse to compute the rest of the sequence, and then add the values for current invocation to that result. So, if n
is 2, this means we'll compute hailstone_rec(2//2)
which will return ((1,), 1)
, and then we add the current values and return the result (((2, 1), 2)
).
Iterative process
With an iterative process, the result is computed while proceeding down the call chain, and you pass the current state into the next function call when recursing. This means that the result does not depend on calls higher up in the call chain, which means that when the end is reached, the innermost function can just return the result. This has significance in languages with tail call optimization, since the state of the outer calls may be discarded.
When implementing a function with this process it's often helpful to use an inner helper function with extra parameters to pass on the state, so let's define a user-facing function and an inner helper function:
# user facing function
def hailstone_it(n):
# Call the helper function with initial values set
return hailstone_it_helper(n, (), 0)
# internal helper function
def hailstone_it_helper(n, seq, length):
pass
As can be seen, the user-facing function just calls the internal function with the state parameters set to initial values. Let's continue implementing the actual logic, all of which will reside inside the helper. As with the previous solution, let's start with the end condition (n == 1
):
def hailstone_it_helper(n, seq, length):
if n == 1:
return seq + (n,), length + 1
This time around, we take partial result from the previous call(s), add the current values, and return that. Now, handle the remaining cases:
def hailstone_it_helper(n, seq, length):
if n == 1:
return seq + (n,), length + 1
if n % 2 == 0: # Even
return hailstone_it_helper(n//2, seq + (n,), length + 1)
else:
return hailstone_it_helper(n*3+1, seq + (n,), length + 1)
def hailstone_it(n):
return hailstone_it_helper(n, (), 0)
When recursing here, we pass in the next n in the series (depending on whether the current n is even or odd), and the result so far.
A more pythonic solution
Finally, since you would normally not code like this in Python, let's finish with a more pythonic solution as a bonus (i.e, one that's not using recursion at all):
def hailstone_pythonic(n):
seq = []
while n != 1:
seq.append(n)
if n % 2 == 0:
n //= 2
else:
n = n * 3 + 1
seq.append(n)
return tuple(seq), len(seq)