Shuffle related problem.

本文介绍了一种名为in-shuffle的数组洗牌算法,该算法能在O(1)的空间复杂度下实现数组元素的完美洗牌。文章首先通过一个具体例子说明了如何利用额外空间进行洗牌,接着详细阐述了如何在不使用额外空间的情况下完成洗牌过程,并介绍了如何找到循环的起始点以优化算法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Shuffle is quite a frequently asked question in interviews. The most famous one is shuffle a deck of cards.

Peiyush Jain, 2004 published a paper "A simple in-place Algorithm for in-Shuffle". However this paper is a bit hard to understand without some math background.

Blog  gives a good way to understand this paper. Here, I just tweak it in my own way of understanding.

To better understand the paper, we need to first think about this problem.

Suppose we have an array, 1 2 3 4 5 6 7 8. We want to shuffle it as 5 1 6 2 7 3 8 4. It is like we have two hand of cards. One hand with 1 2 3 4, the other with 5 6 7 8

We want to perfectly shuffle it.

If we are allowed to use extra space, we can place them easily. 1--> pos: 2, 2 --> pos: 4, 3--> pos: 6, 4 --> pos: 8; 5--> pos: 1, 6--> pos: 3, 7 --> pos: 5, 8 -->pos: 7

thus, the i number goes to 2*i % (n+1)

#include "header.h"
using namespace std;
// Time complexity O(N), Space complexity O(N)
void shuffle(vector<int>& nums) {
  int n = nums.size();
  vector<int> target(n, 0);
  for(int i = 1; i <= nums.size(); ++i) {
    int pos = i;
    int targetPos = (2 * i) % (n + 1);
    target[targetPos - 1] = nums[pos - 1];
  }
  for(int i = 0; i < n; ++i) nums[i] = target[i];
}

int main(void) {
  vector<int> nums{1, 2, 3, 4, 5, 6, 7, 8};
  shuffle(nums);
  for(int i = 0; i < nums.size(); ++i) {
    cout << nums[i] << " ";
  }

}

To review the above process, we can write it in another way. 

1--> pos: 2,  [2, 1, 3, 4, 5, 6, 7, 8] mark pos: 2 as negative

2 --> pos: 4, [4, 1, 3, 2, 5, 6, 7, 8] mark pos: 4 as negative

3--> pos: 6, [5, 1, 6, 2, 7, 3, 8, 4] mark pos:  6 as negative

4 --> pos: 8; [8, 1, 3, 2, 5, 6, 7, 4] mark pos: 8 as negative 

5--> pos: 1, [5, 1, 3, 2, 7, 6, 8, 4] mark pos: 1 as negative 

6--> pos: 3, [5, 1, 6, 2, 7, 3, 8, 4] mark pos: 3 as negtaive

7 --> pos: 5, [5, 1, 3, 2, 7, 6, 8, 4] mark pos: 5 as negative

8 -->pos: 7; [7, 1, 3, 2, 5, 6, 8, 4] mark pos: 7 as negative

It thus forms two cycles: 1--> 2-->4-->8-->7-->5-->1; 3-->6-->3;

To perform this rule of shuffle, we need to remember the head, as long as we dont reach the head again, we can just keep on shuffle!

void shuffle(vector<int>& nums, int from, int mod) { // from is the head of cycle, mod equals to n + 1
  for(int i = from * 2 % mid; i != from; i = i * 2 % mod) {
    swap(nums[from], nums[i]);
  }
}

Great! Right now, seems like we can shuffle the array in O(1) space, however, how to find the cycle head might be a problem. Here comes the paper!

"A Simple In-Place Algorithm for In-Shuffle" paper summarize a conclusion that: 

to every 2 * n = (3 ^ k -1), this array will have k cycles, and each cycle starts with 1, 3, 9 ... 3 ^ k - 1. This conclusion makes life easier!

The algorithm is stated as follows:

Input: An array A[1 ..., 2n]

Step 1: Find a 2m = 3 ^ k - 1 such that 3^k <= 2n <= 3 ^ (k+1)

Step 2: Do a right cycle shift of A[m + 1, ...., n + m] by a distance m

Step 3: For each i (0, 1, ..., k - 1), starting at 3 ^ i, do the cycle leader. Algorithm for the in-shuffle permutation of order 2m.

Step 4: Recursively do the in-shuffle algorithm on A[2m + 1, ...., 2n].


The above statement however only solves the array whose length meets: 2 * n = 3 ^ k - 1. (8 = 9 - 1) In the above given example, n = 4, 8 = 3 ^ k - 1; K = 2. Thus, the cycle starting point would be 3^ 0, 3 ^ 1 ... 3 ^ (k-1) which is (1, 3....)

However, if the length != 3 ^ k - 1, we can use divide and conquer! 

if we have an array: a1, a2, a3, a4, a5, a6, a7, b1, b2, b3, b4, b5, b6, b7: array.size() == 14.

We can first partition the array into  two parts: a1, a2, a3, a4,    a5, a6, a7,     b1, b2, b3, b4,    b5, b6, b7

We cab shuffle the first part: a1, a2, a3, a4, b1, b2, b3, b4     a5, a6, a7, b5, b6, b7(this is the part partition left over)

//copyright@caopengcs 8/24/2013
//时间O(n),空间O(1)
void PerfectShuffle2(int *a, int n)
{
    int n2, m, i, k, t;
    for (; n > 1;)
    {
        // step 1
        n2 = n * 2;
        for (k = 0, m = 1; n2 / m >= 3; ++k, m *= 3)
          ;
        m /= 2;
        // 2m = 3^k - 1 , 3^k <= 2n < 3^(k + 1)

        // step 2
        right_rotate(a + m, m, n);

        // step 3
        for (i = 0, t = 1; i < k; ++i, t *= 3)
        {
          cycle_leader(a , t, m * 2 + 1);
        }

        //step 4
        a += m * 2;
        n -= m;

    }
    // n = 1
    t = a[1];
    a[1] = a[2];
    a[2] = t;
}



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值