Rotate Array In Place: The Three-Reversal Trick
7 min readBytePatterns
Rotate an array right by k in O(1) extra space: reverse all, then each block, why k % n comes first, the cyclic-replacement version, and a brute-force check.
Rotate array is an easy question with a constraint that makes it interesting: do it in place. Rotating [1, 2, 3, 4, 5] right by 2 gives [4, 5, 1, 2, 3], and with a spare array that is one line of slicing. Without one, you need an idea, and the idea most people remember is three reversals. It is short, it is linear, and it is easy to get subtly wrong at the edges, which is where interviewers look.
The problem it solves
Given an array and a non-negative k, shift every element k places to the right, wrapping the last k elements round to the front. The array must be modified in place.
Three approaches, in the order people usually find them:
- Shift by one,
ktimes. Save the last element, slide everything right, put it at the front.O(n · k)time,O(1)space. Too slow whenkis close ton. - Copy. Write each element to index
(i + k) % nof a new array, then copy back.O(n)time, butO(n)extra space. - Three reversals.
O(n)time,O(1)extra space, and no index arithmetic beyondk % n.
The intuition
Rotating right by k means the last k values move to the front and keep their order, and the first n - k values move to the back and keep theirs. Think of the array as two blocks, A (the first n - k values) followed by B (the last k). The goal is B followed by A.
Reversing the whole array puts B in front of A, but reverses each block internally. For [1, 2, 3 | 4, 5] you get [5, 4 | 3, 2, 1]: the right values in the right places, each block backwards. Now reverse the first k positions and then the remaining n - k, and each block reads forwards again: [4, 5 | 1, 2, 3].
Before any of that, reduce k modulo n. Rotating by a full length changes nothing, so rotating by k is the same as rotating by k % n, and without the reduction k could exceed the array and the block boundaries would point past the end.
Rotating left by k is rotating right by n - k, or equivalently, the same three reversals with the blocks reversed first and the whole array last.
Watch it run
The animation rotates [1, 2, 3, 4, 5] right by k = 2: the last two values have to end up at the front, in order. It reverses [0..4] by swapping the ends and stepping both markers inward, twice, and the middle value stays put. The whole row is flipped, so the last two are at the front already, just backwards, and so is the rest. Reversing [0..1] takes one swap and puts the first block back in order. Then the same for everything after index 1: reversing [2..4] takes one more swap. Three reversals and four swaps, no second array, and a rotation by a full length would have been reduced to nothing by k % n.
Rotate an Array
Step 1 of 8
Rotate right by k = 2: the last two values have to end up at the front, in order.
The same interactive animation as the lesson — step through it with the controls.
The code
The lesson's version, with one reverse helper used three times:
def rotate(nums, k):
if not nums:
return nums
k %= len(nums)
def reverse(lo, hi):
while lo < hi:
nums[lo], nums[hi] = nums[hi], nums[lo]
lo, hi = lo + 1, hi - 1
reverse(0, len(nums) - 1) # flip the whole row
reverse(0, k - 1) # put the first block back in order
reverse(k, len(nums) - 1) # and the rest
return nums
print(rotate([1, 2, 3, 4, 5], 2)) # [4, 5, 1, 2, 3]
print(rotate([1, 2, 3, 4, 5], 7)) # [4, 5, 1, 2, 3] 7 % 5 == 2
print(rotate([1, 2, 3], 3)) # [1, 2, 3]
print(rotate([], 4)) # []
The array after each reversal, the three states the animation settles on:
nums, k = [1, 2, 3, 4, 5], 2
for lo, hi in [(0, 4), (0, k - 1), (k, 4)]:
nums[lo:hi + 1] = nums[lo:hi + 1][::-1]
print(nums)
# [5, 4, 3, 2, 1]
# [4, 5, 3, 2, 1]
# [4, 5, 1, 2, 3]
The other in-place answer is cyclic replacement: pick up a value, drop it k places ahead, pick up the one it displaced, and continue until you return to the start. The moves split into gcd(n, k) separate cycles, so run one cycle from each of the first gcd(n, k) positions:
from math import gcd
def rotate_cycles(nums, k):
n = len(nums)
if n == 0:
return nums
k %= n
for start in range(gcd(n, k) if k else 0):
i, carry = start, nums[start]
while True:
j = (i + k) % n
nums[j], carry = carry, nums[j] # drop the value, pick up the next
i = j
if i == start:
break
return nums
print(rotate_cycles([1, 2, 3, 4, 5, 6], 2)) # [5, 6, 1, 2, 3, 4]
print(gcd(6, 2)) # 2 two cycles: evens and odds
Both against a brute force that shifts by one step k times, and against slicing, on 5,000 random arrays with k often larger than the length:
import random
def brute(nums, k):
out = list(nums)
for _ in range(k):
if out:
out = [out[-1]] + out[:-1] # one step right
return out
random.seed(20)
ok = True
for _ in range(5000):
nums = [random.randint(0, 9) for _ in range(random.randint(0, 10))]
k = random.randint(0, 25)
want = brute(nums, k)
ok &= rotate(list(nums), k) == want == rotate_cycles(list(nums), k)
if nums:
s = k % len(nums)
ok &= want == nums[len(nums) - s:] + nums[:len(nums) - s]
print(ok) # True
The complexity
- Three reversals:
O(n)time, since every element is swapped at most twice, roughlynswaps in total.O(1)extra space. - Cyclic replacement:
O(n)time with exactlynmoves, one per element, andO(1)extra space. Fewer writes, harder to get right. - Copy or slicing:
O(n)time andO(n)extra space. Fine unless the question forbids it. - Shift by one:
O(n · k), which isO(n²)whenkis nearn.
Where it goes wrong
- Skipping
k %= n. Withk = 7on five elements,reverse(0, 6)reaches past the end. - Dividing by zero on an empty array.
k % 0raises; return early when there is nothing to rotate. - Reversing the blocks in the wrong order for the direction. Whole-then-blocks rotates right; blocks-then-whole rotates left. Test both on a small array.
- Off-by-one block edges. The first block is
0..k-1and the second isk..n-1; writing0..kswaps one value too many. - Running one cycle in the cyclic version. When
gcd(n, k)is more than 1, a single cycle moves only part of the array. - Assigning
nums = nums[-k:] + nums[:-k]in a function. That rebinds a local name and leaves the caller's list unchanged; usenums[:] = ...if slicing is allowed.
When it shows up in interviews
It is a common easy question on arrays where the point is the follow-up: "now do it with O(1) extra space." The reversal trick uses the same inward-swapping two pointers as reversing a linked list and rotating a matrix in place, which is built from a transpose plus a reversal of each row. Rotation also underlies search in a rotated sorted array.
How to say it in an interview
"Rotating right by k moves the last k values to the front with their order kept. I first reduce k modulo n, since a full rotation changes nothing. Then I reverse the whole array, which puts the last k values at the front but backwards, and reverse the first k and the remaining n - k separately to fix each block's order. Each reversal is two pointers swapping inward, so the whole thing is O(n) time and O(1) extra space. An alternative with exactly one move per element is cyclic replacement, running gcd(n, k) cycles."