Chapter 4 · Lectures

Lists, stacks, queues, deques

4.1 The List ADT

Chapter 3 surveyed the seven fundamental data structures and the nine common abstract data types, then spent most of its energy on searching and sorting. Chapter 4 starts the depth-first tour: each ADT gets its own chapter where we define the contract, implement it, and analyze the operations. First up: the list.

The list is the workhorse ADT. Nearly every other sequence-based structure (stack, queue, deque) is a list with additional constraints. Understanding what a list’s contract actually is — and what it deliberately leaves unspecified — is the first step to picking the right structure later.

The contract: operations

::: center Operation What it does Example (start: 99, 77)


Append(L, x) Insert xx at the end Append(L, 44) \Rightarrow 99, 77, 44 Prepend(L, x) Insert xx at the front Prepend(L, 44) \Rightarrow 44, 99, 77 InsertAfter(L, w, x) Insert xx immediately after ww InsertAfter(L, 99, 44) \Rightarrow 99, 44, 77 Remove(L, x) Remove first occurrence of xx Remove(L, 77) \Rightarrow 99 Search(L, x) Return item (or null) Search(L, 99) \Rightarrow 99; Search(L, 22) \Rightarrow null Print(L) Print items in order “99, 77” PrintReverse(L) Print items in reverse order “77, 99” Sort(L) Sort items ascending 77, 99 IsEmpty(L) True iff list has no items false for (99, 77) GetLength(L) Number of items 2 :::

What the contract doesn’t say

List is the root ADT; stacks and queues restrict it

C++: std::list, std::vector, std::forward_list

C++ gives you several list-like containers. All three satisfy the list ADT; they differ in which operations are cheap and what extra features they offer.

::: center Operation std::vector std::list std::forward_list


Append at end O(1)O(1) amortized O(1)O(1) no direct op Prepend at front O(n)O(n) O(1)O(1) O(1)O(1) Insert after a known position O(n)O(n) O(1)O(1) O(1)O(1) Indexed access v[i] O(1)O(1) O(i)O(i) O(i)O(i) Search by value O(n)O(n) O(n)O(n) O(n)O(n) size() O(1)O(1) O(1)O(1) not provided :::

What this chapter builds

4.2 Singly Linked Lists: Append and Prepend

This is the first concrete implementation of the list ADT: the singly linked list. Instead of storing items in a contiguous block of memory (like an array), a linked list scatters them across individually-allocated nodes that “point” to each other. The payoff is cheap insertion and deletion at known positions; the price is slower traversal and higher per-item memory overhead.

The node type in C++

Append: add at the end

Two cases: an empty list needs its head and tail both pointed at the new node; a non-empty list only has to reach through the existing tail.

Prepend: add at the front

Symmetric to append, but we touch head instead of tail.

Cost summary

::: center Operation Singly linked list Why


append O(1)O(1) with tail pointer Two writes; no traversal prepend O(1)O(1) Two writes; no traversal :::

Both basic insertions are constant time. That’s the primary advantage of a linked list over an array for workloads that repeatedly add at the ends.

Language conventions for “null”

What’s coming next

4.3 Singly Linked Lists: Insert-After

Append and prepend covered the two endpoints. Insert-after is the general case: given a pointer to some existing node curNode, splice a newNode in right after it. The operation is still O(1)O(1) — because we already have a pointer to the splice point — but the bookkeeping has to handle three cases instead of two.

The three cases

The pseudocode looks like it’s handling three unrelated scenarios, but they all flow from the same question: which of the list’s invariants does this insertion threaten?

::: center Case What needs updating


Empty list Both head and tail curNode is the tail Tail’s next pointer, and tail itself curNode is interior Just two next pointers (no head/tail changes) :::

The algorithm

Picture of the interior splice

::: center Before:
... \to[curNode |\bullet] \to[nextNode | ...]\to ...
Step 1: newNode\tonext = curNode\tonext
... \to[curNode |\bullet] \to[nextNode | ...]\to ...
 \nwarrow
[newNode | \bullet]
Step 2: curNode\tonext = newNode
... \to[curNode |\bullet] \to[newNode |\bullet] \to[nextNode | ...]\to ... :::

Two pointer writes, no scan. Constant time.

