COMPUTER SCIENCE FUNDAMENTALS SERIES

Arrays and
Linked Lists

Contiguous memory · Dynamic arrays · Singly linked ·
Doubly linked · Skip lists · Cache performance
Mid-level software engineer track  ·  20 slides
02

Contiguous vs Linked Memory

Contiguous allocation

Elements stored in adjacent memory addresses. Knowing the base address and element size gives constant-time access to any element.

  • Memory allocated as a single block
  • Address of element i = base + i * sizeof(element)
  • CPU cache prefetcher thrives on sequential access
  • Resizing requires allocating a new block and copying all elements

Linked allocation

Each element (node) stores a pointer to the next node. Nodes can live anywhere in the heap.

  • Memory allocated per-node, on demand
  • No wasted capacity — grows one node at a time
  • No reallocation or copying on insert
  • Pointer chasing defeats CPU cache prefetch
  • Extra memory overhead per node (one or two pointers)

Fundamental trade-off: arrays optimise for read performance; linked lists optimise for insert/delete flexibility.

03

Static Arrays

A fixed-size, contiguous block of memory allocated at compile time (stack) or at runtime with a known size.

Memory layout

Base address: 0x1000
Element size: 4 bytes (int32)

Index:   [  0  ] [  1  ] [  2  ] [  3  ] [  4  ]
Address: 0x1000  0x1004  0x1008  0x100C  0x1010
Value:      10      25      37      42      58

Declaration examples

int scores[5] = {10, 25, 37, 42, 58};   // stack
int *heap_arr = malloc(5 * sizeof(int)); // heap

Characteristics

O(1) Random access — direct address calculation
O(n) Insertion/deletion — shift elements to maintain order
0 Per-element overhead — no pointers, no metadata
Fixed Size determined at creation — cannot grow or shrink
04

Array Indexing & Cache Locality

Why O(1) access matters

The formula base + index * element_size is a single arithmetic operation. No matter the array size, access time is constant.

Spatial locality

Accessing arr[i] loads an entire cache line (64 bytes). If arr[i+1] is next, it is already in L1 cache — a cache hit.

Cache line (64 bytes) loaded on access to arr[3]:
┌────┬────┬────┬────┬────┬────┬────┬────┐
│ [0]│ [1]│ [2]│ [3]│ [4]│ [5]│ [6]│ [7]│
└────┴────┴────┴────┴────┴────┴────┴────┘
               ▲ requested
        entire line loaded into L1

Cache hierarchy latency

LevelLatencyTypical size
L1 cache~1 ns32–64 KB
L2 cache~4 ns256 KB–1 MB
L3 cache~12 ns4–32 MB
Main memory~100 ns8–64 GB

Sequential array traversal achieves near-peak memory bandwidth. Linked list traversal pays the full main-memory latency for each node.

05

Dynamic Arrays

A resizable array that grows automatically when capacity is exceeded. The underlying storage is still a contiguous block.

Growth strategy

When the array is full and a new element must be added:

  • Allocate a new array of k * current_capacity (typically k = 2)
  • Copy all existing elements to the new array
  • Free the old array and insert the new element

Size vs capacity

Capacity: 8    Size: 5

[ 10 ][ 25 ][ 37 ][ 42 ][ 58 ][    ][    ][    ]
                                ▲ size       ▲ capacity

Key operations

OperationAverageWorst
Access by indexO(1)O(1)
Push backO(1)*O(n)
Pop backO(1)O(1)
Insert at indexO(n)O(n)
Delete at indexO(n)O(n)
Search (unsorted)O(n)O(n)

*Amortised — resize triggers full copy

06

Amortised Analysis of Dynamic Arrays

The doubling argument

When capacity doubles on overflow, the total cost of n push-back operations is O(n), giving an amortised cost of O(1) per operation.

Proof sketch (aggregate method)

Operations: 1  2  3  4  5  6  7  8  9
Cost:       1  1  1  1  1  1  1  1  1  (inserts)
Resize at:        +2    +4          +8 (copies)

Total for n inserts:
  n (inserts) + 1 + 2 + 4 + ... + n
= n + (2n - 1)
= 3n - 1 = O(n)

Amortised per insert = O(n)/n = O(1)

