2

Premise: My question is not a duplicate of Cyclic rotation in Python . I am not asking how to resolve the problem or why my solution does not work, I have already resolved it and it works. My question is about another particular solution to the same problem I found, because I would like to understand the logic behind the other solution.

I came across the following cyclic array rotation problem (below the sources):

An array A consisting of N integers is given. Rotation of the array means that each element is shifted right by one index, and the last element of the array is moved to the first place. For example, the rotation of array A = [3, 8, 9, 7, 6] is [6, 3, 8, 9, 7] (elements are shifted right by one index and 6 is moved to the first place). The goal is to rotate array A K times; that is, each element of A will be shifted to the right K times.

which I managed to solve with the following Python code:

def solution(A , K):
    N = len(A)
    if N < 1 or N == K:
        return A
    K = K % N
    for x in range(K):
        tmp = A[N - 1]
        for i in range(N - 1, 0, -1):
            A[i] = A[i - 1]
        A[0] = tmp
    return A

Then, on the following website https://www.martinkysel.com/codility-cyclicrotation-solution/, I have found the following fancy solution to the same problem:

def reverse(arr, i, j):
    for idx in xrange((j - i + 1) / 2):
        arr[i+idx], arr[j-idx] = arr[j-idx], arr[i+idx]

def solution(A, K):
    l = len(A)
    if l == 0:
        return []

    K = K%l

    reverse(A, l - K, l -1)
    reverse(A, 0, l - K -1)
    reverse(A, 0, l - 1)

    return A

Could someone explain me how this particular solution works? (The author does not explain it on his website)

My solution does not perform quite well for large A and K, where K < N, e.g.:

    A = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] * 1000
    K = 1000
    expectedResult = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] * 1000
    res = solution(A, K) # 1455.05908203125 ms = almost 1.4 seconds

Because for K < N, my code has a time complexity of O(N * K), where N is the length of the array. For large K and small N (K > N), my solution performs well thanks to the modulo operation K = K % N:

    A = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    K = 999999999999999999999999
    expectedRes = [2, 3, 4, 5, 6, 7, 8, 9, 10, 1]
    res = solution(A, K) # 0.0048828125 ms, because K is torn down to 9 thanks to K = K % N

The other solution, on the other hand, performs greatly in all cases, even when N > K and has a complexity of O(N).

What is the logic behind that solution?

Thank you for the attention.

tonix
  • 6,671
  • 13
  • 75
  • 136
  • Are you really asking why `a, b = b, a` swaps `a` and `b`? – DeepSpace Sep 18 '19 at 18:49
  • No, I don't. I understand what it does, I did not understand the 3 `reverse()` calls and those relative computations of indices inside of it. – tonix Sep 18 '19 at 19:15
  • There's some info on the algorithm in [this question](https://stackoverflow.com/questions/4457277/algorithm-to-rotate-an-array-in-linear-time). – glibdud Sep 18 '19 at 19:19

2 Answers2

7

Let me talk first the base case with K < N, the idea in this case is to split the array in two parts A and B, A is the first N-K elements array and B the last K elements. the algorithm reverse A and B separately and finally reverse the full array (with the two part reversed separately). To manage the case with K > N, think that every time you reverse the array N times you obtain the original array again so we can just use the module operator to find where to split the array (reversing only the really useful times avoiding useless shifting).

Graphical Example

A graphical step by step example can help understanding better the concept. Note that

  • The bold line indicate the the splitting point of the array (K = 3 in this example);
  • The red array indicate the input and the expected output.

Starting from:

starting array

look that what we want in front of the final output will be the last 3 letter reversed, for now let reverse it in place (first reverse of the algorithm):

first_pass_array

now reverse the first N-K elements (second reverse of the algorithm):

second_pass_array

we already have the solution but in the opposite direction, we can solve it reversing the whole array (third and last reverse of the algorithm):

final_array

