20

I saw this question is a programming interview book, here I'm simplifying the question.

Assume you have an array A of length n, and you have a permutation array P of length n as well. Your method will return an array where elements of A will appear in the order with indices specified in P.

Quick example: Your method takes A = [a, b, c, d, e] and P = [4, 3, 2, 0, 1]. then it will return [e, d, c, a, b]. You are allowed to use only constant space (i.e. you can't allocate another array, which takes O(n) space).

Ideas?

ahmet alp balkan
  • 42,679
  • 38
  • 138
  • 214

9 Answers9

12

There is a trivial O(n^2) algorithm, but you can do this in O(n). E.g.:

A = [a, b, c, d, e]

P = [4, 3, 2, 0, 1]

We can swap each element in A with the right element required by P, after each swap, there will be one more element in the right position, and do this in a circular fashion for each of the positions (swap elements pointed with ^s):

[a, b, c, d, e] <- P[0] = 4 != 0 (where a initially was), swap 0 (where a is) with 4
 ^           ^
[e, b, c, d, a] <- P[4] = 1 != 0 (where a initially was), swap 4 (where a is) with 1
    ^        ^
[e, a, c, d, b] <- P[1] = 3 != 0 (where a initially was), swap 1 (where a is) with 3
    ^     ^
[e, d, c, a, b] <- P[3] = 0 == 0 (where a initially was), finish step

After one circle, we find the next element in the array that does not stay in the right position, and do this again. So in the end you will get the result you want, and since each position is touched a constant time (for each position, at most one operation (swap) is performed), it is O(n) time.

You can stored the information of which one is in its right place by:

  1. set the corresponding entry in P to -1, which is unrecoverable: after the operations above, P will become [-1, -1, 2, -1, -1], which denotes that only the second one might be not in the right position, and a further step will make sure it is in the right position and terminates the algorithm;

  2. set the corresponding entry in P to -n - 1: P becomes [-5, -4, 2, -1, -2], which can be recovered in O(n) trivially.

Santhosh Kumar Tekuri
  • 3,012
  • 22
  • 22
zw324
  • 26,764
  • 16
  • 85
  • 118
  • :( I did not get it, can you elaborate these 4 steps with more comments, please? I understand why you swap `e-a` and then `a-b` but why do you swap `a-d` then? – ahmet alp balkan May 12 '13 at 18:05
  • assume you got `[a, b, c, d]` and `[2 3 0 1]', after swapping `a & c`, you will check `P[2] == 0 == 0 (where a initially was)` and are you going to terminate before fixing `b & d`? I am trying to understand how you'll handle the cases like this and the permutation is already correct. Because this is a very short algorithm, seemingly does the trick but I couldn't get how do you handle these cases. Can you pseudo-code it please? – ahmet alp balkan May 12 '13 at 19:18
  • 1
    @ahmetalpbalkan That's actually a better example, since then you need to move forward until you meet the first unfixed element (`b`), and then fix that. However, since one place only need one fix, it is O(n) in general. – zw324 May 12 '13 at 19:20
  • But when you swap a misplaced index with some other index, won't you break the already correct element? – ahmet alp balkan May 13 '13 at 00:56
  • 1
    Well, I don't think that's the case, since if the current element is misplaced, what is in its rightful place must also be misplaced since it cannot belong there. – zw324 May 13 '13 at 01:05
  • 3
    IIUC, this algorithm does require O(n) extra space. You suggest to get that extra space by overwriting the sign bits of the numbers in P. However, if P were stored in read-only memory _or_ if P's values were unsigned, then this solution wouldn't work without access to something like malloc/new/alloca, is that correct? – Quuxplusone Feb 18 '19 at 01:53
8

Yet another unnecessary answer! This one preserves the permutation array P explicitly, which was necessary for my situation, but sacrifices in cost. Also this does not require tracking the correctly placed elements. I understand that a previous answer provides the O(N) solution, so I guess this one is just for amusement!

We get best case complexity O(N), worst case O(N^2), and average case O(NlogN). For large arrays (N~10000 or greater), the average case is essentially O(N).

Here is the core algorithm in Java (I mean pseudo-code *cough cough*)

int ind=0;
float temp=0;

for(int i=0; i<(n-1); i++){
  // get next index
  ind = P[i];
  while(ind<i)
    ind = P[ind];

  // swap elements in array
  temp = A[i];
  A[i] = A[ind];
  A[ind] = temp;
}

Here is an example of the algorithm running (similar to previous answers):

let A = [a, b, c, d, e]

and P = [2, 4, 3, 0, 1]

then expected = [c, e, d, a, b]

i=0:  [a, b, c, d, e] // (ind=P[0]=2)>=0 no while loop, swap A[0]<->A[2]
       ^     ^
i=1:  [c, b, a, d, e] // (ind=P[1]=4)>=1 no while loop, swap A[1]<->A[4]
          ^        ^
i=2:  [c, e, a, d, b] // (ind=P[2]=3)>=2 no while loop, swap A[2]<->A[3]
             ^  ^
i=3a: [c, e, d, a, b] // (ind=P[3]=0)<3 uh-oh! enter while loop...
                ^
i=3b: [c, e, d, a, b] // loop iteration: ind<-P[0]. now have (ind=2)<3
       ?        ^
i=3c: [c, e, d, a, b] // loop iteration: ind<-P[2]. now have (ind=3)>=3
             ?  ^
i=3d: [c, e, d, a, b] // good index found. Swap A[3]<->A[3]
                ^
done.

This algorithm can bounce around in that while loop for any indices j<i, up to at most i times during the ith iteration. In the worst case (I think!) each iteration of the outer for loop would result in i extra assignments from the while loop, so we'd have an arithmetic series thing going on, which would add an N^2 factor to the complexity! Running this for a range of N and averaging the number of 'extra' assignments needed by the while loop (averaged over many permutations for each N, that is), though, strongly suggests to me that the average case is O(NlogN).

Thanks!

RinRisson
  • 133
  • 1
  • 7
2

@RinRisson has given the only completely correct answer so far! Every other answer has been something that required extra storage — O(n) stack space, or assuming that the permutation P was conveniently stored adjacent to O(n) unused-but-mutable sign bits, or whatever.

Here's RinRisson's correct answer written out in C++. This passes every test I have thrown at it, including an exhaustive test of every possible permutation of length 0 through 11.

Notice that you don't even need the permutation to be materialized; we can treat it as a completely black-box function OldIndex -> NewIndex:

template<class RandomIt, class F>
void permute(RandomIt first, RandomIt last, const F& p)
{
    using IndexType = std::decay_t<decltype(p(0))>;

    IndexType n = last - first;
    for (IndexType i = 0; i + 1 < n; ++i) {
        IndexType ind = p(i);
        while (ind < i) {
            ind = p(ind);
        }
        using std::swap;
        swap(*(first + i), *(first + ind));
    }
}

Or slap a more STL-ish interface on top:

template<class RandomIt, class ForwardIt>
void permute(RandomIt first, RandomIt last, ForwardIt pfirst, ForwardIt plast)
{
    assert(std::distance(first, last) == std::distance(pfirst, plast));
    permute(first, last, [&](auto i) { return *std::next(pfirst, i); });
}
ZachB
  • 13,051
  • 4
  • 61
  • 89
Quuxplusone
  • 23,928
  • 8
  • 94
  • 159
2

The simplest case is when there is only a single swap for an element to the destination index. for ex: array=abcd perm =1032. you just need two direct swaps: ab swap, cd swap

for other cases, we need to keep swapping until an element reaches its final destination. for ex: abcd, 3021 starting with first element, we swap a and d. we check if a's destination is 0 at perm[perm[0]]. its not, so we swap a with elem at array[perm[perm[0]]] which is b. again we check if a's has reached its destination at perm[perm[perm[0]]] and yes it is. so we stop.

we repeat this for each array index. Every item is moved in-place only once, so it's O(N) with O(1) storage.

def permute(array, perm):

for i in range(len(array)):
    elem, p = array[i], perm[i]

    while( p != i ): 
        elem, array[p] = array[p], elem  
        elem = array[p]
        p = perm[p]

return array
Pari Rajaram
  • 422
  • 3
  • 7
  • 1
    Code only answers are discouraged. Please add some explanation as to how this solves the problem, or how this differs from the existing answers. [From Review](https://stackoverflow.com/review/low-quality-posts/24964021) – Nick Dec 29 '19 at 01:53
0

You can consequently put the desired element to the front of the array, while working with the remaining array of the size (n-1) in the the next iteration step.

The permutation array needs to be accordingly adjusted to reflect the decreasing size of the array. Namely, if the element you placed in the front was found at position "X" you need to decrease by one all the indexes greater or equal to X in the permutation table.

In the case of your example:

array                   permutation -> adjusted permutation

A  =  {[a  b  c  d  e]}                 [4 3 2 0 1]
A1 =  { e [a  b  c  d]}   [3 2 0 1] ->    [3 2 0 1] (decrease all indexes >= 4)
A2 =  { e  d [a  b  c]}     [2 0 1] ->      [2 0 1] (decrease all indexes >= 3)
A3 =  { e  d  c [a  b]}       [0 1] ->        [0 1] (decrease all indexes >= 2)
A4 =  { e  d  c  a [b]}         [1] ->          [0] (decrease all indexes >= 0)

Another example:

A0 = {[a  b  c  d  e]}                  [0 2 4 3 1]
A1 = { a [b  c  d  e]}     [2 4 3 1] ->   [1 3 2 0] (decrease all indexes >= 0)
A2 = { a  c [b  d  e]}       [3 2 0] ->     [2 1 0] (decrease all indexes >= 2)
A3 = { a  c  e [b  d]}         [1 0] ->       [1 0] (decrease all indexes >= 2)
A4 = { a  c  e  d [b]}           [0] ->         [0] (decrease all indexes >= 1)

The algorithm, though not the fastest, avoids the extra memory allocation while still keeping the track of the initial order of elements.

pegazik
  • 115
  • 2
  • 9
  • @Ziyao Wei, You say "after one cicle", how do you know what is the "next element not in the right position"? Unless you store the information about sorted elements but that would require booking additional space. My algorithm is slightly more complicated but does not break after one closed loop. – pegazik May 11 '13 at 21:53
  • I see a problem with your algorithm. In the first step you did `e [ a b c d]` which is basically requires shifting of elements, which is `O(n)` and you do this `n` times and your algorithm becomes `O(n^2)`. – ahmet alp balkan May 12 '13 at 18:09
  • Depending how you implement the underlying array structure, for a double-linked list you need to change maximum 3 links for each iteration step, meaning it will be, even together with index manipulation only O(n). In any case, the task was to use better than linear additional space allocation, nothing about the complexity ;-) Still, I agree the algorithm of Ziyao with the modification is faster and simpler. – pegazik May 12 '13 at 23:15
0

Just a simple example C/C++ code addition to the Ziyao Wei's answer. Code is not allowed in comments, so as an answer, sorry:

for (int i = 0; i < count; ++i)
{
    // Skip to the next non-processed item
    if (destinations[i] < 0)
        continue;

    int currentPosition = i;

    // destinations[X] = Y means "an item on position Y should be at position X"
    // So we should move an item that is now at position X somewhere
    // else - swap it with item on position Y. Then we have a right
    // item on position X, but the original X-item now on position Y,
    // maybe should be occupied by someone else (an item Z). So we
    // check destinations[Y] = Z and move the X-item further until we got
    // destinations[?] = X which mean that on position ? should be an item
    // from position X - which is exactly the X-item we've been kicking
    // around all this time. Loop closed.
    // 
    // Each permutation has one or more such loops, they obvisouly
    // don't intersect, so we may mark each processed position as such
    // and once the loop is over go further down by an array from
    // position X searching for a non-marked item to start a new loop.
    while (destinations[currentPosition] != i)
    {
        const int target = destinations[currentPosition];

        std::swap(items[currentPosition], items[target]);
        destinations[currentPosition] = -1 - target;

        currentPosition = target;
    }

    // Mark last current position as swapped before moving on
    destinations[currentPosition] = -1 - destinations[currentPosition];
}

for (int i = 0; i < count; ++i)
    destinations[i] = -1 - destinations[i];

(for C - replace std::swap with something else)

Antoine Cotten
  • 2,673
  • 18
  • 37
Andrian Nord
  • 677
  • 4
  • 11
  • There are a couple of errors in this implementation. Firstly within the while loop all the reference to "i" should be "currentPosition" and additionally the resetting of the destionations array needs to check that the value is negative. – James Feb 26 '15 at 06:12
  • Yeah, thanks for noting. Somehow I've managed posting the wrong version. Actually there were four bugs: 1. The for-loop for skipping negative indices may skip to the after-the-last item; 2. There was missing reversing of the last processed item when we break out of the while loop (it's much better to reverse everything then check for in the loop afterwards - that's just way faster on large arrays). 3. As you've correctly noted - i was misused in the while loop body, should have been currentPosition. 4. Wrong formula to reverse indices back. I've updated the post, thank you. – Andrian Nord Feb 27 '15 at 08:12
  • This is honestly cheating. It's using the negative space of each `int` to store additional information. So it does allocate an `O(n)` array, just implicitly. It clearly wouldn't work for permuting negative integers. – Jan Schultke Sep 02 '20 at 12:25
  • 1
    @JanSchultke This is working on the permutation array, so indices, which are most probably positive only. Items could be anything – Andrian Nord Nov 22 '20 at 18:28
0

Here a clearer version which takes a swapElements function that accepts indices, e.g., std::swap(Item[cycle], Item[P[cycle]])$ Essentially it runs through all elements and follows the cycles if they haven't been visited yet. Instead of the second check !visited[P[cycle]], we could also compare with the first element in the cycle which has been done somewhere else above.

 bool visited[n] = {0};
 for (int i = 0; i < n; i++)   {
     int cycle = i;
     while(! visited[cycle] && ! visited[P[cycle]]) {
         swapElements(cycle,P[cycle]);
         visited[cycle]=true;
         cycle = P[cycle];
     }
 }
phil
  • 1
0

Traceback what we have swapped by checking index.

Java, O(N) swaps, O(1) space:

    static void swap(char[] arr, int x, int y) {
        char tmp = arr[x];
        arr[x] = arr[y];
        arr[y] = tmp;
    }
    public static void main(String[] args) {
        int[] intArray = new int[]{4,2,3,0,1};
        char[] charArray = new char[]{'A','B','C','D','E'};
        for(int i=0; i<intArray.length; i++) {
            int index_to_swap = intArray[i];
            // Check index if it has already been swapped before
            while (index_to_swap < i) {
                // trace back the index
                index_to_swap = intArray[index_to_swap];
            }
            swap(charArray, index_to_swap, i);
        }
    }
Ilya Kharlamov
  • 3,698
  • 1
  • 31
  • 33
-1

I agree with many solutions here, but below is a very short code snippet that permute throughout a permutation cycle:

def _swap(a, i, j):
    a[i], a[j] = a[j], a[i]


def apply_permutation(a, p):
    idx = 0
    while p[idx] != 0:
        _swap(a, idx, p[idx])
        idx = p[idx]

So the code snippet below

a = list(range(4))
p = [1, 3, 2, 0]
apply_permutation(a, p)
print(a)

Outputs [2, 4, 3, 1]

Elior Malul
  • 683
  • 6
  • 8
  • This is not a complete answer, as it doesn't work for any permutation that is _not_ a single cycle. For example, `apply_permutation(a, [1, 0, 3, 2])` will give the wrong answer. – Quuxplusone Feb 17 '19 at 16:53
  • True, but you can always hold an auxiliary array that signals which items you have swapped. Once you finished a cycle, you continue to an item you didn't touch yet (from the auxiliary array), which is not part of the cycle you just finished. – Elior Malul Feb 19 '19 at 14:09
  • 1
    The question is titled "Algorithm to apply permutation **in constant memory space**", though. – Quuxplusone Feb 19 '19 at 14:34