Arrays & Dynamic Arrays
Contiguous memory addressing proofs, cache lines, amortized doubling geometric series, and memory fragmentation.
Reinforce your mental model: run, pause, and inspect live pointers with our interactive visualizer:
Topics Covered:
20. Static Arrays • 21. Dynamic Arrays (Vectors / Resizable Arrays)
Contiguous memory allocation is the foundational building block of computing systems, establishing a direct bridge between physical hardware addressing and high-level algorithmic abstractions. This chapter examines the mechanics of static arrays and dynamically resizable vectors, detailing memory bus addressing equations, CPU cache-line spatial locality, algorithmic invariants for in-place shifts, amortized complexity proofs via the accounting method, and anti-thrashing memory policies.
Learning Objectives
#- Compute exact physical byte addresses for arbitrary 1D and multi-dimensional array coordinates using base-offset arithmetic.
- Explain the physical mechanism of CPU L1/L2 cache prefetching and quantify the performance disparity between contiguous arrays and scattered pointer structures.
- Implement robust boundary-checked insertion, deletion, and search algorithms with formal loop invariants.
- Derive the amortized cost of geometric array doubling using both aggregate analysis and the accounting method.
- Construct memory-efficient resizing policies that prevent allocation hysteresis and resize thrashing during alternating insertions and deletions.
Topic 20: Static Arrays
#1. Conceptual & Architectural Foundations
A Static Array is a fixed-size, homogeneous sequence of elements stored consecutively in physical memory. Its defining algorithmic trait is constant-time random access: any element can be read or mutated in a single step using its integer index.
The Physical Memory Model
Modern computer memory (RAM) is organized as a linear array of byte-addressable cells. When a program requests a static array of elements where each element requires bytes, the runtime environment requests an unbroken contiguous segment of bytes from the operating system.
The hardware relies on the Base-Offset Memory Formula:
where:
- is the Base Address (physical address of the first byte of ).
- is the Zero-Based Index ().
- is the Element Size in bytes (e.g., for a 32-bit integer, for a 64-bit pointer or float).
Because integer multiplication and pointer addition execute in a single CPU cycle, calculating does not require scanning intermediate elements . This is why array indexing is strictly .
Physical Layout in RAM:
+---------------------------------------------------------------------------------+
| Base α (0x1000) | α + 4 (0x1004) | α + 8 (0x1008) | α + 12 (0x100C) |
| A[0] = 42 | A[1] = 17 | A[2] = 89 | A[3] = 05 |
+---------------------------------------------------------------------------------+Physical Memory Mapping Table
| Physical RAM Address | Offset Formula | Array Element | Stored Value | Byte Offset () |
|---|---|---|---|---|
0x1000 | A[0] | 42 | ||
0x1004 | A[1] | 17 | ||
0x1008 | A[2] | 89 | ||
0x100C | A[3] | 05 | ||
0x1010 | A[4] | 63 |
2. Hardware Symbiosis: CPU Cache Lines & Spatial Locality
The primary performance advantage of arrays over pointer-linked nodes is Hardware Spatial Locality:
- Cache Lines: The CPU memory controller does not read individual bytes from RAM. Instead, it transfers memory in fixed blocks called Cache Lines (typically 64 bytes on modern x86-64 and ARM architectures).
- L1/L2 Prefetching: When the processor reads (4 bytes), the memory controller loads the entire 64-byte block containing directly into the L1 data cache in a single memory transaction.
- Latency Differential:
- Accessing L1 Cache: ().
- Accessing Main Memory (RAM): ().
Consequently, traversing a static array sequentially incurs a cache miss only once every 16 elements (for 4-byte integers), allowing the hardware prefetcher to stream data seamlessly. Pointer-based structures (like linked lists) scatter nodes across the heap, causing nearly every node traversal to incur a full cache miss.
3. Supported Operations & Complexity Matrix
#| Operation | Description | Best Case | Average Case | Worst Case | Auxiliary Space |
|---|---|---|---|---|---|
| Access() | Read element at position via | ||||
| Update() | Overwrite cell at index | ||||
| Search() | Linear scan for target value | ||||
| InsertEnd() | Append to back when size | ||||
| InsertAt() | Shift trailing items right and insert | ||||
| DeleteEnd() | Decrement size counter | ||||
| DeleteAt() | Shift trailing items left over target slot |
Interactive Simulation:
Observe memory addressing and bounds verification live in the Array Access Visualizer.
4. Algorithmic Mechanics: Insertion & Deletion
#Right-Shift Insertion Mechanism
Inserting an element at index in an array with current size and capacity requires shifting all elements from index down to one slot to the right.
⚠️ Critical Trap: Shift Order:
Shifting must proceed right-to-left (from index down to ). If shifted left-to-right, overwrites , duplicating across all subsequent cells.
Insertion State Transition Table (, Capacity , Insert at Index )
| Step | Action | Array State | Active Elements |
|---|---|---|---|
| 0 | Initial State | [10, 20, 30, 40, _] | |
| 1 | Shift | [10, 20, 30, 40, 40] | |
| 2 | Shift | [10, 20, 30, 30, 40] | |
| 3 | Write | [10, 20, 99, 30, 40] |
Deletion State Transition Table (Delete Element at Index from )
| Step | Action | Array State | Effective Size |
|---|---|---|---|
| 0 | Initial State | [5, 8, 2, 9] | |
| 1 | Shift | [5, 2, 2, 9] | |
| 2 | Shift | [5, 2, 9, 9] | |
| 3 | Decrement size () | [5, 2, 9, (stale)] |
5. Canonical Specification & Invariants
#CLASS StaticArray:
fields:
data: Array[Capacity] of Type
size: Integer
capacity: Integer
INVARIANT 0 <= size <= capacity
INVARIANT valid indices for elements are in range [0, size - 1]
CONSTRUCTOR(cap: Integer):
assert cap > 0
this.capacity <- cap
this.size <- 0
this.data <- allocate_memory(cap * sizeof(Type))
FUNCTION Get(i: Integer) -> Type:
if i < 0 or i >= this.size:
raise IndexOutOfBoundsException()
return this.data[i]
FUNCTION Set(i: Integer, val: Type) -> Void:
if i < 0 or i >= this.size:
raise IndexOutOfBoundsException()
this.data[i] <- val
FUNCTION InsertAt(i: Integer, val: Type) -> Void:
if this.size >= this.capacity:
raise CapacityExceededException()
if i < 0 or i > this.size:
raise IndexOutOfBoundsException()
// Right-to-left shift
for j from this.size - 1 down to i:
this.data[j + 1] <- this.data[j]
this.data[i] <- val
this.size <- this.size + 1
FUNCTION DeleteAt(i: Integer) -> Type:
if i < 0 or i >= this.size:
raise IndexOutOfBoundsException()
val <- this.data[i]
// Left-to-right shift
for j from i to this.size - 2:
this.data[j] <- this.data[j + 1]
this.size <- this.size - 1
return val6. Multi-Dimensional Array Addressing (Row-Major vs Column-Major)
#A two-dimensional array with rows and columns must be linearized into physical 1D RAM:
- Row-Major Order (C, C++, Python, Java): Consecutive elements of a row are stored adjacently.
- Column-Major Order (Fortran, MATLAB, R): Consecutive elements of a column are stored adjacently.
💡 Performance Rule:
In Row-Major languages, always iterate rows in the outer loop and columns in the inner loop (for i: for j:). Inverting this order (for j: for i:) causes a cache miss on every single memory access, degrading throughput by up to to .
Topic 21: Dynamic Arrays
#1. Conceptual Architecture & Geometric Growth
A Dynamic Array (such as C++ std::vector, Java ArrayList, or Python list) abstracts the fixed-capacity limitation of static arrays. It maintains an internal static array on the heap, automatically reallocating a larger backing buffer when capacity is exhausted.
Geometric vs. Arithmetic Resizing
How much should a dynamic array grow when it becomes full?
- Arithmetic Growth (+K slots):
Suppose capacity increases by a constant (e.g., slots) whenever full. For total insertions, the array resizes times.Dividing by operations yields an average cost of per insertion. Arithmetic growth is catastrophically slow for large collections.
- Geometric Growth ( factor, ):
Suppose capacity doubles () whenever full. For insertions, resizes occur at sizes .The total copy overhead across all insertions is strictly bounded by . Dividing by operations gives an average cost of per insertion.
2. Formal Proof: Amortized Complexity
#Method A: Aggregate Analysis
Let be the cost of the -th PushBack operation:
- If is not an exact power of 2: (write element directly).
- If (triggering doubling): (copy elements, then write the new element).
Summing the cost over consecutive insertions:
Method B: The Accounting (Banker's) Method
Assign an amortized charge (fee) of credits to each inserted element:
- credit is spent immediately to pay for its own insertion into the array slot.
- credit is deposited as savings into the element's bank account.
- credit is deposited into the bank account of an earlier element from the first half of the array that has already spent its savings.
When the array doubles from capacity to , exactly new elements have been inserted since the prior resize. Each deposited 2 credits in savings, yielding a total surplus of credits. The cost to copy all existing elements to the new buffer is exactly units of work. The accumulated credits completely pay for the reallocation with zero deficit remaining.
3. Lifecycle Walkthrough & State Tracking
#Sequence of 5 PushBack Operations (Initial Capacity = 1)
| Operation | Invocation | Pre-Op | Doubling Triggered? | New | Elements Copied | Final Buffer | Post-Op | Actual Cost |
|---|---|---|---|---|---|---|---|---|
| 1 | PushBack(10) | No | 1 | 0 | [10] | 1 | ||
| 2 | PushBack(20) | Yes () | 2 | 1 (copy 10) | [10, 20] | |||
| 3 | PushBack(30) | Yes () | 4 | 2 (copy 10, 20) | [10, 20, 30, _] | |||
| 4 | PushBack(40) | No | 4 | 0 | [10, 20, 30, 40] | 1 | ||
| 5 | PushBack(50) | Yes () | 8 | 4 (copy 10..40) | [10, 20, 30, 40, 50, _, _, _] |
Total Cumulative Operations: operations for 5 insertions.
Empirical Amortized Cost: operations per insertion .
4. Memory Thrashing & The Hysteresis Principle
A naïve deallocation strategy shrinks the buffer by half whenever . This creates a critical vulnerability known as Resize Thrashing (Hysteresis):
- Suppose current capacity is and size is .
- An insertion triggers a double to ( work).
- The next operation is a deletion (
PopBack), dropping size to . The array immediately halves back to ( work). - Another insertion immediately doubles back to ( work).
Alternating PushBack and PopBack at the threshold forces an reallocation on every single operation, destroying amortized efficiency.
Thrashing Threshold (Anti-Pattern):
PushBack -> Double to 2C (O(n))
PopBack -> Halve to C (O(n))
PushBack -> Double to 2C (O(n))
Result: O(n) worst-case per operation!The Quarter-Capacity Solution
To prevent thrashing, introduce hysteresis:
- Double capacity when .
- Halve capacity only when .
This guarantees that after halving to , at least subsequent deletions or insertions are required before another resize can trigger, restoring amortized bounds across all operations.
5. Production Dynamic Array Specification
#CLASS DynamicArray:
fields:
buffer: Array of Type
size: Integer
capacity: Integer
CONSTRUCTOR(initialCapacity: Integer = 2):
assert initialCapacity > 0
this.capacity <- initialCapacity
this.size <- 0
this.buffer <- allocate_memory(initialCapacity * sizeof(Type))
FUNCTION PushBack(val: Type) -> Void:
if this.size == this.capacity:
this.Resize(2 * this.capacity)
this.buffer[this.size] <- val
this.size <- this.size + 1
FUNCTION PopBack() -> Type:
if this.size == 0:
raise UnderflowException("Cannot pop from empty array")
this.size <- this.size - 1
val <- this.buffer[this.size]
// Anti-thrashing shrink condition
if this.size > 0 and this.size <= this.capacity / 4 and this.capacity > 4:
this.Resize(this.capacity / 2)
return val
PRIVATE FUNCTION Resize(newCapacity: Integer) -> Void:
newBuffer <- allocate_memory(newCapacity * sizeof(Type))
for i from 0 to this.size - 1:
newBuffer[i] <- this.buffer[i]
free_memory(this.buffer)
this.buffer <- newBuffer
this.capacity <- newCapacity6. Architectural Trade-offs & Comparisons
#| Metric | Static Array | Dynamic Array (vector) | Singly Linked List |
|---|---|---|---|
| Size Boundary | Fixed at allocation time | Grows geometrically | Grows element-by-element |
| Memory Locality | Maximum (single block) | Maximum (contiguous heap buffer) | Poor (scattered heap nodes) |
| Index Access () | |||
| Insert / Delete at End | (bounded by cap) | Amortized , Worst | with tail pointer |
| Insert / Delete at Head | shifts | shifts | pointer update |
| Per-Element Memory Overhead | (unused capacity) | pointer per node | |
| Cache Miss Frequency | Low ( per 64 bytes) | Low ( per 64 bytes) | High ( per node) |
7. Key Takeaways
#- Direct Memory Addressing: Arrays achieve random access through single-cycle arithmetic: .
- Hardware Symbiosis: Contiguous layouts maximize CPU L1/L2 cache-line prefetching, outperforming node-based structures by orders of magnitude for sequential scans.
- Geometric Doubling: Reallocating capacity by a geometric multiplier () yields amortized insertions; arithmetic growth () degrades performance to .
- Hysteresis Prevents Thrashing: Halving capacity at rather than prevents worst-case thrashing during alternating insertions and deletions.
- In-Place Shift Discipline: Array insertions require right-to-left shifts to prevent data corruption; deletions require left-to-right shifts.
Academic Attribution & References
#- Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2022). Introduction to Algorithms (4th ed.), Chapter 10: Elementary Data Structures, Chapter 17: Amortized Analysis. MIT Press.
- Sedgewick, R., & Wayne, K. (2011). Algorithms (4th ed.), Section 1.3: Bags, Queues, and Stacks (Resizing Arrays). Addison-Wesley.
- Hennessy, J. L., & Patterson, D. A. (2019). Computer Architecture: A Quantitative Approach (6th ed.), Chapter 2: Memory Hierarchy Design. Morgan Kaufmann.
- Knuth, D. E. (1997). The Art of Computer Programming, Volume 1: Fundamental Algorithms (3rd ed.), Section 2.2: Linear Lists. Addison-Wesley.