skip to content

questions

8

Why does a flat pixel buffer index as y*width+x, and what breaks if you write x*width+y?

level: juniorimportance: must knowfreq 65%

answer

  1. picture how the rows sit in memory
  2. one row is a contiguous run of slots
  3. how many slots to skip per row
  4. the multiplier is the row stride
  5. swapping the two factors transposes the grid

basics

~20 s

A flat row-major buffer stores each row as width consecutive slots, so the cell at column x, row y lives at ywidth+x. Writing xwidth+y addresses the transposed cell: harmless-looking on square images, corrupting or overrunning everything else.

solid answer

~60 s

Memory is one long line of slots, so a grid has to be flattened. In row-major order, row 0 occupies the first `width` slots, row 1 the next `width`, and so on; reaching row `y` therefore means skipping `y * width` slots and then stepping `x` into that row. The multiplier is the row stride, and it must be the width — the length of the thing you skip whole copies of. `x * width + y` swaps the roles: it is the address of the cell at column y, row x, so the picture comes out reflected across the main diagonal. On a square image the formula is still a one-to-one map into the buffer, so nothing crashes and the only symptom is a transposed picture that symmetric test fixtures hide. On a non-square image it stops being one-to-one: a grid wider than it is tall runs indices past the end of the buffer, and one taller than wide aliases distinct cells onto the same slot while leaving other slots untouched.

code

pseudocode · 11 lines
pseudocode
// pixels and src each hold width*height slots, row-major
for y in 0..height-1
    for x in 0..width-1
        idx = y * width + x        // skip y whole rows, then x slots
        pixels[idx] = gray(src[idx])
...
// the version that shipped:
for y in 0..height-1
    for x in 0..width-1
        idx = x * width + y        // addresses the cell at column y, row x
        pixels[idx] = gray(src[idx])

go deeper

for a junior

Be ready to derive the index rule out loud rather than recite it: skip whole rows first, then step within the row. Say which factor is the stride and what happens to the picture when the two factors are swapped.

for a middle

Explain why the correct rule is a one-to-one map onto the buffer, and walk the three shapes — square, wider than tall, taller than wide — showing that the same bug transposes, overruns, and aliases respectively.

for a senior

Show how you stop this class of bug reaching production: a single addressing helper, a non-square asymmetric fixture in the suite, and a stride stored separately from the width so padding can be introduced without a sweep of every access site.

for a principal

Own the convention. Decide once whether the codebase speaks in column-row or row-column order, encode it in the type or the helper signature so the two cannot be mixed, and be able to say what that consistency is worth against the cost of a silent corruption reaching users.

## A grid is a fiction laid over a line Addressable memory is one-dimensional: a numbered sequence of slots. A two-dimensional grid is something we impose on that line by agreeing where each row starts. A **flat buffer** representation makes the agreement explicit — allocate `width * height` slots once, and define an addressing rule that turns a `(column, row)` pair into a single offset. **Row-major order** is the usual agreement: the whole of row 0 first, then the whole of row 1, and so on. To reach the cell at column `x`, row `y`, you skip `y` complete rows — that is `y * width` slots — and then step `x` slots into the row you landed on: ``` index = y * width + x ``` The multiplier is called the **row stride**: how far apart, in slots, two vertically adjacent cells sit. In the simplest layout the stride equals the width, because rows are packed with no gap between them. ## The property that makes an addressing rule correct An addressing rule is correct when it is a **bijection**: every valid `(x, y)` with `0 <= x < width` and `0 <= y < height` maps to a distinct slot, and together they cover exactly `0 .. width*height - 1`. Check `y * width + x`: the largest value is `(height-1) * width + (width-1) = width*height - 1`, and no two pairs collide because `x` never reaches `width`, so the `x` part can never carry into the row part. Exactly the buffer, exactly once. ## Why the transposed formula is so quiet `x * width + y` is also a formula, also produces integers, and is not obviously wrong when read. It is precisely the address that `y * width + x` would give for the cell `(y, x)` — so writing it transposes the image. It fails in three different ways depending on the shape: - **Square grid (`width == height`).** Still a bijection. Nothing goes out of range, nothing is overwritten twice, and every slot is written. The only symptom is that the result is reflected across the main diagonal. If your test fixtures are a solid fill, a centred circle, or a checkerboard, they are diagonally symmetric and the tests pass. - **Wider than tall (`width > height`).** The largest index becomes `(width-1) * width + (height-1)`, which is roughly `width^2` — larger than the buffer's `width * height`. The write runs past the end of the buffer: at best a detected out-of-range access, at worst silent corruption of whatever sits after it. - **Taller than wide (`width < height`).** Indices stay in range but stop being distinct. With `width = 2, height = 5`, cell `(0, 2)` maps to `2` and cell `(1, 0)` also maps to `2`. Two logical cells fight over one slot, and slots `7, 8, 9` are never touched at all — the classic "the output has stripes of garbage" bug. That progression is why this bug reaches production. It is invisible on the square fixtures in the test suite, and only the third case looks like a memory bug loud enough to investigate. ## The deeper cause: two naming conventions in one file Most occurrences are not arithmetic errors, they are convention collisions. Image work says `(x, y)` with `x` horizontal; grid and matrix work says `(row, col)` with row first. Both are fine alone; mixing them in the same code means the reader has to remember which of two adjacent identifiers is the one that gets multiplied. The rule that survives review is: **the index that is multiplied by the stride is always the one that selects the row**, whatever it is called. ## Defences that actually work - **One addressing helper, used everywhere.** If the multiplication appears in forty places, it will be wrong in one of them. If it appears once, it is either right everywhere or wrong everywhere — and "wrong everywhere" fails immediately. - **A non-square, asymmetric fixture.** Test on a grid whose width and height differ *and* whose content is not diagonally symmetric — a left-to-right brightness ramp, for instance. That fixture fails on the transposed formula in every one of the three cases above. - **Assert the bijection cheaply.** In a debug build, walking every `(x, y)` and marking the slot it maps to should mark every slot exactly once. - **Separate the stride from the width.** Layouts often pad each row so it begins on an aligned boundary, making the stride larger than the width. Storing the stride as its own value, rather than reusing the width, keeps the addressing rule honest when padding is introduced later. ## Generalizing The same construction extends to any number of dimensions: pick an order for the axes, and each axis's multiplier is the product of the sizes of all axes that vary faster than it. For a volume of `width` by `height` by `depth` with `x` fastest, the address is `(z * height + y) * width + x`. The rule never changes — a multiplier is the size of the block you are skipping whole copies of.

  • Your rows are padded so each one starts on an aligned boundary. How does the addressing rule change?
    The multiplier stops being the width and becomes the stride — the padded distance from the start of one row to the start of the next, which is at least the width. The rule becomes `index = y * stride + x`, and the buffer needs `stride * height` slots. Keeping the stride as its own stored value, rather than reusing the width, is what lets padding be added later without touching every access.
  • How does the same construction extend to a three-dimensional volume buffer?
    Pick which axis varies fastest and give each axis a multiplier equal to the product of the sizes of all faster axes. With `x` fastest, then `y`, then `z`, the address is `(z * height + y) * width + x`. The correctness test is unchanged: over all valid coordinate triples the rule must hit every slot in `0 .. width*height*depth - 1` exactly once.
  • What single test fixture would have caught the transposed index before release?
    A grid whose width and height differ and whose content is not diagonally symmetric — for example a horizontal brightness ramp on a wide, short grid. Non-square breaks the bijection, so the bug either runs past the buffer or aliases cells; asymmetric content means even the in-range case produces visibly wrong output rather than an identical-looking result.