Growth factor trade-offs

2x growth

Fewer reallocations; simple bit-shift. Up to 50% wasted space.

GCC libstdc++

1.5x growth

Less wasted space (~33% max). More frequent reallocations.

MSVC Java ArrayList

~1.125x growth

Minimal waste. Very frequent reallocations for small lists.

Python list

07

Singly Linked Lists

A sequence of nodes where each node stores a value and a pointer to the next node. The last node points to NULL.

Node structure

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

Memory layout

10 next 0x2A00 25 next 0x3F10 37 next 0x1B80 42 NULL 0x4C20 head

Characteristics

  • O(n) access — must traverse from head to reach element i
  • O(1) insert/delete at head — just update the head pointer
  • O(n) insert/delete at position — must traverse to predecessor
  • No wasted capacity — each node allocated individually
  • Extra 8 bytes per node (one pointer on 64-bit)
  • No reallocation — the list never copies elements

Nodes are scattered across the heap — addresses are not sequential. Every traversal step is potentially a cache miss.

08

Singly Linked List Operations

Insertion at head — O(1)

Node* insert_head(Node* head, int value) {
    Node* n = malloc(sizeof(Node));
    n->data = value;
    n->next = head;
    return n;  // new head
}

Insertion after a given node — O(1)

void insert_after(Node* prev, int value) {
    Node* n = malloc(sizeof(Node));
    n->data = value;
    n->next = prev->next;
    prev->next = n;
}

Deletion after a node — O(1)

void delete_after(Node* prev) {
    Node* target = prev->next;
    prev->next = target->next;
    free(target);
}

Traversal — O(n)

void print_list(Node* head) {
    Node* curr = head;
    while (curr != NULL) {
        printf("%d -> ", curr->data);
        curr = curr->next;
    }
    printf("NULL\n");
}

Key insight: finding the predecessor is the bottleneck. Doubly linked lists solve this by storing a prev pointer.

09

Doubly Linked Lists

Each node stores pointers to both the next and previous nodes, enabling bidirectional traversal.

Node structure

struct DNode {
    int    data;
    DNode* prev;
    DNode* next;
};

Memory layout

NULL 10 head 25 37 tail NULL

Advantages over singly linked

  • O(1) delete given a node pointer — no need to find predecessor
  • Backward traversal — iterate from tail to head
  • O(1) insert before a given node — direct access to predecessor
  • Easier LRU cache, undo/redo implementation

Cost

  • Two pointers per node — 16 bytes overhead on 64-bit
  • More complex updates — four pointer changes per insert/delete
10

Sentinel Nodes

A sentinel (dummy) node simplifies boundary conditions by eliminating NULL checks. Used with doubly linked lists.

Without sentinel

Every operation must check for empty list, head insert, and tail insert as special cases.

With sentinel

Empty list:
  sentinel.next = sentinel
  sentinel.prev = sentinel

After inserting 10, 25, 37:
  SEN ──▶ 10 ──▶ 25 ──▶ 37 ──▶ SEN
  SEN ◀── 10 ◀── 25 ◀── 37 ◀── SEN

Simplified insertion

// Insert after any node — no NULL checks
void insert_after(DNode* node, int val) {
    DNode* n = malloc(sizeof(DNode));
    n->data       = val;
    n->next       = node->next;
    n->prev       = node;
    node->next->prev = n;
    node->next       = n;
}

node->next is always valid — it may be the sentinel itself. No special cases needed.

The Linux kernel's list_head uses sentinel-based circular doubly linked lists throughout. See include/linux/list.h.

11

Circular Linked Lists

The last node points back to the first, forming a cycle. Can be singly or doubly linked.

Singly circular

10 25 37

Traversal

void print_circular(Node* start) {
    if (!start) return;
    Node* curr = start;
    do {
        printf("%d -> ", curr->data);
        curr = curr->next;
    } while (curr != start);
}

Use cases

Round-robin scheduling

Process scheduler cycles through tasks endlessly

Circular buffers

Fixed-size ring buffer for streaming data

Josephus problem

Classic elimination game, naturally modelled as a circle

Media playlists

Loop playback without special end-of-list logic

Token ring networks

Token passed around a circular topology

12

Skip Lists

