Skip to content

Fundamental Data Structures: Arrays, Linked Lists, Stacks and Queues

Prerequisite:Complexity and Big-O Notation: Measuring Speed as a Function of Input Size

Raw
  • A data structure is judged not by what it can do but by how fast each operation is. Arrays and linked lists represent the same kind of sequence, and the operations they are good at are exactly the opposite of one another.
  • An array supports indexed access in Θ(1)\Theta(1) time and insertion or deletion in the middle in Θ(n)\Theta(n) time. A linked list, given a reference to the node marking the position, inserts or deletes right after it in Θ(1)\Theta(1) time, but reaching the kk-th element costs Θ(k)\Theta(k) (Proposition 3.2, Proposition 4.2).
  • A dynamic array that doubles its capacity has worst-case cost Θ(n)\Theta(n) for a single push, yet the total over nn pushes stays below 3n3n. The amortized cost is O(1)O(1) (Theorem 3.3).
  • “Amortized O(1)O(1)” does not mean “fast on average”. No probability distribution on the input is assumed; the bound on the total cost holds for every sequence of operations.
  • A stack (LIFO) and a queue (FIFO) are abstract data types, specifiable by axioms without mentioning any implementation. The same axioms can be met by an array and by a linked list alike.
  • A queue can be realised with worst-case Θ(1)\Theta(1) operations by a ring buffer (Proposition 7.2), or with amortized O(1)O(1) operations by two stacks (Theorem 8.2). We analyse the latter by the potential method.

Most of the data a program handles is a sequence. Text is a sequence of characters, an image a sequence of pixels, an account history a sequence of transactions. How a sequence is laid out inside the machine is a matter of choice, and that choice can change the running time by orders of magnitude.

Let us start from a naive question. Why have two representations, the array and the linked list, both stayed in use for more than half a century? Because the operations they are good at are exactly complementary. An array is strong at “show me the ii-th element” and weak at “squeeze something into the middle”. The linked list is the reverse. Had either one won on every operation, the other would have disappeared from history.

The two also arose from different demands. Arrays have existed as a language feature since the earliest high-level language, Fortran (1957). Linked lists came slightly later: around 1956 Allen Newell, Cliff Shaw and Herbert Simon designed the information processing language IPL for their theorem-proving program Logic Theorist, and IPL introduced them; LISP inherited them in 1958. What those authors needed was a representation in which an element could be inserted in the middle without shifting everything behind it.

In this article we take the OO and Θ\Theta notation of the previous chapter, Complexity and big-O notation (Definition 3.1[Complexity and Big-O Notation]), as our instrument, and begin by separating “what can be done” (the abstract data type) from “how it is realised” (the data structure). We then take up arrays, linked lists, stacks and queues, and determine the cost of each operation with proofs.

2. Preliminaries: the machine model and abstract data types

Section titled “2. Preliminaries: the machine model and abstract data types”

To prove anything about running time we must first fix what counts as one unit of time. In this article we use the word RAM model. It refines the uniform-cost RAM model(Definition 2.1)[Complexity and Big-O Notation] of the previous chapter by making the addressing of memory explicit.

Memory is a sequence of words M[0],M[1],M[0], M[1], \ldots indexed by addresses 0,1,2,0, 1, 2, \ldots, and one word can hold a single integer or a single address. Reading or writing M[i]M[i] for a given address ii, and adding, subtracting, multiplying, dividing or comparing words, each takes one unit of time. This assumption — that a read or write at a given address is constant time — is what will justify the claim that indexed access into an array costs Θ(1)\Theta(1).

Remark 2.1

The word RAM is an idealisation. Real machines have a hierarchy of caches, and the time to access one word is not constant. We still use the model because the essential differences between algorithms (Θ(n)\Theta(n) versus Θ(logn)\Theta(\log n) versus Θ(1)\Theta(1)) are unaffected by the presence of caches. Measured differences between two Θ(n)\Theta(n) procedures lie outside the model; we discuss them in Remark 5.1.

Next we introduce the basic distinction on which any discussion of data structures rests.

Definition 2.2Abstract data type

An abstract data type (ADT) is a triple, containing no internal representation of values whatsoever, consisting of:

  1. a set of values;
  2. the names of the operations, together with the types of their arguments and return values;
  3. the properties the operations must satisfy. These may be written as preconditions and postconditions, or as equations (axioms) relating the operations to one another.

A realisation of an abstract data type by a concrete layout of memory together with concrete procedures is called a data structure.

This distinction earns its keep because one abstract data type can have many data structures. Which one to use is not settled by correctness; it is settled by how often each operation is used and what each operation costs.

Definition 3.1Array

An array of length nn consists of nn cells of equal size ss (in words) placed at consecutive addresses starting from a base address bb. The ii-th element (0in10 \le i \le n-1) is written A[i]A[i], and the starting address of its cell is

addr(A[i])=b+si\mathrm{addr}(A[i]) = b + s \cdot i

This single formula in the definition determines both the strength and the weakness of the array. Since the address is a linear function of ii, any element can be reached by arithmetic alone. On the other hand, squeezing in one element shifts the index ii of every later element by one, and hence shifts all of their addresses too. We now verify this in turn.

Proposition 3.2Cost of the basic array operations

For an array AA of length nn, the following hold in the word RAM model.

  1. Reading or writing the element at a given index ii (0in10 \le i \le n-1) takes Θ(1)\Theta(1) time.
  2. Deciding whether a given value xx occurs in AA takes Θ(n)\Theta(n) time in the worst case, assuming nothing about the order of the elements. Linear search, comparing the elements from the front, attains this bound.
  3. Insertion at position ii (0in0 \le i \le n) requires exactly nin - i moves of existing elements, and deletion of the element at position ii (0in10 \le i \le n-1) requires n1in - 1 - i moves; both take Θ(ni)\Theta(n-i) time. In particular the worst case (i=0i = 0) is Θ(n)\Theta(n), and if the insertion position ii is uniformly distributed on 0,1,,n0, 1, \ldots, n, the average number of moves is n/2n/2.
Proof(Proposition 3.2)

1. By Definition 3.1, addr(A[i])=b+si\mathrm{addr}(A[i]) = b + s \cdot i. Since bb and ss are fixed for a given array, this computation is one multiplication and one addition. In the word RAM an arithmetic operation and an addressed read or write each take one unit of time, so the total is constant, that is Θ(1)\Theta(1). What matters is that it depends neither on nn nor on ii.

2. The upper bound is given by linear search: for i=0,1,,n1i = 0, 1, \ldots, n-1 read A[i]A[i] and compare it with xx, returning true on a match and false if none occurs. Each iteration is constant time by part 1, so the whole is O(n)O(n).

For the lower bound we use an adversary argument. Suppose a correct algorithm B\mathcal{B}, on an input AA not containing xx, answers “not present” without ever reading some position jj. Feed it AA', obtained from AA by replacing position jj by xx. Since B\mathcal{B} never reads jj, it follows the same computation and again answers “not present” — which is wrong, because AA' contains xx. Hence all nn positions must be read, giving Ω(n)\Omega(n), and together with the upper bound, Θ(n)\Theta(n).

3. We treat insertion. The array AA' after the insertion satisfies A[j]=A[j]A'[j] = A[j] for j<ij < i, A[i]=xA'[i] = x, and A[j+1]=A[j]A'[j+1] = A[j] for ijn1i \le j \le n-1. Every element with jij \ge i has its address shifted back by ss without exception, so at least nin - i writes are necessary. Conversely, moving from the back — A[j+1]A[j]A[j+1] \leftarrow A[j] for j=n1,n2,,ij = n-1, n-2, \ldots, i — loses nothing to overwriting and uses exactly nin-i writes, after which one further write A[i]xA[i] \leftarrow x finishes the job. One move is constant time by part 1, so the total is Θ(ni)\Theta(n-i). Deletion is analogous: closing the gap forwards with A[j1]A[j]A[j-1] \leftarrow A[j] for j=i+1,,n1j = i+1, \ldots, n-1 takes n1in-1-i moves, that is Θ(ni)\Theta(n-i).