Why we need the curNode argument

Mapping to std::list

Cost

::: center Operation Singly linked list Why


insertAfter(curNode, …) O(1)O(1) Two pointer writes; no scan Find curNode by value or index O(n)O(n) Walk from head Insert by index kk O(k)+O(1)=O(n)O(k) + O(1) = O(n) Walk first, then splice :::

4.4 Singly Linked Lists: Remove

Removal in a singly linked list has the same shape of problem as insertion: you need to update a next pointer that lives in the predecessor of the node you want to delete — and a singly linked list doesn’t make that predecessor easy to reach. The workaround is the same one used by insertAfter: name the operation RemoveAfter, and make the caller pass the predecessor pointer.

Why predecessor-based removal?

The three cases

Two variables carry the state of the splice:

  • curNode — the node before the one being removed (or null if we’re removing the head).

  • sucNode — the node after the one being removed (i.e., the new successor that the splice will connect to).

::: center Case What gets updated


Remove the head (curNode null) head moves to successor; if that’s null, also null out tail. Remove an interior node curNode\tonext moves to successor. No endpoint changes. Remove the tail (curNode\tonext\tonext null) curNode\tonext becomes null; also move tail to curNode. :::

The algorithm

Picture of the interior splice

::: center Before:
... \to[curNode |\bullet] \to[victim |\bullet] \to[sucNode | ...]\to ...
After curNode\tonext = sucNode:
... \to[curNode |\bullet] \to[sucNode | ...]\to ...
The victim node is unreachable — delete it. :::

Cost

::: center Operation Singly linked list Why


removeAfter(curNode) O(1)O(1) Two pointer writes; no scan Remove by value (find + remove) O(n)O(n) Linear walk to find the predecessor Remove at index kk O(k)=O(n)O(k) = O(n) Walk to position, then splice :::

Why we don’t have Remove(node) directly

All five SLL operations together

Now that we have append / prepend / insert-after / remove-after, we can fill in the canonical cost table for a singly linked list with both head and tail pointers:

::: center Operation Cost Assumption


append(x) O(1)O(1) Tail pointer cached prepend(x) O(1)O(1)insertAfter(p, x) O(1)O(1) Caller has pointer p removeAfter(p) O(1)O(1) Caller has pointer p search(x) O(n)O(n) Linear scan at(i) O(i)O(i) Walk from head remove(x) by value O(n)O(n) Search + splice :::

4.5 Linked List Search

Search on a linked list is exactly what you’d expect: start at the head, walk node-by-node following next pointers, stop when you find a match or hit null. This is linear search from Chapter 3, applied to a linked-list structure. It’s worth its own section because linked-list search exposes a recurring pattern — the walker idiom — that will be the scaffolding for traversal, reversal, length computation, and many other linked-list algorithms.

The algorithm

Why return a pointer instead of an index

Cost

::: center Case Comparisons Runtime


Key at head 1 O(1)O(1) Key at position kk k+1k + 1 O(k)O(k) Key absent nn O(n)O(n) :::

Linked-list search is hostile to CPU caches

Two common variations

What search gives you

4.6 Doubly Linked Lists

The pain of a singly linked list lives at the prev pointer it doesn’t have: no cheap reverse traversal, no removal without a predecessor argument, no insertion “before” a known node. The doubly linked list spends one extra pointer per node to erase all of these problems. You get bidirectional traversal, O(1)O(1) removal given the victim itself, and a cleaner (if wordier) splice operation.

The node type in C++

Invariants

Append

Prepend

Symmetric to append. Every tail becomes head; every next swaps with prev.

What the back-pointer unlocks

When the extra pointer is not worth it

Cost of the endpoint operations

::: center Operation SLL DLL Notes


append O(1)O(1) O(1)O(1) One extra write on DLL prepend O(1)O(1) O(1)O(1) One extra write on DLL insertAfter(n) O(1)O(1) O(1)O(1) DLL also has insertBefore remove(n) needs prev, so O(n)O(n) to find it, then O(1)O(1) O(1)O(1) given node DLL wins outright :::

std::list is a circular DLL

Where this is going

4.7 Doubly Linked Lists: Insert-After

