Chapter 11 - Introduction to Abstract Data Types (ADTs) 11.1 An Example ADT Arithmetic Operations on Complex Numbers Designing an ADT for Complex Numbers 11.2 Lists Array Implementation Linked List Implementation 11.3 Stacks Array Implementation Linked List Implementation 11.4 Queues Array Implementation Linked List Implementation 11.5 Applications The Mandelbrot Set Generator RPN Expression Evaluator Infix to Postfix Expression Converter Drawing the Centipede Chapter 11 - Introduction to Abstract Data Types (ADTs) In this chapter we formalize the concept of an abstract data type (ADT), as a data structure and the functions and procedures that give the programmer access to it. We will build ADTs for a number of common structured data types including the list, stack and queue. 11.1 An Example ADT We are familiar with literal data types such as float, integer, character and string. We have also studied arrays of these literal types. Abstract data types (ADTs) are sets of mathematical and logical operations on entities (of any literal type) in which the underlying data structure and the specific implementation of the operations is unknown to the user. As a first example we will build an ADT for complex numbers. Before designing the adt_complex package, we need to review the properties of complex numbers. Let p = a+bi represent a complex number, where i 1 . The number a is called the real part of p and b is called the imaginary part of p. Graphically a complex number is represented as a point in the complex plane. Arithmetic Operations on Complex Numbers The basic arithmetic operations of addition, subtraction, multiplication and division on complex numbers are given by, where p and q are complex numbers. Notice that for each operation there is a real part and an imaginary part that remain separated. The + sign between the real and imaginary parts is not a mathematical operation but rather a notational separator. Designing an ADT for Complex Numbers The ADT for complex numbers should permit the user (application programmer) to create, read and display complex numbers as well as compute the results of +, -, * and /. The user also needs to extract the real and imaginary components as separate entities (real values). However, the user should not be given direct access to the components of a complex number. All these functions should be provided as part of the ADT interface. Choosing a Data Representation We first need to establish an internal representation for complex number types for our ADT. private type complex is record real : float; imag : float; end record; We will make this type private so that the user of the ADT package will not be able to manipulate the real and imaginary components directly. Next we need to list the operations we plan to include in our ADT. Reading and Writing Complex Numbers As part of the interface we include get( ) and put( ) procedures for reading and writing complex numbers. These procedures can be implemented in a number of ways. In our ADT we will ask the user to enter the real and imaginary components (separated by a space) and we will display a complex number using the real_part + i imaginary_part format. The specification for the get( ) and put( ) procedures do not use these components. Instead we simply pass a complex value. procedure get(c:out complex); procedure put(c:in complex); Math Operations The basic math operations are written as infix function in order to simplify expressions using complex values. function function function function "+"(a,b: "-"(a,b: "*"(a,b: "/"(a,b: complex) complex) complex) complex) For example the following adt_complex package, return return return return complex; complex; complex; complex; expression could be written using the r := ((p+q)*(p-q))/(q*q); where p, q and r are complex. The use of the same arithmetic operator symbols for complex math that are used for floating point and integer values is a powerful feature of Ada, called overloading. Creating New Complex Numbers The application programmer may want to create new complex numbers within the program. This can be implemented by adding a make_complex( ) function to our set of operations. function make_complex(r,i : float) return complex; Using make_complex(real,imag) the programmer can create a complex number by passing the real and imaginary parts into the function as floating point (real) values. Extracting the Real and Imaginary Components The interface should also permit the programmer to extract the real or imaginary parts from a complex number. function get_real(c: complex) return float; function get_imag(c: complex) return float; These functions return the components of a complex number as floating point values. Computing the Magnitude Another common operation performed on complex number is the computation of the magnitude. The magnitude of a complex number is a real value representing the distance of the complex number from the origin in the complex plane. function magnitude(c:complex) return float; The adt_complex Package Specification The complete listing of the adt_complex specification is shown below. with ada.float_text_io; use ada.float_text_io; package adt_complex is type complex is private; procedure get(c:out complex); procedure put(c:in complex); function "+"(a,b: complex) return complex; function "-"(a,b: complex) return complex; function "*"(a,b: complex) return complex; function "/"(a,b: complex) return complex; function make_complex(r,i : float) return complex; function get_real(c: complex) return float; function get_imag(c: complex) return float; function magnitude(c:complex) return float; private type complex is record real : float; imag : float; end record; end adt_complex; The specification should provide the application programmer with all the information needed to use the adt_complex package. A comment block containing additional details and/or examples of the use of the interface would be helpful. In particular, we might want to describe the use of the get( ) procedure. The adt_complex Package Body We include in the partial listing below some of the arithmetic operations for the adt_complex interface. Note that the square root used in the magnitude( ) function forces us to include a mathematics package. with ada.text_io, ada.float_text_io, ada.numerics.elementary_functions; use ada.text_io, ada.float_text_io, ada.numerics.elementary_functions; package body adt_complex is -- this is not a complete listing function "+"(a,b: complex) return complex is c : complex; begin c.real:=a.real+b.real; c.imag:=a.imag+b.imag; return c; end "+"; function "*"(a,b: complex) return complex is c : complex; begin c.real:=a.real*b.real-a.imag*b.imag; c.imag:=a.real*b.imag+b.real*a.imag; return c; end "*"; function "/"(a,b: complex) return complex is c : complex; begin c.real:=(a.real*b.real+a.imag*b.imag)/ (b.real*b.real+b.imag*b.imag); c.imag:=(a.imag*b.real-a.real*b.imag)/ (b.real*b.real+b.imag*b.imag); return c; end "/"; function magnitude(c:complex) return float is begin return sqrt(c.real*c.real+c.imag*c.imag); end magnitude; end adt_complex; Inside the body of the package we can use dot-notation to access and manipulate the real and imaginary components of the complex values. Making the complex type private prevents the application programmer from using dot-notation to access the real or imag fields of the record. We can write a simple program to test the adt_complex package. The listing below asks the user to enter two complex numbers p and q. Then these numbers are added together, subtracted, multiplied and divided with the results displayed. Also the real and imaginary components of p are extracted and the magnitude of p is computed and displayed. with ada.text_io, adt_complex, ada.float_text_io; use ada.text_io, adt_complex, ada.float_text_io; procedure test_complex is p,q : complex; begin put("Enter p..... "); get(p); put("Enter q..... "); get(q); new_line; put("p+q = "); put(p+q); new_line; put("p-q = "); put(p-q); new_line; put("p*q = "); put(p*q); new_line; put("p/q = "); put(p/q); new_line; put("magnitude(p) = "); put(magnitude(p)); new_line; put("real part of p = "); put(get_real(p)); new_line; put("imag part of p = "); put(get_imag(p)); new_line; end test_complex; The adt_complex package specification, body and the test program above are included in the source code list associated with this chapter. 11.2 Lists A list is an ordered sequence of items of any type we choose with one of the items designated as the current item. For example the values below previous 4 first 17 9 42 current next 3 6 12 last represent a list with the value 42 tagged as the current item. We may also designate other special values such as first, last, previous and next. Note that previous and next are defined with respect to the current item. For now we will limit our tags to designating the current item. An ADT provides for a set of operations on the data type such as adding items, removing them, counting the number of items or checking for the presence of a particular item (or value). The set of operations that give the programmer access and control of the data type is called the ADT interface. Typical operations of the interface are shown below: create_list(list) add_before(list,position,value) add_after(list,position,value) delete(list,position) find_item(list,value) display(list,current) destroy_list(list) advance(list,current) decriment(list,current) get_current(list) Before we can build an ADT for the list, we need to decide on the underlying data structure to be used. In our first version of the list ADT we will use an array to represent the list. Array Implementation We can implement a list as a contiguous array using the predefined type array in Ada. n_max : constant integer := 1000; the_list : is array(1..n_max) of any_literal_type; Our operations on the ADT must be compatible with this data structure. Using a linear contiguous array (as opposed to a linked list) will simplify the implementation of some of the operations on the ADT such as display( ) and find( ). Unfortunately the array implementation will cause us some trouble with other operations such as add_before( ) and delete( ). In order to insert a value x at the ith position in the list we would have to move all values ai+1, . . , an forward one position to make room for x. Similarly when we delete a value from the ith position we need to move all successors of ai backward one position to fill the hole left by the deleted value. Using the contiguous array also creates the problem of forcing us to limit the size of the list to some maximum number of values n_max. In the idealized list ADT there should be no limit to the size of the list. Actually there will always be a finite maximum list size due to finite memory capacity, but the array implementation creates an immediate problem that must be solved. We would like to set the array size to be a large as the largest list we will need for any application, but we cannot anticipate this value. Alternatively we will need to modify and recompile the list ADT for each application. This greatly diminishes the usefulness of the list ADT since it defeats the primary purpose of building the list ADT as a separate package that can be used in multiple applications without further modification. Linked List Implementation The problem of wasting memory by having to establish a large array that may not be used can be avoided with linked lists. Consider a singly-linked list implementation for the list ADT. The current item can be designated with a pointer to the appropriate list element. Creation and insertion of new values can be accomplished in the manner demonstrated in the previous chapter. singly-linked list head null current doubly-linked list head null null current Unfortunately a singly-linked list does not give us direct access to the node that is previous to the current node. This makes is difficult to implement some of the interface functions for the list ADT. We can build a doubly-linked list implementation that gives us access to nodes on either side of the current node using a dynamic data structure similar to the following: type node; type pointer is access node; type node is record data : any_data_type; next : pointer; prev : pointer; end record; This record provides a pointer to the next node and to the previous node in the list. The interface function add_after( ) in which an element is inserted before the current node is implemented by the following code segment. Assume that current and newnode are pointers to pointer records as defined above. newnode := new node; newnode.data := val; newnode.next := current; newnode.prev := current.prev; current.prev := newnode; newnode.prev.next := newnode; In this code segment the data value, val is placed into a newly created record and inserted into the list just before the record pointed by current. head current null null val newnode head current null null val newnode head null current val newnode null 11.3 Stacks The stack is an abstract data type (ADT) that can be used to save data elements in a last-in firstout (LIFO) order. The only item in a stack that can be accessed is the last item placed on the stack. This data structure is analogous to a stack of trays in a cafeteria. As you put trays onto the stack the rest of the stack is pushed down onto a spring loaded platform. As you take trays off the top of the stack the platform pops up to give access to the next tray on the stack. Array Implementation We will look at various ways to implement a stack ADT but for now we will build a stack for integers using an ordinary (i.e. one-dimensional, linear and contiguous) list data structure. maxindex : constant integer := 1000; type stacktype is array(1..maxindex) of integer; S : stacktype; itop : integer; In this declaration we have specified that the maximum number of items in the stack will be 1000, that itop will be the index pointing to the item on the top of the stack and that the stack itself will be called S. We will create a number of functions and procedures that provide access and control of the stack data type. These will include a mechanism for adding items to the stack called push(S,x), taking items off the stack pop(S,x), checking the value on the top of the stack top(S) and testing to see if the stack is empty is_empty(S), or full is_full(S). procedure push(S: in out stacktype; x : in integer); procedure pop(S: in out stacktype; x: out integer); function top(S) return integer; function is_empty(S) return boolean; function is_full(S) return boolean; These functions and procedures provide all the interactivity we need to use the stack. We never want to have to worry about the details of its implementation but sometimes this is not possible. The function is_full(S) is necessary since we are dealing with a fixed-size list for our implementation of the stack. In the ideal case we would never have to check to see if the stack was full since the abstract concept of a stack does not place a limit on its size. The procedure push(S,x) pushes the value contained in x onto the stack S. The procedure pop(S,x) loads the value of the item on the top of the stack into the variable space labeled x and removes this value from the top of the stack S. Actually all we need to do is decrement the index itop so that it points to the next lower position in the stack. The function top(S) returns the value at the top of the stack S without removing it. You may question why is it OK for top(S) to be a function while we must implement the pop operation as a procedure. Remember that the sole purpose of a function is to compute and return a value. Functions never perform any other operation or affect the values of any variables beyond the scope of the function itself. If we were to implement pop as a function such as y:=pop(S) then this function would have to decrement the index itop pointing to the top of the stack in addition to returning the value S(itop). While Ada allows side-effects, they are discouraged, and are not considered acceptable in good program design. Lets take a closer look at how the stack data structure is managed as a list. The list S is declared to hold 1000 integers S(1), S(2), . . . , S(1000). The index itop points to the item currently at the top of the stack. We can let itop=0 when the stack is empty, so our boolean function is_empty(S) only needs to check the value of the index itop. function is_empty(S : stacktype) return boolean is begin return (itop=0); end is_empty; Remember that the equal sign is an operator that compares two values of the same type and returns true if they are equal. It is a good practice to imagine a question mark over the operators <, <=, =, >=, > to remind ourselves of their purpose. So this function returns true if itop=0 otherwise it returns false. The boolean function is_full(S) has an equivalent form. function is_full(S : stacktype) return boolean is begin return (itop=maxindex); end is_full; Recall that maxindex is a constant set to 1000 in this example. The procedure push(S,x) must check to see if the stack is full and if not, it places the value of the parameter x into the next available position in the list S. What should we do if the list is full and someone is trying to push another value onto the stack? When we study exceptions we will see how to trap on such errors and report the problem to the user. For now we can let the push procedure ignore attempts to push values onto a full stack We are providing a mechanism for checking to see if the stack is full, so if someone pushes a value onto a full stack it their own fault when the program fails to operate correctly. procedure push(S : in out stacktype; x : in integer) is begin if not(is_full(S)) then itop:=itop+1; S(itop):=x; else put("Error: Stack Full"); end if; end push; Actually, these types of decisions are central to issues of code correctness and software risk. As programmers we cannot predict how our source code may be used (or abused) in some future application. Therefore it is important that we build our code as carefully as we can, making sure that we provide protection against those run-time and application errors that we can predict. There will still be plenty of "unknown unknowns" to cause problems. The pop procedure has a similar form. In this procedure we need to check to make sure that the stack is not empty. procedure pop(S : in out stacktype; x : out integer) is begin if not(is_empty(S)) then x:=S(itop); itop:=itop-1; else put("Error: Stack Empty"); end if; end pop; Finally, the function top(S) returns the value on the top of the stack. If the stack is empty we can return 0 or we can create an exception. Since top(S) is a function we choose to simply return a zero when the stack is empty and include comments in the package specification instructing the programmer to check for an empty stack before using this function. function top(S : stacktype) return integer is begin if not(is_empty(S)) then return S(itop); else return 0; end if; end top; 11.4 Queues A queue is another type of list data structure to which the user has restricted access. New items are placed at the back of a queue and the next available item is at the front of the queue. This is called first-in first-out (FIFO) order. An idealized queue is an infinite capacity list with special tags for the entries front and back. QUEUE … W back V U T S R Q P … front We will define the following interface for a queue abstract data type. enqueue(Q,x) - appends the value in x to the back of the queue Q dequeue(Q,x) - loads the value at the front of the queue Q into the parameter x front(Q) - returns the value at the front of the queue Q without changing the queue is_empty(Q) - a boolean function that returns true if the queue is empty is_full(Q) - a boolean function that returns true if the queue is full Array Implementation The is_full(Q) Boolean function will only be needed if we use a fixed-size array as our underlying list data structure. Let's build a package for the queue ADT. First we need a specification for the package. In the spec we declare the data structure and list the first lines of the functions and procedures that will be the ADT interface. maxindex : constant integer := 1000; type listype is array(1..maxindex)of float; type queue is record list : listype; front : integer; back : integer; end record; We declare the queue type in the specification but we do not declare an identifier Q of type queue. This is because we are not executing the package specification. Also there are no other packages withed or used in this specification. Remember that we create a package specification to permit the user to see the functions and procedures that are available without having to show their implementations. Our specification will be saved as adt_queue.ads and has the following form. package adt_queue is : [data structure goes here] : [function and procedure specifications go here] : end adt_queue; Next, we need to build the adt_queue package body. The specification and body of the package are compiled only. We don't try to build them since there is no main executable. The shown below is not complete. package body adt_queue is function is_empty(Q:queue)return boolean is begin return Q.front=0; end is_empty; function is_full(Q:queue)return boolean is begin return Q.back=Q.front; end is_full; function front(Q:queue)return float is begin if not(is_empty(Q)) then return Q.list(Q.front); else return 0.0; end if; end front; procedure enqueue(Q: in out queue; x : in float) is begin if not(is_full(Q)) then Q.list(Q.back):=x; Q.back:=Q.back+1; if Q.back>maxindex then Q.back:=1; end if; else put("ERROR: Queue full"); end if; end enqueue; procedure dequeue(Q: in out queue; x : out float) is begin if not(is_empty(Q)) then x:=Q.list(Q.front); Q.front:=Q.front+1; if Q.front>maxindex then Q.front:=1; end if; else put("ERROR: Queue Empty"); end if; end dequeue; end adt_queue; The package body includes the word body in its descriptor line and is saved as adt_queue.adb. If we set the index called Q.front to zero (i.e. outside the range of valid indices of the list) we can simply return the boolean value of Q.front=0 to indicate whether the queue is empty. Since Q.back is the index of the next available position in the Q list, we know the queue is full when Q.back = Q.front. Implementing the queue as a fixed-size list creates a few complications for us. We need to maintain the index values for front and back as items are enqueued (added to the list) and dequeued (removed from the list). As we reach the maxindex we need to wrap-around to the first index while checking to make sure that the list is not full or empty. Consider the following example case. Initially a queue list is empty with back=1 (corresponding to the first available position in the list). As values are enqueued (in order 7, 2, 4, 8) the index back is incremented to the next available position in the list. As values are dequeued the index front is incremented to the next item in the queue list. When front=back then we know that the queue is empty. Eventually these indices will reach the maximum index of the queue list (called maxindex). If either back or front are increased to a value>maxindex then they are set back to 1. enqueue(Q,8) enqueue(Q,4) enqueue(Q,2) enqueue(Q,7) : dequeue(Q,X) dequeue(Q,Y) : enqueue(Q,2) enqueue(Q,8) enqueue(Q,5) euqueue(Q,9) These complications can be eliminated by choosing a different representation for the list data structure. Linked List Implementation Using a linked-list to represent the list data structure of our queue simplifies our interface functions and procedures. While we are rebuilding the adt_queue we will also make it generic. That is we will leave the data_type being managed by the queue unspecified until we are building an application. The sample code below is a generic version of the adt_queue package that uses dynamic memory allocation rather than a static array. We no longer need to concern ourselves with the issue of a full queue. generic type data_type is private; package adt_queue is type qpointer is private; type qnode is private; type qtype is private; function is_empty(Q : qtype) return boolean; procedure enqueue(Q: in out qtype; x : in data_type); procedure dequeue(Q: in out qtype; x : out data_type); private type qpointer is access qnode; type qnode is record data : data_type; next : qpointer; end record; type qtype is record front : qpointer := null; back : qpointer := null; end record; end adt_queue; The package body includes an instantiation of the unchecked_deallocation procedure which we have named remove_qnode. The type called data_type is generic in the adt_queue specification which permits the use of this package for any data type. In order to use adt_queue this generic package must be instantiated. More than one instantiation of adt_queue can be declared in a single program. with ada.text_io, ada.unchecked_deallocation; use ada.text_io; package body adt_queue is procedure remove_qnode is new ada.unchecked_deallocation(qnode,qpointer); function is_empty(Q : qtype) return boolean is begin return Q.front=null; end is_empty; procedure enqueue(Q: in out qtype; x : in data_type) is newQ : qpointer; begin newQ := new qnode; newQ.data := x; newQ.next := null; if Q.front=null then Q.front:=newQ; Q.back:=newQ; else Q.back.next:=newQ; Q.back:=newQ; end if; end enqueue; procedure dequeue(Q: in out qtype; x : out data_type) is dumpme : qpointer; begin if Q.front=null then put("EMPTY QUEUE ERROR"); else x:=Q.front.data; dumpme:=Q.front; Q.front:=Q.front.next; remove_qnode(dumpme); end if; end dequeue; end adt_queue; Designating the private types in the adt_queue limits access to the underlying linked-list data structure so that only the enqueue() procedure can allocate new memory from the available heap. The dequeue( ) procedure includes a call to the remove_qnode( ) procedure we instantiated above, which releases (deallocates) memory when we are finished with it. If remove_qnode( ) was not called in dequeue( ) , the discarded pointer records would not be available for later use. Not releasing memory back to the heap (i.e. available memory) creates what is called a memory leak. Our ADT prevents sloppy coding from wasting memory. 11.5 Applications The Mandelbrot Set Generator 11.1: Write an Ada program that determines if a particular complex value is a member of the Mandelbrot Set. The Mandelbrot Set is defined on the complex plane. Every complex number has a corresponding point in the complex plane. By convention the real component specifies the position on the horizontal axis and the imaginary component specifies the position on the imaginary axis. The magnitude of a complex number p is the length (a real value) of line connecting the origin and the point representing p in the complex plane. The Mandelbrot set is the set of all complex points C for which the magnitude of Z in Z := Z*Z + C converges to a fixed value when iterated. The parameter Z is a complex number initially set to zero (Z=0+i0). To iterate this expression we set Z=0+i0 and C to the chosen number in the complex plane. We compute a new value for Z and check its magnitude. We then use the current value of Z to compute the next Z value. Eventually we find that the magnitude of Z either converges to a fixed value or it diverges (continually grows to a larger and larger value). If we color the points C that converge to zero in the complex plane and color the points that diverge white, we obtain an image similar to the one shown below, in which the black region represents points in the Mandelbrot Set. Mandelbrot Set Our task in this exercise is to create a boolean function that returns true if its complex argument is in the Mandelbrot Set and false otherwise. All the points in the Mandelbrot Set are within a radius of magnitude 2.0 from the origin of the complex plane. This means that we can terminate the iteration when the magnitude of the iterated value of Z exceeds 2.0 We can also terminate the iteration when we have reached a fixed value for Z. This may not be so easy to determine, as we will see. We can attempt to detect when the iterated value of Z has reached a fixed point (constant value) using a method similar to the following put("Enter a complex number C = "); get(C); Z:=make_complex(0.0,0.0); Zmag_old:=float'last; loop Z:=Z*Z + C; Zmag:=magnitude(Z); exit when Zmag>=2.0 or Zmag-Zmag_old; Zmag_old:=Zmag; end loop; put("The value C "); if magnitude(Z)>2.0 then put("is not a member of the Mandelbrot Set"); else put("is a member of the Mandelbrot Set"); end if; Note: This code segment uses the interface provided in the ADT_complex package for creating, displaying and performing arithmetic operations on complex values. The problem with this method is that we are testing to see if two floating point values, namely Zmag and Zmag_old are equal. As we have seen this can create problems due to the way floating point values are internally represented in the computer. We need a better way to test for convergence. We could try to test for the difference between the magnitudes of two successive iterations of Z being less than some small amount, say 1x10-6. exit when magnitude(z)>=2.0 or abs(magnitude(z)-zmag_old)<1.0E-6; We will use this approach to implement our Mandelbrot Set Boolean function. function is_mandel(c : complex) return boolean is z : complex; begin z:= c; zmag_old:=float'last; loop z:=z*z + c; exit when magnitude(z)>=2.0 or abs(magnitude(z)-zmag_old)<1.0E-6; zmag_old:=magnitude(z); end loop; if magnitude(z)>2.0 then return false; else return true; end if; end is_mandel; When we test this function we find that certain complex values cause our program to "hang", or get caught in an infinite loop. The fact is, some values of C that are in (or very near) the Mandelbrot Set are strange attractors. This means that the iterated values of Z stay inside a confined region of the complex plane but do not converge to a fixed point. Since they do not diverge they are not excluded from the Mandelbrot set, but since they do not converge to zero or any other fixed point they cannot be determined to be in the Mandelbrot Set. Therefore we have three conditions, divergence, non-divergence and convergence. For this program we will arbitrarily choose a fixed number of iterations N max, and claim that the point C is a member of the Mandelbrot Set if the magnitude of the iterated value of Z does not exceed 2.0 after Nmax iterations. As we can get a more accurate estimate of which points in the boundary are members of the set by increasing the value of N max. RPN Arithmetic Expression Evaluator 11.2: Write an Ada program that evaluates Reverse Polish Notation (RPN) arithmetic expressions. You may assume that operands are single characters A..Z and that the expressions will involve only +, -, *, and /. An RPN Expression is also called to a postfix expressions. RPN expression are evaluated by scanning them in a left-to-right order. The operands are placed to the left of the operation being performed on them. So the infix expression X + Y would become XY+ and the expression (X+Y)/Z – (X-Y)*Z would become XY+Z/XY-Z*-. The first example is straightforward. However the procedure for converting general arithmetic infix expressions into their postfix equivalent expressions is not obvious. This will be discussed in detail in the next application. For now we will concern ourselves with how to implement an RPN expression evaluator. We can use a stack data structure to hold the intermediate values as we evaluate an RPN expression. The rules for RPN evaluation are: (1) Scan the expression one character (or token) at a time from the left to the right (2) When an operand is encountered push the corresponding value onto the stack (3) When an operator is encountered pop the top two values off the stack, perform the indicated operation and push the result back onto the stack. (4) Repeat steps (1)..(3) above until expression is completely scanned. The value on the top of the stack is the result. Before we attempt to build our program, let's try out this procedure by hand, using the example postfix expression above. The problem statement says that we can assume that the operands (variables) will be represented by single letters A through Z. This will simplify our expression scanning procedure. Let’s assume that we have values associated with our operands as we practice Assume that X=1, Y=2 and Z=-1 as you evaluate the RPN expression, XY+Z/XY-Z*-. XY+Z/XY-Z*- push the value of X onto the stack _1_ XY+Z/XY-Z*- push the value of Y onto the stack 2 _1_ XY+Z/XY-Z*- pop the top two values off the stack and push the sum (1+2) _3_ XY+Z/XY-Z*- push the value of Z onto the stack -1 _3_ XY+Z/XY-Z*- pop the top two values and push the ratio (3/[-1]) -3_ XY+Z/XY-Z*- push the value of X onto the stack 1 -3_ XY+Z/XY-Z*- push the value of Y onto the stack 2 1 -3_ XY+Z/XY-Z*- pop the top two values and push the difference (1-2) -1 -3_ XY+Z/XY-Z*- push the value of Z onto the stack -1 -1 -3_ XY+Z/XY-Z*- pop the top two values and push the product ([-1]*[-1]) 1 -3_ XY+Z/XY-Z*- pop the top two value and push the difference ([-3]-1) -4_ Therefore, the result of this expression is the value remaining on the top of the stack (i.e. -4). Besides performing these operations properly, we may want to include error checking in our program. For example, there always must be at least one more operand encountered than operators as we scan an RPN expression from left to right (why?). In general an RPN expression can be evaluated using a stack that is not empty. The value at the top of the stack following the evaluation of a properly formatted RPN expression will be the same. Actually, this situation is quite common. Infix to Postfix Expression Converter (Optional Material) 11.3*: Write an Ada program that generates a postfix expression given, as input, a string representing the corresponding infix arithmetic expression. As with the previous example you may assume that all operands will be represented as single ASCII characters A..Z. Given a fully parenthesized infix expression we can convert it to postfix (RPN) notation by hand without much difficulty, by using the following rules. (1) Starting with the innermost (first to be computed) operations, rearrange the symbols or tokens (i.e. sets of symbols being treated as atomic) such that the operator is just to the right of the operand (or token) pair. (2) Repeat Step (1) until all operators have been moved to their postfix locations We can demonstrate this approach with the following example. Convert the following infix expression into postfix notation: ((X+Y)*(X-Z))/((X+Z)*(X-Y)) The parentheses tell us which operations must be performed first (these are highlighted), ((X+Y)*(X-Z))/((X+Z)*(X-Y)) For each of these operations we move the operator to the right of the operands. From this point, we must consider these postfix terms as single elements (i.e. they are atomic). We will place these terms in square brackets to indicate that they must be treated as single elements. ([XY+]*[XZ-])/([XZ+]*[XY-]) The next operations to be considered in this example are the multiplications. ([XY+]*[XZ-])/([XZ+]*[XY-]) These operators are moved to the right of their operands. [XY+XZ-*]/[XZ+XY-*] Finally the division is performed, so we move this operator to the right of its operands to obtain the final result. XY+XZ-*XZ+XY-*/ This expression is the postfix equivalent to the original infix expression. The postfix expression is easier for a computer to evaluate since there is not operator precedence (e.g. multiplication and division over addition and subtraction), no parentheses to consider and no need to look ahead to find the next operation to perform. The computer can scan the postfix expression from left to right one token at a time to compute the resulting value. We can check to see if our postfix expression is valid (i.e. matches the infix expression from which it was derived) by evaluating it just as we did the numeric postfix expression above, but this time we will push the algebraic terms onto the stack rather than their computed values. XY+XZ-*XZ+XY-*/ _X_ push the operand X onto the stack Y _X_ push the operand Y onto the stack XY+XZ-*XZ+XY-*/ _X+Y_ pop the last 2 operands and push the addition XY+XZ-*XZ+XY-*/ X _X+Y_ push Y XY+XZ-*XZ+XY-*/ Z X _X+Y_ push Z XY+XZ-*XZ+XY-*/ X-Z _X+Y_ XY+XZ-*XZ+XY-*/ (X+Y)*(X-Z) X (X+Y)*(X-Z) pop the operands (infix terms) and push the product push X XY+XZ-*XZ+XY-*/ Z X (X+Y)*(X-Z) push Z XY+XZ-*XZ+XY-*/ X+Z (X+Y)*(X-Z) pop the operands and push the resulting operation XY+XZ-*XZ+XY-*/ X X+Z (X+Y)*(X-Z) push X XY+XZ-*XZ+XY-*/ Y X X+Z (X+Y)*(X-Z) push Y XY+XZ-*XZ+XY-*/ X-Y X+Z (X+Y)*(X-Z) pop the operands and push the resulting operation XY+XZ-*XZ+XY-*/ (X+Z)*(X-Y) (X+Y)*(X-Z) pop the operands and push the resulting operation XY+XZ-*XZ+XY-*/ ((X+Y)*(X-Z))/((X+Z)*(X-Y)) XY+XZ-*XZ+XY-*/ XY+XZ-*XZ+XY-*/ pop the operands and push the resulting operation completed infix expression We have learned how to evaluate a numerical postfix expression and how to generate postfix expressions from infix expressions. We have also learned that postfix expressions are much easier for a computer to evaluate than infix expressions. High-level languages such as Ada allow us to write our arithmetic expressions as assignments in infix notation. The language compiler then converts them to postfix as the machine language executable is being built.\ Our task here is to write an Ada program that automatically converts an infix expression into its equivalent postfix form, but the hand-method we have learned for generating postfix expressions is not easily implemented in a computer program. We will want to find another approach. A popular computer algorithm for converting infix expressions into postfix expressions uses two stack ADTs. Computer Generation of Postfix Expressions (Optional Material) The algorithm provided below as pseudo code is to be executed on an input string representing a valid infix expression. Assume you have established a stack for OPERATORs and an ANSWER string. Scan the input string from the left to the right as you exercise the algorithm. while tokens remain in the input string loop get the next input_token from the input string if the input_token is an operand then output(ANSWER,input_token) elsif the input_token is an operator then while OPERATOR stack not empty and top(OPERATOR) /= '(' and top(OPERATOR) precedence is >= input_token precedence loop operator_token=pop(OPERATOR) if operator_token /= '(' then output(ANSWER,operator_token) end if end loop push(OPERATOR,input_token) elsif input_token = '(' then push(OPERATOR,input_token) else -- input token must be ')' while top(OPERATOR) /= '(' loop operator_token=pop(OPERATOR) output(ANSWER,operator_token) end loop operator_token=pop(OPERATOR) -- this token must be a '(' end if end loop while OPERATOR stack not empty loop --flush the OPERATOR stack output(ANSWER,pop(OPERATOR)) end loop This algorithm relies on the establishment of an order of precedence for each operator. This precedence follows our notion of operator precedence from basic arithmetic. That is, multiplication and division have higher precedence than addition and subtraction. We can test our algorithm on the same example infix expression we used in the previous exercise. Infix Expression ((X+Y)*(X-Z))/((X+Z)*(X-Y)) ((X+Y)*(X-Z))/((X+Z)*(X-Y)) ((X+Y)*(X-Z))/((X+Z)*(X-Y)) ((X+Y)*(X-Z))/((X+Z)*(X-Y)) ((X+Y)*(X-Z))/((X+Z)*(X-Y)) ((X+Y)*(X-Z))/((X+Z)*(X-Y)) ((X+Y)*(X-Z))/((X+Z)*(X-Y)) OPERATOR stack ( __(___ ( __(___ + ( __(___ __(___ ANSWER postfix expression X XY XY+ * __(___ ( * __(___ XY+ ______ XY+X XY+ ((X+Y)*(X-Z))/((X+Z)*(X-Y)) ((X+Y)*(X-Z))/((X+Z)*(X-Y)) ((X+Y)*(X-Z))/((X+Z)*(X-Y)) ((X+Y)*(X-Z))/((X+Z)*(X-Y)) ((X+Y)*(X-Z))/((X+Z)*(X-Y)) ((X+Y)*(X-Z))/((X+Z)*(X-Y)) ((X+Y)*(X-Z))/((X+Z)*(X-Y)) ((X+Y)*(X-Z))/((X+Z)*(X-Y)) ((X+Y)*(X-Z))/((X+Z)*(X-Y)) ( * __(___ * __(___ __ ___ ( ( __/___ + ( ( __/___ ( * ( __/___ ( * ( __/___ __/___ __ ___ XY+XZ XY+XZXY+XZ-* XY+XZ-*X XY+XZ-*XZ XY+XZ-*XZ+X XY+XZ-*XZ+XY XY+XZ-*XZ+XY-* XY+XZ-*XZ+XY-*/ The implementation of this algorithm is challenging for the beginning programmer. It requires a substantial program design effort before sitting down at the keyboard. An effective design approach is to use a top-down problem decomposition. As we scan the pseudo code we find a list of operations that should be written as separate functions or procedures. is_empty(input_string) - a boolean function to tell us when we have read all the tokens from the input string. get_next_token(input_token) - a procedure to get the next input_token (for our example we can assume that all tokens are single ASCII characters) is_operand(input_token) - a boolean function to recognize if an input_token is an operand is_operator(input_token) - a boolean function to recognize if an input_token is an operator precedence(input_token) - a function to return the precedence of an operator (we will need to establish a number precedence for +, -, * and / operators (e.g. +=1, -=1, *=2, /=2). output(ANSWER,token) - a procedure to append the next token to the ANSWER string When testing for '(' and ')' we can simply compare the token to these ASCII characters directly Finally we can use the interface provided in the adt_stack for the pop( ) and push( ) operations in our algorithm implementation. This application gives you a rough sketch of the effort required to implement the infix to postfix expression converter. This program is a component of the Spreadsheet Class Project in Appendix B. Drawing the Centipede 11.4: Build a graphical program to display a centipede (segmented worm) moving across the screen. The user should be able to specify the number and size of the segments and the flexibility of the centipede (i.e. how quickly the centipede can change directions). The segments can be drawn as circles of radius r (specified by the user). They need to be separated by an amount equal to their diameters, so we can compute the position of the next segment by converting the segment center position from Polar to Cartesian coordinates. centipede body segments ... current direction del_ang px px _ old 2 * r cos(ang ) py py _ old 2 * r sin( ang ) new segment The maximum change in direction between segments del_ang (specified by the user) can be used to generate a random angle ang for the coordinate conversions. Choosing 2*r for the magnitude of the change in position ensures that the segments will be contiguous. ang:=ang+(ranu-0.5)*del_ang; px:=integer(float(px_old)+float(size)*cos(ang)); py:=integer(float(py_old)+float(size)*sin(ang)); As usual we need to be careful about type casting. In this case we convert our position px_old and py_old to floating point so that we can multiply by the cosine and sine of the angle ang. The problem statement does not mention what we should do when our centipede moves off the edge of the display window. As the centipede leaves one side of the window we will begin displaying it on the opposite side. This window wrapping operation can be implemented with a set of if...then statements. if px>=xm-size then px:=size; elsif px<=size then px:=xm-size; end if; if py>=ym-size then py:=size; elsif py<=size then py:=ym-size; end if; Next we need to decide how we will use the adt_queue to help us implement the centipede program. In general we will use the queue to maintain a record of the locations of the individual segments. As we generate new segments we will place their positions into the queue and draw them in the display window. When we are ready to erase a segment, we dequeue the position and redraw that segment using the background color. For this application, our generic data type will be a record containing the px, py plotting positions of the segments, type segmentype is record px : integer; py : integer; end record; package my_queue is new adt_queue(segmentype); use my_queue;