The worst case is i=0i = 0, with nn moves, hence Θ(n)\Theta(n). The average is

1n+1i=0n(ni)=1n+1k=0nk=1n+1n(n+1)2=n2\frac{1}{n+1}\sum_{i=0}^{n}(n-i) = \frac{1}{n+1}\sum_{k=0}^{n}k = \frac{1}{n+1}\cdot\frac{n(n+1)}{2} = \frac{n}{2}

which is again Θ(n)\Theta(n). Averaging offers no escape from Θ(n)\Theta(n).

Arrays have a second weakness: their length must be fixed in advance. If the number of elements grows at run time, a bare array cannot cope. The dynamic array (resizable array) solves this. The naive scheme — “each time one element is added, allocate an array one longer and copy everything” — incurs k=1nk=Θ(n2)\sum_{k=1}^{n} k = \Theta(n^2) copies over nn additions. Yet merely changing the growth rule from "+1+1" to "×2\times 2" brings the total down to linear.

Theorem 3.3Amortized cost of the dynamic array

Consider a data structure realising append-at-the-end, push\mathrm{push}, by the following rule. It maintains an array AA of capacity cc and the current number of elements nn (0nc0 \le n \le c), starting from n=0n = 0, c=1c = 1. The operation push(x)\mathrm{push}(x) behaves as follows.

  • If n<cn < c, set A[n]xA[n] \leftarrow x and then nn+1n \leftarrow n + 1.
  • If n=cn = c, first allocate a new array of length 2c2c, move the existing cc elements into it, set c2cc \leftarrow 2c, and then perform the step above.

Then the total number of element writes performed by nn pushes (n1n \ge 1) starting from the empty state is less than 3n3n. Hence the amortized cost per push\mathrm{push} is O(1)O(1). The worst-case cost of an individual push\mathrm{push}, however, is Θ(n)\Theta(n).

Proof(Theorem 3.3)

We use the aggregate method: estimate the total cost directly and divide by the number of operations.

Split the writes into two kinds. The write A[n]xA[n] \leftarrow x at the end occurs exactly once in every push\mathrm{push}, contributing nn writes in total.

The rest are the copies caused by capacity growth. Growth happens when n=cn = c just before a push\mathrm{push}; since the capacity runs through the powers 1,2,4,1, 2, 4, \ldots, this happens exactly when the number of elements just before is 2k2^k (k=0,1,2,k = 0, 1, 2, \ldots), and the number of copies is then exactly 2k2^k. During nn pushes, the element count reaches 2k2^k only for those kk with 2k<n2^k < n, so letting KK be the largest such kk, the total number of copies is

k=0K2k=2K+11<22K2n\sum_{k=0}^{K} 2^{k} = 2^{K+1} - 1 < 2 \cdot 2^{K} \le 2n

where the last inequality uses 2K<n2^K < n.

Hence the total number of writes is less than n+2n=3nn + 2n = 3n. Since nn operations cost less than 3n3n in total, the amortized cost per operation is less than 33, that is O(1)O(1).

As for the worst case, a push\mathrm{push} in the state n=c=2kn = c = 2^k performs 2k=n2^k = n copies, and by part 1 of Proposition 3.2 this takes Θ(n)\Theta(n) time.

Example 3.4Counting ten pushes

Let us count the writes made by ten pushes from the empty state. Growth occurs when the element count just before is 1,2,4,81, 2, 4, 8, that is, at pushes number 2, 3, 5 and 9.

push number(n,c)(n, c) beforecapacity after growthcopiestotal writes this time
1(0,1)(0, 1)01
2(1,1)(1, 1)212
3(2,2)(2, 2)423
5(4,4)(4, 4)845
9(8,8)(8, 8)1689
4, 6, 7, 8, 1001 each

The total is 10+(1+2+4+8)=2510 + (1 + 2 + 4 + 8) = 25 writes, below the bound 3×10=303 \times 10 = 30 guaranteed by the theorem. The ninth push alone needs 9 writes, but levelled out this is 2.52.5 writes per operation.

Remark 3.5Amortized is not average

Amortized cost involves no probability at all. What Theorem 3.3 asserts is the deterministic guarantee that for every sequence of pushes the total is below 3n3n. This differs in meaning and in strength from arguments that assume “if the input is uniformly distributed”, as in the average cost of a hash table (Theorem 5.3[探索アルゴリズム]). On this distinction see also Remark 6.5[探索アルゴリズム].

The theorem also depends on the growth being by a constant factor, so that the number of copies forms a geometric series. With ”+1+1 each time”, the kk-th push causes k1k-1 copies, for a total of k=1n(k1)=Θ(n2)\sum_{k=1}^{n}(k-1) = \Theta(n^2), and amortized O(1)O(1) collapses. Conversely the factor need not be 22: as long as the ratio is a constant greater than 11, the same argument goes through, whether the factor is 1.51.5 or 1.1251.125.

The weakness of the array was that the address is determined by the index. Then let the elements carry their addresses themselves — that is the idea behind the linked list.

Definition 4.1Singly linked list

A node is a cell consisting of a field value\mathrm{value} holding a value and a field next\mathrm{next} holding either a reference to another node or nil\mathrm{nil}. When nodes p0,p1,,pn1p_0, p_1, \ldots, p_{n-1} satisfy

pj.next=pj+1(0jn2),pn1.next=nilp_j.\mathrm{next} = p_{j+1} \quad (0 \le j \le n-2), \qquad p_{n-1}.\mathrm{next} = \mathrm{nil}

this sequence is called a singly linked list, and the whole list is represented by a reference head=p0\mathrm{head} = p_0 to its first node. The empty list is represented by head=nil\mathrm{head} = \mathrm{nil}. If each node also has a field prev\mathrm{prev} satisfying pj+1.prev=pjp_{j+1}.\mathrm{prev} = p_j, the list is called doubly linked.

There is no constraint at all on the addresses of the nodes. They may be scattered anywhere in memory; the links are carried entirely by the values of next\mathrm{next}. This freedom is precisely what shapes the cost profile.

Proposition 4.2Cost of the basic linked-list operations

For a singly linked list of nn nodes, the following hold in the word RAM model.

  1. Given a reference to a node pp, inserting a new node immediately after pp, and deleting the node immediately after pp, each take Θ(1)\Theta(1) time.
  2. Reaching the kk-th node counted from the front (zero-based, 0kn10 \le k \le n-1) requires exactly kk traversals of next\mathrm{next} and takes Θ(k)\Theta(k) time. The worst case is Θ(n)\Theta(n).
  3. In a singly linked list, deleting pp itself takes Θ(n)\Theta(n) time in the worst case even when a reference to pp is given. In a doubly linked list it takes Θ(1)\Theta(1) time.
Proof(Proposition 4.2)

1. For insertion, prepare a new node qq and perform the two writes

q.nextp.next,p.nextqq.\mathrm{next} \leftarrow p.\mathrm{next}, \qquad p.\mathrm{next} \leftarrow q

Afterwards qq follows pp and the original p.nextp.\mathrm{next} follows qq, so the linking condition of Definition 4.1 is preserved. For deletion, when p.nextnilp.\mathrm{next} \ne \mathrm{nil}, the single write

p.nextp.next.nextp.\mathrm{next} \leftarrow p.\mathrm{next}.\mathrm{next}