Insert-after on a doubly linked list is the same algorithm as the SLL version from 4.3, with one extra pointer write per affected side to keep the prev links in sync. The interior case — where all four invariants meet — requires four pointer updates instead of the SLL’s two. Each of the four has to happen in the right order, and every pattern you pick here will be the template for the rest of the DLL operations.

The three cases

::: center Case Pointer writes Structures affected


Empty list 2 head, tail curNode is tail 3 tail’s next, new node’s prev, tail Interior 4 curNode’s next, sucNode’s prev, new node’s next and prev :::

The algorithm

Case C, step by step

The interior case is four pointer writes. The order is flexible, but the intent must stay consistent: wire the new node first, then redirect the existing ones. Writing the old pointers first while the new node is still dangling breaks invariants mid-flight and can get you into trouble on multi-threaded or signal-interrupted code. Safe ordering:

::: center Step Write


1 newNode\tonext = curNode\tonext (remember the successor first) 2 newNode\toprev = curNode (wire the predecessor) 3 curNode\tonext = newNode (splice forward) 4 sucNode\toprev = newNode (splice backward) :::

Picture of the interior splice

::: center Before:
... \leftrightarrow[curNode]\leftrightarrow[sucNode]\leftrightarrow ...
After:
... \leftrightarrow[curNode]\leftrightarrow[newNode]\leftrightarrow[sucNode]\leftrightarrow ... :::

Four arrows redirected. Nothing else changes — the head, the tail, and any nodes further away are untouched.

Why inserting before is now trivial

Cost

::: center Operation Cost Assumption


insertAfter(curNode, x) O(1)O(1) Caller has curNode insertBefore(curNode, x) O(1)O(1) Caller has curNode insert at index kk O(k)O(k) Walk from head, then splice :::

In the STL

4.8 Doubly Linked Lists: Remove

This is where the extra prev pointer pays its rent. On a DLL, removing a node takes O(1)O(1) given nothing but the node itself — no predecessor argument, no backward scan. The SLL couldn’t do this. The algorithm is also structurally cleaner: instead of three scenario branches, we use four independent checks that each handle one of the pointers that might need updating.

Four independent checks instead of three cases

The SLL’s RemoveAfter had three cases. The DLL’s Remove is organized differently: four independent “if this pointer exists, patch it” checks. Any combination of them fires depending on where the victim sits.

::: center Condition Action


sucNode \neq null sucNode\toprev = predNode predNode \neq null predNode\tonext = sucNode curNode == list.head list.head = sucNode curNode == list.tail list.tail = predNode :::

The algorithm

Picture

::: center Before, for an interior removal:
... \leftrightarrow[predNode]\leftrightarrow[curNode]\leftrightarrow[sucNode]\leftrightarrow ...
After:
... \leftrightarrow[predNode]\leftrightarrow[sucNode]\leftrightarrow ... :::

The curNode is short-circuited by updating exactly two pointers: predNode\tonext and sucNode\toprev. The victim is then unreachable from the list and can be safely freed.

The big comparison to SLL

This is the place where the SLL/DLL trade-off lands most clearly.

::: center Operation SLL DLL


Remove, given node pointer O(n)O(n) (scan for prev) or use awkward RemoveAfter(prev) O(1)O(1) from the node Remove by value O(n)O(n) (walk to find) O(n)O(n) (walk to find) + O(1)O(1) unlink Remove during a walk O(1)O(1) with trailing prev O(1)O(1) natively Splice a node between two existing lists painful; need prev in both places trivial with the node’s prev/next pointers :::

Cost

::: center Operation Cost Assumption


remove(node) O(1)O(1) Caller has a pointer to the node Remove by value O(n)O(n) Linear search to find, then O(1)O(1) unlink Remove at index kk O(k)O(k) Walk to position, then O(1)O(1) unlink Remove all of value xx O(n)O(n) Single walk; remove matches in place :::

Handling ownership in C++

Where this leaves the list ADT

4.9 Linked List Traversal

Traversal is the loop that ties linked lists back to every other container: “do something to each element.” It is also the cheapest linked list operation to get right, because a linked list has exactly one shape of walk through it --- curNode = curNode->next, repeated until the pointer falls off the end.

The walker idiom, one more time