A probabilistic data structure that layers multiple sorted linked lists on top of each other. Invented by William Pugh (1989).

Structure

L3 L2 L1 L0 H H H H 42 NULL 17 42 NULL 6 17 25 42 58 NULL 6 17 25 33 42 58 73 N

How it works

  • Level 0 is a standard sorted linked list containing all elements
  • Each higher level is a "fast lane" containing a random subset
  • A node is promoted with probability p (typically 0.5)
  • Expected height: O(log n)

Why skip lists?

  • Simpler to implement than balanced BSTs
  • Lock-free variants are easier than lock-free trees
  • Expected O(log n) search, insert, delete
  • No rotations — balancing is probabilistic
13

Skip List Operations

Search — expected O(log n)

Start at the highest level. Move right while the next node's key is less than the target. Drop down one level and repeat.

Search for 33:

L3: H ─────────────── 42 (too far, drop)
L2: H ────── 17 ───── 42 (too far, drop)
L1: ... 17 ── 25 ──── 42 (too far, drop)
L0: ... 25 ── 33 FOUND

Insert

  • Search for position (track predecessors at each level)
  • Flip a coin repeatedly to determine the new node's height
  • Splice the node into each level up to its height

Complexity

OperationExpectedWorst
SearchO(log n)O(n)
InsertO(log n)O(n)
DeleteO(log n)O(n)
SpaceO(n)O(n log n)

Delete

  • Search for the node (track predecessors at each level)
  • Remove the node from every level it appears in
  • If top level is now empty, reduce max level

Redis sorted sets (ZSET) use skip lists internally. LevelDB and RocksDB use skip lists for their in-memory memtable.

14

XOR Linked Lists

A memory-efficient doubly linked list that stores only one pointer per node instead of two, using the XOR of the previous and next addresses.

The trick

Each node stores:
  link = prev_addr XOR next_addr

Forward traversal:
  next = prev_addr XOR node->link

Backward traversal:
  prev = next_addr XOR node->link

Example

Node A         Node B         Node C
link = 0^B     link = A^C     link = B^0
     = B            = A^C          = B

Forward from A (prev=0):
  next = 0 XOR B = B
From B (prev=A):
  next = A XOR (A^C) = C
From C (prev=B):
  next = B XOR B = 0 (end)

Trade-offs

Advantages

  • Halves pointer overhead — one pointer per node vs two
  • Bidirectional traversal — same as doubly linked

Disadvantages

  • Incompatible with garbage collection — XOR-encoded pointers are invisible to GC
  • Cannot use in managed languages — only C/C++ with raw pointers
  • Debugging is painful — pointer values not directly inspectable
  • Rarely used in practice — savings seldom justify complexity

XOR linked lists are a clever interview topic but almost never appear in production code. Prefer standard doubly linked lists.

15

Unrolled Linked Lists

A hybrid that stores multiple elements per node, combining array cache friendliness with linked list insertion flexibility.

Structure

count: 4 10 25 37 42 count: 4 58 63 71 80 count: 2 85 91

How it works

  • Each node holds an array of up to B elements (block size, e.g. 4–16)
  • Nodes are linked together like a standard linked list
  • Overflow: node splits into two half-full nodes
  • Underflow: node merges with a neighbour

Performance comparison

OperationUnrolledStandard
TraversalCache-friendlyPointer chasing
SearchO(n) better constantsO(n)
InsertO(B) shift + O(1) spliceO(1) at known pos
Overhead1 ptr per B elements1–2 ptrs per element

Used in text editors (rope variants), B+ tree leaf chains, and gap buffer hybrids. Delivers 2–5x speedup over standard linked lists for sequential access.

16

Arrays vs Linked Lists — Performance Comparison

OperationArrayDynamic arraySingly linkedDoubly linked
Access by indexO(1)O(1)O(n)O(n)
Search (unsorted)O(n)O(n)O(n)O(n)
Search (sorted)O(log n)O(log n)O(n)O(n)
Insert at frontO(n)O(n)O(1)O(1)
Insert at backN/AO(1)*O(n)O(1)
Insert at middleO(n)O(n)O(n)O(n)
Delete at frontO(n)O(n)O(1)O(1)
Delete at backN/AO(1)O(n)O(1)
Delete given nodeO(n)O(n)O(n)O(1)
Memory per element0 extra0 extra+8 bytes+16 bytes
Cache performanceExcellentExcellentPoorPoor

