Which operations exist, what they return, and which ordering rule is preserved—for example, LIFO or FIFO.
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.
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.
Where elements live, which metadata is stored, which references remain valid, and what each operation costs on the target machine.
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.
Start at address 1000: read data 10, then follow next = 5000.
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.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.
| Property | Typical 32-bit target | Typical 64-bit target |
|---|---|---|
sizeof(int) | 4 bytes | 4 bytes |
sizeof(void *) | 4 bytes | 8 bytes |
Offset of next | 4 | 8, after 4 bytes of padding |
sizeof(struct Node) | Usually 8 bytes | Usually 16 bytes |
| One million nodes, excluding allocator overhead | About 8 MB | About 16 MB |
| Theoretical pointer address range | 232 byte addresses | 264 byte addresses; current systems implement fewer bits |
Common total: 8 bytes, aligned to 4 bytes.
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.
-m32 outputpointer size : 4 node size : 8 next offset : 4 node align : 4
-m64 outputpointer 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 tointtruncates 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
longdiffers.
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.
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.
Dependent traversal
The address of node i+1 is unavailable until node i arrives. This dependency limits prefetching and memory-level parallelism.
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.
Scattered nodes may touch more virtual-memory pages, consuming more TLB entries and occasionally triggering page-table walks.
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 request | Textbook bound | What the bound assumes | End-to-end path |
|---|---|---|---|
| Insert after a known node | O(1) | Predecessor already available | allocate + initialize + two pointer writes |
| Insert after key 57 | Often quoted O(1) | Search omitted | O(n) search + allocation + O(1) rewiring |
| Delete a singly linked node | O(1) | Predecessor and ownership known | rewire + destroy payload + release storage |
| Dynamic-array append | Amortized O(1) | Geometric growth over a sequence | Usually one write; occasionally O(n) allocate-and-copy |
| Stack or queue update | O(1) | No search; resize/allocation abstracted | Constant metadata work with possible latency variation |
06Choosing a representation
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.
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.
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.
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.
07Implementation details for stacks and queues
Array stacks: fast paths and latency spikes
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
if rear = 4 and capacity = 5, next rear = (4 + 1) % 5 = 0
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.
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.
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.
every reachable node is owned exactly oncetail == NULL or tail->next == NULL0 ≤ size ≤ capacitystorage[0 … size−1] is initializedempty ⇔ front == NULL and rear == NULLnon-empty ⇒ rear->next == NULL0 ≤ size ≤ capacityfront and rear indices are always in rangeThese 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.
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.
| Factor | Array / dynamic array | Linked list |
|---|---|---|
| Traversal complexity | O(n) | O(n) |
| Spatial locality | Strong | Usually weak |
| Per-element overhead | Low | Pointer(s), padding, allocator metadata |
| Allocation pattern | Occasional bulk resize | Often one allocation per node |
| Known-location insertion | May shift elements | Constant-time relinking |
| CPU optimization | Prefetching and vectorization friendly | Limited by dependencies between pointers |
Benchmark the representation, not a toy operation
Decide whether allocation, destruction, searching, resizing, and payload work belong inside the measurement. Report that boundary explicitly.
Vary collection size, payload size, mutation ratio, allocator, and cache state. Report tail latency when occasional slow operations matter.
Review checklist
- Which precondition makes the quoted operation bound true?
- How many useful payload bytes fit in each fetched cache line?
- Will growth or mutation invalidate stored references?
- Who owns each allocation, and when is it reclaimed?
- Which invariants make empty and full states unambiguous?
- Does the benchmark include the costs the application will actually pay?
observed time = algorithmic work + memory stalls + allocation + synchronization