You have written this loop (or a close cousin) in every earlier section. It deserves its own name because it is the single building block under search, insert-after-value, remove-by-value, size, clear, copy, and almost any other “go look at everything” operation.

Three things make this loop airtight. First, the starting state is the list handle (head), not a guess --- if the list is empty, head is nullptr and the loop body never runs. Second, the termination condition is cur != nullptr, which works for length 0, length 1, and length n with no special cases. Third, the advance step is a single field read; next is either a valid node or the sentinel nullptr that the list’s append/prepend/remove code is contractually required to maintain.

Doubly-linked lists: reverse traversal for free

Because every node in a DLL carries a prev pointer and the list keeps a tail handle, you can walk the list backwards at the same cost as forwards. This is one of the concrete payoffs for the extra pointer per node discussed in section 4.6.

A singly-linked list has no efficient reverse traversal. The only way to visit the nodes in reverse is to either reverse the list first (O(n) destructive), push every node onto a stack during a forward pass (O(n) space), or use recursion (which is push-onto- a-stack by another name, using the call stack). All three cost Theta(n) extra work or space on top of the walk itself. The DLL does it in zero extra work because the backward links were already built.

Cost

::: center Operation Cost Notes


Forward traversal (SLL or DLL) Θ(n)\Theta(n) Must visit each node Reverse traversal (DLL) Θ(n)\Theta(n) Symmetric to forward Reverse traversal (SLL) Θ(n)\Theta(n) Plus Θ(n)\Theta(n) space :::

The big-Theta is non-negotiable: to touch every element you pay n operations. The interesting cost is per-step, which is O(1) for both SLL and DLL because each step is one pointer indirection. This is what makes the linked list traversal the same asymptotic cost as an array’s for (int i = 0; i < n; ++i) even though the underlying machine work is quite different.

Traversal as the hidden primitive

Once you have the walker loop, almost every other “touch everything” operation is a specialization of it.

::: center Operation What the walker body does


size() Increment a counter print() Print cur->data search(x) Compare, return if found sum() Accumulate cur->data clear() delete prev, advance copy() Append cur->data to new list contains(x) Compare, return bool :::

The reason linked list code looks repetitive across the chapter is that it is repetitive: every one of these operations is the same loop with a different body. If you find yourself writing the traversal scaffold for the fifth time in one class, that is the signal to factor out a higher-order helper that takes a callable --- which is exactly what the STL does in std::for_each, std::find_if, and std::accumulate.

std::list exposes iterators (begin(), end(), rbegin(), rend()) that are the library’s name for “the current walker.” Advancing an iterator with ++it is exactly the cur = cur->next step; the iterator just hides the pointer. Once you understand the raw loop, the STL version stops looking magical.

4.10 Sorting Linked Lists

Sorting is the first algorithm in this chapter that isn’t just a walker variant. It’s also where the cost difference between arrays and linked lists stops being theoretical --- many classic array sorts straight up don’t work on a linked list, and the ones that do have a different implementation on each list flavor. Insertion sort was first analyzed on arrays in ; here we adapt it to a linked-list traversal.

Insertion sort on a DLL: the easy case

Insertion sort on a doubly-linked list is almost identical to the array version. The outer loop visits each node starting from the second; the inner loop walks prev pointers back through the sorted prefix until it finds the right slot, then splices the current node there.

The cache-nxt-before-mutating pattern is the same one you saw in remove-during-traversal (section 4.9). You can’t advance via cur->next after L.remove(cur) splices cur out --- its next may now point somewhere else, or may be stale.

Cost

::: center Variant Best Average/Worst Why


DLL insertion sort O(n)O(n) O(n2)O(n^2) Already-sorted skips inner loop SLL insertion sort O(n)O(n) O(n2)O(n^2) Descending list = trivial :::

The best-case O(n)O(n) trigger differs between the two: a sorted DLL never enters the inner loop (the prev data is already smaller), while a reverse-sorted SLL inserts every element at the head --- findInsertPos returns nullptr immediately, which is also a no-op.

Which array sorts survive the linked-list trip?

::: center Algorithm Port? Reason