*Amortised O(1) for dynamic array push-back.

In practice, arrays win more often than theory suggests. Cache locality dominates modern hardware. Linked lists only outperform when insertions/deletions at arbitrary positions are the primary operation and elements are large.

17

Real-World Implementations

C++ std::vector

  • Contiguous storage guarantee
  • Growth: 2x (GCC) or 1.5x (MSVC)
  • push_back amortised O(1)
  • Iterators invalidated on realloc

Java ArrayList

  • Backed by Object[]; growth 1.5x
  • Autoboxing overhead for primitives
  • LinkedList almost never the right choice

Python list

  • Array of pointers; growth ~1.125x
  • append() amortised O(1)
  • deque = unrolled linked list

Go slice

  • Pointer + length + capacity triple
  • Growth: 2x when small, tapering to 1.25x
  • append() may return new header on realloc

Rust Vec<T>

  • Heap-allocated dynamic array; growth 2x
  • Ownership prevents iterator invalidation at compile time
  • VecDeque<T> = ring buffer for front/back ops
18

Cache Performance & Memory Fragmentation

Why arrays dominate in benchmarks

HW Hardware prefetcher — detects stride patterns and preloads cache lines
TLB TLB efficiency — contiguous arrays span fewer virtual memory pages
BP Branch prediction — loop-based array traversal is highly predictable
SIMD SIMD vectorisation — compilers process 4–8 elements per instruction

The linked list cache problem

  • Each node may reside on a different cache line — pointer chasing defeats prefetch
  • Allocator scatters nodes across pages — poor TLB hit rate
  • Small nodes waste most of a 64-byte cache line
  • Traversal can be 10–100x slower than array traversal

Mitigations

  • Arena/pool allocators — allocate nodes from a contiguous block; restores locality
  • Unrolled linked lists — amortise pointer overhead across multiple elements
  • Intrusive lists — embed the link inside the data structure (Linux kernel pattern)
19

Applications & Use Cases

Arrays / Dynamic arrays

  • Random access lookups (tables, matrices)
  • Sequential iteration (buffers, streams)
  • Known or predictable element count
  • Cache-critical workloads (numerics, games)
  • Minimal memory footprint needed

Linked lists

  • Frequent insert/delete at arbitrary positions
  • Constant-time splicing of sub-lists
  • FIFO queues and deques
  • LRU caches (doubly linked + hash map)
  • Undo/redo stacks
  • Graph adjacency lists
  • OS kernel structures (task lists, drivers)

Specialised variants

  • Skip lists — sorted collections needing O(log n) ops; concurrent access (Redis ZSET)
  • Unrolled lists — text editors, file system leaf chains
  • Circular lists — round-robin schedulers, ring buffers
  • Intrusive lists — OS kernels, zero-allocation designs

Decision heuristic: default to a dynamic array. Switch to a linked structure only when profiling shows that insert/delete cost dominates, or when you need O(1) splice/remove with a direct node reference.

20

Summary & Further Reading

Key takeaways

  • Arrays provide O(1) access and excellent cache performance — the default choice
  • Dynamic arrays give O(1) access + amortised O(1) append
  • Linked lists trade cache performance for O(1) insert/delete at known positions
  • Doubly linked lists enable O(1) deletion given a node pointer — essential for LRU caches
  • Skip lists offer O(log n) expected performance with simpler concurrency than balanced trees
  • XOR and unrolled linked lists are niche optimisations — know they exist
  • Cache locality dominates real-world performance — profile before choosing a linked structure
  • Arena allocators can restore locality to linked structures when needed

Recommended reading

SourceDescription
Cormen et al.Introduction to Algorithms (CLRS) — elementary data structures and amortised analysis
Pugh, W."Skip Lists: A Probabilistic Alternative to Balanced Trees" (1990)
Stroustrup, B."Why you should avoid Linked Lists" — cache effects on vector vs list
Linux kernelinclude/linux/list.h — intrusive doubly linked list
Sedgewick & WayneAlgorithms, 4th ed. — linked lists and dynamic arrays