List-like ADTs
A description of four basic abstract data types (ADTs)
In this note, we will define four basic abstract data types (ADTs): List
, and Deque
, Stack
, and Queue
. All of these ADTs specify similar functionality: storing an ordered collection of elements. They differ in precisely how the elements in the collection can be accessed. The List
ADT is the most generic of the four ADTs. Indeed, given a List
implementation, one can implement any of the three remaining ADTs. Yet
We specify ATDs purely formally in terms of states and operations. In each case, the state of an ADT consists of a sequence of elements, which we will write \((x_0, x_1, \ldots, x_{n-1})\). Formally, the variables \(x_i\) are symbols that could represent any object: mathematical objects—such as numbers—or instances of classes in Java—such as Integer
s or String
s. The important thing is that each \(x_i\) refers to some object, and the state \(S = (x_0, x_1, \ldots, x_{n-1})\) is specified by the order of objects in the sequence. (We allow for the possibility that some variables \(x_i\) and \(x_j\) with \(i \neq j\) may refer to the same object.)
While the state of an ADT represents the contents of the ADT, operations specify how the contents can be accessed and manipulated. Specifically, an operation may return a value—such as the value stored in one of the variables \(x_i\)—or modify the state of the ADT, or both.
An ADT is specified by (1) its allowable states, and (2) the effect of each allowable operation on each state. Thus, given (1) an ADT, (2) an initial state for the ADT, and (3) any sequence of operations for the ADT, one can determine from the ADT specification the resulting state and the effects of each operation performed in succession. In this way, the ADT completely describes all possible interactions with the ADT, just as the rules of chess determine all possible (legal) games of chess that can ever be played.
The List
ADT
Informally, a List
is meant to represent an ordered list of elements, such as a to-do list. Access is provided to the list by index—the relative position of the item in the list. For example the item at index \(0\) is first on the list, the item at index \(1\) is second, and so on. The List
specifies operations for adding, removing, viewing, and modifying elements on the list.
As in all of the ADTs discussed in this note, the state of a List
is a finite sequence of elements \(S = (x_0, x_1, \ldots, x_{n-1})\).
- \(\mathrm{size}()\):
- returns \(n\), the size/length of the sequence \(S\).
- \(\mathrm{isEmpty}()\):
- returns
true
if \(\mathrm{size}() = 0\) andfalse
otherwise.
- returns
- \(\mathrm{get}(i)\):
- returns \(x_i\) if \(0 \leq i \leq n - 1\).
- Undefined if \(i\) does not satisfy \(0 \leq i \leq n - 1\).
- \(\mathrm{set}(i, y)\):
- Updates \(S \leftarrow (x_0, x_1, \ldots, x_{i-1}, y, x_{i+1}, \ldots, x_{n-1})\) if \(0 \leq i \leq n - 1\).
- Undefined if \(i\) does not satisfy \(0 \leq i \leq n - 1\).
- \(\mathrm{add}(i, y)\):
- Updates \(S \leftarrow (x_0, x_1, \ldots, x_{i-1}, y, x_i, \ldots, x_{n-1})\) if \(0 \leq i \leq n\).
- Undefined if \(i\) does not satisfy \(0 \leq i \leq n\).
- Informally: insert \(y\) between elements \(x_{i-1}\) and \(x_i\) in the list. The index of the newly inserted element \(y\) will be \(i\) after insertion.
- \(\mathrm{remove}(i)\):
- Updates \(S \leftarrow (x_0, x_1, \ldots, x_{i-1}, x_{i+1}, \ldots, x_{n-1})\) and returns \(x_i\) if \(0 \leq i \leq n-1\).
- Undefined if \(i\) does not satisfy \(0 \leq i \leq n-1\).
- Informally: remove and return the element at index \(i\).
The Deque
ADT
The Deque
(a.k.a., double-ended queue) ADT can be thought of as the restriction of the List
ADT where only the first and last elements in the list (i.e., those with indices \(0\) and \(n-1\)) can be accessed. As with a List
, the state of a Deque
is a sequence \(S = (x_0, x_1, \ldots, x_{n-1})\).
- \(\mathrm{size}()\):
- Returns \(n\), the size/length of the sequence \(S\).
- \(\mathrm{isEmpty}()\):
- Returns
true
if \(\mathrm{size}() = 0\) andfalse
otherwise.
- Returns
- \(\mathrm{addFirst}(y)\):
- Updates \(S \leftarrow (y, x_0, x_1, \ldots, x_{n-1})\).
- \(\mathrm{peekFirst}()\):
- Returns \(x_0\) if the deque is not empty.
- Undefined if \(\mathrm{isEmpty}()\).
- \(\mathrm{removeFirst}()\):
- Updates \(S \leftarrow (x_1, x_2, \ldots, x_{n-1})\) and returns \(x_0\) if the deque is not empty.
- Undefined if \(\mathrm{isEmpty}()\).
- \(\mathrm{addLast}(y)\):
- Updates \(S \leftarrow (x_0, x_1, \ldots, x_{n-1}, y)\).
- \(\mathrm{peekLast}()\):
- Returns \(x_{n-1}\) if the deque is not empty.
- Undefined if \(\mathrm{isEmpty}()\).
- \(\mathrm{removeLast}()\):
- Updates \(S \leftarrow (x_0, x_1, \ldots, x_{n-2})\) and returns \(x_{n-1}\) if the deque is not empty.
- Undefined if \(\mathrm{isEmpty}()\).
Note. Given a sequence/state \(S\), the Deque
operations for \(\mathrm{addFirst}(y)\), \(\mathrm{peekFirst}()\), and \(\mathrm{removeFirst}()\) have the same effect as the List
operations \(\mathrm{add}(0, y)\), \(\mathrm{get}(0)\), and \(\mathrm{remove}(0)\), respectively. Similarly, \(\mathrm{addLast}(y)\), \(\mathrm{peekLast}()\), and \(\mathrm{removeLast}()\) are equivalent to \(\mathrm{add}(n, y)\), \(\mathrm{get}(n-1)\), and \(\mathrm{remove}(n-1)\). Thus, the Deque
is a formal restriction of a List
in the sense that the Deque
operations are subsumed by List
operations. Given any List
implementation (i.e., program that provides the functionality specified by List
), one can use the List
to implement a Deque
.
The Stack
ADT
A Stack
can be viewed as yet a further restriction of the Deque
(hence List
) ADT in which access is only provided to one end of the list. Once again, the state of a Stack
can be represented as a sequence \(S = (x_0, x_1, \ldots, x_{n-1})\).
- \(\mathrm{size}()\):
- Returns \(n\), the size/length of the sequence \(S\).
- \(\mathrm{isEmpty}()\):
- Returns
true
if \(\mathrm{size}() = 0\) andfalse
otherwise.
- Returns
- \(\mathrm{push}(y)\):
- Updates \(S \leftarrow (x_0, x_1, \ldots, x_{n-1}, y)\).
- \(\mathrm{peek}()\):
- Returns \(x_{n-1}\) if the stack is not empty.
- Undefined if \(\mathrm{isEmpty}()\).
- \(\mathrm{pop}()\):
- Updates \(S \leftarrow (x_0, x_1, \ldots, x_{n-2})\) and returns \(x_{n-1}\) if the stack is not empty.
- Undefined if \(\mathrm{isEmpty}()\).
Note that the final three operations have precisely the same effects as a Deque
’s \(\mathrm{addLast}(y)\), \(\mathrm{peekLast}()\), and \(\mathrm{removeLast}()\), respectively. Thus, any Deque
or List
implementation can also be used as a Stack
.
The Queue
ADT
The Queue
is a different restriction of a Deque
(hence List
) in which elements can only be added/pushed to one side of the sequence and removed/popped from the other. Once again, the state of a Queue
is a sequence \(S = (x_0, x_1, \ldots, x_{n-1})\). The operations have the following effects:
- \(\mathrm{size}()\):
- Returns \(n\), the size/length of the sequence \(S\).
- \(\mathrm{isEmpty}()\):
- Returns
true
if \(\mathrm{size}() = 0\) andfalse
otherwise.
- Returns
- \(\mathrm{add}(y)\):
- Updates \(S \leftarrow (x_0, x_1, \ldots, x_{n-1}, y)\).
- \(\mathrm{peek}()\):
- Returns \(x_{0}\) if the queue is not empty.
- Undefined if \(\mathrm{isEmpty}()\).
- \(\mathrm{remove}()\):
- Updates \(S \leftarrow (x_1, x_2, \ldots, x_{n-1})\) and returns \(x_{0}\) if the queue is not empty.
- Undefined if \(\mathrm{isEmpty}()\).
Observe that \(\mathrm{add}(y)\) is equivalent to a Deque
’s \(\mathrm{addLast}(y)\), while \(\mathrm{peek}()\) and \(\mathrm{remove}\) are equivalent to \(\mathrm{peekFirst}()\) and \(\mathrm{removeFirst}()\), respectively.
Relationships Between ADTs
The formal specifications of ADTs given above serve two main purposes. First, by defining an ADT purely in terms of symbolic manipulations, we can—in principle—mathematically prove that a program implements the ADT. Second, formal specifications often reveal connections between ADTs that might not be apparent if only information descriptions are given. For example, all of the operations for Deque
, Stack
, and Queue
are restrictions of operations defined for the List
ADT. Thus, it is apparent that List
s are more general than the other ADTs.
It is also clear that the List
ADT is strictly more general than Deque
, Stack
, and Queue
, as List
s provide operations that cannot be performed by the other ADTs. For example, given a single Deque
, Stack
, or Queue
with state \(S = (x_0, x_1, \ldots, x_{n-1})\), there is no operation that allows one to insert a new element at a specified index other than \(0\) or \(n\), as in the List
’s \(\mathrm{add}(i, y)\). Nonetheless, simulating this operation may be possible given multiple instances of a weaker ADT.
Example. Suppose we have two Queue
instances, \(Q_1\) and \(Q_2\), and that initially, \(Q_1\) is in the state \(S_1 = (x_0, x_1, \ldots, x_{n-1})\), while \(Q_2\) is in the state \(S_2 = \varepsilon\)—i.e., \(Q_2\) is empty. We can update the state of \(S_1\) to \(S_1' = (x_0, x_1, \ldots, x_{i-1}, y, x_{i}, \ldots, x_{n-1})\) as follows:
- Remove the first \(i - 1\) elements from \(Q_1\) and add them to \(Q_2\)
- a single removal/addition can be performed using \(Q_1.\mathrm{add}(Q_2.\mathrm{remove}())\)
- after doing this \(i - 1\) times, the new states of the queues will be \(S_1 = (x_i, x_{i+1}, \ldots, x_{n-1})\) and \(S_2 = (x_0, x_1, \ldots, x_{i-1})\)
- Add \(y\) to \(S_2\)
- \(Q_2\)’s updated state will be \(S_2 = (x_0, x_1, \ldots, x_{i-1}, y)\)
- Add the remaining \(n - i + 1\) elements from \(Q_1\) to \(Q_2\)
- now we’ll have \(S_1 = \varepsilon\) and \(S_2 = (x_0, x_1, \ldots, x_{i-1}, y, x_i, \ldots, x_{n-1})\)
- Add all of the elements from \(Q_2\) to \(Q_1\)
- now \(S_1 = (x_0, x_1, \ldots, x_{i-1}, y, x_i, \ldots, x_{n-1})\) and \(S_2 = \varepsilon\).
Following the sequence of steps as above, we can simulate the operation \(\mathrm{add}(i, y)\) using two queues. Thus, while a single queue is not powerful enough to perform this operation on its own, using two queues allows us to provide this functionality. Similarly, given two queues, one can simulate the \(\mathrm{get}(i)\) and \(\mathrm{remove}(i)\) operations.
The Moral. A single List
offers strictly more functionality than a Deque
, Stack
, or Queue
, but two Queue
instances can be used to simulate all of the List
operations.
Caveat. Even though we can simulate List
operations using two Queue
s, it may be that the simulation is very inefficient compared to a “native” List
implementation. In particular, to perform a single \(\mathrm{add}(i, y)\) operation using two Queue
s, the method described above requires that we copy the entire contents of \(Q_1\) to \(Q_2\), and vice versa.
Exercises
-
Given two
Queue
s, \(Q_1\) and \(Q_2\) in states \(S_1 = (x_0, x_1, \ldots, x_{n-1})\) and \(S_2 = \varepsilon\), respectively, how can you simulate the \(\mathrm{remove}(i)\) and \(\mathrm{get}(i)\) operations from theList
ADT for \(Q_1\)? -
Suppose you are given two
Stack
s in states \(S_1 = (x_0, x_1, \ldots, x_{n-1})\) and \(S_2 = \varepsilon\). How can you simulate theList
operations for \(S_1\)? -
Given that a
List
can perform operations equivalent to all of the operations specified byDeque
,Stack
, andQueue
, why do we need the latter three ADTs? Why might it be advantageous to use an implementation of, say, aQueue
that specifically does not provide the additional functionality of a generalList
?