Insertion sort Yes Only needs neighbor access Merge sort Yes Splitting & merging is pointer surgery Selection sort Yes (ish) Needs nn linear “find min” passes Shell sort No Gap-jumps require indexed access Quicksort Poor Partition needs both-ends access Heap sort No Heap needs parent/child by index :::

4.11 Sentinel (Dummy) Nodes

Every removal and insertion algorithm you’ve written so far has a prologue: a cluster of if statements for the empty list, the head, and the tail. Sentinel nodes --- also called “dummy” nodes --- are a structural trick that makes those special cases disappear. You pay one pointer of overhead and a pinch of conceptual weirdness; in return, every other method gets shorter.

Why the head-and-tail cases exist

Take a close look at ListRemove from section 4.8: the very first lines check if (curNode == list->head) and if (curNode == list->tail). Those branches exist because head has no predecessor and tail has no successor. Most of the body is just “update the list handle instead of an in-list pointer.”

A sentinel makes head-has-no-predecessor false. Every real node now has a valid prev pointing to either another real node or the sentinel. The removal code never has to ask “am I at the head?” because it always has a predecessor to splice around.

One dummy vs. two dummies

::: center Variant Simplifies Still special-cases


No sentinel --- head, tail, empty One head sentinel head/empty tail Two sentinels everything none (for interior ops) :::

The two-sentinel DLL is the cleanest: ListRemove is just “unsplice,” with no branches at all, because both neighbors are guaranteed to exist.

Compare that with the no-sentinel version’s four pointer checks (section 4.8). Both are O(1)O(1) --- but the branchless version is faster in practice (the CPU loves straight-line code), shorter, and almost impossible to get wrong.

What sentinels cost

::: center Cost Amount Notes


Extra memory 1 or 2 nodes Constant, regardless of list size Extra complexity “size” is tracked separately size() != number of nodes Extra care iterators must skip sentinels Usually hidden in begin()/end() :::

The “size is tracked separately” point matters for correctness. A sentinel-based list must either keep a size counter or define “size” as “number of nodes minus the sentinels.” Walking the whole list and counting no longer gives the right answer for a caller who expects 0 on an empty list.

Do real C++ lists use sentinels?

Yes. std::list in libstdc++ and libc++ is a doubly-linked circular list with a single sentinel. The list object itself contains the sentinel inline (no heap allocation for an empty list), and end() returns an iterator to that sentinel. That’s why dereferencing end() is undefined behavior but comparing against it is fine: the sentinel exists, it just doesn’t hold a user value.

The payoff: std::list::splice

The circular-sentinel layout enables one operation that no contiguous container can match: moving an entire range of nodes between two lists in O(1)O(1), regardless of range size. The STL exposes this as std::list::splice.

The cost is O(1)O(1) because splice only rewires four pointers --- the next/prev of the boundary nodes on each side. The range itself isn’t touched. std::vector cannot do this without copying or moving every element in the range, an inherent O(k)O(k) cost. This is the canonical answer to “why would I ever use std::list when vector usually wins?” If your workload splices large ranges between lists, the linked structure pays for its cache-hostility and per-node allocation overhead. Otherwise it usually doesn’t.

4.12 Recursion on Linked Lists

A linked list is a recursive data structure in disguise: it is “a head node plus the rest of the list, which is also a linked list.” That definition practically hands you the recursive algorithms for free. Every iterative walker from the last few sections has a recursive twin that is often shorter, and occasionally clearer --- at a real performance cost.

Forward traversal, recursive

Three lines, one branch. The base case is “empty list,” which maps exactly to node == nullptr. The inductive step is “visit head, recurse on tail.” This is the cleanest expression of a traversal you can write --- and the iterative version from section 4.9 is still faster in practice.

Forward vs. reverse: one line swap

The striking thing about recursive list code is how little has to change to reverse the order. Swap “visit” and “recurse” and you get a reverse traversal:

This works on a singly-linked list --- no prev pointer needed. The reversal is hiding in the call stack: each activation records a node, all activations unwind in LIFO order, and visits happen on the way out.

Recursive search

Three cases, each a single line. The third case is a tail call: the recursive call is the last thing the function does. Some compilers (including modern g++/clang with -O2) can recognize this and rewrite it as a loop, reclaiming the stack frame each step. With tail-call optimization, the recursive version has the same stack cost as the iterative one. Without it, a 10,000-element list will blow the default 8MB stack on Linux somewhere around recursion depth 100,000 --- well above 10k, but closer than you think.