Here the final output, the original array cyclical rotated with K = 3.

Code Example

Let give also another step by step example with python code, starting from:

A = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
K = 22
N = len(A)

we find the splitting index:

K = K%N
#2

because, in this case, the first 20 shift will be useless, now we reverse the last K (2) elements of the original array:

reverse(A, N-K, N-1)
# [1, 2, 3, 4, 5, 6, 7, 8, 10, 9]

as you can see 9 and 10 has been shift, now we reverse the first N-K elements:

reverse(A, 0, N-K-1)
# [8, 7, 6, 5, 4, 3, 2, 1, 10, 9]

And, finally, we reverse the full array:

reverse(A, 0, N-1)
# [9, 10, 1, 2, 3, 4, 5, 6, 7, 8]

Note that reversing an array have time complexity O(N).

FabioL
  • 932
  • 6
  • 22
  • By the way, is it correct to say that the overall time complexity of this cyclic rotation algorithm is `O(N)`, where `N` is the number of elements in the list **because**, assuming `K % N`, we have `O(K/2)` for the first reversing operation of the right chunk, `O(N-K/2)` for the left reversing operation of the left chunk and O(N/2) for the reversing the whole list, which leads to `O(K/2 + N-K/2 + N/2) = O((K + N - K + N)/2) = O(2*N/2) = O(N)`, correct? – tonix Sep 19 '19 at 06:44
  • Not exactly, formally if you talk in time complexity terms you can't say `O(K/2)` because the instance of the problem is `N`, `K` is only a constant term. So the worst reversing case is `0(N)`, that repeated 3 times gives again `O(N)`. You are correct if you talk about the effectively numbers of swaps (pure swaps, we are note talking about `O()` anymore), in that case we can say that the algorithm makes `K/2 + N-K/2 + N/2 = N` swaps. Note that many times analyzing the numbers of swaps is very important to give and idea of the efficiency of the algorithm. – FabioL Sep 19 '19 at 08:52
  • OK. But with: `So the worst reversing case is O(N), that repeated 3 times gives again O(N)` doesn't that imply that `O(N)` repeated **3** times should lead to `O(3 * N)`? Which does not apply in the case of this algorithm, which has `N` number of swaps, where `N` is the number of elements of the list/array too. Also, I would say that the reverse function has always a time complexity of `O(N/2)`, because elements are swapped by looping until reaching the middle of the array... I am just trying to follow this reasoning so that I can understand better. – tonix Sep 19 '19 at 15:09
  • 1
    You are using the asymptotic notation in the wrong way. Look at [this](https://en.wikipedia.org/wiki/Big_O_notation#Multiplication_by_a_constant) property of Big O notation. `O(3*N) = O(N/2) = O(N)` because when you use this notation you are giving a superior limit to the time/space complexity of the algorithm. You're analysis on the pure swaps is perfect by the way. But you can not use big O notation. Look wikipedia pages of [Big O notation](https://en.wikipedia.org/wiki/Big_O_notation) – FabioL Sep 24 '19 at 08:47
  • Thank you for your reply Fabio! So it is kinda like with the mathematical concept of the limits of a function, where e.g. `lim x -> +∞` of `f(x) = k * x` is `f(∞) = k * ∞ = ∞`. Correct? – tonix Sep 25 '19 at 16:50
  • Exactly! O big notation, briefly, indicate how fast the algorithm go to `∞`. – FabioL Sep 25 '19 at 20:07
0

Here is a very simple solution in Ruby. (scored 100% in codility) Remove the last element in the array, and insert it in the beginning.

def solution(a, k)
  if a.empty?
    return []
  end
  modified = a
  1.upto(k) do 
    last_element = modified.pop
    modified = modified.unshift(last_element)
  end
  return modified
end
Bilal Basharat
  • 3,066
  • 6
  • 21
  • 20
Enow B. Mbi
  • 309
  • 3
  • 8