Example of Row Major Order
Now that you know how row-major order serialises a multi-dimensional array into a flat memory ribbon, let’s cement the idea with a concrete worked example. We’ll use a three-dimensional array — one dimension more than you’re used to — so the pattern really sinks in.
The Array: 2 × 2 × 3
Consider a 3D integer array with these dimensions:
| Dimension | Symbol | Size |
|---|---|---|
| Outermost (layer) | D₃ | 2 |
| Middle (row) | D₂ | 2 |
| Innermost (column) | D₁ | 3 |
Think of it as 2 layers, each layer being a 2×3 grid. The logical representation looks like this:
---
config:
theme: base
themeVariables:
primaryColor: "#dbeafe"
primaryBorderColor: "#3b82f6"
primaryTextColor: "#1e3a5f"
lineColor: "#64748b"
secondaryColor: "#ede9fe"
tertiaryColor: "#fef9c3"
---
flowchart TB
subgraph L0["Layer 0 ── D₃ = 0"]
direction LR
subgraph R00["D₂ = 0"]
direction LR
a000["[0][0][0]"] --- a001["[0][0][1]"] --- a002["[0][0][2]"]
end
subgraph R01["D₂ = 1"]
direction LR
a010["[0][1][0]"] --- a011["[0][1][1]"] --- a012["[0][1][2]"]
end
R00 ~~~ R01
end
subgraph L1["Layer 1 ── D₃ = 1"]
direction LR
subgraph R10["D₂ = 0"]
direction LR
a100["[1][0][0]"] --- a101["[1][0][1]"] --- a102["[1][0][2]"]
end
subgraph R11["D₂ = 1"]
direction LR
a110["[1][1][0]"] --- a111["[1][1][1]"] --- a112["[1][1][2]"]
end
R10 ~~~ R11
end
L0 ~~~ L1
Logical representation of the 3D array (D₃=2, D₂=2, D₁=3) — 2 layers, each a 2×3 grid, totalling 12 elements.
Each element is identified by three indices: [layer][row][column], or [I₃][I₂][I₁].
Layout in Memory
Now we flatten this 3D structure into a single linear strip. Row-major order applies the same rule as always:
The lowest dimension (D₁ — column) moves the fastest. The highest dimension (D₃ — layer) moves the slowest.
Think of the three indices as an odometer with three digits. The rightmost digit (D₁ — column) ticks up on every step. The middle digit (D₂ — row) only increments when the column overflows. The leftmost digit (D₃ — layer) only increments when both column and row overflow.
The complete traversal order looks like this:
---
config:
theme: base
themeVariables:
primaryColor: "#dbeafe"
primaryBorderColor: "#3b82f6"
primaryTextColor: "#1e3a5f"
lineColor: "#64748b"
secondaryColor: "#ede9fe"
tertiaryColor: "#fef9c3"
---
flowchart LR
subgraph L0["Layer 0 (D₃=0)"]
direction LR
subgraph L0R0["D₂=0 — D₁ races 0→1→2"]
direction LR
a000["[0][0][0]"] --> a001["[0][0][1]"] --> a002["[0][0][2]"]
end
subgraph L0R1["D₂=1 — D₁ races 0→1→2"]
direction LR
a010["[0][1][0]"] --> a011["[0][1][1]"] --> a012["[0][1][2]"]
end
a002 -->|"D₂ increments"| a010
end
subgraph L1["Layer 1 (D₃=1)"]
direction LR
subgraph L1R0["D₂=0 — D₁ races 0→1→2"]
direction LR
a100["[1][0][0]"] --> a101["[1][0][1]"] --> a102["[1][0][2]"]
end
subgraph L1R1["D₂=1 — D₁ races 0→1→2"]
direction LR
a110["[1][1][0]"] --> a111["[1][1][1]"] --> a112["[1][1][2]"]
end
a102 -->|"D₂ increments"| a110
end
a012 -->|"D₃ increments"| a100
The lowest dimension D₁ moves the fastest in row-major order — it completes a full sweep before D₂ ticks up, and D₂ completes a full sweep before D₃ ticks up.
The resulting serialisation order is:
[0][0][0] → [0][0][1] → [0][0][2] → [0][1][0] → [0][1][1] → [0][1][2]
→ [1][0][0] → [1][0][1] → [1][0][2] → [1][1][0] → [1][1][1] → [1][1][2]
Twelve elements, one after another, lowest dimension cycling fastest.
Structure in Memory
Let’s now map this onto actual memory. Assume:
- Base address = 2
- Element size = 4 bytes (integer)
Each element occupies 4 bytes. The address of the element at offset k is:
address = 2 + k × 4
Here is exactly what the array looks like in memory, laid out slot by slot:
---
config:
theme: base
themeVariables:
primaryColor: "#dbeafe"
primaryBorderColor: "#3b82f6"
primaryTextColor: "#1e3a5f"
lineColor: "#64748b"
secondaryColor: "#ede9fe"
tertiaryColor: "#fef9c3"
---
block-beta
columns 6
A["[0][0][0]<br/>addr 2"]
B["[0][0][1]<br/>addr 6"]
C["[0][0][2]<br/>addr 10"]
D["[0][1][0]<br/>addr 14"]
E["[0][1][1]<br/>addr 18"]
F["[0][1][2]<br/>addr 22"]
G["[1][0][0]<br/>addr 26"]
H["[1][0][1]<br/>addr 30"]
I["[1][0][2]<br/>addr 34"]
J["[1][1][0]<br/>addr 38"]
K["[1][1][1]<br/>addr 42"]
L["[1][1][2]<br/>addr 46"]
Structure of the 3D array stored in row-major order in memory — 12 elements, base address 2, each element 4 bytes wide.
Notice the pattern: Layer 0 occupies addresses 2–22, Layer 1 occupies 26–46. Within each layer, Row 0 comes first, Row 1 second. Within each row, the column index climbs 0→1→2.
# Verify the memory layout formula
base = 2
element_size = 4
D3, D2, D1 = 2, 2, 3
print(f"{'Index':<16} {'Offset':>6} {'Address':>8}")
print("-" * 32)
for i3 in range(D3):
for i2 in range(D2):
for i1 in range(D1):
offset = i3 * (D2 * D1) + i2 * D1 + i1
address = base + offset * element_size
print(f"[{i3}][{i2}][{i1}] {offset:>6} {address:>6}")
Calculating the Address of Elements
When you write array[0][0][2] or array[1][1][2] in code, the subscript operator silently computes the memory address using the formula we derived in the previous lesson — now extended to three dimensions:
offset = I₃ × (D₂ × D₁) + I₂ × D₁ + I₁
address = base + offset × element_size
Each term in the offset formula represents one “level” of skipping:
- I₃ × (D₂ × D₁) — skip past I₃ complete layers, each of size D₂ × D₁
- I₂ × D₁ — skip past I₂ complete rows within the current layer, each of size D₁
- I₁ — step I₁ positions into the current row
Let’s work through both examples:
---
config:
theme: base
themeVariables:
primaryColor: "#dbeafe"
primaryBorderColor: "#3b82f6"
primaryTextColor: "#1e3a5f"
lineColor: "#64748b"
secondaryColor: "#ede9fe"
tertiaryColor: "#fef9c3"
---
flowchart LR
subgraph Calc1["array[0][0][2] — I₃=0, I₂=0, I₁=2"]
direction TB
C1A["Layer skip<br/>I₃ × (D₂×D₁) = 0 × (2×3) = 0"]
C1B["Row skip<br/>I₂ × D₁ = 0 × 3 = 0"]
C1C["Column step<br/>I₁ = 2"]
C1D["offset = 0 + 0 + 2 = 2"]
C1E["address = 2 + (2 × 4) = 10 ✓"]
C1A --> C1B --> C1C --> C1D --> C1E
end
subgraph Calc2["array[1][1][2] — I₃=1, I₂=1, I₁=2"]
direction TB
C2A["Layer skip<br/>I₃ × (D₂×D₁) = 1 × (2×3) = 6"]
C2B["Row skip<br/>I₂ × D₁ = 1 × 3 = 3"]
C2C["Column step<br/>I₁ = 2"]
C2D["offset = 6 + 3 + 2 = 11"]
C2E["address = 2 + (11 × 4) = 46 ✓"]
C2A --> C2B --> C2C --> C2D --> C2E
end
Calculating the base address for array[0][0][2] (offset 2, address 10) and array[1][1][2] (offset 11, address 46) using the subscript operator formula.
Cross-check against the memory layout diagram above: array[0][0][2] is at address 10 ✓ and array[1][1][2] is the very last element at address 46 ✓.
# Subscript operator formula — verify both elements
base = 2
element_size = 4
D2, D1 = 2, 3 # layer size is D2 × D1
def address_of(i3, i2, i1):
offset = i3 * (D2 * D1) + i2 * D1 + i1
return base + offset * element_size, offset
addr, offset = address_of(0, 0, 2)
print(f"array[0][0][2] → offset={offset}, address={addr}") # expect 2, 10
addr, offset = address_of(1, 1, 2)
print(f"array[1][1][2] → offset={offset}, address={addr}") # expect 11, 46
Dereferencing the Value
Once the subscript operator resolves the address, the program still needs to read the actual value stored there. This step is called dereferencing.
The language already knows two things:
- The starting address — computed by the formula above
- The datatype size — for
int, that’s 4 bytes
It simply reads element_size consecutive bytes starting at that address and interprets the bit pattern as the stored datatype:
---
config:
theme: base
themeVariables:
primaryColor: "#dbeafe"
primaryBorderColor: "#3b82f6"
primaryTextColor: "#1e3a5f"
lineColor: "#64748b"
secondaryColor: "#ede9fe"
tertiaryColor: "#fef9c3"
---
flowchart LR
Addr["Resolved address<br/>e.g. 10"]
Read["Read 4 consecutive bytes<br/>from address 10, 11, 12, 13"]
Interp["Interpret as the stored datatype<br/>(int → 4 bytes → one integer value)"]
Val["Return value to caller<br/>array[0][0][2] = value ✓"]
Addr --> Read --> Interp --> Val
Dereferencing — once the address is known, the language reads element_size bytes starting there and interprets them according to the stored datatype.
The same mechanism works for any datatype — float, double, char, or even a struct. The only thing that changes is how many bytes are read and how those bytes are interpreted. The array indexing formula itself stays identical.
Key Takeaways
- A 3D array
D₃ × D₂ × D₁is flattened layer by layer, row by row, column by column - D₁ (innermost) moves fastest; D₃ (outermost) moves slowest — same odometer rule for any N dimensions
- The 3D address formula:
base + (I₃ × D₂ × D₁ + I₂ × D₁ + I₁) × element_size - After the address is computed, the language reads
element_sizebytes from that address and interprets them as the stored type — this is dereferencing - All of this happens invisibly every time you write
arr[i][j][k]— the subscript operator does the maths for you