An Introduction to Recursion, Part 1

来源:互联网 发布:实名制淘宝小号 编辑:程序博客网 时间:2024/05/17 00:07
 

Recursion is a wonderful programming tool. It provides a simple,powerful way of approaching a variety of problems. It is often hard,however, to see how a problem can be approached recursively; it can behard to "think" recursively. It is also easy to write a recursiveprogram that either takes too long to run or doesn't properly terminateat all. In this article we'll go over the basics of recursion andhopefully help you develop, or refine, a very important programmingskill.

What is Recursion?
In order to say exactly what recursion is, we first have to answer"What is recursion?" Basically, a function is said to be recursive ifit calls itself. Below is pseudocode for a recursive function thatprints the phrase "Hello World" a total of count times:

function HelloWorld(count)
{
if(count<1)return
print("Hello World!")
HelloWorld(count - 1)
}

It might not be immediately clear what we're doing here - so let's follow through what happens if we call our function with count set to 10. Since countis not less than 1, we do nothing on the first line. On the next, weprint "Hello World!" once. At this point we need to print our phrase 9more times. Since we now have a HelloWorld function that can do justthat, we simply call HelloWorld (this time with count set to 9)to print the remaining copies. That copy of HelloWorld will print thephrase once, and then call another copy of HelloWorld to print theremaining 8. This will continue until finally we call HelloWorld with countset to zero. HelloWorld(0) does nothing; it just returns. OnceHelloWorld(0) has finished, HelloWorld(1) is done too, and it returns.This continues all the way back to our original call of HelloWorld(10),which finishes executing having printed out a total of 10 "HelloWorld!"s.

You may be thinking this isnot terribly exciting, but this function demonstrates some keyconsiderations in designing a recursive algorithm:

  1. It handles a simple "base case" without using recursion.
    In this example, the base case is "HelloWorld(0)"; if the function isasked to print zero times then it returns without spawning any more"HelloWorld"s.
  2. It avoids cycles.
    Imagine if "HelloWorld(10)" called "HelloWorld(10)" which called"HelloWorld(10)." You'd end up with an infinite cycle of calls, andthis usually would result in a "stack overflow" error while running. Inmany recursive programs, you can avoid cycles by having each functioncall be for a problem that is somehow smaller or simpler than theoriginal problem. In this case, for example, countwill be smaller and smaller with each call. As the problem gets simplerand simpler (in this case, we'll consider it "simpler" to printsomething zero times rather than printing it 5 times) eventually itwill arrive at the "base case" and stop recursing. There are many waysto avoid infinite cycles, but making sure that we're dealing withprogressively smaller or simpler problems is a good rule of thumb.
  3. Each call of the function represents a complete handling of the given task.
    Sometimes recursion can seem kind of magical in the way it breaks downbig problems. However, there is no such thing as a free lunch. When ourfunction is given an argument of 10, we print "Hello World!" once andthen we print it 9 more times. We can pass a part of the job along to arecursive call, but the original function still has to account for all10 copies somehow.

Why use Recursion?
The problem we illustrated above is simple, and the solution we wroteworks, but we probably would have been better off just using a loopinstead of bothering with recursion. Where recursion tends to shine isin situations where the problem is a little more complex. Recursion canbe applied to pretty much any problem, but there are certain scenariosfor which you'll find it's particularly helpful. In the remainder ofthis article we'll discuss a few of these scenarios and, along the way,we'll discuss a few more core ideas to keep in mind when usingrecursion.

Scenario #1: Hierarchies, Networks, or Graphs
In algorithm discussion, when we talk about a graph we're generally nottalking about a chart showing the relationship between variables (likeyour TopCoder ratings graph, which shows the relationship between timeand your rating). Rather, we're usually talking about a network ofthings, people, or concepts that are connected to each other in variousways. For example, a road map could be thought of as a graph that showscities and how they're connected by roads. Graphs can be large,complex, and awkward to deal with programatically. They're also verycommon in algorithm theory and algorithm competitions. Luckily, workingwith graphs can be made much simpler using recursion. One common typeof a graph is a hierarchy, an example of which is a business'sorganization chart:

NameManagerBettySamBobSallyDilbertNathanJosephSallyNathanVeronicaSallyVeronicaSamJosephSusanBobVeronica 

Inthis graph, the objects are people, and the connections in the graphshow who reports to whom in the company. An upward line on our graphsays that the person lower on the graph reports to the person abovethem. To the right we see how this structure could be represented in adatabase. For each employee we record their name and the name of theirmanager (and from this information we could rebuild the whole hierarchyif required - do you see how?).

Nowsuppose we are given the task of writing a function that looks like"countEmployeesUnder(employeeName)". This function is intended to tellus how many employees report (directly or indirectly) to the personnamed by employeeName. For example, suppose we're calling "countEmployeesUnder('Sally')" to find out how many employees report to Sally.

Tostart off, it's simple enough to count how many people work directlyunder her. To do this, we loop through each database record, and foreach employee whose manager is Sally we increment a counter variable.Implementing this approach, our function would return a count of 2: Boband Joseph. This is a start, but we also want to count people likeSusan or Betty who are lower in the hierarchy but report to Sallyindirectly. This is awkward because when looking at the individualrecord for Susan, for example, it's not immediately clear how Sally isinvolved.

A good solution, as youmight have guessed, is to use recursion. For example, when we encounterBob's record in the database we don't just increment the counter byone. Instead, we increment by one (to count Bob) and then increment itby the number of people who report to Bob. How do we find out how manypeople report to Bob? We use a recursive call to the function we'rewriting: "countEmployeesUnder('Bob')". Here's pseudocode for thisapproach:

function countEmployeesUnder(employeeName)
{
declare variable counter
counter = 0
for each person in employeeDatabase
{
if(person.manager == employeeName)
{
counter = counter + 1
counter = counter + countEmployeesUnder(person.name)
}
}
return counter
}

If that's not terribly clear, yourbest bet is to try following it through line-by-line a few timesmentally. Remember that each time you make a recursive call, you get anew copy of all your local variables. This means that there will be aseparate copy of counter for each call. If that wasn't thecase, we'd really mess things up when we set counter to zero at thebeginning of the function. As an exercise, consider how we could changethe function to increment a global variable instead. Hint: if we wereincrementing a global variable, our function wouldn't need to return avalue.

Mission Statements
A very important thing to consider when writing a recursive algorithmis to have a clear idea of our function's "mission statement." Forexample, in this case I've assumed that a person shouldn't be countedas reporting to him or herself. This means"countEmployeesUnder('Betty')" will return zero. Our function's missionstatment might thus be "Return the count of people who report, directlyor indirectly, to the person named in employeeName - not including the person named employeeName."

Let'sthink through what would have to change in order to make it so a persondid count as reporting to him or herself. First off, we'd need to makeit so that if there are no people who report to someone we return oneinstead of zero. This is simple -- we just change the line "counter =0" to "counter = 1" at the beginning of the function. This makes sense,as our function has to return a value 1 higher than it did before. Acall to "countEmployeesUnder('Betty')" will now return 1.

However,we have to be very careful here. We've changed our function's missionstatement, and when working with recursion that means taking a closelook at how we're using the call recursively. For example,"countEmployeesUnder('Sam')" would now give an incorrect answer of 3.To see why, follow through the code: First, we'll count Sam as 1 bysetting counter to 1. Then when we encounter Betty we'll count her as1. Then we'll count the employees who report to Betty -- and that willreturn 1 now as well.

It's clearwe're double counting Betty; our function's "mission statement" nolonger matches how we're using it. We need to get rid of the line"counter = counter + 1", recognizing that the recursive call will nowcount Betty as "someone who reports to Betty" (and thus we don't needto count her before the recursive call).

Asour functions get more and more complex, problems with ambiguous"mission statements" become more and more apparent. In order to makerecursion work, we must have a very clear specification of what eachfunction call is doing or else we can end up with some very difficultto debug errors. Even if time is tight it's often worth starting out bywriting a comment detailing exactly what the function is supposed todo. Having a clear "mission statement" means that we can be confidentour recursive calls will behave as we expect and the whole picture willcome together correctly.

In partII,we'll look at how recursion works with multiple related decisions, suchas navigating a maze, and with explicit recursive relationships.

原创粉丝点击