suffices. In both cases the number of reads and writes depends neither on nn nor on kk, so the time is Θ(1)\Theta(1). Part 3 of Proposition 3.2 required nin-i moves for an insertion at position ii; here not a single element has been moved.

2. The upper bound is given by the procedure that starts at head\mathrm{head} and follows next\mathrm{next} kk times. Each traversal reads one word and so is constant time, giving Θ(k)\Theta(k) in total.

For the lower bound we use the fact that in this model there are only two ways to learn the address of a node: reading head\mathrm{head}, and reading the next\mathrm{next} field of an already reached node. Unlike the array of Definition 3.1, there is no formula computing the address of the kk-th node from kk. Hence the set of reached nodes grows by at most one per traversal, and at least kk traversals are needed to arrive at pkp_k. This gives Θ(k)\Theta(k).

3. Deleting pp itself requires finding the preceding node pp' and setting p.nextp.nextp'.\mathrm{next} \leftarrow p.\mathrm{next}. In a singly linked list one cannot get from pp to pp', so the only option is to walk from head\mathrm{head} looking for the node whose next\mathrm{next} equals pp, which by part 2 is Θ(n)\Theta(n) in the worst case. In a doubly linked list p=p.prevp' = p.\mathrm{prev} is available in constant time, so the two writes

p.prev.nextp.next,p.next.prevp.prevp.\mathrm{prev}.\mathrm{next} \leftarrow p.\mathrm{next}, \qquad p.\mathrm{next}.\mathrm{prev} \leftarrow p.\mathrm{prev}

suffice, giving Θ(1)\Theta(1) time (the case analysis for nil\mathrm{nil} at the two ends can be removed by the sentinel of Remark 4.4).

Example 4.3Running insertion and deletion

We check part 1 of Proposition 4.2 in Python.

class Node:
__slots__ = ("value", "next")
def __init__(self, value, next=None):
self.value = value
self.next = next
def insert_after(p: Node, x) -> Node:
"""Insert a node with value x right after p and return it."""
q = Node(x, p.next) # q.next <- p.next
p.next = q # p.next <- q
return q
def delete_after(p: Node) -> None:
"""Delete the node right after p; do nothing if there is none."""
if p.next is not None:
p.next = p.next.next
def to_list(head: Node) -> list:
out, cur = [], head
while cur is not None:
out.append(cur.value)
cur = cur.next
return out
head = Node(3, Node(1, Node(4)))
print(to_list(head)) # [3, 1, 4]
insert_after(head, 5)
print(to_list(head)) # [3, 5, 1, 4]
delete_after(head.next)
print(to_list(head)) # [3, 5, 4]

The body of insert_after is two lines and that of delete_after is one, and neither contains a loop depending on the length of the list. That is what Θ(1)\Theta(1) amounts to. By contrast to_list follows next\mathrm{next} to the end and therefore costs Θ(n)\Theta(n); even merely learning the length requires a full traversal (keeping a separate element count makes it Θ(1)\Theta(1)).

Remark 4.4Sentinel nodes

Implementations commonly place one dummy node carrying no value (a sentinel) at the front and keep a reference to the sentinel instead of head\mathrm{head}. Then “insert at the front” and “delete the first element” both become “insert or delete right after the sentinel”, and part 1 of Proposition 4.2 applies verbatim. Testing for the empty list becomes testing whether the sentinel’s next\mathrm{next} is nil\mathrm{nil}. Fewer cases mean fewer bugs, so it is a good idea to start from a sentinel when implementing.

Let us line up the results so far. First compare how the two look in memory.

Array: consecutive addresses. The address of A[i] is computed as b + s·i314159bb+sb+2sb+3sb+4sb+5sA[0]A[1]A[2]A[3]A[4]A[5]Linked list: addresses are scattered; each node holds a value and a reference3141headvaluenext referencenil
Memory layout of an array and of a linked list. Array addresses are computed from the index; linked-list nodes can only be reached by following references.

Collecting Proposition 3.2, Proposition 4.2 and Theorem 3.3 gives the following table. The linked list is assumed to keep a reference to its last node as well.

operationdynamic arraysingly linked list
read or write the ii-th elementΘ(1)\Theta(1)Θ(i)\Theta(i) (worst case Θ(n)\Theta(n))
search by value (no order assumed)Θ(n)\Theta(n)Θ(n)\Theta(n)
search by value (sorted)Θ(logn)\Theta(\log n) (binary search)Θ(n)\Theta(n)
insert at the front, delete the firstΘ(n)\Theta(n)Θ(1)\Theta(1)
append at the endamortized Θ(1)\Theta(1)Θ(1)\Theta(1)
delete the last elementΘ(1)\Theta(1)Θ(n)\Theta(n) (needs the preceding node)
insert or delete at a position given by index iiΘ(ni)\Theta(n-i)Θ(i)\Theta(i)
insert or delete right after a node in handΘ(ni)\Theta(n-i)Θ(1)\Theta(1)
memory beyond the elements themselvesunused capacity (up to about the element count)one reference per node

The second-to-last row captures the characters of the two most sharply. “Put something next to where I am” is Θ(1)\Theta(1) for the linked list and Θ(ni)\Theta(n-i) for the array. Conversely “jump to the ii-th element” is Θ(1)\Theta(1) for the array and Θ(i)\Theta(i) for the linked list. The choice is decided by how often each of these two kinds of operation is used.

Remark 5.1Constant factors and the cache

The Θ\Theta notation hides constant factors, and in measurements what is hidden can matter. Modern CPUs fetch main memory not word by word but in cache lines (typically 64 bytes), so scanning an array from the front loads several elements into the cache per fetch. In a linked-list traversal the address of the next node is unknown until the current one has been read, and the more the nodes are scattered in memory, the more cache misses occur.

As a result, the judgement “many insertions in the middle, therefore a linked list” is sometimes contradicted by measurement up to element counts in the thousands. When in doubt, write it with a dynamic array first, measure, and then decide.

From here we move to the side of abstract data types. A stack is a type specified by one thing only: what goes in last comes out first (last in, first out, LIFO).

Definition 6.1Stack

A stack is the abstract data type with the following operations. Write XX for the type of values and S\mathcal{S} for the set of stacks.

  • emptyS\mathrm{empty} \in \mathcal{S} (the empty stack)
  • push:S×XS\mathrm{push} : \mathcal{S} \times X \to \mathcal{S} (put on top)
  • pop:SS\mathrm{pop} : \mathcal{S} \to \mathcal{S} (remove the top)
  • top:SX\mathrm{top} : \mathcal{S} \to X (read the top)
  • isEmpty:S{true,false}\mathrm{isEmpty} : \mathcal{S} \to \{\mathrm{true}, \mathrm{false}\}

These satisfy the following axioms for all SSS \in \mathcal{S} and xXx \in X.

isEmpty(empty)=true,isEmpty(push(S,x))=false,top(push(S,x))=x,pop(push(S,x))=S.\begin{aligned} &\mathrm{isEmpty}(\mathrm{empty}) = \mathrm{true}, \qquad \mathrm{isEmpty}(\mathrm{push}(S, x)) = \mathrm{false}, \\ &\mathrm{top}(\mathrm{push}(S, x)) = x, \qquad \mathrm{pop}(\mathrm{push}(S, x)) = S. \end{aligned}

top(empty)\mathrm{top}(\mathrm{empty}) and pop(empty)\mathrm{pop}(\mathrm{empty}) are undefined (an implementation raises an error).

The axiom pop(push(S,x))=S\mathrm{pop}(\mathrm{push}(S, x)) = S is LIFO itself: push xx and then remove it, and you are back exactly where you started. Nowhere in these four equations do arrays or nodes appear. Whatever the implementation, if it satisfies them it is a stack. There are two implementations, and the cost of each can be read off from results we already have.