A flat buffer is a bookshelf holding one long row of books. To find page x of chapter y you skip y whole chapters — y times the chapter length — and then count x pages in. Swapping which number gets multiplied by the chapter length sends you to a completely different book.

saying these in an interview costs you the question

  • Treats y*width+x and x*width+y as interchangeable
  • Cannot say what the multiplier in the index formula represents
  • Believes a flat row-major buffer stores columns contiguously
  • Validates the layout only on square test grids
  • Assumes an out-of-range index always crashes rather than corrupting
  • Reuses the width as the stride even when rows are padded

context

open as a page

Why does transposing a square grid then reversing each row rotate it a quarter turn clockwise?

level: juniorimportance: must knowfreq 78%

basics

~20 s

Transposing mirrors the grid across its main diagonal, which puts every element in its correct destination row but with the columns running backwards. Reversing each row fixes that order. Two mirrors at 45 degrees to each other compose into one quarter turn.

open as a page

Why can a column-order scan of a huge row-major elevation grid run an order of magnitude slower than a row-order scan?

level: middleimportance: should knowfreq 58%

basics

~20 s

Both loops touch the same number of cells, but row-major storage puts a row's cells next to each other. A row-order scan walks memory one slot at a time; a column-order scan jumps a full row width per step, so each access lands in a different region.

open as a page

Reviewing this in-place transpose, what does swapping m[i][j] and m[j][i] over every i and j produce?

level: middleimportance: should knowfreq 52%

basics

~20 s

Nothing changes. Each off-diagonal pair gets swapped twice, once as (i, j) and again as (j, i), so the second swap undoes the first, and diagonal swaps are no-ops. The inner loop must start at j = i + 1.

open as a page

In a spiral-order walk with top, bottom, left and right bounds, why recheck the bounds mid-lap?

level: middleimportance: should knowfreq 58%

basics

~20 s

A lap can exhaust the region halfway through. Once the top row and right column are consumed, what remains may be a single row or column, and without rechecking the bounds the return passes walk it twice.

open as a page

For a spreadsheet-like grid, when does one flat buffer beat an array of separate row arrays?

level: seniorimportance: should knowfreq 45%

basics

~20 s

One flat buffer wins when whole-grid passes dominate: a single allocation, contiguous scans, no per-access indirection. Separate row arrays win when the structure changes at row granularity — reordering, inserting or sharing rows costs a handle move instead of copying every cell.

open as a page

For a quarter-turn board rotation on memory-tight devices, how do you choose in-place cycling over a fresh copy?

level: principalimportance: should knowfreq 38%

basics

~20 s

Ask first whether the original must survive and whether the board is square: those two answers usually decide it outright. Only when peak memory is genuinely the binding constraint is the harder in-place ring cycle worth its review cost, and transpose-plus-reverse is already in-place anyway.

open as a page

In a jagged schedule table with a different slot count per day, which grid operations silently break?

level: middleimportance: nice to knowfreq 28%

basics

~20 s

Anything that treats one row's length as the table's width: column reads, per-column aggregates, transposition, and neighbour lookups. A jagged table has no single column count, so those operations either read past a short row or quietly ignore the tail of a long one.

open as a page