Tip
Remember that our goal for Monday’s lesson and Tuesday’s section is to get you practicing how to read recursive code and understand how it works. It’s okay if the process of how to come up with a solution is still kind of hazy, since this is a skill that comes with practice; Thursday’s section is all about getting you to solve problems recursively.
Working with Digits¶
When solving problems recursively, there are usually common patterns that show up in various types of problems depending on what type of data you’re working with. In class with the countdown
example, we saw how to write something that could be written like a loop using recursion instead. In this example, we will work with an example that will show another common pattern: working with digits of a number.
Say we wanted to write a method called repeatDigits
that takes a number n
and returns the number that resulted from repeating each digit of n
twice. For example repeatDigits(123)
should return 112233
while repeatDigits(2)
should return 22
. While we are solving the problem, let’s assume the number n
is non-negative.
Trying to use the pattern we saw in class, we might first start by writing a base case and recursive case that matched the writeStars
example that would look something like this
public static int repeatDigits(int n) {
if (n == 0) {
// do base case
} else {
int smallerResult = repeatDigits(n - 1);
// somehow combine smallerResult to get bigger result
}
}
This ends up not quite working because we blindly applied a pattern where it doesn’t quite fit. I would not suggest immediately starting with pattern matching, but instead think through the cases for recursion. Thinking about these cases can be very challenging at first! It’s okay if you struggle with this now and don’t really understand the following list, but hopefully with some practice and after seeing the rest of the problem you will have some strong case analysis muscles to flex on future recursion problems!
- What is the simplest instance of this problem I can solve immediately? This is usually the base case.
- What is the smallest part of the problem I can break off so that I can
- Use the remaining input in a recursive call that we will assume works.
- Combine that result the value we broke off get the answer
Our previous approach doesn’t really work because it doesn’t break up the problem correctly; making our recursive call on n - 1
takes the number from 123
to 122
, but that doesn’t really give us a smaller version of the problem we wanted to solve. Let’s start by identifying what the proper base case is and then using that to identify how to write the recursive case.
Base Case¶
What’s the smallest number that I know I can automatically repeat the digits of without having to break it up in to smaller numbers? Many students would say 0
which is correct. We know the result of repeating 0
would still have the value 0
(since leading 0s don’t matter in mathematics). This means we know we will have a base case for
public static int repeatDigits(int n) {
if (n == 0) {
return 0;
}
}
There is however, another set of numbers that are really easy to repeat digits of: numbers that are only one digit long. If I gave you the number 1
, you could tell me right away, without much work, that the answer is 11
. For this problem we will actually have another base case to handle if n
is a single digit. This means we will have more than one base case in our problem since there are multiple types of inputs that we can automatically answer the question to.
How do we know if n
only has one digit? All numbers less than ten have one digit. So now we will have in our code
public static int repeatDigits(int n) {
if (n == 0) {
return 0;
} else if (n < 10) {
// this is the same as `10 * n + n`, which is what we want.
return 11 * n;
}
}
Pause: What cases have we considered so far and does our code work for those cases? We have considered the number 0
and single digit numbers. If you were to plug our code in jGrasp you would see it works for those cases. It might help to think of recursion as some kind of “case analysis”; you only need to think about the simple cases at first to get your base case working.
Recursive Case¶
So now we are confident our code works in the simplest cases, let’s try a slightly bigger example like 123
. If I were to repeat the digits of this number, I should get 112233
.
We need some way to identify how to break this number 123
up into smaller number(s) so that we can recursively call the method we are writing on those smaller problems; if at each level we are decreasing the size of the input, we will eventually hit our base case which we know works! What is the smallest amount of work we can break of from 123
to pass the rest off to someone else? A single digit!
You might remember from 142 that there is a certain operator called “mod” (%
) that lets us easily grab digits from numbers. For example with n = 123
n / 10 | n % 10
-------+-------
12 | 3
Notice that we now have two numbers, one with two digits and the other with one, that we want to repeat and combine to get the result we want of 112233
somehow repeat digits of (n / 10) | somehow repeat digits of (n % 10)
----------------------------------+----------------------------------
1122 | 33
Now we just need to figure out: how do I repeat the digits of n / 10
and n % 10
? If only I had a method, given a number, would return to me the result of repeating the digits of that number. Oh wait! We are writing that method right now! This is the recursive calls! Then we just need to do a little numerical trick to “scoot” the 1122
over by two digits so that we can add the 33
on to get our final result. This will look like:
// n = 123
public static int repeatDigits(int n) {
if (n == 0) {
return 0;
} else if (n < 10) {
return 11 * n;
} else {
// When n = 123: n % 10 == 3, n / 10 == 12
int repeatedLastDigit = repeatDigits(n % 10); // repeatedLastDigit = 33
int repeatedRemainingDigits = repeatDigits(n / 10); // repeatedRemainingDigits = 1122
// now combine: 112200 + 33 = 112233
return 100 * repeatedRemainingDigits + repeatedLastDigit;
}
}
The last thing to notice about our solution is the fact that we don’t actually need two base cases to solve this problem! In this case the n < 10
case also handles the case when n = 0
. It’s not necessarily bad to have more than one base case, but we always want to reduce the number of cases if one case can completely handle the other. For brevity, we also remove the variables but those can be helpful in many cases for readability! Here is our final solution:
Final Version¶
// pre : n >= 0
// post: returns the number resulting from repeating each digit of n twice. For example,
// given the number 348, will return 334488.
public static int repeatDigits(int n) {
if (n < 10) {
return 11 * n;
} else {
return 100 * repeatDigits(n / 10) + repeatDigits(n % 10);
}
}
Does it Work?¶
It can be pretty tricky to know if your solution is correct and for many, it can be really hard to see how this works in the first place! Thinking recursively is something that does not come naturally to most people so it’s okay to see this and feel super lost. What usually helps is to see what we call a “recursive trace” to understand what this solution does starting from the top, much like we did in class today. Here is a picture of a trace of this program that shows all the recursive calls made with what values and how it combines those results. Below is a trace of repeatDigits(348)
:
repeatDigits(348)
not < 10, so execute second branch
compute repeatDigits(34)
| not < 10, so execute second branch
| compute repeatDigits(3)
| | is < 10, so execute first branch
| | return n * 11 (33)
| compute repeatDigits(4)
| | is < 10, so execute first branch
| | return n * 11 (44)
| return 100 * first + second (33 * 100 + 44 = 3344)
compute repeatDigits(8)
| is < 10, so execute first branch
| return n * 11 (88)
return first * 100 + second (3344 * 100 + 88 = 334488)
Section tomorrow will focus a lot more on how to look at recursive code and figure out it’s output, so don’t worry too much if it was tricky right now to follow along with what the trace was saying. This is something that becomes easier with practice and seeing lots of problems. While the skill is tricky, it’s immensely helpful when we are actually writing recursive functions to check if our code is correct.
Check your understanding¶
Try these Practice-It problems