skip to content

The juggling rotation writes each element once — why does three-reversal often still win?

level: seniorimportance: nice to knowfreq 30%

answer

  1. both are linear; ignore the write count
  2. ask how each one walks memory
  3. juggling jumps by the shift each step
  4. gcd cycles scatter across the whole array
  5. prefetchers reward predictable order

basics

~20 s

Both are O(n) time and O(1) extra space, so asymptotics cannot separate them. Juggling halves the writes but strides through memory in gcd-sized cycles, and those scattered touches cost more than the extra sequential pass reversal pays for.

solid answer

~50 s

Juggling walks index orbits: from each of `gcd(n, s)` starting points it follows `i`, `i+s`, `i+2s` modulo n, carrying one saved element around the cycle, so each element is written exactly once — n writes against reversal's 2n. Both are O(n) time and O(1) auxiliary space, so the asymptotic labels are identical and the decision belongs entirely to constants and memory behaviour. Reversal's three passes are converging sequential scans that prefetchers handle well; juggling's stride of s jumps across the array every step, so once the data exceeds cache nearly every step costs a miss, and on memory-mapped data a translation miss too. Halving the writes does not pay for that. Juggling wins when the array fits in cache, when elements are large enough that move count dominates, or when a measurement says so — and it always costs review time, since a cycle argument is harder to check than three reversals.

code

pseudocode · 15 lines
pseudocode
n = length(a)
s = s mod n
g = gcd(n, s)
for i in 0..g-1
    saved = a[i]
    j = i
    while true
        d = j + s
        if d >= n
            d = d - n
        if d == i
            break
        a[j] = a[d]
        j = d
    a[j] = saved

go deeper

for a junior

Know that a rotation can be done in place in linear time without a second array, and that more than one such method exists.

for a middle

Explain the cycle structure — gcd(n, s) orbits each of length n/gcd(n, s) — and state the write counts of both approaches accurately.

for a senior

Demonstrate that identical asymptotics leave the decision to constants and memory locality, and describe the measurement that would settle it on your data.

for a principal

Own the maintainability side of the call: an unmeasured constant-factor win rarely justifies a routine whose correctness argument a reviewer cannot check quickly.

## The two candidates Both rotate an array in place with no auxiliary array, and both are O(n) time and O(1) extra space. **Three-reversal:** reverse the whole array, then reverse the two blocks. Three converging two-pointer scans, 2n element writes. **Juggling (cycle) rotation:** move each element directly to its destination, chasing the permutation's cycles. For a left rotation by s, the element at index i belongs at index `(i - s) mod n`, so following `i -> (i + s) mod n` walks a cycle of source positions. Save the first element, shift each successor into the hole, and close the cycle with the saved value. n element writes. ## Why there are exactly gcd(n, s) cycles Starting at index i and repeatedly adding s modulo n, you return to i after the smallest t with `t*s = 0 (mod n)`, which is `t = n / gcd(n, s)`. Every orbit therefore has the same length `n / gcd(n, s)`, and since the orbits partition all n indices, there are `gcd(n, s)` of them. That is why the outer loop runs over `0 .. gcd(n, s) - 1`: those starting points hit each cycle exactly once. When `gcd(n, s) = 1` the whole array is a single cycle; when s divides n there are s short cycles. Computing the gcd is O(log min(n, s)) — irrelevant next to the n moves, so no, juggling is not superlinear because of it. ## Where the real difference lives Since both are O(n) time and O(1) space, big-O says nothing about which to choose. Asymptotic notation is an upper bound on growth, not a promise about runtime on a particular machine, and here the growth rates are identical. The decision is made entirely by constants and by the memory hierarchy. | | three-reversal | juggling | |---|---|---| | element writes | 2n | n | | access pattern | sequential, converging | stride s, wrapping | | cache behaviour | prefetch-friendly | a miss per step once n exceeds cache | | cycles/passes | 3 passes | gcd(n, s) cycles | | review cost | three calls to a trusted primitive | cycle and gcd argument to verify | On an array comfortably inside cache, memory is effectively uniform-cost and the write count is the whole story — juggling's n moves beat 2n. Once the array is much larger than cache, each juggling step lands on an unrelated cache line, and the machine pays a miss for nearly every element; on very large or memory-mapped data the stride can also cross page boundaries constantly, adding translation misses and faults. Reversal's three passes stream through the same total bytes twice in perfectly predictable order, and predictable order is what the prefetcher rewards. Two sequential passes routinely beat one scattered pass by a wide margin. ## When juggling is the right answer - The array fits in cache, so scattering costs nothing. - Elements are large records, so the byte cost of *moving* dominates and halving the moves halves the real work. - Writes themselves are expensive relative to reads — for instance storage with limited write endurance, where write amplification is the metric being minimised. - A benchmark on the actual data says so. This is a constants question, and constants are measured, not derived. ## The judgment being tested The wrong answer is "fewer operations is faster", stated as a law. The right answer names the model the operation count assumes — uniform memory cost — and points out that the model stops holding at exactly the sizes where rotation performance starts to matter. The second half of the answer is about people: the three-reversal version is three calls to a primitive any reviewer can check, while the juggling version hides its correctness in a cycle-counting argument and an easy-to-invert direction convention. On code that will be maintained by an on-call rotation, that difference is worth more than a constant factor you have not measured.

  • How many cycles does the juggling rotation traverse, and why that number?
    Exactly `gcd(n, s)`. Repeatedly adding s modulo n returns to the start after `n / gcd(n, s)` steps, so every orbit has that length; since the orbits partition all n indices, there must be `gcd(n, s)` of them. With `gcd(n, s) = 1` the entire array is one cycle, which is why the outer loop must exist at all — it visits one starting point per cycle.
  • Does either rotation change the asymptotic space bound?
    Neither. Juggling holds one saved element plus a few indices, and three-reversal holds two pointers plus a swap temporary — both O(1) auxiliary. The only footnote is a recursively written gcd, whose depth is O(log min(n, s)) stack frames; negligible in practice, but worth naming since recursion depth is space.
  • How would you settle the choice on a specific workload?
    Measure, because this is a constant-factor question and both are O(n). Benchmark at sizes that straddle the cache boundary, with the real element size, and watch cache-miss counters rather than only the wall clock. Absent a measurement showing a meaningful win, take three-reversal: equal asymptotics, far cheaper to review and to fix at three in the morning.

Collecting mail from every seventh house on a long street means constant walking; two sweeps down the street end to end move more letters but far fewer steps.

saying these in an interview costs you the question

  • Says fewer writes always means faster
  • Claims juggling is superlinear because of the gcd
  • Thinks the number of cycles is always k
  • Assumes three-reversal needs O(n) extra space
  • Says cache behaviour is irrelevant once both are O(n)
  • Picks the cleverer routine without measuring anything

context