问题
Well, I have many 'C language' tests that involve finding the output of a given function,moreover, I need to explain precisely what's the purpose of it. Some of them are recursive functions. And when I meet recursion, I always struggle on finding how to follow it schematically, and even if I do succeed, sometimes I might not understand what is the purpose of the recursive function.
Here are 2 pieces of code:
Main
#include <stdio.h>
#include <conio.h>
int f2(int *a, int n, int x);
int main()
{
int a[6] = {4, 3, 4, 2};
printf("%d\n", f2(a, 4, 5));
getch();
}
f2 function:
int f2(int *a, int n, int x)
{
if(n>0 && x!=0){
return (f2(a+1,n-1,x-a[0]) ? 1 : f2(a+1,n-1,x));
}
return (x ? 0 : 1);
}
Well, the 'purpose' of the function is to check whether there is a group of numbers in the array that their sum will give x's value. (which is x=5 in this particular example). In this case, it will return true, because 2,3 are inside the array and 2+3=5.
My question is: How can I, on paper, follow it schematically and understand its purpose. Or, how will you guys approach this kind of question? Any help is highly appreciated!!
回答1:
I won't add to the stack examples that came before me; they could have been taken from my own lecture materials.
I want to the three pieces that @Edwin gave you: those are your critical tools. I generally reverse the first two. Applied to your specific problem:
Termination: We continue so long as n is positive and x is not 0. When we fail either of those checks, we return whether x == 0 (interpreting the return values as false/true).
Result returned: Note that this boolean is also the sole return result.
Recursion: We try calling the function with a smaller problem:
- Chop the first element off the array a.
- Subtract that value from x
- Decrement n
{Note what we've learned so far: n is a counter; x is a running sum, where we've been given the final result; a is a list of components.}
Now, if this returns succeeds (returns true), then we pass that true back up the call stack (that's the 1 in the ternary expression). If it fails, we try again with the above bulleted steps, except without reducing x. Then pass this result back up the line, regardless of value.
So the breakdown is something like this:
- If we get to where x is 0, any time before n gets down to 0, we win.
- If we have x != 0 when n hits 0, we fail.
- Until then, our "try the next thing" step is to grab the next available number. Subtract it from x, and then call ourselves again with (a) the remainder of the list; (b) one less try (that's n), and (c) the running sum properly reduced. If that doesn't work, skip this number and try the next one.
BTW, should the middle term of that last call be n, rather than n-1? We didn't use up a guess when we skipped over a[0].
That said, I really question what this course is trying to teach you. Unless this problem is an isolated example, I don't feel that it's trying to turn you into a professional programmer. The code is uncommented, the identifiers come from the punched-card days, and the return values are "magic numbers".
回答2:
We had to do this too in school, took forever to write out during exams but it did sort of force understanding.
Basically, you end up with an execution stack, just like the compiler and runtime use behind the scenes to actually execute the code: You start at main, with a certain set of variables. This is the top of the stack. Then main calls f2, pushing that call onto the stack. This new stack frame has different local variables. Write down their values at this frame. Then f2 calls itself, pushing another frame onto the stack (it's the same function, but a different call to it with different arguments). Write down the values again.
When a function returns, pop it off the stack, and write down the return value.
It can help to use indentation to indicate the current stack depth (or just write the whole stack out if you have room). Generally there's only a few variables involved across the entire program invocation, so it makes sense to put them into a table (making it easier to follow what's going on).
A short example:
Stack | a | n | x | ret
-----------------------------------------
mn 4342 4 5
mn f2 4342 4 5
mn f2 f2 342 3 1
mn f2 f2 f2 42 2 -2
mn f2 f2 f2 f2 2 1 -6
mn f2 f2 f2 f2 f2 0 -8 0
mn f2 f2 f2 f2 (2 1 -6)
mn f2 f2 f2 f2 f2 0 -6 0
mn f2 f2 f2 f2 (2 1 -6) 0
mn f2 f2 f2 (42 2 -2)
...
回答3:
The best place to understand recursive functions is to take up a quick study of deductive reasoning, using a divide and conquer strategy for some easy problem. This won't cover every recursive problem, but it covers more than 80% of the times people use recursion.
Basically you need to discover the three elements of a recursive solution: 1. An deductive rule (or set of rules) that reduce the problem to smaller problems. 2. A set of termination rules, designed such that you are guaranteed to reach them. 3. Some place to hold the intermediate results (typically the call stack, sometimes a stack on the heap).
to provide an example, I'll use the dumbest example I can think of, let's attempt string length typically you'd calculate string length with a while loop (assuming you don't use system libraries, etc.)
(pseudo code)
int length = 0;
while (current_char != newline) {
length = length + 1;
current_char = next_char;
}
A recursive strategy would be like
(pseudo logic)
The length of a string is one more than the length of a string with one less character
The length of the "" string is zero
(pseudo java code)
int recursive_length(String s) {
if (s.equals("")) {
return 0;
}
return 1 + recursive_length(s.substring(1))
}
For a call of recursive_length, your call stack grows like so
recursive_length("hello");
which evaluates to
{1 + recursive_length("ello")}
which evaluates to
{1 + {1 + recursive_length("llo")} }
which evaluates to
{1 + {1 + {1 + recursive_length("lo")} } }
which evalutes to
{1 + {1 + {1 + {1 + recursive_length("o")} } } }
which evaulates to
{1 + {1 + {1 + {1 + {1 + recursive_length("")} } } } }
which now, due the the explicit termination rule evaluates to
{1 + {1 + {1 + {1 + {1 + {0}}}}}}
which as we return from the innermost call evaluates to
{1 + {1 + {1 + {1 + {1 + 0}}}}}
which then the addition happens (finally)
{1 + {1 + {1 + {1 + {1}}}}}
and then we return from that call
{1 + {1 + {1 + {1 + {1 + 1}}}}
and another addition
{1 + {1 + {1 + {1 + {2}}}}}
and so on
{1 + {1 + {1 + {1 + 2}}}}
{1 + {1 + {1 + {3}}}}
{1 + {1 + {1 + 3}}}
{1 + {1 + {4}}}
{1 + {1 + 4}}
{1 + {5}}
{1 + 5}
{6}
6
So what is holding all of those intermediate 1 + ...s? Well, they are in the call stack, which is being exited as we evaluate the next recursive call. As the internal calls return, the code moves back up the call stack, accumulating the answer.
Since recursion is very stack oriented it is a natural fit for some kinds of data structures. The only problem is that if you mess up your algorithm, you never hit your termination states, and your grow your stack forever.
To fix this, the execution environment is spying on the progress of the call stack, and when it feels it is going too deep, it interrupts the program with a StackOverflow error.
I this example the fact that I call the function within the function is the hint I'm recursive. The early-exit condition with a hard coded test is an obvious termination rule. The returned result is obviously the inductive reasoning that chops the problem up into a smaller one.
This means that validating a recursive function is typically a logic problem, and not a programming one. Still sometimes there are programming bugs that can foil even the best logical arguments :)
The most important part in tracking execution through one is to document the states in the various stacks. To do so in this example, I used parenthesis, but as long as you have a consistent way of keeping track of them, it doesn't matter what notation you use.
回答4:
The best way to follow a recursive function is by using a stack. Lets take the factorial function as an example
long fact(int n)
{
if (n <= 1)
return 1;
return n * fact(n - 1);
}
This is what fact(4) would look like
At the bottom of the stack is the base case: 1 factorial is 1. Now that we know 1 factorial is 1, we know what 2 factorial is, which is 2 * 1, which is 2. But since we know what 2 factorial is, we can solve 3 factorial, which is 3 * 2, which is 6. Now we know what 3 factorial is, so we can solve 4 factorial, or 4 * 6, which is 24. All the calls have been popped off the stack. Thus, we know that 4 factorial is 24, and that is returned from the function.
Using this same method, you can figure out what the function above does.
来源:https://stackoverflow.com/questions/35373736/how-to-follow-recursion-systematically