There are many possible implementations of mappings with finite domains. For example, hash tables are an excellent choice in many situations, but one whose discussion we shall defer to Chapter 4. Any mapping with a finite domain can be represented by the list of pairs (d1, r1), (d2, r2), . . . , (dk, rk), where d1, d2, . . . , dk are all the current members of the domain, and ri is the value that the mapping associates with di, for i = 1, 2 , . . . ,k. We can then use any implementation of lists we choose for this list of pairs.
To be precise, the abstract data type MAPPING can be implemented by lists of elementtype, if we define type elementtype = record domain: domaintype; range: rangetype end;
procedure MAKENULL ( var M: MAPPING ); var
i: domaintype; begin
for i := firstvalue to lastvalue do M[i] := undefined
end; { MAKENULL }
procedure ASSIGN ( var M: MAPPING; d: domaintype; r: rangetype );
begin
M[d] := r end; { ASSIGN }
function COMPUTE ( var M: MAPPING; d: domaintype; var r: rangetype ): boolean; begin if M[d] = undefined then return (false) else begin r := M[d]; return (true) end end; { COMPUTE }
Fig. 2.23. Array implementation of mappings.
whatever implementation of lists we choose. The three mapping commands are defined in terms of commands on type LIST in Fig. 2.24.
2.6 Stacks and Recursive Procedures
One important application of stacks is in the implementation of recursive procedures in programming languages. The run-time organization for a programming language is the set of data structures used to represent the values of the program variables during program execution. Every language that, like Pascal, allows recursive procedures, uses a stack of activation records to record the values for all the variables belonging to each active procedure of a program. When a procedure P is called, a new activation record for P is placed on the stack, regardless of whether there is already another activation record for P on the stack. When P returns, its activation record must be on top of the stack, since P cannot return until all
for this call of P to cause control to return to the point at which P was called (that point, known as the return address, was placed in P's activation record when the call to P
procedure MAKENULL ( var M: MAPPING ); { same as for list }
procedure ASSIGN ( var M: MAPPING; d: domaintype; r: rangetype );
var
x: elementtype; { the pair (d, r) }
p: position; { used to go from first to last position on list M } begin
x.domain := d; x.range := r; p := FIRST(M);
while p <> END(M) do
if RETRIEVE(p, M).domain = d then
DELETE(p, M) { remove element with domain d } else
p := NEXT(p, M);
INSERT(x, FIRST(M), M) { put (d, r) at front of list } end; { ASSIGN }
function COMPUTE ( var M: MAPPING; d: domaintype; var r: rangetype ): boolean; var
p: position; begin
p := FIRST(M);
while p <> END(M) do begin
if RETRIEVE(p, M).domain = d then begin r := RETRIEVE(p, M).range;
return (true) end;
p := NEXT(p, M) end;
return (false) { if d is not in domain } end; { COMPUTE }
was made).
Recursion simplifies the structure of many programs. In some languages, however, procedure calls are much more costly than assignment statements, so a program may run faster by a large constant factor if we eliminate recursive procedure calls from it. We do not advocate that recursion or other procedure calls be
eliminated habitually; most often the structural simplicity is well worth the running time. However, in the most frequently executed portions of programs, we may wish to eliminate recursion, and it is the purpose of this discussion to illustrate how recursive procedures can be converted to nonrecursive ones by the introduction of a user-defined stack.
Example 2.3. Let us consider recursive and nonrecursive solutions to a simplified
version of the classic knapsack problem in which we are given target t and a collection of positive integer weights w1, w2 , . . . , wn. We are asked to determine whether there is some selection from among the weights that totals exactly t. For example, if t = 10, and the weights are 7, 5, 4, 4, and 1, we could select the second, third, and fifth weights, since 5+4+ 1 = 10.
The image that justifies the name "knapsack problem" is that we wish to carry on our back no more than t pounds, and we have a choice of items with given weights to carry. We presumably find the items' utility to be proportional to their weight,† so we wish to pack our knapsack as closely to the target weight as we can.
In Fig. 2.25 we see a function knapsack that operates on an array weights : array [l..n] of integer.
A call to knapsack(s, i) determines whether there is a collection of the elements in
weight[i] through weight[n] that sums to exactly s, and prints these weights if so. The
first thing knapsack does is determine if it can respond immediately. Specifically, if s = 0, then the empty set of weights is a solution. If s < 0, there can be no solution, and if s > 0 and i > n, then we are out of weights to consider and therefore cannot find a sum equal to s.
If none of these cases applies, then we simply call knapsack(s-wi, i + 1) to see if there is a solution that includes wi. If there is such a solution, then the total problem is solved, and the solution includes wi, so we print it. If there is no solution, then we call knapsack(s, i + 1) to see if there is a solution that does not use wi.