Implementation by a dynamic array. Store the elements as A[0],,A[n1]A[0], \ldots, A[n-1] with the top at A[n1]A[n-1]. Then push\mathrm{push} is an append at the end and so is amortized Θ(1)\Theta(1) (Theorem 3.3), while pop\mathrm{pop} is nn1n \leftarrow n-1 and top\mathrm{top} is a read of A[n1]A[n-1], both worst-case Θ(1)\Theta(1) (part 1 of Proposition 3.2). The element moves demanded by part 3 of Proposition 3.2 do not arise, because at the end ni=0n - i = 0.

Implementation by a linked list. Let the top be the first node, with push\mathrm{push} an insertion at the front and pop\mathrm{pop} a deletion of the first node. With a sentinel, both reduce to part 1 of Proposition 4.2 and are worst-case Θ(1)\Theta(1). Since this is Θ(1)\Theta(1) in the worst case rather than amortized, it is preferable where a bound on the latency of a single operation is required. The price is one reference per node of extra space.

Let us look at one use of a stack in a form we can prove: deciding whether brackets match.

Theorem 6.2Recognising balanced bracket strings

Let the alphabet be Σ={(,),[,]}\Sigma = \{\,\texttt{(}\,,\,\texttt{)}\,,\,\texttt{[}\,,\,\texttt{]}\,\} and define the set BΣB \subseteq \Sigma^{*} of balanced bracket strings as the smallest set generated by the following three rules.

εB,wB    (w)B and [w]B,u,vB    uvB.\varepsilon \in B, \qquad w \in B \implies \texttt{(} w \texttt{)} \in B \ \text{and}\ \texttt{[} w \texttt{]} \in B, \qquad u, v \in B \implies uv \in B.

Consider the following algorithm A\mathcal{A} on an input wΣw \in \Sigma^{*}. Start with an empty stack SS and read ww one symbol at a time from the left.

  • If the symbol read is an opening bracket, push\mathrm{push} it onto SS.
  • If it is a closing bracket, reject immediately if SS is empty; otherwise read top\mathrm{top}, pop\mathrm{pop}, and reject if the symbol removed is not the opening bracket of the same kind as this closing bracket.

When all symbols have been read, accept if SS is empty and reject otherwise. Then A\mathcal{A} accepts ww if and only if wBw \in B. Moreover the running time of A\mathcal{A} is Θ(w)\Theta(|w|).

Proof(Theorem 6.2)

Preliminary (relativity of the stack). A\mathcal{A} consults only the top of the stack, and it rejects the moment it tries to pop\mathrm{pop} an empty stack. Hence a run processing uu from an initial stack σ\sigma and a run processing the same uu from the empty stack undergo exactly the same changes above σ\sigma, except in the case where the latter rejects because of a ”pop\mathrm{pop} from empty”. We use this fact repeatedly below.

(\Rightarrow) If wBw \in B then A\mathcal{A} accepts. We prove the following stronger statement by structural induction on the generation rules of BB.

