完美洗牌算法(2013年UC校招笔试、2016阿里实习生笔试)

题目详情:有个长度为2n的数组{a1,a2,a3,...,an,b1,b2,b3,...,bn},希望排序后{a1,b1,a2,b2,....,an,bn},要求时间复杂度O(n),空间复杂度0(1)的解法。

思路一:位置置换pefect_shuffle1算法    
数组下标:1 2 3 4 5 6 7 8
最终序列:b1 a1 b2 a2 b3 a3 b4 a4
从上面的例子我们能看到,前n个元素中,

时间复杂度O(n)   空间O(n)

我们设定数组的下标从1开始,下标范围是[1..2n]。 还是通过之前n=4的例子,来看下每个元素最终去了什么地方。

起始序列:a1 a2 a3 a4 b1 b2 b3 b4

1、第1个元素a1到了原第2个元素a2的位置,即1->2; 

2、第2个元素a2到了原第4个元素a4的位置,即2->4; 

3、第3个元素a3到了原第6个元素b2的位置,即3->6; 

4、第4个元素a4到了原第8个元素b4的位置,即4->8;

那么推广到一般情况即是:前n个元素中,第i个元素去了 第(2 * i)的位置。

上面是针对前n个元素,那么针对后n个元素,可以看出:

5、第5个元素b1到了原第1个元素a1的位置,即5->1; 

6、第6个元素b2到了原第3个元素a3的位置,即6->3; 

7、第7个元素b3到了原第5个元素b1的位置,即7->5; 

8、第8个元素b4到了原第7个元素b3的位置,即8->7;

推广到一般情况是,后n个元素,第i个元素去了第 (2 * (i - n) ) - 1 = 2 * i - (2 * n + 1) = (2 * i) % (2 * n + 1) 个位置。

再综合到任意情况,任意的第i个元素,我们最终换到了 (2 * i) % (2 * n + 1)的位置。

为何呢?因为:

当0< i <n时, 原式= (2i) % (2 * n + 1) = 2i; 

当i>n时,原式(2 * i) % (2 * n + 1)保持不变。

因此,如果题目允许我们再用一个数组的话,我们直接把每个元素放到该放得位置就好了。也就产生了最简单的方法pefect_shuffle1。

 

但很明显,它的时间复杂度虽然是O(n),但其空间复杂度却是O(n)《需要开辟一个新的数组》,仍不符合本题所期待的时间O(n),空间O(1)。

注意:根据上面变换的节奏,我们可以看出有两个圈:

一个是1 -> 2 -> 4 -> 8 -> 7 -> 5 -> 1; 

一个是3 -> 6 -> 3。

 

思路二:完美洗牌算法2---分治的力量 

时间复杂度:O(nlogn)   空间复杂度:O(1)

a1, a2,a3,a4,b1,b2,b3,b4

我们先要把前半段的后2个元素(a3,a4)与后半段的前2个元素(b1,b2)交换,得到a1,a2,b1,b2,a3,a4,b3,b4。

于是,我们分别求解子问题A (a1,a2,b1,b2)和子问题B (a3,a4,b3,b4)就可以了。

最终得到:a1,b1,a2,b2,a3,b3,a4,b4

如果n = 5,是偶数怎么办?我们原始的数组是a1,a2,a3,a4,a5,b1,b2,b3,b4,b5,我们先把a5拎出来,后面所有元素前移,再把a5放到最后,变为a1,a2,a3,a4,b1,b2,b3,b4,b5,a5。可见这时最后两个元素b5,a5已经是我们要的结果了,所以我们只要考虑n=4就可以了。

