๐ŸŽ“ A1Learner
๐Ÿ”

DSA Rotating an Array

Rotating a list right by k shifts every value k places, with values that fall off the end reappearing at the front. Three reversals perform the rotation inside the same list.

The problem

Rotating a list right by k shifts every value k places to the right. Values that fall off the right end reappear at the front. The order of the values does not change.

A rotation by k uses k after a wrap. A list of five values rotated by seven behaves the same as a rotation by two.

The cases used in this article

cases = [
    ([1, 2, 3, 4, 5], 2, [4, 5, 1, 2, 3]),
    ([1, 2, 3, 4, 5, 6, 7], 3, [5, 6, 7, 1, 2, 3, 4]),
    ([1, 2, 3], 0, [1, 2, 3]),
    ([1, 2, 3, 4, 5], 7, [4, 5, 1, 2, 3]),
]
for values, k, expected in cases:
    print(values, 'right by', k, '->', expected)

Intuition

The first idea is a rotation by one. Pop the last value. Insert it at the front.

Repeating that step k times gives the answer. The trap is cost and wrap. A large k repeats whole turns of the list that change nothing.

Rotating by one, repeated

values = [1, 2, 3, 4, 5]
for step in range(2):
    last = values.pop()
    values.insert(0, last)
    print('after step', step + 1, ':', values)

Brute force

The repeated single rotation is the brute force. Each step moves every value in the list. Inserting at the front shifts the other values right.

Counting the shifts in the brute force

def rotate_brute(values, k):
    moves = 0
    n = len(values)
    for _ in range(k % n):
        last = values.pop()
        moves += n
        values.insert(0, last)
    return values, moves

for n, k in [(5, 2), (10, 4), (20, 8)]:
    result, moves = rotate_brute(list(range(1, n + 1)), k)
    print('n =', n, 'k =', k, 'moves:', moves, 'first values:', result[:3])

The count grows with both n and k. A rotation method should touch each value a fixed number of times. It should not depend on k in this way.

The optimal solution: three reversals

The key idea is three reversals. Reverse the whole list, then reverse the first k values, then reverse the rest.

The three-reversal solution

def reverse_part(values, left, right):
    while left < right:
        values[left], values[right] = values[right], values[left]
        left += 1
        right -= 1

def rotate(values, k):
    n = len(values)
    if n == 0:
        return values
    k = k % n
    reverse_part(values, 0, n - 1)
    reverse_part(values, 0, k - 1)
    reverse_part(values, k, n - 1)
    return values

cases = [
    ([1, 2, 3, 4, 5], 2),
    ([1, 2, 3, 4, 5, 6, 7], 3),
    ([1, 2, 3], 0),
    ([1, 2, 3, 4, 5], 7),
]
for values, k in cases:
    print(values, 'right by', k, '->', rotate(list(values), k))

Walkthrough

Take [1, 2, 3, 4, 5] with k = 2. The last two values must reach the front. The three stages below move them there.

The three stages for k = 2

def reverse_part(values, left, right):
    while left < right:
        values[left], values[right] = values[right], values[left]
        left += 1
        right -= 1

values = [1, 2, 3, 4, 5]
print('start:', values)
reverse_part(values, 0, 4)
print('reversed all:', values)
reverse_part(values, 0, 1)
print('reversed first 2:', values)
reverse_part(values, 2, 4)
print('reversed rest:', values)

The first reversal lands 4 and 5 at the front in reverse order. The second reversal fixes the front pair. The third reversal restores the order of the back part.

Complexity

Time is O(n). The three reversals together swap each pair at most once per stage, and the stages are fixed at three.

Space is O(1). The reversals swap inside the same list and build no copy.

Counting swaps instead of timing

def rotate_swaps(n, k):
    values = list(range(n))
    swaps = 0
    def rev(left, right):
        nonlocal swaps
        while left < right:
            values[left], values[right] = values[right], values[left]
            swaps += 1
            left += 1
            right -= 1
    k = k % n
    rev(0, n - 1)
    rev(0, k - 1)
    rev(k, n - 1)
    return swaps

for n in [5, 10, 20]:
    print('n =', n, 'k = 2 swaps:', rotate_swaps(n, 2))

Edge cases and pitfalls

An empty list has no rotation. The code returns at once, before k % n can divide by zero.

A k of 0 still runs the reversal steps in a plain version. The first and third stages cancel out, and the middle stage reverses an empty stretch. The result is the original list, as the run below shows.

Edge cases, run

def reverse_part(values, left, right):
    while left < right:
        values[left], values[right] = values[right], values[left]
        left += 1
        right -= 1

def rotate(values, k):
    n = len(values)
    if n == 0:
        return values
    k = k % n
    reverse_part(values, 0, n - 1)
    reverse_part(values, 0, k - 1)
    reverse_part(values, k, n - 1)
    return values

print('empty:', rotate([], 3))
print('single:', rotate([9], 5))
print('k = 0:', rotate([1, 2, 3], 0))
print('k = n:', rotate([1, 2, 3], 3))
print('k > n:', rotate([1, 2, 3, 4, 5], 7))

Exercise

Rotating [1, 2, 3, 4] right by 1 gives which list?