If wBw \in B then, from any initial stack σ\sigma, processing ww causes A\mathcal{A} not to reject, and the stack afterwards is again σ\sigma.

  • Case w=εw = \varepsilon. Nothing is read, so there is no rejection and the stack remains σ\sigma.
  • Case w=(u)w = \texttt{(} u \texttt{)} with uBu \in B. Reading the initial (\texttt{(} makes the stack σ(\sigma\texttt{(}. Applying the induction hypothesis to uu with initial stack σ(\sigma\texttt{(}, there is no rejection and the stack afterwards is again σ(\sigma\texttt{(}. Reading )\texttt{)} next, the stack is non-empty and its top is (\texttt{(}, so the pop\mathrm{pop} matches in kind and there is no rejection. The stack afterwards is σ\sigma. The case w=[u]w = \texttt{[} u \texttt{]} is identical.
  • Case w=uvw = uv with u,vBu, v \in B. Applying the induction hypothesis to uu with initial stack σ\sigma, there is no rejection and the stack returns to σ\sigma. Applying the same hypothesis to vv, again there is no rejection and the stack is σ\sigma.

Taking σ\sigma to be the empty stack in particular, A\mathcal{A} does not reject, and the stack after reading everything is empty, so it accepts.

(\Leftarrow) If A\mathcal{A} accepts then wBw \in B. We argue by strong induction on w|w|. If w=εw = \varepsilon then wBw \in B by the first rule. Suppose w1|w| \ge 1.

If w1w_1 were a closing bracket, the stack would be empty at that moment and A\mathcal{A} would reject, contradicting the hypothesis. So w1w_1 is an opening bracket; write it cc and write cˉ\bar{c} for the matching closing bracket. The symbol cc is pushed at position 1, and since the stack is empty on acceptance, it is popped somewhere. Say this happens when the jj-th symbol is read.

Since cc sits at the bottom of the stack and is removed for the first time at position jj, it remains on the stack throughout the processing of positions 2,,j12, \ldots, j-1. That is, the processing of u=w2wj1u = w_2 \cdots w_{j-1} takes place entirely above cc, and the stack immediately before position jj consists of exactly the one symbol cc. By the preliminary observation, processing uu from an empty stack likewise causes no rejection and ends with an empty stack. Since u<w|u| < |w|, the induction hypothesis gives uBu \in B.

At position jj the symbol cc is popped, and since A\mathcal{A} did not reject, wj=cˉw_j = \bar{c}. The processing of the remainder v=wj+1wwv = w_{j+1} \cdots w_{|w|} starts from an empty stack, and since the whole input is accepted, it ends with an empty stack. As v<w|v| < |w|, we get vBv \in B. Hence w=cucˉvw = c\,u\,\bar{c}\,v, and the second rule gives cucˉBc u \bar{c} \in B while the third gives wBw \in B.

Cost. Each symbol causes at most one push\mathrm{push} or one pop\mathrm{pop} together with at most one top\mathrm{top}, and with the linked-list implementation these are worst-case Θ(1)\Theta(1) (part 1 of Proposition 4.2). Hence the total is Θ(w)\Theta(|w|).

Example 6.3Running the recogniser

We trace A\mathcal{A} on w=([])()w = \texttt{([])()}. The stack is written with its bottom on the left.

symbol readactionstack afterwards
(\texttt{(}push(\texttt{(}
[\texttt{[}push([\texttt{([}
]\texttt{]}pop (matches [\texttt{[})(\texttt{(}
)\texttt{)}pop (matches (\texttt{(})empty
(\texttt{(}push(\texttt{(}
)\texttt{)}pop (matches (\texttt{(})empty

The stack is empty at the end of the input, so the string is accepted. Indeed ([])B\texttt{([])} \in B and ()B\texttt{()} \in B, so the third rule gives wBw \in B. By contrast, for w=(]w' = \texttt{(]} the pop\mathrm{pop} at the second symbol yields (\texttt{(}, a different kind from ]\texttt{]}, so the string is rejected. For w=(()w'' = \texttt{(()} no rejection occurs during the scan, but one (\texttt{(} is left on the stack at the end, so it is rejected. The implementation is as follows.

def is_balanced(w: str) -> bool:
partner = {")": "(", "]": "["}
stack = []
for c in w:
if c in "([":
stack.append(c)
elif c in ")]":
if not stack or stack.pop() != partner[c]:
return False
else:
raise ValueError(f"unexpected symbol: {c!r}")
return not stack
print(is_balanced("([])()")) # True
print(is_balanced("(]")) # False
print(is_balanced("(()")) # False

The final return not stack corresponds to the acceptance condition “the stack is empty at the end”. Forget that one line and "(((" is accepted.

This recogniser has the same shape as depth-first search and as the management of recursive calls. Wherever the structure “close what has been opened in the reverse order of opening” appears, a stack is usually there.

A queue is the type in which what goes in first comes out first (first in, first out, FIFO). It obeys the same discipline as a line at a counter, which is why it is also called a waiting line.

Definition 7.1Queue

A queue is the abstract data type with the following operations. Write XX for the type of values and Q\mathcal{Q} for the set of queues.

  • emptyQ\mathrm{empty} \in \mathcal{Q} (the empty queue)
  • enqueue:Q×XQ\mathrm{enqueue} : \mathcal{Q} \times X \to \mathcal{Q} (add at the back)
  • dequeue:QQ\mathrm{dequeue} : \mathcal{Q} \to \mathcal{Q} (remove the front)
  • front:QX\mathrm{front} : \mathcal{Q} \to X (read the front)
  • isEmpty:Q{true,false}\mathrm{isEmpty} : \mathcal{Q} \to \{\mathrm{true}, \mathrm{false}\}

These satisfy the following axioms for all QQQ \in \mathcal{Q} and xXx \in X.

isEmpty(empty)=true,isEmpty(enqueue(Q,x))=false,front(enqueue(Q,x))={x(Q=empty)front(Q)(Qempty)dequeue(enqueue(Q,x))={empty(Q=empty)enqueue(dequeue(Q),x)(Qempty)\begin{aligned} &\mathrm{isEmpty}(\mathrm{empty}) = \mathrm{true}, \qquad \mathrm{isEmpty}(\mathrm{enqueue}(Q, x)) = \mathrm{false}, \\[2pt] &\mathrm{front}(\mathrm{enqueue}(Q, x)) = \begin{cases} x & (Q = \mathrm{empty}) \\ \mathrm{front}(Q) & (Q \ne \mathrm{empty}) \end{cases} \\[2pt] &\mathrm{dequeue}(\mathrm{enqueue}(Q, x)) = \begin{cases} \mathrm{empty} & (Q = \mathrm{empty}) \\ \mathrm{enqueue}(\mathrm{dequeue}(Q), x) & (Q \ne \mathrm{empty}) \end{cases} \end{aligned}

front(empty)\mathrm{front}(\mathrm{empty}) and dequeue(empty)\mathrm{dequeue}(\mathrm{empty}) are undefined.

Compare this with Definition 6.1. For a stack, pop(push(S,x))=S\mathrm{pop}(\mathrm{push}(S,x)) = S: what was just pushed comes straight back off. For a queue, dequeue\mathrm{dequeue} slips past enqueue\mathrm{enqueue} and burrows further in. This slipping past is what FIFO is, and it is why recursion appears on the right-hand side of the axioms.

The naive array implementation does not work. If the front is always kept at A[0]A[0], then enqueue\mathrm{enqueue} is an append at the end and so is amortized Θ(1)\Theta(1), but dequeue\mathrm{dequeue} is a deletion at position 00, hence Θ(n)\Theta(n) by part 3 of Proposition 3.2, giving Θ(n2)\Theta(n^2) over nn operations. The culprit is pinning the front to A[0]A[0], so unpinning it solves the problem.

Proposition 7.2Ring buffer

Maintain an array A[0..m1]A[0..m-1] of capacity mm, a front position hh (0hm10 \le h \le m-1) and an element count nn (0nm0 \le n \le m), with the operations defined as follows.

  • enqueue(x)\mathrm{enqueue}(x): when n<mn < m, set A[(h+n)modm]xA[(h + n) \bmod m] \leftarrow x and nn+1n \leftarrow n + 1.
  • dequeue()\mathrm{dequeue}(): when n>0n > 0, return A[h]A[h], then set h(h+1)modmh \leftarrow (h + 1) \bmod m and nn1n \leftarrow n - 1.
  • front()\mathrm{front}(): when n>0n > 0, return A[h]A[h].

Then, writing the contents of the queue from the front as q0,q1,,qn1q_0, q_1, \ldots, q_{n-1}, the identity qi=A[(h+i)modm]q_i = A[(h + i) \bmod m] holds at all times, and the axioms of Definition 7.1 are satisfied. Moreover each of the three operations takes worst-case Θ(1)\Theta(1) time.

Proof(Proposition 7.2)

We prove the asserted identity qi=A[(h+i)modm]q_i = A[(h+i) \bmod m] (0in10 \le i \le n-1) as an invariant, by induction on the number of operations.

In the initial state n=0n = 0, so the condition holds vacuously.

Case enqueue(x)\mathrm{enqueue}(x). The new contents are q0,,qn1,xq_0, \ldots, q_{n-1}, x and hh is unchanged. We check that the target position (h+n)modm(h+n) \bmod m does not coincide with an existing position (h+i)modm(h+i) \bmod m (0in10 \le i \le n-1). From 0i<nm10 \le i < n \le m-1 the difference between ii and nn cannot be a multiple of mm, so the two differ modulo mm. Hence no existing element is destroyed, and the new last element satisfies the identity as the element with index nn.

Case dequeue()\mathrm{dequeue}(). The value returned is A[h]=q0A[h] = q_0, the front of the queue. Renumbering the new contents q1,,qn1q_1, \ldots, q_{n-1} as q0,,qn2q'_0, \ldots, q'_{n-2} and setting h=(h+1)modmh' = (h+1) \bmod m, we get

qi=qi+1=A[(h+i+1)modm]=A[(h+i)modm]q'_i = q_{i+1} = A[(h + i + 1) \bmod m] = A[(h' + i) \bmod m]

so the invariant is preserved.

That front()\mathrm{front}() returns q0q_0 is the case i=0i = 0 itself. Hence dequeue\mathrm{dequeue} and front\mathrm{front} always act on the front element while enqueue\mathrm{enqueue} adds only at the back, so the axioms of Definition 7.1 are satisfied. As for the cost, each operation consists of a constant number of additions, remainders and comparisons together with one array access (Θ(1)\Theta(1) by part 1 of Proposition 3.2), hence worst-case Θ(1)\Theta(1).

Example 7.3A ring buffer of capacity 4

Take m=4m = 4 and start from h=0h = 0, n=0n = 0. Unused cells of AA are written _.

operationcell written / readAAhhnn
enqueue(a)\mathrm{enqueue}(a)A[(0+0)mod4]=A[0]A[(0+0) \bmod 4] = A[0]a _ _ _01
enqueue(b)\mathrm{enqueue}(b)A[1]A[1]a b _ _02
enqueue(c)\mathrm{enqueue}(c)A[2]A[2]a b c _03
dequeue()\mathrm{dequeue}()A[0]=aA[0] = aa b c _12
enqueue(d)\mathrm{enqueue}(d)A[(1+2)mod4]=A[3]A[(1+2) \bmod 4] = A[3]a b c d13
enqueue(e)\mathrm{enqueue}(e)A[(1+3)mod4]=A[0]A[(1+3) \bmod 4] = A[0]e b c d14
dequeue()\mathrm{dequeue}()A[1]=bA[1] = be b c d23

In the final state the queue contains c,d,ec, d, e, and as the invariant demands, A[(2+0)mod4]=A[2]=cA[(2+0) \bmod 4] = A[2] = c, A[3]=dA[3] = d and A[(2+2)mod4]=A[0]=eA[(2+2) \bmod 4] = A[0] = e. Writing ee runs off the right end of the array and wraps to the left end — hence the name “ring” — and not a single element has been moved.

Remark 7.4Distinguishing full from empty, and growing the capacity

Some implementations keep only hh and the position past the back, t=(h+n)modmt = (h+n) \bmod m; but then t=ht = h both when n=0n = 0 and when n=mn = m, so full and empty cannot be told apart. There are two remedies: keep the element count nn explicitly, as in Proposition 7.2 above, or always leave one cell free so that the effective capacity is m1m-1.

When the buffer fills up, allocate an array of capacity 2m2m and repack the elements from the front. The cost and frequency of this growth have exactly the same form as in Theorem 3.3, so enqueue\mathrm{enqueue} stays amortized Θ(1)\Theta(1). With a linked-list implementation holding references to both the front and the back, both operations are worst-case Θ(1)\Theta(1) by part 1 of Proposition 4.2.

Stacks drive depth-first search and queues drive breadth-first search. Both are central to Graph algorithms: that breadth-first search computes shortest distances correctly follows from the FIFO discipline of the queue (Theorem 3.2[グラフアルゴリズム]), and in depth-first search the vertices under processing form exactly a stack (Lemma 4.1[グラフアルゴリズム]). Dynamic programming, in turn, uses an array as a table and relies entirely on Θ(1)\Theta(1) indexed access (Theorem 3.2[動的計画法]).

8. The potential method and a queue from two stacks

Section titled “8. The potential method and a queue from two stacks”

In Theorem 3.3 we counted the total cost directly (the aggregate method). When there are several kinds of operation that influence one another, such counting becomes hard. The tool for that situation is the potential method: represent the “accumulated work” stored in the data structure by a real-valued function and watch how it rises and falls with each operation.

Lemma 8.1The potential method

Let a sequence of states D0,D1,,DmD_0, D_1, \ldots, D_m of a data structure be given (D0D_0 the initial state and DiD_i the state just after the ii-th operation), together with the actual cost cic_i of the ii-th operation. Suppose a real-valued function Φ\Phi on the set of states satisfies

Φ(Di)Φ(D0)(i=0,1,,m)\Phi(D_i) \ge \Phi(D_0) \qquad (i = 0, 1, \ldots, m)

Defining the amortized cost by c^i:=ci+Φ(Di)Φ(Di1)\hat{c}_i := c_i + \Phi(D_i) - \Phi(D_{i-1}), we have

i=1mci=i=1mc^i+Φ(D0)Φ(Dm)i=1mc^i\sum_{i=1}^{m} c_i = \sum_{i=1}^{m} \hat{c}_i + \Phi(D_0) - \Phi(D_m) \le \sum_{i=1}^{m} \hat{c}_i

In particular, if some constant aa satisfies c^ia\hat{c}_i \le a for all ii, then the total cost of mm operations is at most amam.

Proof(Lemma 8.1)

By definition,

i=1mc^i=i=1mci+i=1m(Φ(Di)Φ(Di1))\sum_{i=1}^{m} \hat{c}_i = \sum_{i=1}^{m} c_i + \sum_{i=1}^{m}\bigl(\Phi(D_i) - \Phi(D_{i-1})\bigr)

The second sum on the right telescopes: adjacent terms cancel, leaving Φ(Dm)Φ(D0)\Phi(D_m) - \Phi(D_0). Hence

i=1mc^i=i=1mci+Φ(Dm)Φ(D0)\sum_{i=1}^{m} \hat{c}_i = \sum_{i=1}^{m} c_i + \Phi(D_m) - \Phi(D_0)

and rearranging gives the equality. By hypothesis Φ(Dm)Φ(D0)\Phi(D_m) \ge \Phi(D_0), that is Φ(D0)Φ(Dm)0\Phi(D_0) - \Phi(D_m) \le 0, so the inequality follows. The last assertion is immediate from c^iam\sum \hat{c}_i \le am.

Think of Φ\Phi as the debt carried by the current state. Cheap operations run up a little debt at a time, and an expensive operation repays it all at once. At the moment of repayment the actual cost is large, but Φ\Phi drops sharply, so the amortized cost is small.

Theorem 8.2A queue from two stacks

Take two stacks all of whose operations are worst-case Θ(1)\Theta(1) (for instance the linked-list implementation based on part 1 of Proposition 4.2), and call them in\mathrm{in} and out\mathrm{out}. Define the operations as follows.

  • enqueue(x)\mathrm{enqueue}(x): push\mathrm{push} xx onto in\mathrm{in}.
  • dequeue()\mathrm{dequeue}(): if out\mathrm{out} is empty, repeat ”pop\mathrm{pop} from in\mathrm{in} and push\mathrm{push} onto out\mathrm{out}” until in\mathrm{in} is empty. Then, if out\mathrm{out} is empty raise an error; otherwise pop\mathrm{pop} from out\mathrm{out} and return the result.
  • front()\mathrm{front}(): perform the same transfer, then return the top\mathrm{top} of out\mathrm{out}.

Then this implementation satisfies the axioms of Definition 7.1. Moreover the total cost of any sequence of mm operations starting from the empty state is at most 3m3m, so the amortized cost per operation is O(1)O(1). The worst-case cost of an individual dequeue\mathrm{dequeue}, however, is Θ(n)\Theta(n), where nn is the number of elements at that moment.

Proof(Theorem 8.2)

Correctness. Write the contents of the queue from the front as q0,,qn1q_0, \ldots, q_{n-1} and take the following as the invariant.

There exists an rr (0rn0 \le r \le n) such that reading out\mathrm{out} from the top to the bottom gives q0,q1,,qr1q_0, q_1, \ldots, q_{r-1}, and reading in\mathrm{in} from the bottom to the top gives qr,qr+1,,qn1q_r, q_{r+1}, \ldots, q_{n-1}.

Initially n=0n = 0 and both stacks are empty, so the invariant holds with r=0r = 0.

Under enqueue(x)\mathrm{enqueue}(x), reading in\mathrm{in} from the bottom gives qr,,qn1,xq_r, \ldots, q_{n-1}, x, and the new contents of the queue are q0,,qn1,xq_0, \ldots, q_{n-1}, x, so the invariant is preserved with the same rr.

A transfer occurs when out\mathrm{out} is empty, that is when r=0r = 0, and then reading in\mathrm{in} from the bottom gives q0,,qn1q_0, \ldots, q_{n-1}. The order in which elements are popped from in\mathrm{in} is qn1,,q0q_{n-1}, \ldots, q_0, and pushing them onto out\mathrm{out} in that order makes out\mathrm{out} read qn1,,q0q_{n-1}, \ldots, q_0 from the bottom, that is q0,,qn1q_0, \ldots, q_{n-1} from the top. Since in\mathrm{in} is now empty, the invariant holds with r=nr = n. The order is reversed twice and thereby restored: that is the trick of this implementation.

When out\mathrm{out} is non-empty (r1r \ge 1), its top is q0q_0 by the invariant, that is the front of the queue. Hence front\mathrm{front} returns q0q_0 and dequeue\mathrm{dequeue} removes q0q_0. After the removal, out\mathrm{out} holds q1,,qr1q_1, \ldots, q_{r-1} from the top, so after renumbering the invariant is preserved with r=r1r' = r - 1. When n=0n = 0 both stacks are empty, and out\mathrm{out} remains empty even after a transfer, so an error is raised — consistent with Definition 7.1 leaving dequeue(empty)\mathrm{dequeue}(\mathrm{empty}) undefined.

Amortized cost. Define the potential by

Φ:=2(number of elements in in)\Phi := 2 \cdot (\text{number of elements in } \mathrm{in})

Then Φ0\Phi \ge 0 and Φ(D0)=0\Phi(D_0) = 0 in the initial state, so the hypothesis Φ(Di)Φ(D0)\Phi(D_i) \ge \Phi(D_0) of Lemma 8.1 is met. We count one stack operation as cost 1 (by assumption it takes worst-case Θ(1)\Theta(1) time).

  • enqueue\mathrm{enqueue}: the actual cost is one push\mathrm{push}, so c=1c = 1. The number of elements in in\mathrm{in} grows by one, so ΔΦ=2\Delta\Phi = 2. Hence c^=1+2=3\hat{c} = 1 + 2 = 3.
  • dequeue\mathrm{dequeue} with out\mathrm{out} non-empty: the actual cost is one pop\mathrm{pop}, so c=1c = 1. Nothing changes in in\mathrm{in}, so ΔΦ=0\Delta\Phi = 0. Hence c^=1\hat{c} = 1.
  • dequeue\mathrm{dequeue} with out\mathrm{out} empty and k1k \ge 1 elements in in\mathrm{in}: the transfer performs kk pops and kk pushes, and the subsequent pop is one more, so c=2k+1c = 2k + 1. The number of elements in in\mathrm{in} falls from kk to 00, so ΔΦ=2k\Delta\Phi = -2k. Hence c^=2k+12k=1\hat{c} = 2k + 1 - 2k = 1.
  • front\mathrm{front}: it only reads top\mathrm{top} instead of popping, so the same computations as in the previous two cases give c^=1\hat{c} = 1.

In every case c^3\hat{c} \le 3. By Lemma 8.1, the total cost of mm operations is at most 3m3m and the amortized cost per operation is O(1)O(1).

Worst case. A dequeue\mathrm{dequeue} in the state where all nn elements are on in\mathrm{in} and out\mathrm{out} is empty performs 2n+12n + 1 stack operations, hence Θ(n)\Theta(n). This does not contradict the amortized bound of O(1)O(1), because before such an expensive dequeue\mathrm{dequeue} can occur, nn enqueues are needed to pile up the nn elements.

flowchart LR
E["enqueue x pushes onto in"] --> I["stack in (from the bottom: c, d, e)"]
I -->|"when out is empty, move until in is empty"| O["stack out (from the top: c, d, e)"]
O --> D["dequeue pops the top of out"]
A queue built from two stacks. Moving from in to out reverses the order, bringing the oldest element to the top of out.

Example 8.3Running the two stacks

Here is an implementation together with a trace following the changes in Φ\Phi.

class TwoStackQueue:
def __init__(self):
self._in = [] # the last entry is the top
self._out = [] # the last entry is the top
def enqueue(self, x):
self._in.append(x)
def _transfer(self):
while self._in:
self._out.append(self._in.pop())
def dequeue(self):
if not self._out:
self._transfer()
if not self._out:
raise IndexError("dequeue from empty queue")
return self._out.pop()
def front(self):
if not self._out:
self._transfer()
if not self._out:
raise IndexError("front of empty queue")
return self._out[-1]
def __len__(self):
return len(self._in) + len(self._out)
q = TwoStackQueue()
for x in "abc":
q.enqueue(x)
print(q.dequeue(), q.dequeue()) # a b
q.enqueue("d")
print(q.dequeue(), q.dequeue()) # c d

The actual cost cc, the potential Φ=2in\Phi = 2\,|\mathrm{in}| and the amortized cost c^=c+ΔΦ\hat{c} = c + \Delta\Phi of each operation come out as follows.

operationin\mathrm{in} afterwards (from the bottom)out\mathrm{out} afterwards (from the top)ccΦ\Phic^\hat{c}
initial state0
enqueue(a)\mathrm{enqueue}(a)aa123
enqueue(b)\mathrm{enqueue}(b)a,ba, b143
enqueue(c)\mathrm{enqueue}(c)a,b,ca, b, c163
dequeue()=a\mathrm{dequeue}() = ab,cb, c701
dequeue()=b\mathrm{dequeue}() = bcc101
enqueue(d)\mathrm{enqueue}(d)ddcc123
dequeue()=c\mathrm{dequeue}() = cdd121
dequeue()=d\mathrm{dequeue}() = d301

The dequeue\mathrm{dequeue} on the fourth row stands out with an actual cost of 77 (three pops, three pushes, one pop), but Φ\Phi falls from 66 to 00, so its amortized cost is 11. The total actual cost is 1616 over 88 operations, below the bound 3×8=243 \times 8 = 24 of Lemma 8.1.

Exercise 9.1Easy

Using only the axioms of Definition 7.1, derive the following equation, stating explicitly which axiom is used at each step.

dequeue(enqueue(enqueue(empty,a),b))=enqueue(empty,b)\mathrm{dequeue}\bigl(\mathrm{enqueue}(\mathrm{enqueue}(\mathrm{empty}, a), b)\bigr) = \mathrm{enqueue}(\mathrm{empty}, b)

Then use this result together with the axioms for front\mathrm{front} to confirm that removing one element from the queue built by inserting aa and then bb leaves bb.

Solution

Put Q:=enqueue(empty,a)Q := \mathrm{enqueue}(\mathrm{empty}, a). Applying the axiom isEmpty(enqueue(Q,x))=false\mathrm{isEmpty}(\mathrm{enqueue}(Q', x)) = \mathrm{false} with Q=emptyQ' = \mathrm{empty} and x=ax = a gives QemptyQ \ne \mathrm{empty}. Hence the second case of the dequeue\mathrm{dequeue} axiom applies:

dequeue(enqueue(Q,b))=enqueue(dequeue(Q),b)\mathrm{dequeue}(\mathrm{enqueue}(Q, b)) = \mathrm{enqueue}(\mathrm{dequeue}(Q), b)

Next, applying the first case of the same axiom (the inner argument being empty\mathrm{empty}) to dequeue(Q)=dequeue(enqueue(empty,a))\mathrm{dequeue}(Q) = \mathrm{dequeue}(\mathrm{enqueue}(\mathrm{empty}, a)) gives dequeue(Q)=empty\mathrm{dequeue}(Q) = \mathrm{empty}. Substituting,

dequeue(enqueue(Q,b))=enqueue(empty,b)\mathrm{dequeue}(\mathrm{enqueue}(Q, b)) = \mathrm{enqueue}(\mathrm{empty}, b)

as required.

Furthermore, the first case of the front\mathrm{front} axiom gives front(enqueue(empty,b))=b\mathrm{front}(\mathrm{enqueue}(\mathrm{empty}, b)) = b. The element aa, inserted first, left first: this is indeed FIFO. Carrying out the same computation with the axioms of Definition 6.1 gives pop(push(push(empty,a),b))=push(empty,a)\mathrm{pop}(\mathrm{push}(\mathrm{push}(\mathrm{empty}, a), b)) = \mathrm{push}(\mathrm{empty}, a), leaving aa. The difference in the axioms is exactly the difference in behaviour.

Exercise 9.2Standard

Write a procedure reversing a singly linked list in Θ(n)\Theta(n) time and O(1)O(1) additional space. Create no new nodes; only rewrite the next\mathrm{next} fields of the existing ones. State a loop invariant and use it to prove correctness.

Solution

Three variables suffice.

def reverse(head):
prev = None
cur = head
while cur is not None:
nxt = cur.next # save it first, or we lose our way the moment next is overwritten
cur.next = prev # flip the direction
prev = cur
cur = nxt
return prev

Invariant. Write the original list as p0,p1,,pn1p_0, p_1, \ldots, p_{n-1}. At the start of each iteration the following hold for some kk (0kn0 \le k \le n).

  • prev is the head of the list consisting of pk1,pk2,,p0p_{k-1}, p_{k-2}, \ldots, p_0 in that order (nil\mathrm{nil} when k=0k = 0).
  • cur is pkp_k (nil\mathrm{nil} when k=nk = n).
  • The two lists are disjoint and together comprise all the original nodes.

Initialisation. Just before entering the loop, prev = None and cur = head =p0= p_0, so the invariant holds with k=0k = 0.

Maintenance. Suppose the invariant holds with kk at the start of an iteration and cur =pknil= p_k \ne \mathrm{nil}. We save pk+1p_{k+1} (or nil\mathrm{nil} if it does not exist) in nxt. The assignment cur.next = prev makes pk1p_{k-1} follow pkp_k, so the list starting at prev becomes pk,pk1,,p0p_k, p_{k-1}, \ldots, p_0. The only thing lost by this assignment is the reference from pkp_k to pk+1p_{k+1}, and that is safely stored in nxt. Finally prev = cur and cur = nxt make the invariant hold for k+1k+1.

Termination. The loop ends when cur is nil\mathrm{nil}, that is when k=nk = n. By the invariant, prev is the head of the list pn1,,p0p_{n-1}, \ldots, p_0, which is the reversal of the original list.

Cost. The loop runs exactly once per node, nn times in total, and one pass through the body consists of four assignments, hence constant time (for the same reason as part 1 of Proposition 4.2). So the time is Θ(n)\Theta(n). The extra memory is three variables, hence O(1)O(1).

Note that omitting the save into nxt destroys any means of reaching pk+1p_{k+1} after cur.next = prev, because, as the proof of part 2 of Proposition 4.2 shows, the address of a node can only be obtained by following references.

Exercise 9.3Hard

Add to the dynamic array of Theorem 3.3 an element removal pop\mathrm{pop} together with a shrinking rule for returning memory.

  1. With the rule “after a pop\mathrm{pop}, halve the capacity as soon as the element count drops to at most half the capacity”, amortized O(1)O(1) fails. Construct an explicit sequence of operations for which mm operations cost Θ(m2)\Theta(m^2) in total.
  2. With the rule “halve the capacity as soon as the element count falls below one quarter of the capacity”, the amortized costs of push\mathrm{push} and pop\mathrm{pop} stay constant. Estimate how many operations must occur between two consecutive capacity changes, and explain why.
Solution

1. Start from capacity c=m0c = m_0 with n=m0n = m_0 elements (full) and alternate push\mathrm{push} and pop\mathrm{pop}.

  • push\mathrm{push}: the array is full, so the capacity grows to 2m02m_0 and m0m_0 elements are copied. Afterwards n=m0+1n = m_0 + 1, c=2m0c = 2m_0.
  • pop\mathrm{pop}: afterwards n=m0n = m_0, and since m02m0/2=m0m_0 \le 2m_0/2 = m_0, the shrinking rule fires, the capacity returns to m0m_0, and m0m_0 elements are copied.

This restores the initial state exactly, so the same thing repeats forever. Every two operations cause 2m02m_0 copies, so the total cost is Θ(mm0)\Theta(m \cdot m_0), which is Θ(m2)\Theta(m^2) if we take m0=Θ(m)m_0 = \Theta(m). The cause is that the threshold for growing and the threshold for shrinking sit at the same point.

2. Separating the thresholds means that immediately after a capacity change we land far from either boundary. Look at the state just after a change.

  • Just after growing: capacity cc, element count n=c/2n = c/2.
  • Just after shrinking: the capacity before was 2c2c, and the count had just fallen below 2c/4=c/22c/4 = c/2, so n=c/2n = c/2 (up to an error of one). The capacity is cc.

In both cases nc/2n \approx c/2. For the next growth, nn must reach cc, requiring at least c/2c/2 pushes; for the next shrink, nn must fall below c/4c/4, requiring at least c/4c/4 pops. Hence at least c/4c/4 operations lie between two consecutive capacity changes.

A capacity change at capacity cc costs Θ(c)\Theta(c), and spreading this over the at least c/4c/4 preceding operations gives a constant per operation. Formally, taking as the potential of Lemma 8.1

Φ={2nc(nc/2)c/2n(n<c/2)\Phi = \begin{cases} 2n - c & (n \ge c/2) \\ c/2 - n & (n < c/2) \end{cases}

one checks by cases on whether a growth or a shrink occurs that the amortized cost is bounded by a constant. The point is that both expressions vanish just after a capacity change (n=c/2n = c/2) and grow as either boundary is approached.

  • T. H. Cormen, C. E. Leiserson, R. L. Rivest, C. Stein, Introduction to Algorithms, 4th ed., MIT Press, 2022 — Chapter 10 (Elementary Data Structures) covers arrays, linked lists, stacks and queues, and Chapter 16 (Amortized Analysis) covers the aggregate, accounting and potential methods. The analysis of the dynamic array (table doubling) is also in Chapter 16.
  • D. E. Knuth, The Art of Computer Programming, Volume 1: Fundamental Algorithms, 3rd ed., Addison-Wesley, 1997 — §2.2 (Linear Lists). §2.2.1 treats stacks, queues and deques, §2.2.2 sequential allocation (arrays) and §2.2.3 linked allocation (linked lists), corresponding to §3 through §7 of this article. The historical background of linked lists is described in §2.6.
  • A. V. Aho, J. E. Hopcroft, J. D. Ullman, Data Structures and Algorithms, Addison-Wesley, 1983 — Chapter 2 (Basic Abstract Data Types). The organisation of this article, fixing the abstract data type first and supplying implementations afterwards, follows the style of this book.
  • R. E. Tarjan, “Amortized Computational Complexity”, SIAM Journal on Algebraic and Discrete Methods 6 (1985), 306–318. DOI: 10.1137/0606031 — the paper that systematically formulated amortized complexity and the potential method.
  • R. Sedgewick, K. Wayne, Algorithms, 4th ed., Addison-Wesley, 2011 — §1.3 (Bags, Queues, and Stacks). Implements stacks and queues both with resizing arrays and with linked lists, and compares measured running times.
  • Ishihata Kiyoshi, Algorithm to Data Kōzō (Algorithms and Data Structures), Iwanami Shoten (Iwanami Lectures on Software Science 3), 1989 (in Japanese) — a well-regarded textbook available in Japanese, treating the various representations of linear lists in detail.

Appendix: Correspondence with the standard libraries of major languages

Section titled “Appendix: Correspondence with the standard libraries of major languages”

The data structures treated in this article are present as they stand in the standard libraries of the major languages. Since the names do not always reveal the implementation, we list the correspondences.

Dynamic arrays and linked lists. C++‘s std::vector, Java’s ArrayList and Python’s list are dynamic arrays. The growth factor differs by implementation: Java’s ArrayList uses roughly 1.51.5, and CPython’s list roughly 1.1251.125 (the capacity grows as 0,4,8,16,25,35,46,58,72,88,0, 4, 8, 16, 25, 35, 46, 58, 72, 88, \ldots); but as noted in Remark 3.5, any constant greater than 11 preserves amortized O(1)O(1). Linked lists appear as C++‘s std::list (doubly linked) and std::forward_list (singly linked), and Java’s LinkedList (doubly linked). Python’s standard library has no pure linked-list type because occasions calling for one are rare, and the circumstances of Remark 5.1 lie behind that too.

Stacks and queues. C++‘s std::stack and std::queue are adapters exposing only the interfaces of Definition 6.1 and Definition 7.1 over an existing container (std::deque by default), so the separation of abstract data type from implementation appears directly in the types. Java’s ArrayDeque is a ring buffer (Proposition 7.2) usable both as a stack and as a queue. In Python one uses list (with append and pop) for a stack and collections.deque (a doubly linked list of fixed-size blocks, with worst-case O(1)O(1) addition and removal at both ends) for a queue. Using list as a queue via pop(0) costs Θ(n)\Theta(n) per operation by part 3 of Proposition 3.2, hence Θ(n2)\Theta(n^2) over nn operations — a frequent cause of slowdowns in practice.

Report an error in this article ・Operated by: Mugen Giken LLCPricingTermsLegal notice

© 2026 夢現技研合同会社 ・Feeding the text to an LLM is welcome. Code samples are MIT licensed.