skip to content

In a virtual-1D binary search over a fully sorted grid, why does mid map through the column count?

level: middleimportance: should knowfreq 55%

answer

  1. reading order numbers the cells
  2. how far apart are two row starts?
  3. the stride is a row's length
  4. square grids hide the wrong divisor
  5. hi is one less than the cell count

basics

~20 s

Reading order packs cols entries into every row, so flat position mid sits at row mid / cols, column mid % cols. Dividing by the row count instead coincides only on square grids, which is why that bug survives square tests.

solid answer

~50 s

Treating a fully sorted grid as a virtual sorted sequence means numbering cells `0 .. rows*cols - 1` in reading order. The stride between one row's start and the next is the number of **columns**, so the flat index `mid` decomposes as `r = mid / cols` (integer division) and `c = mid % cols`. Dividing by `rows` is the classic bug: on a square grid the two counts are equal so every test passes, and on a wide or tall grid the probe lands on the wrong cell or out of range. Two other boundaries matter: the search window is `hi = rows*cols - 1`, not `rows*cols`, and the midpoint should be computed as `lo + (hi - lo) / 2` so it stays representable for very large grids. You never materialise a flattened copy — that would cost O(mn) time and space and defeat the whole point.

code

pseudocode · 11 lines
pseudocode
if rows == 0 or cols == 0: return NOT_FOUND
lo = 0
hi = rows * cols - 1
while lo <= hi:
    mid = lo + (hi - lo) / 2      // integer division
    r = mid / cols                // stride is cols, not rows
    c = mid % cols
    if grid[r][c] == target: return (r, c)
    if grid[r][c] < target: lo = mid + 1
    else: hi = mid - 1
return NOT_FOUND

go deeper

for a junior

Be able to write the two lines that turn a flat position into a cell and say which dimension the division uses. Know that the last valid flat position is one less than the total cell count.

for a middle

Explain the stride argument — consecutive row starts are a row length apart — and why a square grid makes the wrong divisor invisible. Mention the overflow-safe midpoint and the empty-grid guard.

for a senior

Demonstrate that you choose fixtures that can fail: non-square and degenerate shapes. Be ready to reject the technique outright for jagged input and to explain why copying into a flat sequence defeats the bound.

for a principal

Frame the mapping as a contract on the input shape — rectangular, uniformly strided — and argue for validating or normalising that contract at the boundary rather than letting a silent index bug travel through a service.

## The virtual coordinate system When a grid is fully sorted — each row non-decreasing, and every row starting at or above where the previous row ended — the reading-order sequence of its cells is one sorted sequence. Binary search only needs three things: a total order, random access by position, and a way to compare a probed position to the target. The grid already gives all three; the only missing piece is a translation between a *flat position* and a *cell*. Number the cells in reading order: cell (0,0) is flat position 0, (0,1) is 1, ..., (0, cols−1) is cols−1, and (1,0) is cols. The pattern is immediate — each new row begins exactly `cols` positions after the previous row began. So the number of complete rows before flat position `i` is `i / cols` under integer division, and what remains, `i % cols`, is the offset within that row. ``` row = mid / cols column = mid % cols ``` ## Why `rows` is the tempting wrong divisor The two dimension counts sit next to each other in the signature and both feel like "the size of the thing", so `mid / rows` gets written constantly. It is wrong for a reason worth stating precisely: the divisor must be the **stride**, the distance in flat positions between the start of consecutive rows, and that distance is the row's length — the column count. The bug is unusually durable because on a **square** grid `rows == cols`, so every probe is right and every test is green. It surfaces the first time someone searches a wide grid (say 3 rows by 40 columns, where `mid / rows` overshoots the row index by more than ten times and the column offset wraps nonsensically) or a tall one (where the computed row index stays far too small and the search converges on the wrong cell, returning a wrong "not found" or, worse, a wrong hit). Make your fixtures non-square on purpose; a 1×n and an m×1 grid are the two cheapest tests that catch it. ## The window boundaries There are `rows*cols` cells, so valid flat positions run `0` through `rows*cols − 1`. With the inclusive form of the loop (`while lo <= hi`), `hi` starts at `rows*cols − 1`. Initialising `hi = rows*cols` makes the first midpoint reachable one past the last cell on some inputs and turns a clean search into an out-of-range probe. The half-open variant (`lo < hi` with `hi = rows*cols`) is equally valid, but then the loop exits with a *candidate* rather than a verdict and you must compare the surviving position to the target once at the end. Pick one form and keep its invariant straight rather than mixing them. Use `mid = lo + (hi - lo) / 2` rather than `(lo + hi) / 2`. On a grid with a billion cells the sum can exceed the range of a fixed-width index type; the subtractive form never exceeds `hi`. ## Guard the degenerate shapes first If `rows == 0`, or if the grid has rows but `cols == 0`, then `rows*cols == 0` and `hi` becomes −1; with the inclusive loop that is harmless (the loop body never runs) but any code that probes before checking will divide by zero or index nothing. Handle the empty grid explicitly. The mapping also assumes every row has the same length. On a **jagged** grid — rows of differing lengths — there is no single stride, so flat position `i` cannot be decomposed by one division at all. The whole virtual-array technique requires a rectangular grid; if the input is jagged, you either normalise it or fall back to a different method, and saying so unprompted is a good signal. ## Never materialise the flattening A frequent misstep is to copy the grid into a real one-dimensional sequence and binary search that. It is correct, but it costs O(mn) time and O(mn) extra space to set up a search that was going to cost O(log mn) — you have paid a linear price to avoid two arithmetic operations. The virtual mapping exists precisely so that no copy happens; the flat sequence is a coordinate system, not a data structure. ## The two-stage alternative If the modular arithmetic feels error-prone, the same O(log mn) is reachable in two stages without it: binary search the **first column** for the last row whose first entry is at most the target, then binary search inside that single row. That is O(log m) + O(log n), the same total, and each stage is an ordinary binary search over an ordinary sequence. It is a perfectly good answer, and offering both shows you understand that O(log mn) and O(log m + log n) are the same bound written differently.

  • Which test fixture exposes the mid / rows bug fastest?
    Any non-square grid, and the cheapest are the degenerate ones: a single row of many columns and a single column of many rows. On a 1 x n grid, dividing by the row count leaves the row index at `mid`, immediately out of range. Square fixtures pass with the wrong divisor, so they prove nothing here.
  • Can you get O(log mn) on a fully sorted grid without any modular arithmetic?
    Yes. Binary search the first column for the last row whose leading entry does not exceed the target, then binary search that row. The cost is O(log m) + O(log n), which is the same bound as O(log mn). Both stages are plain binary searches, which some people find easier to defend under pressure.
  • Why not just copy the grid into one flat sequence and binary search that?
    The copy costs O(mn) time and O(mn) extra memory to enable a search that costs O(log mn). You would spend a linear budget to avoid one division and one remainder. The virtual mapping is the whole point: the flat index is a coordinate system, not a materialised structure.

saying these in an interview costs you the question

  • Divides the midpoint by the row count instead of the column count
  • Sets hi to rows times cols instead of one less
  • Materialises a flat copy of the grid before searching
  • Tests only with square grids and declares it correct
  • Applies the mapping to a jagged grid with unequal row lengths

context