|CSC 153: Computer Science Fundamentals||Grinnell College||Spring, 2005|
|Laboratory Exercise Reading|
This reading reviews strategies of problem solving discussed in this class. Then, building on recent work, the reading introduces stacks and queues as examples of useful classes.
Since many computer applications address complex problems, problem-solving with computers must include a consideration of the handling of complexity. Already in this course, we have seen several such strategies and use a variety of language capabilities.
Solutions can be built up using a sequence of procedures, where each procedure accomplishes a relatively simple task.
Local variables and parameters allow work within a procedure to focus on a narrow, tightly controlled task even if the procedure will be used within a more complex problem.
Using recursion, processing can focus on relatively simple base cases and the reduction of complex cases to simpler ones.
Data abstraction allows data to be viewed in logical groups and structures, such as lists, vectors, association lists, and records.
Procedural abstraction allows general procedures to be defined abstractly to cover many types of cases; mechanisms for variable arity procedures, procedure parameters, and currying give flexibility and allow general procedures to be tailored to specific needs.
Libraries allow commonly used operations to be developed once and reused in other applications.
Classes and objects package data and operations together in a convenient package that can be used in various applications.
Each of these topics allow us to organize a complex problem or task into relatively manageable parts and then to focus on those parts. Note that several of the items lists involve abstraction: we think of processing at a relatively high level, with details pushed aside to a separate procedure, structure, or file.
For example, association lists allow a natural matching of keyword and data, so we can focus upon the storage and retrieval of these information pairs. The assoc procedure retrieves relevant data, and we need not be bothered about the details of this retrieval.
Stacks provide a common and useful example of classes and objects.
Conceptually, the stack class or abstract data type mimics the information kept in a pile on a desk. Informally, we first consider materials on a desk, where we may keep separate piles for bills that need paying, magazines that we plan to read, and notes we have taken. These piles of materials have several properties. Each pile contains some papers (information). In addition, for each pile, we can do several tasks:
These operations allow us to do all the normal processing of data at a desk. For example, when we receive bills in the mail, we add them to the pile of bills until payday comes. We then take the bills, one at a time, off the top of the pile and pay them until the money runs out.
When discussing these operations it is customary to call the addition of an item to the top of the pile a Push operation and the deletion of an item from the top a Pop operation.
More formally, a stack is defined as a class that can store data and that has the following operations:
This specification says nothing about how we will program the various stack operations; rather, it tells us how stacks can be used. We can also infer some limitations on how we can use the data. For example, stack operations allow us to work with only the top item on the stack. We cannot look at other pieces of data lower down in the stack without first using Pop operations to clear away items above the desired one.
A Push operation always puts the new item on top of the stack, and this is the first item returned by a Pop operation. Thus, the last piece of data added to the stack will be the first item removed.
One approach to problem solving involves an initial focus on data elements, operations on those elements, and relationships among data. Within computer science, such an approach motivates the viewpoint of object-oriented programming or OOP. When working within an OOP framework, it is useful to distinguish between the general notion of a class and specific uses of the class with definite data values, which constitutes an object.
Utilizing the notion of abstraction, we think of objects as self-contained entities, just as we might consider each pile on a desk as an separate, independent collection of material. To support this image, processing involving objects consists of sending messages to the objects, letting the object react to the object in an appropriate way, and receiving responses back. Within an OOP context, a mechanism for interpreting messages is called a method. For example, if our desk had two piles -- one for magazines and one for bills, we might have the following interaction:
> (define mag-pile ... ) ; make stack for magazines ; (details of call dependent on implementation) > (define bill-pile ... ) ; make stack for bills ; (details of call dependent on implementation) > (bill-pile 'push! "mortgage") ; send push message to bill-pile > (bill-pile 'push! "doctor's bill") > (bill-pile 'push! "credit card") > (bill-pile 'empty?) ; send empty? message to bill-pile #f ; response from empty? message > (mag-pile 'empty?) #t > (mag-pile 'push! "Communications of the ACM - April 1997") > (mag-pile 'push! "CS Education Bulletin - Spring 1998") > (bill-pile 'top) ; send top message to bill-pile "credit card" ; data returned as message response > (mag-pile 'top) "CS Education Bulletin - Spring 1988" > (bill-pile 'pop!) "credit card" > (bill-pile 'top) "doctor's bill"
To implement a class within Scheme, we may proceed following the recent reading on Abstract Data Types. The simplest approach is to maintain an internal list within the abstract data type or class. With this approach, push and pop operations insert or delete an item from the front of the list, the empty operation tests if the list is empty, and the top operation returns the head of the list.
Sometimes we want a data structure that provides access to its elements on "first-in, first-out" basis, rather than the "last-in, first-out" constraint that a stack imposes. (For example, it might be prudent to treat that pile of unpaid bills a little differently, adding new elements at the bottom of the pile rather than the top, Paying off the most recent bill first, as in a stack, can make one's other, older creditors a little testy.)
Such a structure is called a queue. Like a line of people waiting for some service, a queue acquires new elements at one end (the rear of the queue) and releases old elements at the other (the front). Here is the abstract data type definition for queues, with the conventional names for the operations:
Create a new, empty queue object.
Determine whether the queue is empty; return true if it is and false if it is not.
Add a new element at the rear of a queue.
Remove an element from the front of the queue and return it. (This operation cannot be performed if the queue is empty.)
Return the element at the front of the queue (without removing it from the queue). (Again, this operation cannot be performed if the queue is empty.)
The implementation of queues in Scheme is somewhat trickier than the
implementation of stacks. Again, we'll keep the elements of the queue in a
list. However, it turns out that the enqueue operation can be
slightly faster if we represent an empty queue by a list containing one
element, a "dummy header," and store the actual queue elements after this
header, oldest first. The dummy header is not inserted by enqueue
and cannot be removed by the dequeue. It is not there to provide a
value, but just to keep the list from becoming null, so that one can always
set-cdr! procedure to it without first testing to
see whether it is null. The fact that the underlying list never becomes
completely null is an invariant of this implementation of queues.
The other novel feature of this implementation is that we'll actually be
accessing the list through two different fields,
front field always contains the entire
list structure, beginning with the dummy header;
is the list of the actual elements of the queue, and
front) is the first element of the queue (when it is not empty).
rear field, on the other hand, is always a one-element
list; it contains the last element of the queue, except when the queue is
empty, in which case the
rear field contains the dummy header.
The following box-and-pointer diagram shows a queue into which the symbols
c have been enqueued, in
Here is the constructor for queue objects:
(define queue-class (lambda () (let* ((front (list 'dummy-header)) (rear front)) (lambda (message . arguments) (case message ((empty?) (null? (cdr front))) ((enqueue!) (if (null? arguments) (error 'queue-class "method ENQUEUE!: An argument is required") (begin ; Attach a new cons cell behind the current rear ; element. (set-cdr! rear (list (car arguments))) ; Advance REAR so that it is once more a list ; containing only the last element. (set! rear (cdr rear))))) ((dequeue!) (if (null? (cdr front)) (error 'queue-class "method DEQUEUE!: The queue is empty") ; Recover the first element of the queue (not including ; the dummy header). (let ((removed (cadr front))) ; Splice out the element to be dequeued. (set-cdr! front (cddr front)) ; If you just spliced out the last element of the ; queue, reset REAR so that it holds the dummy ; header. (if (null? (cdr front)) (set! rear front)) removed))) ((front) (if (null? (cdr front)) (error 'queue "method FRONT: The queue is empty") (cadr front))) (else (error 'queue-class "unrecognized message")))))))
This document is available on the World Wide Web as
created April 28, 1997
last revised February 3, 2005
|For more information, please contact Henry M. Walker at firstname.lastname@example.org.|