Cost: recursive vs iterative

::: center Operation Rec. time Rec. space Iterative space


Forward traverse Θ(n)\Theta(n) Θ(n)\Theta(n)^* Θ(1)\Theta(1) Reverse traverse Θ(n)\Theta(n) Θ(n)\Theta(n) Θ(n)\Theta(n) (explicit stack) Search Θ(n)\Theta(n) Θ(n)\Theta(n)^* Θ(1)\Theta(1) :::

^* With tail-call optimization, Θ(1)\Theta(1). C++ does not guarantee TCO --- treat it as a performance optimization, not a correctness property.

4.13 Stack ADT

Stacks are the first “restricted list” ADT. Where a list lets you touch any element, a stack lets you touch one: the top. That single restriction is the entire point. It buys you a mental model (LIFO, last-in-first-out), a suite of cheap operations (everything O(1)O(1)), and a surprisingly long list of real-world algorithms that just fall out of it.

The five operations

::: center Op Semantics Cost


push(x) Add x at top O(1)O(1) pop() Remove and return top O(1)O(1) peek() Return top without removing O(1)O(1) empty() True iff no elements O(1)O(1) size() Number of elements O(1)O(1) :::

All five are guaranteed O(1)O(1) --- that’s not a coincidence, it’s the contract. A data structure that calls itself a stack but has O(n)O(n) push is mislabeled; the whole reason stacks exist is to be cheap.

C++: std::stack

C++ provides std::stack as a container adapter --- a wrapper around an underlying container that exposes only the stack operations. The underlying container defaults to std::deque but can be any container that supports push_back, pop_back, and back.

Notice top() is the STL’s name for peek(), and pop() returns void --- you must call top() before pop() if you want the value. This split is deliberate: it avoids forcing a copy on every pop, which matters for stacks of expensive-to-copy types.

Why restrict access at all?

A stack has strictly fewer operations than a list. That seems strictly worse. The payoff is threefold:

  1. Speed guarantee. A list has O(n)O(n) random access; a stack’s operations are all O(1)O(1). The restriction is what makes that guarantee possible.

  2. Code clarity. A function that takes a stack<T>& advertises its intent: “I will use this LIFO.” A function taking vector<T>& is making no such promise.

  3. Implementation freedom. Once you commit to LIFO semantics, the implementer can swap in any underlying container (array, DLL, circular buffer) without breaking callers.

4.14 Stacks via Linked Lists

Section 4.13 defined what a stack is. This section picks one concrete implementation: a singly-linked list where the head is the top. Every stack operation becomes a list operation you already know.

The mapping

::: center Stack op List op (head = top) Cost


push(x) prepend(x) O(1)O(1) pop() Read head, remove head O(1)O(1) peek() Read head->data O(1)O(1) empty() head == nullptr O(1)O(1) size() Counter updated on push/pop O(1)O(1) :::

The key design choice is head-as-top. Why not tail? Because in a singly-linked list, prepending is O(1)O(1) and appending-then-removing is O(n)O(n) without a tail pointer (you’d still need to walk to find the new tail after popping). Putting the top at the head lines the cheap operations up perfectly.

Implementation

A few things worth pointing out:

  • Destructor walks and frees. A stack owns its nodes. Without the destructor, every Stack that goes out of scope while non-empty leaks the whole chain.

  • n_ counter. Keeping size() O(1)O(1) requires maintaining a counter. Walking the list every time would make size() O(n)O(n), violating the ADT contract.

  • pop returns by value; peek by const-reference. Pop has to return because the node is about to be deleted, so you can’t hand out a reference into freed memory. Peek can return a reference because the node is still alive.

Linked list vs. array backing

A stack doesn’t have to be a linked list. A dynamic array (std::vector) with push-back-as-push and pop-back-as-pop is often faster in practice because the elements are contiguous and the CPU prefetches. The tradeoff:

::: center Metric Linked list Dynamic array


