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 Integers or Strings. 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\) and false otherwise.
  • \(\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\) and false otherwise.
  • \(\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\) and false otherwise.
  • \(\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\) and false otherwise.
  • \(\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 Lists 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 Lists 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:

  1. 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})\)
  2. Add \(y\) to \(S_2\)
    • \(Q_2\)’s updated state will be \(S_2 = (x_0, x_1, \ldots, x_{i-1}, y)\)
  3. 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})\)
  4. 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 Queues, 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 Queues, the method described above requires that we copy the entire contents of \(Q_1\) to \(Q_2\), and vice versa.

Exercises

  1. Given two Queues, \(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 the List ADT for \(Q_1\)?

  2. Suppose you are given two Stacks in states \(S_1 = (x_0, x_1, \ldots, x_{n-1})\) and \(S_2 = \varepsilon\). How can you simulate the List operations for \(S_1\)?

  3. Given that a List can perform operations equivalent to all of the operations specified by Deque, Stack, and Queue, why do we need the latter three ADTs? Why might it be advantageous to use an implementation of, say, a Queue that specifically does not provide the additional functionality of a general List?