DSA DSA In Depth
0%
lesson arraysin-placerotations

In-Place Array Rotations (Juggling/Reversal) & Matrix Transposition

7 min read Module 1.2: Contiguous Arrays, Bit Manipulation & Pointer Idioms Updated Oct 23, 2026
📄 Cheat Sheet
In-Place Array Rotation via Three-Reversal Involution
Nick White In-Place Array Rotation via Three-Reversal Involution Watch on YouTube ↗
0:00 / --:--

Three-Reversal Involution vs. $\gcd(N, k)$ Juggling Cycles

Consider rotating an array $A = [X \;\vert\; Y]$ so that a prefix $X$ of length $n - k$ and a suffix $Y$ of length $k$ swap positions to become $Y X$ (a right rotation by $k \equiv k \bmod n$).In abstract algebra, reversing an array is an involution ($R(R(A)) = A$) that reverses the order of concatenated blocks: $(X Y)^R = Y^R X^R$. Therefore, reversing $X$ and $Y$ individually and then reversing the whole array (or reversing the whole array first, then reversing the first $k$ and last $n-k$ elements) yields:

$$\left( X^R Y^R \right)^R = (Y^R)^R (X^R)^R = Y X$$

Alternatively, consider Bentley's Juggling Algorithm: moving each element directly from index $i$ to $(i + k) \bmod n$ traces out cyclic orbits under the subgroup generated by $+k$ in $\mathbb{Z}_n$. By elementary group theory (Lagrange's theorem), the subgroup $\langle k \rangle \subseteq \mathbb{Z}_n$ has size $L = \frac{n}{\gcd(n, k)}$ and partitions the $n$ indices into exactly $g = \gcd(n, k)$ disjoint cycles, each of length $n / g$! Starting at each $s \in [0, g - 1]$ and chasing the cycle moves every element exactly once ($N$ writes vs. $3N$ reads/writes in 3-Reversal).

Dihedral Group $D_4$ Invariant: $90^\circ$ Matrix Rotation = Transpose $\circ$ Reflection

The symmetries of a square $N \times N$ grid form the dihedral group $D_4$ of order 8, generated by two reflections: diagonal transposition $T(r, c) = (c, r)$ and horizontal row reversal $H(r, c) = (r, N - 1 - c)$. Their composition produces a $90^\circ$ clockwise rotation:

$$(H \circ T)(r, c) = H(c, r) = (c,\; N - 1 - r)$$

Conversely, reversing each row first and then transposing ($T \circ H$) produces a $90^\circ$ counter-clockwise rotation $(N - 1 - c, r)$. What about transposing a non-square $R \times C$ matrix stored in a 1D row-major buffer of size $R \cdot C$? An element at $(r, c)$ starts at index $i = rC + c$ and moves to $(c, r)$ at index $i' = cR + r$. Observe that for every $i < RC - 1$:

$$i' = cR + r = cR + \lfloor i / C \rfloor \equiv R \cdot i \pmod{RC - 1}$$

Thus in-place rectangular transposition is simply cycle-following on the multiplicative permutation $i \mapsto (R \cdot i) \bmod (RC - 1)$!

Comparison of In-Place Rotation & Transposition Algorithms

Algorithm Time Complexity Element Moves Cache Locality Profile Best Use Case
3-Reversal Rotation $O(N)$ $\approx 1.5 N$ swaps ($3N$ writes) Sequential stride-1 (L1 prefetcher optimal) Contiguous arrays in RAM (std::rotate on random-access)
Juggling ($gcd(N, k)$ Cycles) $O(N)$ $N + \gcd(N, k)$ moves Stride-$k$ jumps (TLB/cache thrashing for large $k$) Large objects where move construction is expensive
Gries-Mills Block Swap $O(N)$ $\approx 2N$ moves Sequential block swaps Forward iterators without random access
Cache-Tiled $B \times B$ Transpose $O(N^2)$ $N^2$ moves Zero conflict misses when $2 B^2 \cdot \text{sizeof}(T) \le \text{L1d}$ Large matrices ($N \ge 512$)