push worst case O(1)O(1) O(n)O(n) (on resize) push amortized O(1)O(1) O(1)O(1) pop O(1)O(1) O(1)O(1) Cache friendliness Poor Excellent Memory overhead / elem 1 pointer 0—1 slot (slack) Handles huge sizes Any Any :::

For almost every use case, array-backed is the right default --- which is why std::stack defaults to std::deque (array-ish under the hood) rather than std::list. The linked-list version shines when you need guaranteed worst-case O(1)O(1) with no amortization.

4.15 Queue ADT

A queue is the stack’s twin. Where a stack is LIFO (last-in, first-out), a queue is FIFO (first-in, first-out). Both are restricted list ADTs with exactly two access points --- the only difference is whether you remove from the same end you insert at (stack) or the opposite end (queue).

The five operations

::: center Op Semantics Cost


push(x) / enqueue Add x at rear O(1)O(1) pop() / dequeue Remove and return front O(1)O(1) peek() / front Return front without removing O(1)O(1) empty() True iff no elements O(1)O(1) size() Number of elements O(1)O(1) :::

Every operation is O(1)O(1), same as a stack. The restriction (only two access points) is what makes this achievable --- a general list cannot guarantee O(1)O(1) on both ends without extra structure.

C++: std::queue

std::queue mirrors std::stack: a container adapter with a default underlying container (std::deque) and a minimal interface. The names are slightly different because a queue has two ends.

Notice the output is in insertion order, not reversed like a stack. Also notice std::queue exposes both front() and back(); you can peek at either end but only pop from the front and push to the back.

Where queues show up

::: center Use case What the queue represents


BFS graph traversal Frontier of nodes to visit Scheduling (OS, jobs) Tasks waiting to run Producer/consumer Work items, producer pushes, consumer pops Message passing Inbox (network, IPC) Printer / print spool Jobs queued for the device Buffered I/O Bytes waiting to be read or written :::

The unifying theme: “things arrive over time; serve them in arrival order.” That is a much more common pattern than LIFO, which is why queues are everywhere the moment you leave single- threaded algorithm land.

Why a queue needs two pointers on a linked list

On a singly-linked list, pop-from-front is cheap (just advance head) but push-to-back is O(n)O(n) --- unless you keep a tail pointer. A stack’s linked implementation needs only the head, but a queue needs both head and tail because the two operations live at opposite ends.

4.16 Queues via Linked Lists

The queue-via-linked-list implementation is structurally almost identical to the stack version from 4.14, but with one critical addition: the queue needs a tail pointer so push-to-back stays O(1)O(1).

The mapping

::: center Queue op List op (head = front, tail = rear) Cost


push(x) append(x) via tail pointer O(1)O(1) pop() Read head, remove head O(1)O(1) peek() Read head->data O(1)O(1) empty() head == nullptr O(1)O(1) size() Counter updated on push/pop O(1)O(1) :::

The design is head-as-front, tail-as-rear. Pop removes from the head (O(1)O(1): advance head). Push adds at the tail (O(1)O(1) only if we keep a tail pointer).

Implementation

Two lines deserve close attention:

  • The empty-push case. When tail_ is nullptr, the queue is empty; the new node is simultaneously the head and the tail, so both pointers must be set. Missing this turns a push on an empty queue into a silent no-op.

  • The empty-pop case. When the pop removes the last element, tail_ must be reset to nullptr as well. Missing this leaves a dangling pointer: the next push will write to tail_->next, which is a pointer into freed memory.

Why not a doubly-linked list?

A DLL would work, but it’s overkill. A queue never traverses backward, never inserts in the middle, and never removes from anywhere other than the head. The prev pointer on every node would be dead weight --- pure memory overhead with no corresponding operation to justify it.

::: center Operation Needs prev?


push(x) No (append at tail, only need tail->next) pop() No (remove head, only need head->next) peek() No :::

An SLL with head+tail is the minimum sufficient structure. That’s usually the right way to choose data structures: add capability up to exactly what the operations demand, no further.

Implementing the ring buffer

The notebox above gestures at the ring buffer; it deserves a concrete implementation, since it is the structure used in OS kernels for bounded producer-consumer queues, in audio/video pipelines for fixed-latency buffering, and inside std::deque block-by-block.

4.17 Deque ADT

