被大腾讯问到了完美洗牌算法,瞬间就跪了,其实原来看过,只可惜都忘了啊,现在在补充进来吧。
其实完美洗牌算法,应该给我说明白题,最少举个例子吧,当时确实大意了,也没问清楚就直接不会了,其实题意是有个长度为2n的数组{a1,a2,a3,a4,..,an,b1,b2,b3,b4,...,bn},希望排序后{a1,b1,a2,b2,...,an,bn},最好要求时间复杂度为O(n),空间复杂度为O(1)。
解法一:蛮力交换方法
1.1 说白了就是b1和a2,a3,a4,...,an交换,然后是b2和a3,a4,a5,...,an交换,直到bn不交换。复杂度O(n2),肯定不行。
1.2中间交换
交换中间的元素方法,例如:
a1,a2,a3,a4,b1,b2,b3,b4交换a4,b1为:a1,a2,a3,b1,a4,b2,b3,b4
交换中间两个,交换a3,b1和a4,b2为a1,a2,b1,a3,b2,a4,b3,b4,
下一次交换中间三个,依次增加,最后即可但时间复杂度依然为O(n2)
解法二:完美洗牌算法O(n)
有人研究出了完美洗牌算法可以将a1,a2,a3,a4,b1,b2,b3,b4通过O(n)变成b1,a1,b2,a2,b3,a4,b4,a4,通过两两交换即可完成a1,b1,a2,b2,a3,b3,a4,b4.这里来说一下如何用O(n)完成。
2.1 位置置换算法,需要空间复杂度O(n),时间复杂度O(n)
原序: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;2->4,3->6,4->8,5->1,6->3,7->5,8->7上即第i个元素放到了第(2*i)%(2*n+1)位置上
代码:
1 void pefect_shuffle1(int *a,int n){ 2 int n2=n*2,i,b[N]; 3 for(int i=1;i<=n2;i++) 4 { 5 b[(i*2)%(n2+1)]=a[i]; 6 } 7 for(int i=1;i<=n2;i++) 8 a[i]=b[i]; 9 }
我们注意到:1->2->4->8->7->5->1;和3->6->3这就是完美洗牌的关键后面继续说。
2.2 分治处理O(nlogn)
可以将大问题化成两个子问题处理,例如:
n=4情况
a1,a2,a3,a4,b1,b2,b3,b4,交换前半段的后n/2和后半段的前n/2为a1,a2,b1,b2,a3,a4,b3,b4就变成两个子问题。
n=5情况
a1,a2,a3,a4,a5,b1,b2,b3,b4,b5,将中间的给放到最后,剩下的前移即:a1,a2,a3,a4,b1,b2,b3,b4,b5,a5,变成处理n=4情况
1 #include <iostream> 2 using namespace std; 3 void perfect_shuffle2(int *a ,int n) 4 { 5 int t,i; 6 if(n==1) 7 { 8 swap(a[1],a[2]); 9 return; 10 } 11 int n2=n*2,n3=n/2; 12 if(n%2==1) 13 { 14 t=a[n]; 15 for(i=n+1;i<=n2;i++) 16 { 17 a[i-1]=a[i]; 18 } 19 a[n2]=t; 20 --n; 21 } 22 for(i=n3+1;i<=n;i++) 23 { 24 t=a[i]; 25 a[i]=a[i+n3]; 26 a[i+n3]=t; 27 } 28 perfect_shuffle2(a,n3); 29 perfect_shuffle2(a+n,n3); 30 }
时间复杂度O(nlogn),空间不算递归调用栈的话为O(1)
2.3 真正完美算法O(n),O(1)
利用走圈方法:
1->2->4->8->7->5->1;
3->6->3;
1 void circle_leader(int *a ,int form ,int mod) //mod=2*n+1 2 { 3 int last=a[from],t,i; 4 for(i=from*2%mod;i!=from;i=i*2%mod) 5 { 6 t=a[i]; 7 a[i]=last; 8 last=t; 9 } 10 a[from]=last; 11 }
神级结论:若2*n=(3^k-1),则可以确定圈的个数及各自头部的起始位置
对于2*n=(3^k-1)这种长度的数组,恰好只有k个圈,且每个圈头部的起始位置分别为:1,3,9,...,3^(k-1)。
如果n满足神结论,则直接可以计算,如果不满足时,采用分治的思想。将n分成m满足结论,剩下的n-m继续这样做。
a1,a2,a3,..,am,a(m+1),a(m+2),a(m+3),...,an,b1,b2,b3,...,bm,b(m+1),b(m+2),b(m+3),...,b(n).
通过翻转方法:a1,a2,a3,..am,b1,b2,b3,...,bm,a(m+1),a(m+2),a(m+3),a(m+4),...an,b(m+1),b(m+2),b(m+3),...bn
reverse代码:
1 void reverse(int *a,int from ,int to) 2 { 3 int t; 4 for(;from<to;++from,--to){ 5 t=a[from]; 6 a[from]=a[to]; 7 a[to]=t; 8 } 9 } 10 void right_rotate(int *a ,int num,int n) //n为总数,num为右面的个数 11 { 12 reverse(a,1,n-num); 13 reverse(a,n-num+1,n); 14 reverse(a,1,n); 15 }
确定m方法:3^k<=2*m<3^(k+1)。
最终真正完美洗牌算法代码实现:
1 void circle_leader(int *a ,int from ,int mod) //mod=2*n+1 from为头 2 { 3 int last=a[from],t,i; 4 for(i=from*2%mod;i!=from;i=i*2%mod) 5 { 6 t=a[i]; 7 a[i]=last; 8 last=t; 9 } 10 a[from]=last; 11 } 12 13 void reverse(int *a,int from ,int to) 14 { 15 int t; 16 for(;from<to;++from,--to){ 17 t=a[from]; 18 a[from]=a[to]; 19 a[to]=t; 20 } 21 } 22 void right_rotate(int *a ,int num,int n) //n为总数,num为右面的个数 23 { 24 reverse(a,1,n-num); 25 reverse(a,n-num+1,n); 26 reverse(a,1,n); 27 } 28 void perfect_shuffle3(int * a ,int n) 29 { 30 int n2,m,i,k,t; 31 while(n>1) 32 { 33 n2=n*2; 34 for(k=0,m=1;n2/m>=3;k++,m*=3) 35 ; 36 m/=2; //m即为分出的可以用神结论的,正常应该是n1*2=3^k-1(即为m) 37 //,但因为2的倍数和三的倍数,所以不用减一就可以。 38 right_rotate(a+m,m,n); 39 for(i=0,t=1;i<k;++i,t*=3) 40 { 41 cycle_leader(a,t,m*2+1); 42 } 43 a+=m*2; //a指针前进 44 n-=m; 45 } 46 //剩只有a1,b1情况 47 t=a[1]; 48 a[1]=a[2]; 49 a[2]=t; 50 } 51 //全部完事了,但是注意这里是b1,a1,b2,a2,b3,a3,b4,a4,....,bn,an