完美洗牌问题:给定一个数组a1, a2, a3, ..., an, b1, b2, b3, ..., bn,把它最终变换为b1, a1,b2, a2, ..., bn, an这样的。
为方便起见,我们考虑一个下标从1开始的数组,下标范围是[1, 2n]。我们看一下每个元素最终去了什么地方。
前n个元素 a1 -> a2, a2->a4, ... 第i个元素去了第(2 * i)个的位置。
后n个元素a(n + 1)->a1, a(n + 2)->a3, ... 第i个元素去了第 ((2 * (i - n)) - 1) = 2 * i - (2 * n + 1) = (2* i) % (2 * n + 1) 个的位置。
综合到任意情况,任意的第i个元素,最终换到了 (2 * i) % (2 * n + 1)的位置。为何呢?因为:
当1 <= i <= n时,原式= (2 * i) % (2 * n + 1) = 2 * i;
当n < i <= 2*n时,原式= (2 * i) % (2 * n + 1)保持不变。
(1)完美洗牌算法1
如果允许我们使用一个额外数组的话,我们直接把每个元素放到对应的位置就好了。于是产生了最简单的方法pefect_shuffle1,它的时间复杂度是O(n),空间复杂度也是O(n)。
//时间O(n),空间O(n),数组下标从1开始
void pefect_shuffle1(int *a, int n)
{
int n2 = n * 2, i, b[N];
for (i = 1; i <= n2; i++) {
b[(i * 2) % (n2 + 1)] = a[i];
}
for (i = 1; i <= n2; i++) {
a[i] = b[i];
}
}
(2)完美洗牌算法2——分治
考虑分治法,假设n是偶数。我们考虑把数组拆成两半(我们只写数组的下标):
原始数组为(1, 2, ..., n, n+1, n+2, ..., 2n)
拆分之后为(1, ...,n/2, ..., n)和(n+1, ..., n+n/2, ..., 2n)
前半段长度为n,后半段长度也为n。
我们把前半段的后n / 2个元素(n/2 + 1, ..., n)与后半段的前n / 2 个元素 (n+1, ..., n + n/2 )交换,得到:
新的前n个元素A :(1, ..., n/2, n+1, ..., n + n/2)
新的后n个元素B :(n/2 + 1, ..., n, n + n/2 + 1, ..., n)
因为n是偶数,我们得到了A,B两个子问题。问题转化为了求解n' = n / 2的两个问题。
那么当n是奇数怎么办?我们可以把前半段多出来的那个元素先拿出来,后面所有元素前移,再把当时多出的那个元素放到末尾,这样数组最后两个元素已经满足要求了。于是只考虑前2 * (n - 1)个元素就可以了,于是转换成了(n - 1)的问题。
针对上述 n 分别为偶数和奇数的情况,下面举 n=4 和 n=5 两个例子来说明下。
当n=4 时,原始数组即为
a1 a2 a3 a4 b1 b2 b3 b4
按照之前n为偶数时的思路,把前半段的后2个元素a3 a4同后半段的前2个元素b1 b2交换,可得:
a1 a2 b1 b2 a3 a4 b3 b4
然后再分别求解 A(a1 a2 b1 b2)和 B(a3 a4 b3 b4)两个子问题就可以了。
当 n=5 时,原始数组则为
a1 a2 a3 a4 a5 b1 b2 b3 b4 b5
按照之前 n 为奇数时的思路,先把 a5 先单独拎出来放在最后,然后所有剩下的元素全部前移,变为:
a1 a2 a3 a4 b1 b2 b3 b4 b5 a5
此时,最后的两个元素b5 a5已经是我们想要的结果,只要跟之前n=4的情况一样考虑即可。
//时间O(nlogn),空间O(1),数组下标从1开始
void perfect_shuffle2(int *a, int n)
{
int tmp, i;
if (n == 1) {
tmp = a[1];
a[1] = a[2];
a[2] = tmp;
return;
}
int n2 = n * 2, n3 = n / 2;
if (n % 2 == 1) { //奇数的处理
tmp = a[n];
for (i = n + 1; i <= n2; i++) {
a[i - 1] = a[i];
}
a[n2] = tmp;
--n;
}
//到此n是偶数
for (i = n3 + 1; i <= n; ++i) {
tmp = a[i];
a[i] = a[i + n3];
a[i + n3] = tmp;
}
// [1.. n /2]
perfect_shuffle2(a, n3);
perfect_shuffle2(a + n, n3);
}
分析下此算法的复杂度:每次,我们交换中间的n个元素,需要O(n)的时间,n是奇数的话,我们还需要O(n)的时间先把后两个元素调整好,但这不影响总体时间复杂度。
故事实上,当我们采用分治算法的时候,其时间复杂度的计算公式为: T(n) = 2*T(n/2)+ O(n),这个就是跟归并排序一样的复杂度式子,由《算法导论》中文第二版 44 页的主
定理,可最终解得 T(n) = O(nlogn)。至于空间,此算法在数组内部折腾的,所以是 O(1)(在不考虑递归的栈的空间的前提下)。
(3)完美洗牌算法
首先,对于每一个元素,它最终都会到达一个位置,我们如果记录每个元素应到的位置会形成圈。为什么会形成圈?
比如原来位置为a的元素最终到达b,而b又要达到c……,因为每个新位置和原位置都有一个元素,所以一条链a->b->c->d……这样下去的话,必然有一个元素会指向a,(因为中间那些位置b, c, d……都已经被其它元素指向了)。这就是圈的成因。
当n=4的情况:
起始序列:a1 a2 a3 a4 b1 b2 b3 b4
数组下标:1 2 3 4 5 6 7 8
最终序列:b1 a1 b2 a2 b3 a3 b4 a4
我们可以看出有两个圈:
一个是1 -> 2 -> 4 -> 8 -> 7 -> 5 -> 1
一个是3 -> 6 -> 3
这两个圈可以表示为(1, 2, 4, 8, 7, 5)和(3, 6),且perfect_shuffle1算法也已经告诉了我们,不管你n是奇数还是偶数,每个位置的元素都将变为第(2*i) % (2*n+1)个元素:
因此我们只要知道圈里最小位置编号的元素即圈的头部,顺着圈走一遍就可以达到目的,且因为圈与圈是不想交的,所以这样下来,我们刚好走了O(N)步。
上面沿着圈走的算法我们给它取名为cycle_leader,这部分代码如下://数组下标从1开始,from是圈的头部,mod是要取模的数
//mod应该为 2*n + 1,时间复杂度O(圈长)
void cycle_leader(int *a,int from, int mod)
{
int last = a[from], tmp, i;
for (i = 2*from % mod; i != from; i = 2*i % mod) {
tmp = a[i];
a[i] = last;
last = tmp;
}
a[from] = last;
}
那么如何找到每个圈的头部呢?引用一篇论文A Simple In-Place Algorithm for In-Shuffle.里面的结论:
对于2n = (3^k - 1)这种长度的数组,恰好只有k个圈,且每个圈头部的起始位置分别是1, 3, 9, ..., 3^(k-1)。
利用上述这个结论,我们可以解决这种特殊长度2n = (3^k - 1)的数组问题,那么若给定的长度n是任意的咋办呢?此时,我们可以采取分而治之算法的思想,把整个数组一分为二,即拆分成两个部分:让前一部分满足上述结论,后一部分单独计算。
假设2m = (3^k - 1),其中m<=n,我们把n分解成m和n-m两部分。
当把n分解成m和n-m两部分后,原始数组对应的下标如下(为了方便描述,我们依然只需要看数组下标就够了):
原始数组下标:1, ..., m, m+1, ..., n, n+1, ..., n+m, n+m+1, ..., 2n
且为了能让前部分的序列满足2m = (3^k - 1),我们可以把中间那两段长度为n-m和m的段交换位置,即相当于把m+1, ..., n, n+1, ..., n+m的段循环右移m次(为什么要这么做?因为如此操作后,数组的前部分的长度为2m,而根据结论:当2m = (3^k - 1),可知这长度2m的部分恰好有k个圈)。
循环移位的代码://翻转字符串时间复杂度O(to - from)
void reverse(int *a, int from, int to)
{
int tmp;
for (; from < to; ++from, --to) {
tmp = a[from];
a[from] = a[to];
a[to] = tmp;
}
}
//循环右移num位 时间复杂度O(n)
void right_rotate(int *a, int num, int n)
{
reverse(a, 1, n - num);
reverse(a, n - num + 1, n);
reverse(a, 1, n);
}
当给定n=7时,若要满足结论2m = (3^k - 1),k只能取2,继而m=4。
原始数组:a1 a2 a3 a4 a5 a6 a7b1 b2 b3 b4 b5 b6 b7
既然m=4,即让上述数组中有下划线的两个部分交换,得到:
目标数组:a1 a2 a3 a4 b1 b2 b3 b4a5 a6 a7 b5 b6 b7
继而目标数组中的前半部分a1 a2 a3 a4 b1 b2 b3 b4部分可以用走圈算法cycle_leader搞定,于此我们最终求解的n长度变成了n’=3,单独再解决后半部分a5 a6 a7 b5 b6 b7即可。
所以这也是一种分治算法。算法流程:
输入数组a[1, ..., 2n]
step 1 找到 2m = 3^k - 1 使得 3^k <=2n < 3^(k +1)
step 2 把a[m + 1, ..., n + m]那部分循环右移m位
step 3 对每个i = 0, 1, 2, ..., k- 1,3^i是每个圈的头部,做cycle_leader算法,数组长度为m,所以对2 * m + 1取模。
step 4 对数组的后面部分a[2m + 1, ...,2n]继续使用本算法,这相当于n减小了m。
以上各个步骤对应的时间复杂度分析如下:
1. 因为循环不断乘3的,所以时间复杂度O(logn)
2. 循环移位O(n)
3. 每个圈,每个元素只走了一次,一共2m个元素,所以复杂度omega(m),而m < n,所以也在O(n)内。
4. T(n - m)
因此,复杂度为 T(n) = T(n - m) + O(n),m = omega(n) 解得:总复杂度T(n) = O(n)。
算法代码:
//时间O(n),空间O(1)
void perfect_shuffle3(int *a, int n)
{
int n2, m, i, k, tmp;
for (; n > 1; ) {
// step 1
n2 = 2 * n;
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, 2 * m + 1);
}
//step 4
a += 2 * m;
n -= m;
}
// n = 1
tmp = a[1];
a[1] = a[2];
a[2] = tmp;
}
问题扩展:
1. 给定的输入为a1, a2, a3, ..., an, b1, b2, b3, ..., bn, 要求输出为a1, b1,a2, b2, ..., an, bn. 如何实现?
我们可以利用完美洗牌算法得到b1, a1, b2, a2, ..., bn, an,在此基础上再两两交换相邻元素即可。
或者保持a1和bn不变,对中间的a2, a2, ..., an, b1, b2, ..., bn-1这2*(n-1)个元素使用完美洗牌算法即可。
2. 完美洗牌问题的逆问题:给定b1, a1, b2, a2, ..., bn, an,输出a1, a2, a3, ..., an,b1, b2, b3, ..., bn.
这相当于把偶数位上的数放到一起,奇数位上的数放到一起。
关键问题:我们需要把cycle_leader算法改一下,沿着圈换回去。改造后的叫reverse_cycle_leader,代码如下:
//逆变换,数组下标从1开始,from是圈的头部
//mod是要取模的数mod应该为2n + 1,时间复杂度O(圈长)
void reverse_cycle_leader(int *a, int from, int mod)
{
int last = a[from], next, i;
for (i = from; ; i = next) {
next = 2 * i % mod;
if (next == from) {
a[i] = last;
break;
}
a[i] = a[next];
}
}
按照完美洗牌算法,我们同样把数分为m和(n - m)两部分。
假设我们把前面若干项已经置换成先a后b的形式了,现在把这m项也置换成先a后b的形式,我们需要把这m项中的a部分换到前面去,这里需要一个循环右移,还要知道以前处理了多长。总之,这个逆shuffle算法需要小心实现一下,代码如下:
//逆shuffle 时间O(n),空间O(1)
void reverse_perfect_shuffle3(int *a, int n)
{
int n2, m, i, k, t, done = 0;
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)
for (i = 0, t = 1; i < k; ++i, t *= 3) {
reverse_cycle_leader(a, t, m * 2 + 1);
}
if (done) {
right_rotate(a - done, m, done + m); //移位
}
a += m * 2;
n -= m;
done += m;
}
// n = 1
right_rotate(a - done, 1, done + 2);
}
3. 如果输入是a1, a2, ..., an, b1, b2, ...,bn, c1, c2, ..., cn,要求输出是c1, b1 ,a1,c2, b2, a2, ..., bn, an。怎么办?
对于任意位置i = 1, 2, ..., 3n 我们发现:
原始1 <= i <= n 时,即a部分,转移到的位置是3i
原始n < i <= 2n 时即b部分,转移到的位置是 3i - (3n + 1)
原始2n < i <= 3n时,即c部分,转移到的位置是 3i - 2 * (3n + 1)
于是我们得到映射位置 i'= 3i % (3n + 1)
剩下的问题和完美洗牌算法差不多,我们试图对一个特定的长度解决掉。
仿照完美洗牌算法的思路,验证了3是7的原根,是49的原根,于是3是7^k的原根。于是,我们可以把原来的圈截取出一个m,满足3m = 7^k - 1,截取出一个m长度后,我们同样需要循环移位,使得(a1...am)(b1...bm)(c1...cm)在一起,这里要循移位两次:
a1 ... am am+1 ... an b1 ... bm bm+1 ... bn c1 ... cm cm+1 ... cn
首先对上面加粗的部分,即下标[m+1, n+m]循环右移m位,变成
a1 ... am b1 ... bm am+1 ... an bm+1 ... bn c1 ... cm cm+1 ... cn
然后对上面加粗的部分,即下标[2m+1, 2n+m]循环右移m位,变成
a1 ... am b1 ... bm c1 ... cm am+1... an bm+1 ... bm cm+1 ... cn
算法的步骤如下:
输入数组a[1, ..., 3n]
step 1 找到 3m = 7^k -1 使得 7^k <= 3n < 7^(k +1)
step 2 把a[m+1, ..., n+m]那部分循环右移m位,再把a[2m+1, ...,2n+m]那部分循环右移m位,这样把数组分成了m和(n - m)两部分。
step 3 对每个i = 0, 1 ,2,..., k-1,7^i是每个圈的头部,做cycle_leader算法,数组长度为m,所以对3m + 1取模。
step4 对数组的后面部分a[3m+1, ..., 3n]继续使用本算法,这相当于n减小了m。
//数组下标从1开始,from是圈的头部
//mod是要取模的数,mod应该为3n+1,时间复杂度O(圈长)
void cycle_leader(int *a, int from, int mod)
{
int last = a[from], t, i;
for (i = from*3 % mod; i != from; i = i*3 % mod) {
t = a[i];
a[i] = last;
last = t;
}
a[from] = last;
}
//时间O(n),空间O(1)
void perfect_shuffle3n(int *a, int n)
{
int n3, m, i, k, t;
for (; n > 2; ) {
// step 1
n3 = n * 3;
for (k = 0, m = 1; n3 / m >= 7; ++k, m *= 7)
;
m /= 3;
// 3m = 7^k - 1 , 7^k <= 3n < 7^(k + 1)
// step 2
right_rotate(a + m, m, n);
right_rotate(a + m * 2, m , n * 2 - m);
// step 3
for (i = 0, t = 1; i < k; ++i, t *= 7) {
cycle_leader(a , t, m * 3 + 1);
}
//step 4
a += m * 3;
n -= m;
}
if (n == 2) {
cycle_leader(a, 1, 7);
}
else if (n == 1) {
t = a[1];
a[1] = a[3];
a[3] = t;
}
}
参考链接
1. http://blog.csdn.net/caopengcs/article/details/10176093
2. http://blog.csdn.net/caopengcs/article/details/10521603
以下是完整程序:
#include <stdio.h>
//翻转字符串
void reverse(int *a, int from, int to)
{
int tmp;
while (from < to) {
tmp = a[from];
a[from++] = a[to];
a[to--] = tmp;
}
}
//循环右移num位,数组下标从1开始
void right_rotate(int *a, int num, int n)
{
reverse(a, 1, n - num);
reverse(a, n - num + 1, n);
reverse(a, 1, n);
}
void cycle_leader(int *a, int from, int mod)
{
int last = a[from], tmp, i;
for (i = 2 * from % mod; i != from; i = 2 * i % mod) {
tmp = a[i];
a[i] = last;
last = tmp;
}
a[from] = last;
}
void perfect_shuffle(int *a, int n)
{
int m, k, tmp, i, t, n2;
while (n > 1) {
//step1
n2 = n * 2;
k = 0;
m = 2;
tmp = 3;
while (m <= n2) {
k++;
tmp *= 3;
m = tmp - 1;
}
m = (tmp/3 - 1) / 2;
// 2m = 3^k - 1 <= 2n
//step2
if (m < n) {
right_rotate(a + m, m, n);
}
//step3
for (i = 0, t = 1; i < k; i++, t *= 3) {
cycle_leader(a, t, 2 * m + 1);
}
//step4
a += 2 * m;
n -= m;
}
// n = 1
tmp = a[1];
a[1] = a[2];
a[2] = tmp;
}
void reverse_cycle_leader(int *a, int from, int mod)
{
int last = a[from], next, i;
i = from;
next = 2 * from % mod;
while (next != from) {
a[i] = a[next];
i = next;
next = 2 * i % mod;
}
a[i] = last;
}
void reverse_perfect_shuffle(int *a, int n)
{
int m, k, n2, i, tmp, t, done;
while (n > 1) {
n2 = n * 2;
k = 0;
m = 2;
tmp = 3;
while (m <= n2) {
k++;
tmp *= 3;
m = tmp - 1;
}
m = (tmp/3 - 1) / 2;
for (i = 0, t = 1; i < k; i++, t *= 3) {
reverse_cycle_leader(a, t, 2 * m + 1);
}
if (done) {
right_rotate(a - done, m, done + m);
}
a += 2 * m;
n -= m;
done += m;
}
// n = 1
right_rotate(a - done, 1, done + 2);
}
int main(void)
{
int a[11] = {-1, 2, 4, 6, 8, 10, 1, 3, 5, 7, 9};
int i;
perfect_shuffle(a, 5);
for (i = 1; i <= 10; i++)
printf("%d ", a[i]);
printf("\n");
reverse_perfect_shuffle(a, 5);
for (i = 1; i <= 10; i++)
printf("%d ", a[i]);
printf("\n");
return 0;
}