Visualizing 3-Reversal Rotation & $D_4$ Matrix Transpose + Reflect

Right-Rotate by k = 3 via 3 Reversals: [1 2 3 4 | 5 6 7] --> [5 6 7 | 1 2 3 4] 0. Initial: X = [1, 2, 3, 4] Y = [5, 6, 7] 1. Rev All: Y^R = [7, 6, 5] X^R = [4, 3, 2, 1] 2. Rev Parts: Y = [5, 6, 7] X = [1, 2, 3, 4] 90-Deg Clockwise Rotation = Transpose + Reverse Rows (r, c) Transpose --> 1. (c, r) 2. (c, N - 1 - r) Juggling Orbit Decomposition: gcd(N=6, k=4) = 2 disjoint cycles: (0 -> 4 -> 2 -> 0) and (1 -> 5 -> 3 -> 1) Cache Warning: For large k, Juggling strides by k across RAM; 3-Reversal streams at stride 1 (~3x faster in L1d)!

Juggling Cycle Decomposition ($gcd(N, k)$ Orbits)

flowchart LR A["Compute k %= N
g = std::gcd(N, k)"] --> B["For start = 0 .. g - 1
tmp = A[start], cur = start"] B --> C["next = (cur + N - k) % N
(Pull-chain for right rotate)"] C --> D{"next == start?"} D -- "No" --> E["A[cur] = move(A[next])
cur = next"] E --> C D -- "Yes (Cycle closed)" --> F["A[cur] = move(tmp)"]
Hardware Reality: Why 3-Reversal Beats Juggling in RAM & Power-of-2 Matrix Cache Thrashing

Even though Juggling performs $N$ writes while 3-Reversal performs $3N$ writes, benchmarked on $N = 10^7$ int32_t elements in DRAM, 3-Reversal is $2.5\times$ to $4\times$ faster than Juggling! Why? Because 3-Reversal accesses memory strictly sequentially (stride $pm 1$) and can be vectorized with AVX2 _mm256_permutevar8x32_epi32, utilizing all 64 bytes of every fetched cache line. Juggling jumps by stride $k$; when $k \ge 16$, every single read/write touches a different 64-byte cache line and different 4 KB virtual page, causing catastrophic L1d and dTLB thrashing. Similarly, naive $N \times N$ matrix transposition reads column $A[0 \dots N-1][c]$ at stride $N$: when $N = 1024$ (a power of two), every row is separated by $4096$ bytes, mapping to the exact same L1d cache set (8-way set-associative conflict misses). Processing the matrix in $32 \times 32$ cache-resident tiles completely eliminates these conflict misses.

2D Prefix Inclusion-Exclusion, Difference Array & Kadane Subarray
O(1) Range Query / O(N) Kadane

Trace 2D prefix-sum inclusion-exclusion P[r2][c2] - P[r1-1][c2] - P[r2][c1-1] + P[r1-1][c1-1], O(1) difference-array range updates, and Kadane circular max subarray.

Step 1 / 7
Custom Input:
Timeline

Invariant & State

Kadane Circular Duality: A wrapping subarray equals the complement of a contiguous middle subarray, so max_wrap = TotalSum - minSubarray.
📊 Live Magnitude & Pointer Stage — 2D Prefix Inclusion-Exclusion, Difference Array & Kadane Subarray Active Pivot Match/Sorted
Prefix 5 [0] Min Sub -3 [1] Suffix 5 [2] Total(7 Circul… [3]
[0]5Prefix
[1]-3Min Subarray = -3
[2]5Suffix
[3]Circular Wrap: [5] + [5] = 10Total(7) - Min(-3) = 10
Standard Kadane Max
7 ([5, -3, 5])
Minimum Subarray
-3 ([-3])
Circular Max (Total - Min)
7 - (-3) = 10
[Step 1] Computed both maxSubarray=7 and minSubarray=-3; circular wrap achieves Total - Min = 10.

Production Implementation: 3-Reversal, Juggling & Cache-Tiled Square Transpose

#include <algorithm>
#include <cstdint>
#include <numeric>
#include <utility>
#include <vector>

// Cache-friendly 3-Reversal Right Rotation by k positions
template <typename T>
void rotate_right_reversal(std::vector<T>& a, std::size_t k) noexcept {
  const std::size_t n = a.size();
  if (n <= 1) return;
  k %= n;
  if (k == 0) return;

  std::reverse(a.begin(), a.end());
  std::reverse(a.begin(), a.begin() + static_cast<std::ptrdiff_t>(k));
  std::reverse(a.begin() + static_cast<std::ptrdiff_t>(k), a.end());
}

// Minimum-Move Juggling Left Rotation by k positions using gcd(n, k) cycles
template <typename T>
void rotate_left_juggling(std::vector<T>& a, std::size_t k) noexcept {
  const std::size_t n = a.size();
  if (n <= 1) return;
  k %= n;
  if (k == 0) return;

  const std::size_t cycles = std::gcd(n, k);
  for (std::size_t start = 0; start < cycles; ++start) {
    T tmp = std::move(a[start]);
    std::size_t cur = start;
    while (true) {
      std::size_t next = cur + k;
      if (next >= n) next -= n; // faster than % n!
      if (next == start) break;
      a[cur] = std::move(a[next]);
      cur = next;
    }
    a[cur] = std::move(tmp);
  }
}

// In-Place 90-Degree Clockwise Matrix Rotation (Cache-Tiled Transpose + Row Reverse)
void rotate_matrix_90_cw(std::vector<int>& mat, std::size_t n) noexcept {
  constexpr std::size_t TILE = 32;
  // Step 1: Tiled In-Place Transpose across upper triangle (r < c)
  for (std::size_t r0 = 0; r0 < n; r0 += TILE) {
    for (std::size_t c0 = r0; c0 < n; c0 += TILE) {
      const std::size_t r_end = std::min(r0 + TILE, n);
      const std::size_t c_end = std::min(c0 + TILE, n);
      for (std::size_t r = r0; r < r_end; ++r) {
        const std::size_t c_start = (r0 == c0) ? (r + 1) : c0;
        for (std::size_t c = c_start; c < c_end; ++c) {
          std::swap(mat[r * n + c], mat[c * n + r]);
        }
      }
    }
  }
  // Step 2: Reverse each row sequentially
  for (std::size_t r = 0; r < n; ++r) {
    auto row_begin = mat.begin() + static_cast<std::ptrdiff_t>(r * n);
    std::reverse(row_begin, row_begin + static_cast<std::ptrdiff_t>(n));
  }
}
Production Pitfall: Swapping the Entire $N \times N$ Matrix During In-Place Transpose

When writing an in-place square matrix transpose, iterating for (r = 0; r < N; ++r) for (c = 0; c < N; ++c) swap(M[r][c], M[c][r]) swaps every off-diagonal pair twice—leaving the matrix completely unchanged! Always restrict the inner loop strictly to the upper triangle for (c = r + 1; c < N; ++c) (or lower triangle c < r). Furthermore, never forget k %= n at the start of array rotation—if $k \ge n$, computing a.begin() + k dereferences out-of-bounds memory.

Check yourself Question 1 of 4
Concept Check Number of Disjoint Cycles in Bentley's Juggling Rotation recall

In Bentley's Juggling algorithm for rotating an array of length $N$ by $k$ positions in place, we move elements along cyclic orbits $i \mapsto (i + k) \bmod N$.

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

What causes the $7\times$ latency spike at $N = 1024$, and how does $B \times B$ cache tiling solve it?

Key Takeaways

Rotating an array by k positions decomposes into three stride-1 array reversals: reverse(0, n), reverse(0, k), and reverse(k, n) for right rotation.
Bentley's Juggling algorithm decomposes the index permutation into gcd(n, k) disjoint orbits of length n / gcd(n, k), moving each element once.
Despite performing 3x more writes, 3-Reversal outperforms Juggling on primitive arrays in DRAM because sequential stride-1 access saturates cache lines and SIMD.
Every 90-degree square matrix rotation is the composition of an upper-triangle transpose (c > r) and a horizontal or vertical reflection.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue