amiga-e/amigae33a/E_v3.3a/Docs/BeginnersGuide/Recursion.guide

424 lines
16 KiB
Plaintext

@database beginner.guide
@Master beginner
@Width 75
This is the AmigaGuide® file beginner.guide, produced by Makeinfo-1.55 from
the input file beginner.
@NODE "main" "Recursion"
@Next "OOE.guide/main"
@Prev "FloatingPoint.guide/main"
@Toc "Contents.guide/main"
Recursion
*********
A @{fg shine }recursive@{fg text } function is very much like a function which uses a loop.
Basically, a @{fg shine }recursive@{fg text } function calls itself (usually after some
manipulation of data) rather than iterating a bit of code using a loop.
There are also recursive types, which are objects with elements which have
the object type (in E these would be pointers to objects). We've already
seen a recursive type: linked lists, where each element in the list
contains a pointer to the next element (see @{"Linked Lists" Link "Types.guide/Linked Lists" }).
Recursive definitions are normally much more understandable than an
equivalent iterative definition, and it's usually easier to use recursive
functions to manipulate this data from a recursive type. However,
recursion is by no means a simple topic. Read on at your own peril!
@{" Factorial Example " Link "Factorial Example" }
@{" Mutual Recursion " Link "Mutual Recursion" }
@{" Binary Trees " Link "Binary Trees" }
@{" Stack (and Crashing) " Link "Stack (and Crashing)" }
@{" Stack and Exceptions " Link "Stack and Exceptions" }
@ENDNODE
@NODE "Factorial Example" "Factorial Example"
@Next "Mutual Recursion"
@Toc "main"
Factorial Example
=================
The normal example for a recursive definition is the factorial
function, so let's not be different. In school mathematics the symbol @{b }!@{ub }
is used after a number to denote the factorial of that number (and only
positive integers have factorials). n! is n-factorial, which is defined
as follows:
n! = n * (n-1) * (n-2) * ... * 1 (for n >= 1)
So, 4! is 4*3*2*1, which is 24. And, 5! is 5*4*3*2*1, which is 120.
Here's the iterative definition of a factorial function (we'll @{b }Raise@{ub } an
exception is the number is not positive, but you can safely leave this
check out if you are sure the function will be called only with positive
numbers):
PROC fact_iter(n)
DEF i, result=1
IF n<=0 THEN Raise("FACT")
FOR i:=1 TO n
result:=result*i
ENDFOR
ENDPROC result
We've used a @{b }FOR@{ub } loop to generate the numbers one to @{b }n@{ub } (the parameter to
the @{b }fact_iter@{ub }), and @{b }result@{ub } holds the intermediate and final results. The
final result is returned, so check that @{b }fact_iter(4)@{ub } returns 24 and
@{b }fact_iter(5)@{ub } returns 120 using a @{b }main@{ub } procedure something like this:
PROC main()
WriteF('4! is \\d\\n5! is\\d\\n', fact_iter(4), fact_iter(5))
ENDPROC
If you're really observant you might have noticed that 5! is 5*4!, and,
in general, n! is n*(n-1)!. This is our first glimpse of a recursive
definition--we can define the factorial function in terms of itself. The
real definition of factorial is (the reason why this is the real
definition is because the `...' in the previous definition is not
sufficiently precise for a mathematical definition):
1! = 1
n! = n * (n-1)! (for n > 1)
Notice that there are now two cases to consider. The first case is called
the @{fg shine }base@{fg text } case and gives an easily calculated value (i.e., no recursion
is used). The second case is the @{fg shine }recursive@{fg text } case and gives a definition
in terms of a number nearer the base case (i.e., (n-1) is nearer 1 than n,
for n>1). The normal problem people get into when using recursion is they
forget the base case. Without the base case the definition is meaningless.
Without a base case in a recursive program the machine is likely to crash!
(See @{"Stack (and Crashing)" Link "Stack (and Crashing)" }.)
We can now define the recursive version of the @{b }fact_iter@{ub } function
(again, we'll use a @{b }Raise@{ub } if the number parameter is not positive):
PROC fact_rec(n)
IF n=1
RETURN 1
ELSEIF n>=2
RETURN n*fact_rec(n-1)
ELSE
Raise("FACT")
ENDIF
ENDPROC
Notice how this looks just like the mathematical definition, and is nice
and compact. We can even make a one-line function definition (if we omit
the check on the parameter being positive):
PROC fact_rec2(n) RETURN IF n=1 THEN 1 ELSE n*fact_rec2(n-1)
You might be tempted to omit the base case and write something like this:
/* Don't do this! */
PROC fact_bad(n) RETURN n*fact_bad(n-1)
The problem is the recursion will never end. The function @{b }fact_bad@{ub } will
be called with every number from @{b }n@{ub } to zero and then all the negative
integers. A value will never be returned, and the machine will crash
after a while. The precise reason why it will crash is given later (see
@{"Stack (and Crashing)" Link "Stack (and Crashing)" }).
@ENDNODE
@NODE "Mutual Recursion" "Mutual Recursion"
@Next "Binary Trees"
@Prev "Factorial Example"
@Toc "main"
Mutual Recursion
================
In the previous section we saw the function @{b }fact_rec@{ub } which called
itself. If you have two functions, @{b }fun1@{ub } and @{b }fun2@{ub }, and @{b }fun1@{ub } calls @{b }fun2@{ub },
and @{b }fun2@{ub } calls @{b }fun1@{ub }, then this pair of functions are @{fg shine }mutually@{fg text } recursive.
This extends to any amount of functions linked in this way.
This is a rather contrived example of a pair of mutually recursive
functions.
PROC f(n)
IF n=1
RETURN 1
ELSEIF n>=2
RETURN n*g(n-1)
ELSE
Raise("F")
ENDIF
ENDPROC
PROC g(n)
IF n=1
RETURN 2*1
ELSEIF n>=2
RETURN 2*n*f(n-1)
ELSE
Raise("G")
ENDIF
ENDPROC
Both functions are very similar to the @{b }fact_rec@{ub } function, but @{b }g@{ub } returns
double the normal values. The overall effect is that every other value in
long version of the multiplication is doubled. So, @{b }f(n)@{ub } computes
n*(2*(n-1))*(n-2)*(2*(n-3))*...*2 which probably isn't all that
interesting.
@ENDNODE
@NODE "Binary Trees" "Binary Trees"
@Next "Stack (and Crashing)"
@Prev "Mutual Recursion"
@Toc "main"
Binary Trees
============
This is an example of a recursive type and the effect it has on
functions which manipulate this type of data. A @{fg shine }binary tree@{fg text } is like a
linked list, but instead of each element containing only one link to
another element there are two links in each element of a binary tree
(which point to smaller trees called @{fg shine }branches@{fg text }). The first link points
to the @{i }left@{ui } branch and the second points to the @{i }right@{ui } branch. Each
element of the tree is called a @{fg shine }node@{fg text } and there are two kinds of special
node: the start point, called the @{fg shine }root@{fg text } of the tree (like the head of a
list), and the nodes which do not have left or right branches (i.e., @{b }NIL@{ub }
pointers for both links), called @{fg shine }leaves@{fg text }. Every node of the tree
contains some kind of data (just as the linked lists contained an E-string
or E-list in each element). The following diagram illustrates a small
tree.
+------+
| Root |
+--*---+
/ \\
Left / \\ Right
/ \\
+------* *------+
| Node | | Node |
+--*---+ +--*---+
/ / \\
Left / Left / \\ Right
/ / \\
+--*---+ +----*-+ +-*----+
| Leaf | | Leaf | | Leaf |
+------+ +------+ +------+
Notice that a node might have only one branch (it doesn't have to have
both the left and the right). Also, the leaves on the example were all at
the same level, but this doesn't have to be the case. Any of the leaves
could easily have been a node which had a lot of nodes branching off it.
So, how can a tree structure like this be written as an E object?
Well, the general outline is this:
OBJECT tree
data
left:PTR TO tree, right:PTR TO tree
ENDOBJECT
The @{b }left@{ub } and @{b }right@{ub } elements are pointers to the left and right branches
(which will be @{b }tree@{ub } objects, too). The @{b }data@{ub } element is some data for each
node. This could equally well be a pointer, an @{b }ARRAY@{ub } or a number of
different data elements.
So, what use can be made of such a tree? Well, a common use is for
holding a sorted collection of data that needs to be able to have elements
added quickly. As an example, the data at each node could be an integer,
so a tree of this kind could hold a sorted set of integers. To make the
tree sorted, constraints must be placed on the left and right branches of
a node. The left branch should contain only nodes with data that is @{i }less@{ui }
than the parent node's data, and, similarly, the right branch should
contain only nodes with data that is @{i }greater@{ui }. Nodes with the same data
could be included in one of the branches, but for our example we'll
disallow them. We are now ready to write some functions to manipulate our
tree.
The first function is one which starts off a new set of integers (i.e.,
begins a new tree). This should take an integer as a parameter and return
a pointer to the root node of new tree (with the integer as that node's
data).
PROC new_set(int)
DEF root:PTR TO tree
NEW root
root.data:=int
ENDPROC root
The memory for the new tree element must be allocated dynamically, so this
is a good example of a use of @{b }NEW@{ub }. Since @{b }NEW@{ub } clears the memory it
allocates all elements of the new object will be zero. In particular, the
@{b }left@{ub } and @{b }right@{ub } pointers will be @{b }NIL@{ub }, so the root node will also be a leaf.
If the @{b }NEW@{ub } fails a @{b }"MEM"@{ub } exception is raised; otherwise the data is set to
the supplied value and a pointer to the root node is returned.
To add a new integer to such a set we need to find the appropriate
position to insert it and set the left and right branches correctly. This
is because if the integer is new to the set it will be added as a new
leaf, and so one of the existing nodes will change its left or right
branch.
PROC add(i, set:PTR TO tree)
IF set=NIL
RETURN new_set(i)
ELSE
IF i<set.data
set.left:=add(i, set.left)
ELSEIF i>set.data
set.right:=add(i, set.right)
ENDIF
RETURN set
ENDIF
ENDPROC
This function returns a pointer to the set to which it added the integer.
If this set was initially empty a new set is created; otherwise the
original pointer is returned. The appropriate branches are corrected as
the search progresses. Only the last assignment to the left or right
branch is significant (all others do not change the value of the pointer),
since it is this assignment that adds the new leaf. Here's an iterative
version of this function:
PROC add_iter(i, set:PTR TO tree)
DEF node:PTR TO tree
IF set=NIL
RETURN new_set(i)
ELSE
node:=set
LOOP
IF i<node.data
IF node.left=NIL
node.left:=new_set(i)
RETURN set
ELSE
node:=node.left
ENDIF
ELSEIF i>node.data
IF node.right=NIL
node.right:=new_set(i)
RETURN set
ELSE
node:=node.right
ENDIF
ELSE
RETURN set
ENDIF
ENDLOOP
ENDIF
ENDPROC
As you can see, it's quite a bit messier. Recursive functions work well
with manipulation of recursive types.
Another really neat example is printing the contents of the set. It's
deceptively simple:
PROC show(set:PTR TO tree)
IF set<>NIL
show(set.left)
WriteF('\\d ', set.data)
show(set.right)
ENDIF
ENDPROC
The integers in the nodes will get printed in order (providing they were
added using the @{b }add@{ub } function). The left-hand nodes contain the smallest
elements so the data they contain is printed first, followed by the data
at the current node, and then that in the right-hand nodes. Try writing
an iterative version of this function if you fancy a really tough problem.
Putting everything together, here's a @{b }main@{ub } procedure which can be used
to test the above functions:
PROC main() HANDLE
DEF s, i, j
Rnd(-999999) /* Initialise seed */
s:=new_set(10) /* Initialise set s to contain the number 10 */
WriteF('Input:\\n')
FOR i:=1 TO 50 /* Generate 50 random numbers and add them to set s */
j:=Rnd(100)
add(j, s)
WriteF('\\d ',j)
ENDFOR
WriteF('\\nOutput:\\n')
show(s) /* Show the contents of the (sorted) set s */
WriteF('\\n')
EXCEPT
IF exception="NEW" THEN WriteF('Ran out of memory\\n')
ENDPROC
@ENDNODE
@NODE "Stack (and Crashing)" "Stack (and Crashing)"
@Next "Stack and Exceptions"
@Prev "Binary Trees"
@Toc "main"
Stack (and Crashing)
====================
When you call a procedure you use up a bit of the program's @{fg shine }stack@{fg text }.
The stack is used to keep track of procedures in a program which haven't
finished, and real problems can arise when the stack space runs out.
Normally, the amount of stack available to each program is sufficient,
since the E compiler handles all the fiddly bits quite well. However,
programs which use a lot of recursion can quite easily run out of stack.
For example, the @{b }fact_rec(10)@{ub } will need enough stack for ten calls of
@{b }fact_rec@{ub }, nine of which are recursively called. This is because each call
does not finish until the return value has been computed, so all recursive
calls up to @{b }fact_rec(1)@{ub } need to be kept on the stack until @{b }fact_rec(1)@{ub }
returns one. Then each procedure will be taken off the stack as they
finish. If you try to compute @{b }fact_rec(40000)@{ub }, not only will this take a
long time, but it will probably run out of stack space. When it does run
out of stack, the machine will probably crash or do other weird things.
The iterative version, @{b }fact_iter@{ub } does not have these problems, since it
only takes one procedure call to calculate a factorial using this function.
If there is the possibility of running out of stack space you can use
the @{b }FreeStack@{ub } (built-in) function call (see @{"System support functions" Link "BuiltIns.guide/System support functions" }).
This returns the amount of free stack space. If it drops below about 1KB
then you might like to stop the recursion or whatever else is using up the
stack. Also, you can specify amount of stack your program gets (and
override what the compiler might decide is appropriate) using the @{b }OPT
STACK@{ub } option. See the `Reference Manual' for more details on E's
stack organisation.
@ENDNODE
@NODE "Stack and Exceptions" "Stack and Exceptions"
@Prev "Stack (and Crashing)"
@Toc "main"
Stack and Exceptions
====================
The concept `recent' used earlier is connected with the stack (see
@{"Raising an Exception" Link "Exceptions.guide/Raising an Exception" }). A recent procedure is one which is on the stack,
the most recent being the current procedure. So, when @{b }Raise@{ub } is called it
looks through the stack until it finds a procedure with an exception
handler. That handler will then be used, and all procedures before the
selected one on the stack are taken off the stack.
Therefore, a recursive function with an exception handler can use @{b }Raise@{ub }
in the handler to call the handler in the previous (recursive) call of the
function. So anything that has been recursively allocated can be
`recursively' deallocated by exception handlers. This is a very powerful
and important feature of exception handlers.
@ENDNODE