using System;
public static class CollatzConjecture
{
public static int Steps(int number)
{
if (number <= 0)
throw new ArgumentOutOfRangeException(nameof(number));
return Steps(number, 0);
}
private static int Steps(int number, int stepCount)
{
if (number == 1)
return stepCount;
if (number % 2 == 0)
return Steps(number / 2, stepCount + 1);
return Steps(number * 3 + 1, stepCount + 1);
}
}
The first step is to check the number
parameter for validity:
if (number <= 0)
throw new ArgumentOutOfRangeException(nameof(number));
The next step is to call the overload Steps()
method and return its value.
return Steps(number, 0);
For someone new to the code, it might not be clear what the 0
argument in the Steps(number, 0)
call represents.
You could introduce an appropriately named variable and use that as the argument:
var stepCount = 0;
return Steps(number, stepCount);
This is already much better, but another option is to use a named argument:
return Steps(number, stepCount: 0);
Let's examine the overload Steps()
method, which looks like this:
private static int Steps(int number, int stepCount)
{
if (number == 1)
return stepCount;
if (number % 2 == 0)
return Steps(number / 2, stepCount + 1);
return Steps(number * 3 + 1, stepCount + 1);
}
The first step is to check if the number
parameter is equal to 1
If it is, we return the step count.
This condition is often referred to the terminating condition, which is something that every recursive method must have (we'll get to the recursive component next).
If the number
is not 1
, we need to apply the algorithm:
if (number % 2 == 0)
return Steps(number / 2, stepCount + 1);
return Steps(number * 3 + 1, stepCount + 1);
We can see the algorithm being reflected here, but the interesting thing is that we're calling the same method that we're in but with different arguments. The two recursive calls represent the two options in the algorithm, and each applies the algorithm to the number to pass in the updated number of the first argument. The second argument is the step count, but incremented by one to indicate that we've applied the algorithm once.
The method call stack will look something like this:
Steps(4)
Steps(4, 0) // number = 4, stepCount = 0
Steps(2, 1) // number = 2, stepCount = 1
Steps(1, 2) // number = 1, stepCount = 2
Or in table format:
number |
stepCount |
Returned value |
---|---|---|
4 | 0 | Steps(number / 2, stepCount + 1) |
2 | 1 | Steps(number / 2, stepCount + 1) |
1 | 2 | stepCount |
You can see that the same method is called three times, with the third time no longer doing a recursive call but returning the step count as the terminating condition was reached.
Shortening
A ternary operator can be used instead of an if
statement:
var nextNumer = number % 2 == 0 ? number / 2 : number * 3 + 1;
return Steps(nextNumer, stepCount + 1);
or just inline it:
return Steps(number % 2 == 0 ? number / 2 : number * 3 + 1, stepCount + 1);
Optional parameter
As an alternative to overloading the methods, as seen in the code snippets above, it is also possible to use an optional parameter to solve the problem with only a single method implementation.
This allows callers to use the method either with a single parameter (number
) or both parameters, as used in the recursive method call. If only the number
parameter is provided the stepCount
parameter uses the defined default value.
public static int Steps(int number, int stepCount = 0)
{
if (number <= 0)
throw new ArgumentOutOfRangeException(nameof(number));
if (number == 1)
return stepCount;
if (number % 2 == 0)
return Steps(number / 2, stepCount + 1);
return Steps(number * 3 + 1, stepCount + 1);
}
This version is more concise than the overloading version, but it comes with the drawback that the method API publicly exposes the stepCount
parameter and callers might not know how to use this parameter correctly or may even provide invalid data, which then falsifies the result.