A deque (pronounced “deck,” short for double- ended queue) is the union of a stack and a queue. Where a stack restricts you to one end and a queue to two specific ends (front for pop, back for push), a deque lets you push and pop at either end. That flexibility costs nothing in asymptotic time: all four operations remain O(1)O(1).

The eight operations

::: center Op Semantics Cost


push_front(x) Insert x at front O(1)O(1) push_back(x) Insert x at back O(1)O(1) pop_front() Remove and return front O(1)O(1) pop_back() Remove and return back O(1)O(1) peek_front() Return front without removing O(1)O(1) peek_back() Return back without removing O(1)O(1) empty() True iff no elements O(1)O(1) size() Number of elements O(1)O(1) :::

The ADT hierarchy, visualized

::: center ADT Push front Push back Pop front Pop back


Stack \bullet \bullet Queue \bullet \bullet
Deque \bullet \bullet \bullet \bullet :::

A stack is a deque that only uses one end. A queue is a deque that uses opposite ends. Every stack and queue operation is also a valid deque operation --- deques strictly generalize both.

C++: std::deque

std::deque is a full container, not an adapter. It supports random-access iterators, operator[], and the standard container interface on top of the eight deque ops.

Linked-list implementation: the DLL’s payoff

A deque on a linked list is where the doubly-linked list finally earns its keep. Think about it:

::: center Operation SLL cost DLL cost


push_front O(1)O(1) O(1)O(1) push_back O(1)O(1) (with tail) O(1)O(1) pop_front O(1)O(1) O(1)O(1) pop_back O(n)O(n) (no prev!) O(1)O(1) :::

The fourth row is the killer. To pop the back of a singly-linked list, you have to walk the list to find the new tail (the second-to-last node), because there’s no prev pointer to back up from the current tail. That walk is O(n)O(n) --- game over for the ADT contract.

4.18 Array-Based Lists

The chapter has spent 17 sections on pointer-based lists. This one finally asks: what if we implemented the list ADT on a plain old array instead? The answer reshuffles the cost table. Array-based lists win on random access and cache performance, and lose on insertion/removal at the front. Neither approach is universally better --- they have complementary strengths.

The layout

::: center used unused capacity


[0] [1] [2] [3] --- --- --- --- 45 84 12 78
:::

length = 4, capacity = 8. Elements live at indices [0..length); the rest is reserved space that doesn’t need to be copied on the next append.

Append: amortized O(1)

A single append is usually O(1)O(1): assign into array[length], increment length. But when the array is full, the resize step is O(n)O(n): allocate, copy every element, free the old array. So is append O(1)O(1) or O(n)O(n)?

Prepend and insert: the expensive operations

Appending is cheap because the free space lives at the end. Prepending is O(n)O(n) because every existing element has to shift right by one to make room at index 0. Inserting after index kk shifts everything in [k+1,length)[k+1, \text{length}) --- worst case O(n)O(n), best case (insert at the end) O(1)O(1).

The full cost comparison

::: center Operation Array-based Linked list


append O(1)O(1)^* O(1)O(1) (with tail) prepend O(n)O(n) O(1)O(1) insert at index kk O(nk)O(n-k) O(n)O(n) to find, O(1)O(1) to insert remove at index kk O(nk)O(n-k) O(n)O(n) to find, O(1)O(1) to remove access by index O(1)O(1) O(n)O(n) search by value O(n)O(n) O(n)O(n) append (at end) O(1)O(1)^* O(1)O(1) Cache friendliness Excellent Poor Memory overhead per element 0—1 slot 1—2 pointers :::

^* amortized

The two structures have complementary strengths. Array- based wins on index access, cache behavior, and append. Linked wins on front operations and interior insertion once the position is known. Picking between them is an algorithmic choice driven by what operations dominate.

List-variant decision matrix

The four realizations of the List ADT covered in this chapter — dynamic array (std::vector), singly linked list (SLL), doubly linked list (DLL), and sentinel DLL (or std::list) — trade cost in predictable ways. This matrix is the decision you’ll actually make on assignments.

What this connects to

Companion materials. Compact notes: chapters/ch_4/notes.tex (includes the decision matrix in compact form). Practice prompts (problem \to pseudocode \to C++): chapters/ch_4/practice.md.

interactive mode active