Supplementary guide · Chapter 7

Linked Lists, Stacks & Queues: Implementation Notes

These notes examine details that depend on the machine, compiler, ABI, allocator, and operating system. Examples use C on conventional 32-bit and 64-bit systems; exact sizes should always be checked on the target platform.

PrerequisiteThe definitions and elementary operations of arrays, linked lists, stacks, and queues are assumed. The emphasis here is representation and runtime behavior.

01The abstraction boundary

A stack or queue specifies observable behavior; an array or linked list specifies a representation. Keeping these layers separate lets an implementation change without changing its public interface.

Semantic contract

Which operations exist, what they return, and which ordering rule is preserved—for example, LIFO or FIFO.

Representation contract

Where elements live, which metadata is stored, which references remain valid, and what each operation costs on the target machine.

ConsequenceTwo implementations can satisfy the same ADT and have the same asymptotic bounds while differing in memory consumption, cache misses, latency variation, reference stability, and failure modes.

02What a pointer path really means

struct Node {
    int data;
    struct Node *next;
};

The familiar picture 10 → 20 → 30 describes logical order. Each node is normally a separate heap allocation, so its successor can be far away in the process's address space.

Follow the pointers

Step through the logical list while observing its scattered physical addresses.

node 1 of 3
Address 1000
data10
next5000
Address 5000
data20
next2200
Address 2200
data30
nextNULL

Start at address 1000: read data 10, then follow next = 5000.

Logical order10 → 20 → 30
Physical path1000 → 5000 → 2200
Key ideaLogical adjacency does not imply address adjacency. The next field contains a pointer representation—not the successor's payload. The processor must obtain the node, load that pointer, and only then know which address to request next.
Address translationThe displayed addresses are conceptual virtual addresses. The operating system and memory-management unit translate them to physical memory using page tables, with recent translations cached in the TLB. A node can therefore incur both a data-cache miss and, less frequently, an address-translation miss.

03The same program on 32-bit and 64-bit machines

Consider the same definition on both targets:

struct Node {
    int data;
    struct Node *next;
};

The list algorithm is unchanged: next still identifies the successor and NULL still terminates the list. What changes is the binary representation. The comparison below reflects common ILP32 and LP64/LLP64 ABIs; it is typical, not a language guarantee.

PropertyTypical 32-bit targetTypical 64-bit target
sizeof(int)4 bytes4 bytes
sizeof(void *)4 bytes8 bytes
Offset of next48, after 4 bytes of padding
sizeof(struct Node)Usually 8 bytesUsually 16 bytes
One million nodes, excluding allocator overheadAbout 8 MBAbout 16 MB
Theoretical pointer address range232 byte addresses264 byte addresses; current systems implement fewer bits
Typical 32-bit node
data
4 B
next
4 B

Common total: 8 bytes, aligned to 4 bytes.

Typical 64-bit node
data
4 B
padding
4 B
next
8 B

Common total: 16 bytes, aligned to 8 bytes.

Measure the actual ABI

#include <stddef.h>
#include <stdio.h>

struct Node {
    int data;
    struct Node *next;
};

int main(void) {
    printf("pointer size : %zu\n", sizeof(void *));
    printf("node size    : %zu\n", sizeof(struct Node));
    printf("next offset  : %zu\n", offsetof(struct Node, next));
    printf("node align   : %zu\n", _Alignof(struct Node));
    return 0;
}

When supported by the toolchain, compile the test separately with -m32 and -m64. A 64-bit operating system may run a 32-bit binary through a compatibility subsystem; a 32-bit operating system cannot run a 64-bit binary. The same source is portable only after recompilation for the target architecture.

Representative -m32 output
pointer size : 4
node size    : 8
next offset  : 4
node align   : 4
Representative -m64 output
pointer size : 8
node size    : 16
next offset  : 8
node align   : 8

