A recursive example of dashes

Review iterative dashes

You may recall a method we wrote called dashes which took a String parameter and returned a new String that was equivalent to the given parameter with dashes inserted between every character. For example, a call to dashes("pupper") would return the String

"p-u-p-p-e-r"
Previously we implemented an iterative solution that looked something like this:

public static String dashes(String s) {
    if (s.isEmpty()) {
        return s;
    } else {
        String result = "" + s.charAt(0);
        for (int i = 1; i < s.length(); i++) {
            result += "-" + s.charAt(i);
        }
        return result;
    }
}
While that works just fine, let’s go ahead and explore writing a recursive solution to the same method.

Reviewing the components of recursive methods

Before we dive into the details of solving dashes recursively, we should first remind ourselves the components of a recursive solution. We can recall that a recursive method deals with only one small part of a problem before passing on the rest of the problem to other method calls. We also can recall that recursive solutions are made up of a combination of base cases and recursive cases. It may help to review this brief outline of things needed to wrtie recursive methods:

Outline for writing recursive methods:

  • Figure out the base case(s). These tend to be the simplest, most basic cases of the problem we can solve immediately. It may help to think of them as the “bottom” or “end” of the recursive call chain (hence the name “base case”)
  • Figure out what is the smallest “part” or “section” of the problem we should break off in the recursive case
  • Deal with the recursive case(s). This usually involves solving the small part of the problem that we broke off in the previous step and combining it with the result of a recursive call to the same method on the smaller sub-problem

Implementing dashes recursively

Now that we’ve reviewed the components of recursive methods, let’s keep them in mind as we implement dashes recursively. To start, we will write the method stub

// Returns a String which is the result of adding dashes
// in between the characters of the given String
public static String dashes(String s) {
    //TODO: implement this method
}

We can begin by figuring out what our base case should be. What’s the simplest problem we can solve immediately? The case where we are passed the empty String, in which we want to return an empty String (we can’t insert dashes in-between an empty String). Let’s add our base case

// Returns a String which is the result of adding dashes
// in between the characters of the given String
public static String dashes(String s) {
    if (s.isEmpty()) {
        return "";
    } else {
        //TODO: implement the recursive case
    }
}

What do we want to do if our String is not empty? This is where we begin to write the recursive case. Remember, our recursive case should just do the smallest amount of work to solve part of the problem before passing off the rest of the work to other recursive calls (we don’t want to do too much in each method call!). We need to somehow add dashes to our String, so a small part of the problem we could do is break off one character, add one dash, and then combine that with a dashed version of the rest of the String. Wouldn’t getting a dashed version of the rest of the String be easy if we had a method to add dashes to a String? Thankfully, we are writing one so we can call it recursively to perform this last task! Below is a good example of a recursive case

// Returns a String which is the result of adding dashes
// in between the characters of the given String
public static String dashes(String s) {
    if (s.isEmpty()) {
        return "";
    } else {
        return s.charAt(0) + "-" + dashes(s.substring(1));
    }
}
s.charAt(0) breaks off the first character and then the + "-" adds a dash. Then we need to figure out how to add dashes in between the characters of the rest of the given String (s.substring(1)). If we struggle to figure out exactly how to use our recursive method, it can sometimes help to look at the method comment in order to find the sub-problem to call it with. Our method comment states

Returns a String which is the result of adding dashes
in between the characters of the given String
Note that this is exactly the functionality we want to perform on s.substring(1)!!! Rather than iteratively implementing all of that functionality in our recursive case, we can use the recursive call dashes(s.substring(1)) to achieve the desired result.

A reminder of the importance of testing code

Now that we have an attempt at a solution, we should test it on some sample inputs to make sure it is working as expected. In reality, we want to test on a wide variety of cases, such as

  • The empty String
  • A one character String
  • An even-length String
  • An odd-length String
  • Any other cases specific/special to our problem’s functionality

For now, let’s just test our solution to dashes on the empty String, a String with one character "P", and a slightly longer example "pup".

For the empty String our solution would enter the base case and correctly return the empty String.

dashes("");
    is empty, so return ""
Nice!

For our one character String, we would want to return exactly that character (you can’t have dashes between one character). Let’s trace through what would happen

dashes("P")
    is not empty, so execute recursive case
    compute dashes("")
    |    is empty, so return ""
    return firstChar + "-" + rest ('P' + "-" + "" = "P-")
By tracing through our code, we can see that for an input String with one character our output has an extra dash added at the end. We can see why, as a String with one character isn’t the empty String, so we enter our recursive case and add a dash followed by a call to dashes(""). How can we restructure our code to account for this case? It seems we need a new case for a one-character String such as the following

public static String dashes(String s) {
    if (s.isEmpty()) {
        return "";
    } else if (s.length() == 1) {
        return s;
    } else {
        return s.charAt(0) + "-" + dashes(s.substring(1));
    }
}
Now we can go back and trace through our previous inputs.

dashes("");
    is empty, so return ""
The empty String still works!

dashes("P")
    is not empty but length is 1 so return same String ("P")
And now our method works for a one-character String. Let’s go ahead and try our last test case.

dashes("pup")
    not empty and length > 1 so execute last case
    compute dashes("up")
    |    not empty and length > 1 so execute last case
    |    compute dashes("p")
    |    |    not empty but length == 1 so return same String
    |    return firstChar + "-" + rest ('u' + "-" + "p" = "u-p")
    return firstChar + "-" + rest ('p' + "-" + "u-p" = "p-u-p")
Looks like it’s working for this case too!

Redundant cases

If we look at the first two cases, however, we can’t quite start celebrating our working solution. If we are given the empty String, we end up returning the empty String, and if we are given a one-character String, we return that same one-character String. In both cases, we end up returning the same String we were passed. When writing methods, it is poor code quality to write code with redundant/duplicate cases such as the method written above. Rather than having the cases be separate, let’s go ahead and combine the two cases in order to produce a stylistically-better version of our solution

public static String dashes(String s) {
    if (s.length() <= 1) {
        return s;
    } else {
        return s.charAt(0) + "-" + dashes(s.substring(1));
    }
}

Recap

As a recap, some important takeaways for writing recursive solutions are:

  • Figuring out the base case, which is often the simplest case of our problem that we can solve immediately.
  • Find the small part of the problem we will break off and solve in the recursive case. Solve this small problem and then use a recursive call to solve the rest of the problem.
  • Test recursive solutions just like we would test any other code!!! Testing is super important in programming, as we can often catch small errors that we overlook when writing methods. Often writing an incorrect solution is part of the problem solving process, and there is no harm in writing a wrong solution the first time around as long as we test and fix it!

Practice

Try out this problem and this other problem for more practice with recursion.