0

There is a mathematical puzzle:

  1. Pick a positive integer n as start.
  2. If n is even, then divide it by 2.
  3. If n is odd, multiply it by 3, and add 1.
  4. continue this process until n is 1.

I want to write a recursive function, that takes the n as parameter, and then return a tuple that includes a sequence of track of n in the process, and the length of sequence. But failed.

What wrong is my code? How to achieve the task?

def hailstone(n):
    """the implementation of recursive one,
    return a tuple that includes the hailstone sequence
    starting at n, and the length of sequence.

    >>> a = hailstone(1)
    ([1, 4, 2, 1], 4)
    """
    if n % 2 == 0:
        n = n//2
    else:
        n = n*3 + 1

    if n == 1:
        return [n]
    else:
        """
        for some magic code here,
        it will return a tuple that includes the list track of n,
        and the length of sequence,
        like ([1, 4, 2, 1], 4)
        """
        return ([n for some_magic in hailstone(n)], lenght_of_seq)
soarinblue
  • 1,517
  • 3
  • 21
  • 30

2 Answers2

2

You can use an accumulator:

def hailstone(n):
    """
    return a tuple that includes the hailstone sequence
    starting at n, and the length of sequence.
    1) Pick a positive integer n as start.
    2) If n is even, then divide it by 2.
    3) If n is odd, multiply it by 3, and add 1.
    4) continue this process until n is 1.
    """

    def hailstone_acc(n, acc):
        acc.append(n)
        if n == 1:
            return acc, len(acc)
        elif n % 2 == 0:
            return hailstone_acc(n//2, acc)
        else:
            return hailstone_acc(3*n + 1, acc)

    if n == 1:
        return hailstone_acc(4,[1])
    else:
        return hailstone_acc(n, [])

Now, in action:

(trusty)juan@localhost:~/workspace/testing/hailstone$ python -i hailstone.py 
>>> hailstone(1)
([1, 4, 2, 1], 4)
>>> hailstone(4)
([4, 2, 1], 3)
>>> hailstone(23)
([23, 70, 35, 106, 53, 160, 80, 40, 20, 10, 5, 16, 8, 4, 2, 1], 16)
>>> 
juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172
2

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)
Community
  • 1
  • 1
tobbez
  • 317
  • 2
  • 8
  • Your explanation is so crystal clear, amazing answer, thank you! – soarinblue Sep 15 '16 at 14:26
  • You seem to have deleted the link in the first paragraph. I found your use of "iterative" to describe completion-passing tail calls a bit confusing out of context; since python does not optimise tail calls and has a strict recursion limit, (real) iterative solutions are definitely preferred. – rici Sep 15 '16 at 14:29
  • @rici The terminology is commonly understood in CS. Importantly, the solution is not iterative, it's *using an iterative process* (I never referred to it as just "iterative"). The concepts are generally taught in courses based on (or using) SICP. Many such courses have transitioned to using Python (rather than Scheme or Common Lisp), and in doing so, translated their assignments directly into Python, without adapting them to conform to the Python way. This makes sense in a way, because the goal is to teach the CS concepts, not "good Python". – tobbez Sep 16 '16 at 08:50