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