Consequences beyond node size

  • Cache density: ignoring allocator overhead, a 64-byte cache line could hold eight 8-byte nodes but only four 16-byte nodes if the nodes were contiguous.
  • Address printing: use printf("%p", (void *)pointer). Converting a pointer to int truncates it on common 64-bit targets.
  • Structure layout: compiler options such as packing can change offsets, but packed pointers may be misaligned and slower or invalid on some architectures.
  • Data models: 64-bit Unix-like systems commonly use LP64, while 64-bit Windows uses LLP64. Pointer width is 8 bytes in both, but the size of long differs.
Do not serialize raw nodesA pointer is meaningful only inside the process that created it. Writing struct Node bytes directly to a file also captures machine-dependent padding, byte order, and type sizes. Portable storage uses a defined encoding, fixed-width integers such as int32_t, and identifiers or offsets instead of live pointers.

04Memory-system costs

The source-level node is not the allocation size

On a common 64-bit ABI, an int uses 4 bytes and a pointer 8 bytes. Alignment can add 4 bytes of padding, producing a 16-byte C structure. This is only the object layout—not necessarily the complete allocator cost.

4 Bpayload
16 Bstructure size
≥16 Ballocator footprint

General-purpose allocators round requests to size classes and maintain metadata, often beside the object or in separate bookkeeping structures. One million integers therefore need about 4 MB in a packed C array, at least 16 MB as these nodes, and potentially more after size-class rounding and fragmentation. Exact totals must be measured for the allocator and platform in use.

Why pointer chasing stalls a modern CPU

Contiguous traversal

One cache-line fetch—commonly 64 bytes—delivers several elements. Hardware prefetchers can predict the next lines, and independent loads can overlap.

1020304050

Dependent traversal

The address of node i+1 is unavailable until node i arrives. This dependency limits prefetching and memory-level parallelism.

100050002200
Cache-line waste

If a node occupies only part of a fetched cache line and nearby bytes belong to unrelated allocations, much of the transfer provides no useful list data.

TLB and page pressure

Scattered nodes may touch more virtual-memory pages, consuming more TLB entries and occasionally triggering page-table walks.

Practical consequenceBoth traversals are O(n), but the array can process several useful values per memory transaction while the list may serialize one long-latency request per node. The asymptotic model deliberately hides this difference.

05Reading complexity claims correctly

A complexity statement is meaningful only with its preconditions and cost model. “Linked-list insertion is O(1)” describes pointer rewiring after the relevant node is available; it does not promise that locating or allocating that node is free.

Operation requestTextbook boundWhat the bound assumesEnd-to-end path
Insert after a known nodeO(1)Predecessor already availableallocate + initialize + two pointer writes
Insert after key 57Often quoted O(1)Search omittedO(n) search + allocation + O(1) rewiring
Delete a singly linked nodeO(1)Predecessor and ownership knownrewire + destroy payload + release storage
Dynamic-array appendAmortized O(1)Geometric growth over a sequenceUsually one write; occasionally O(n) allocate-and-copy
Stack or queue updateO(1)No search; resize/allocation abstractedConstant metadata work with possible latency variation
Required contextFor every bound, state what is already known, which resource operations are excluded, and whether the bound is worst-case, average-case, expected, or amortized.

06Choosing a representation

Singly versus doubly linked

A back pointer buys reverse traversal and O(1) unlinking from a known node, but increases node size and the number of writes needed to preserve consistency.

Sentinel nodes

Dummy head or tail nodes replace boundary branches with uniform rewiring. The small space cost often pays for itself through simpler invariants and fewer defects.

Stable addresses

List nodes normally do not move, so pointers and references to surviving nodes remain valid. Dynamic-array growth can relocate the buffer and invalidate pointers, references, and iterators.

Intrusive lists

Systems code may embed link fields directly inside application objects. This avoids wrapper-node allocation but couples the object's layout and lifetime to the container.

Allocation strategy can change the result

A node pool, slab, or arena allocates nodes in batches. This can reduce per-node metadata, improve locality, make allocation more predictable, and limit fragmentation. It also changes reclamation: an arena commonly frees many objects together rather than individually.

Comparison boundaryCompare complete representations rather than isolated container names: include growth policy, allocator, ownership model, reference-stability requirements, and expected workload.

07Implementation details for stacks and queues

Array stacks: fast paths and latency spikes

