| CS 261 | University of Puget Sound | Spring, 2020 |
|
Computer Science II
|
||
|
Abstract Data Types and their Implementations,
Some Basic Algorithms,
Object-oriented Problem Solving, and Efficiency |
||
Warning:
This course is under development.
Although the basic structure of this course is largely established,
nothing on this Web site should be considered official or even possibly
correct.
DO NOT MAKE PLANS BASED ON THE CONTENTS OF THIS SITE UNTIL JANUARY, 2020.
This reading introduces the concept of an Abstract Data Type (ADT) and describes a stack as a specific example.
Most of this reading is an edited version of Henry M. Walker, Introduction to Computing and Computer Science with Pascal, Little, Brown, and Company, 1986, Sections 17.1-17.2, 17.4, with programming examples translated from Pascal to C and then Java. This material is used with permission from the copyright holder.xs
When we want to work conceptually with data, we often describe two basic characteristics: the data we will be storing, and the operations we want to perform on these data. In computer science, these two characteristics combine to give the concept of an abstract data type or ADT which allows us to work with data at a high-level of avstraction without worrying about various programming details.
The stack discussed in this lab provides one example of an abstract data type. The queue, discussed in a forthcoming session, provides a second example.
A stack mimics the information that we might keep in a pile on our desk. For example, on our desk, we may keep separate piles for
These piles have several properties. First, each pile contains the same type of information (e.g., bills, magazines, or notes). In addition, for each pile, we can do several tasks.
These operations allow us to do all of our normal processing of data at our desk. For example, when we receive bills in the mail, we add them to our pile of bills until payday comes. Then, we take our bills, one at a time, off the top of our pile and pay them until our 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 an abstract data type with the following interface, in addition to a constructor:
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.
The followng interface might be used for a generic stack with items of type E.
/* a constructor for an empty stack
*/
/* return whether the stack is empty
pre: stack is an initialized stack
post: returns true if stack is empty;
false otherwise
*/
public boolean isEmpty ()
/* return whether the stack is full
pre: stack is an initialized stack
post: returns true if an additional item
can be added to the stack;
false otherwise
*/
public boolean isFull ()
/* insert given item onto top of the stack
pre: the specified stack is initialized
returns: null if stack is full
otherwise, reference to item added
*/
public E push (E item)
/* removes top item from stack referenced by parameter
pre: the specified stack is initialized
returns: null if stack is empty
otherwise, reference to the item removed
*/
E pop ()
/* returns a pointer to top item on the stack
pre: the specified stack is initialized
returns: null if stack is empty
otherwise, reference to the item returned
*/
E top ()
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.
Jargon: By working consistently with the top element, storage and retrieval in a stack is sometimes said to implement a first-in, last-out or FILO property or, alternately, a last-in, first-out or LIFO property.
One common implementation of a stack involves the use of an array.
More precisely, we store each piece of data as an element of an
array. We place the first data item at one end of the array. Then,
for a Push operation, we add a data item to the next array
element. For a Pop operation, we return the item at the top
of our data, and we record that the top has moved down. This
processing requires several parts, including an array
(stackArray) of data to store our data items, a variable
(topPosition) to keep track of our top element. In this
context, (stackArray.length) keeps track of the size of our
array. Conceptually, this setup fits together as shown in the
figure. Note that a stack implementation may have
the topPosition as either the top element in the stack, or
a pointer to the top empty element. Both implementations of stacks
are valid.
With this figure, we trace what happens in our stack operations. We start with topPosition equal to -1, since we have no data in the array. Then, we perform a Push, we increment topPosition by one, and we store our new item in this new topPosition. Similarly, for a Pop operation, we return the item at the topPosition, and we move the topPosition down by one. Finally, for isFull or isEmpty functions, we compare the topPosition with stackArray.length-1 or -1, respectively.
Additional details arise because we must check if the stack is full or empty before actually performing a Push or Pop operation, respectively.
Finally, we note that sometimes it is convenient to package the
elements together in a class. For example, the
following declarations might be used when the stack is to store
strings.
public class Stack <E> {
protected int topPosition;
protected E [] stackData;
The previous section on stacks described the use of arrays to implement
this abstract data type. That section utilized an
array stackData and index topPosition as its
primary internal fields.
In this section, we implement the same operations using lists and pointers. Further, since applications should consider only the conceptual operations of an abstract data type, not the implementation details, we will be careful that the new code we develop still uses exactly the same methods and signatures as defined earlier for arrays.
When describing this new approach to a stack, we focus on the specification of the top of the stack, and we consider how to locate subsequent times in the stack after we perform a Pop operation. The figure on the right shows how such a structure could be organized.
In this picture, we store a stack Item in a record with a reference to the next lower Item on the stack. Also, we use a reference variable, which specifies the top record.
Turning to the data stored within the node, the generic data field will
contain a reference to items of type E.
This gives rise to the following basic structure for
a StackNode class:
public class StackNode {
E data;
StackNode next;
}
/* getters and/or setters follow */
Beyond the natural getters and setters, two choices might be considered for storing the data.
The node could store a reference to the original E
object:
public StackNode (E item, StackNode next) {
this.data = item;
this.next = next;
}
The node could make a copy of the data, leading to this constructor.
public StackNode (E item, StackNode next) {
this.data = new E (item); // assuming E has a copy constructor
this.next = next;
}
In this case, we would copy the original item into the node for a push
operation. A top or pop operation would then
return a reference to this new item.
With these declarations, a stack itself would have one
field first, which would be initialized
to null in the constructor. Then the Boolean expression
first == null
allows us to test for an empty stack. Then the Push, Pop,
and Top operations involve inserting, deleting, and reading items
at the top of this list structure, respectively. With the use of dynamic
storage allocation, there is no need to declare a list size initially, and
the isFull operation is not needed. However, for compatibility with the
earlier array implementation of stacks, this function is still included
and will always return False. The outlines of these operations are quite
similar to the work in the earlier ones. Also, in the Pop operation, we
free the list item once we have returned the appropriate
information.
Once these methods are defined, we can use them just as we did
before. In a program, we must include the appropriate stack methods
(shown earlier in this section); we must write out these procedures;
and we must call the constructor. However, once these details are
completed, the compatibility of the methods allows us to
use isEmpty, Push, Pop, and Top
without change. Thus, while the implementation of stacks has changed
dramatically, the use of stacks in applications is identical.