在常数存储空间中应用置换的算法

时间:2013-05-11 20:27:35

标签: arrays algorithm permutation in-place

我看到这个问题是编程采访书,在这里我简化了问题。

假设您有一个长度为A的数组n,并且您还有一个长度为P的排列数组n。您的方法将返回一个数组,其中A的元素将出现在P中指定索引的顺序中。

快速示例:您的方法需要A = [a, b, c, d, e]P = [4, 3, 2, 0, 1]。然后它将返回[e, d, c, a, b]。您只能使用常量空间(即您不能分配另一个数组,这需要O(n)空间)。

想法?

8 个答案:

答案 0 :(得分:9)

有一个简单的O(n ^ 2)算法,但您可以在O(n)中执行此操作。 E.g:

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

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

我们可以将A中的每个元素与P所需的正确元素交换,在每次交换后,在正确的位置会有一个元素,并以循环方式为每个元素执行的位置(用^ 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

在一个圆圈之后,我们发现数组中的下一个元素没有保持在正确的位置,并再次执行此操作。所以最后你会得到你想要的结果,并且因为每个位置被触摸一个恒定的时间(对于每个位置,最多只执行一次操作(交换)),它是O(n)时间。

您可以通过以下方式存储哪个位于其正确位置的信息:

  1. 将P中的相应条目设置为-1,这是不可恢复的:在上述操作之后,P将变为[-1, -1, 2, -1, -1],这表示只有第二个可能不在正确的位置,并且进一步的步骤将确保它处于正确的位置并终止算法;

  2. 将P中的相应条目设置为-n - 1:P变为[-5, -4, 2, -1, -2],可以在O(n)中轻松恢复。

答案 1 :(得分:2)

又一个不必要的答案!这个明确地保留了排列数组P,这对我的情况是必要的,但牺牲了成本。此外,这不需要跟踪正确放置的元素。我知道前面的答案提供了O(N)解决方案,所以我想这个只是为了娱乐!

我们得到最佳案例复杂度O(N),最差案例O(N^2)和平均案例O(NlogN)。对于大型数组(N~10000或更高),平均情况基本上为O(N)

这是Java中的核心算法(我的意思是伪代码*咳嗽咳嗽*)

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;
}

以下是运行算法的示例(类似于之前的答案):

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

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

然后预期= [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.

此算法可以在while循环中针对任何索引j<i反弹,在i次迭代期间最多ith次。在最坏的情况下(我认为!)外部for循环的每次迭代都会导致来自i循环的while额外分配,因此我们有一个算术系列的东西继续,这将增加N^2因素的复杂性!运行此范围为N并平均“额外”数量。 while循环所需的分配(平均每个N的许多排列,即),强烈告诉我平均情况为O(NlogN)

谢谢!

答案 2 :(得分:1)

只是一个简单的示例C / C ++代码添加到Ziyao Wei的答案。评论中不允许使用代码,所以作为答案,抱歉:

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];

(对于C - 用其他东西替换std :: swap)

答案 3 :(得分:0)

因此,您可以将所需元素放在数组的前面,同时在下一个迭代步骤中处理大小(n-1)的剩余数组。

需要相应地调整置换阵列以反映阵列的尺寸减小。也就是说,如果放在前面的元素位于“X”位置,则需要在置换表中将所有大于或等于X的索引减1。

就你的例子而言:

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)

另一个例子:

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)

该算法虽然不是最快的,但可以避免额外的内存分配,同时仍然可以跟踪元素的初始顺序。

答案 4 :(得分:0)

这里有一个更清晰的版本,它带有一个swapElements函数,该函数接受索引,例如std::swap(Item[cycle], Item[P[cycle]])$ 本质上,它遍历所有元素并遵循循环(如果尚未访问它们)。除了第二个检查!visited[P[cycle]]之外,我们还可以与循环中第一个元素进行比较,该元素已经在上面的其他地方完成了。

 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];
     }
 }

答案 5 :(得分:0)

@RinRisson has given the only completely correct answer so far!其他所有答案都需要额外的存储空间-O(n)堆栈空间,或者假设置换P方便地存储在O(n)个未使用但可变的符号位附近,等等。

这是RinRisson用C ++编写的正确答案。这通过了我进行的所有测试,包括对长度0到11的每个可能排列的详尽测试。

请注意,您甚至不需要实现排列;我们可以将其视为完全黑盒函数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));
    }
}

或在顶部添加更多具有STL效果的界面:

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); });
}

答案 6 :(得分:0)

最简单的情况是元素到目标索引只有一次交换。例如: array = abcd 烫发= 1032 。您只需要两个直接交换:ab交换,cd交换

在其他情况下,我们需要保持交换状态,直到元素到达其最终目的地为止。例如: abcd,3021 从第一个元素开始,我们交换a和d。我们在 perm [perm [0]] 检查a的目的地是否为0。它不是,所以我们在 array [perm [perm [perm [0]]] 处将a与elem交换为b。再次,我们检查a的到达目的地是否是 perm [perm [perm [perm [0]]] ,是的。所以我们停止。

我们对每个数组索引重复此操作。 每个项目仅就地移动一次,因此它是O(N)和O(1)存储。

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

答案 7 :(得分:0)

通过检查索引追溯我们交换的内容。

Java,O(N) 交换,O(1) 空间:

true