10203040
resize + copy →
1020304050

Most pushes perform one bounds check and one write. A growth push allocates a larger buffer, moves or copies the live elements, and invalidates addresses into the old buffer. Amortized O(1) explains total work over a sequence; it does not guarantee constant latency for an individual push. Systems with strict latency targets may reserve capacity in advance or use fixed-capacity storage.

Circular queues: the invariant is the design

next position = (current position + 1) % capacity
if rear = 4 and capacity = 5, next rear = (4 + 1) % 5 = 0
Size counterAll slots are usable; size distinguishes full from empty.
Reserved slotThe buffer is full when the next rear would equal front.
Monotonic countersLet counters grow and mask only when indexing the bounded array.

Power-of-two capacities permit index & (capacity − 1) instead of modulo, but only when that power-of-two invariant is guaranteed. In concurrent queues, head/tail updates also require a defined atomic-memory-ordering protocol; volatile variables alone are not sufficient.

Logical versus physical removal

Moving top or front changes membership immediately even if old bits remain. Containers of references may clear vacated slots so garbage collection can reclaim referenced objects.

Two stack overflows

A fixed-capacity ADT can reject a push; recursive calls can exhaust the process call stack. The shared term describes different resources and remedies.

08Lifetime, ownership, and executable invariants

Relinking removes a node from the logical structure; it does not, by itself, end the payload's lifetime or release storage. A correct manual-memory implementation needs a clear owner and a defined order: preserve required links, unlink, destroy the payload, release the allocation, and invalidate obsolete references.

List
every reachable node is owned exactly oncetail == NULL or tail->next == NULL
Array stack
0 ≤ size ≤ capacitystorage[0 … size−1] is initialized
Linked queue
empty ⇔ front == NULL and rear == NULLnon-empty ⇒ rear->next == NULL
Circular queue
0 ≤ size ≤ capacityfront and rear indices are always in range

These are not merely documentation. In debug builds they can become assertions checked after every mutation; property-based tests can generate operation sequences and verify that the invariants always survive.

Memory leak

Node *p = malloc(...);
p = NULL;

The only pointer to the allocation is discarded, so the program can no longer free it.

Dangling pointer

free(p);
printf("%d", p->data);

The address remains in p, but the object lifetime has ended. Dereferencing it is invalid.

Lost node

head = head->next;
/* old head not freed */

If the old head was not saved and released first, that node may leak.

Concurrency changes the problemOnce multiple threads mutate a linked structure, pointer correctness alone is insufficient. Atomicity, visibility, reclamation, and problems such as ABA must be addressed with an appropriate algorithm and memory-reclamation scheme.

09Choosing and measuring a representation

Modern CPUs usually reward contiguous storage. Dynamic arrays can outperform linked lists even when occasional shifts occur because they reduce allocations and pointer overhead while enabling prefetching and vectorization. A linked representation remains valuable when stable node addresses, constant-time splicing at known positions, or non-moving objects are actual requirements.

FactorArray / dynamic arrayLinked list
Traversal complexityO(n)O(n)
Spatial localityStrongUsually weak
Per-element overheadLowPointer(s), padding, allocator metadata
Allocation patternOccasional bulk resizeOften one allocation per node
Known-location insertionMay shift elementsConstant-time relinking
CPU optimizationPrefetching and vectorization friendlyLimited by dependencies between pointers

Benchmark the representation, not a toy operation

Include the real path

Decide whether allocation, destruction, searching, resizing, and payload work belong inside the measurement. Report that boundary explicitly.

Use representative conditions

Vary collection size, payload size, mutation ratio, allocator, and cache state. Report tail latency when occasional slow operations matter.

Review checklist

  1. Which precondition makes the quoted operation bound true?
  2. How many useful payload bytes fit in each fetched cache line?
  3. Will growth or mutation invalidate stored references?
  4. Who owns each allocation, and when is it reclaimed?
  5. Which invariants make empty and full states unambiguous?
  6. Does the benchmark include the costs the application will actually pay?
A complete performance modelobserved time = algorithmic work + memory stalls + allocation + synchronization