那么复杂度怎么算? 每次,我们交换中间的n个元素(左边靠后的n/2个元素和右边靠前的n/2个元素,需要O(n)的时间,n是奇数的话,我们还需要O(n)的时间先把后两个元素调整好,但这影响总体时间复杂度。所以,无论如何都是O(n)的时间复杂度。

于是我们有 T(n) = T(n / 2) + O(n)  这个就是跟归并排序一样的复杂度式子,最终复杂度解出来T(n) = O(nlogn)。空间的话,我们就在数组内部折腾的,所以是O(1)。(当然没有考虑递归的栈的空间)

思路三:完美洗牌算法

 首先,对于每一个元素,它最终都会到达一个位置,我们如果记录每个元素应到的位置会形成圈。 为什么会形成圈? 比如原来位置为a的元素最终到达b,而b又要达到c……,因为每个新位置和原位置都有一个元素,所以一条链 a->b->c->d……这样下去的话,必然有一个元素会指向a,(因为中间那些位置b,c,d……都已经被其它元素指向了)。 这就是圈的成因。

比如 6个元素  原始是(1,2,3,4,5,6), 最终是(4,1,5,2,6,3),我们用a->b表示原来下标为a的元素,新下标为b了。


1->2

2->4

3->6

4->1

5->3

6->5

在论文“A Simple In-Place Algorithm for In-Shuffle.   Peiyush Jain, Microsoft Corporation.  ”中,给出了一个很牛的结论:

:对于2 * n = (3^k - 1),这种长度的数组,恰好只有k个圈,并且每个圈的头部是1,3,9,...3^(k - 1)此处的n为特殊的值(偶数);对于任意的n,参考思路二,把它拆成两部分,前一部分是满足上述结论的,后一部分再单独算。

为了把数组分成适当的两部分,我们同样需要交换一些元素,但这时交换的元素个数不相等,不能简单地循环交换,我们需要更强大的工具——循环移。假设满足结论(*)的需要的长度是2 * m = (3^k - 1), 我们需要把n分解成m和n - m两部分,按下标来说,是这样:

原先的数组(1..m) (m + 1.. n) (n + 1..n + m)(n + m + 1..2 * n),我们要达到的数组 (1..m)(n + 1.. n + m)(m + 1..n)(n + m + 1..2  * n)。可见,中间那两段长度为(n - m)和m的段需要交换位置,这个相当于把(m + 1..n + m)的段循环右移m次,而循环右移是有O(长度)的算法的, 主要思想是把前(n - m)个元素和后m个元素都翻转一下,再把整个段翻转一下。

再用a和b举例一下,设n = 7这样m = 4, k = 2

原先的数组是a1,a2,a3,a4,(a5,a6,a7),(b1,b2,b3,b4),b5,b6,b7。

结论(*)是说m = 4的部分可以直接搞定,也就是说我们把中间加括号的那两段(a5,a6,a7) (b1,b2,b3,b4)交换位置,也就是把(a5,a6,a7,b1,b2,b3,b4)整体循环右移4位就可以得到:(a1,a2,a3,a4,b1,b2,b3,b4)(a5,a6,a7,b5,b6,b7)

于是前m = 4个由cycle_leading算法直接搞定,n的长度减小了4。

所以这也是一种分治算法。

算法流程:

输入数组 a[1..2 * n]

step 1 找到 2 * m = 3^k - 1 使得 3^k <= 2 * n < 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[2 * m + 1.. 2 * n]继续使用本算法,这相当于n减小了m。(只对n’等于奇数的那部分使用本算法)

时间复杂度分析:

 1 因为循环不断乘3的,所以时间复杂度O(logn)

 2 循环移位O(n)

3 每个圈,每个元素只走了一次,一共2*m个元素,所以复杂度omega(m),

 而m < n,所以 也在O(n)内。

 4 T(n - m)

因此 复杂度为 T(n) = T(n - m) + O(n)      m = omega(n) 

所以,总复杂度T(n) = O(n)

总结:完美洗牌算法中,先使用神级结论将原始数组进行处理,分成两部分,对于每部分来说,如果m(每部分的长度为2m)是奇数的话就再使用一次完美洗牌算法,m为偶数的那部分直接使用cycle_leading算法搞定就可以了,当然了,如果两部分都是偶数的话,多次使用cycle_leading算法最省时间啦。

//完美洗牌算法(2013年UC校招笔试、2016阿里实习生笔试)

//思路一
//时间复杂度O(n),空间O(n)
void perfect_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];
	}
}

//思路二
//时间O(nlogn) 空间O(1)
void perfect_shuffle2(int *a,int n)//数组a及长度n
{
	int t,i;
	if(n==1)
	{
		t=a[1];
		a[1]=a[2];
		a[2]=t;
		return;
	}
	int n2=n*2,n3=n/2;
	if(n%2==1)
	{
		t=a[n];
		for(i=n+1;i<n2;i++)
		{
			a[i-1]=a[i];
		}
		a[n2]=t;
		--n;
	}
	//到此,n为偶数
	for(i=n3+1;i<=n;++i)
	{
		t=a[i];
		a[i]=a[i+n3];
		a[i+n3]=t;
	}
	
	perfect_shuffle2(a,n3);
	perfect_shuffle2(a+n,n3);
}

//思路三
//时间O(n) 空间O(1)
//数组下标从1开始,from是圈的头部,mod是要取模的数 mod 应该为 2 * n + 1,时间复杂度O(圈长)
void cycle_leader(int *a,int from, int mod)
{
	int last = a[from],t,i;
    
    for (i = from * 2 % mod;i != from; i = i * 2 % mod) 
	{
        t = a[i];
        a[i] = last;
        last = t;        
    }
    a[from] = last;
}

//循环移位
//翻转字符串
void reverse(int *a,int from,int to)
{
	int t;
	while(from<to)
	{
		t=a[from];
		a[from]=a[to];
		a[to]=t;
		from++;
		to--;
	}
}

//循环右移(右移num位,总长度为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);
}

//时间复杂度O(n) 空间复杂度O(1)
void perfect_shuffle3(int *a,int n)
{
	int n2,m,i,k,t;
	while(n>1)
	{
		//step1
		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);//将长度为n的数组右移m位
		//step3
		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、付费专栏及课程。

余额充值