完美洗牌算法(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;
}



























http://www.niftyadmin.cn/n/634840.html

相关文章

图像特征点匹配(视频质量诊断、画面抖动检测)

在视频质量诊断中&#xff0c;我们通常会涉及到“画面抖动”的检测。在此过程中就需要在视频中隔N帧取一帧图像&#xff0c;然后在获取的两帧图像上找出特征点&#xff0c;并进行相应的匹配。 当然了&#xff0c;这一过程中会出现很多的问题&#xff0c;例如&#xff1a;特征点…

MyEclipse下安装FreeMark插件

现在大多人人喜欢用FreeMark模板。但是这个模板在myeclipse或者是eclipse下却是不能只能提示&#xff0c;一大堆只是没有颜色区分的显示在哪里。万能天国总是有办法。 点我去官网下载(比较慢)我的CSDN资源下载(速度快 推荐 已配置好) 配置 如果你选择的是我的CSDN 资源下载直接…

WebForm运行的部分原理

首先WebForm即web窗体包含两个页面文件&#xff1a;aspx前台页面和cs后台页面文件。通过反编译器Reflector我们可以看到在Dll程序集中前台页面和后台页面分别生成了两个不同的类&#xff0c;而且前台页面aspx类继承于后台页面CS类。下面这个登陆的小例子是我们用的最多的: 在as…

从摄像头中读取图像 OpenCV

//(一) 从摄像头中读取图像并保存成视频 //图像类型 IplImage* #include "cv.h" #include "highgui.h" int main( int argc, char** argv ) { CvCapture* capture 0; IplImage* frame 0; capture cvCaptureFromCAM( 0 ); int fps25; //捕捉帧率 //do…

oracle数据库中,数据表无法执行数据操作语句,提示记录已被锁住

在oracle数据库中&#xff0c;数据表无法执行update语句&#xff0c;原因是该数据表已被其他用户锁定&#xff0c;解决方法如下 首先&#xff0c;执行如下sql语句&#xff1a; <span style"font-size:24px;">select * from v$session t1, v$locked_object t2 …

深入浅出OOP(二): 多态和继承

2019独角兽企业重金招聘Python工程师标准>>> 本文是深入浅出OOP第二篇&#xff0c;主要说说继承的话题。 继承的介绍 在OOP中&#xff0c;继承有如下的定义&#xff1a; 继承是一种OOP的机制&#xff0c;用于派生继承预定义的类 在这个继承关系中&#xff0c;预定义…

【分享】最新版PLSQL下载地址

官方下载地址&#xff1a; http://download.allroundautomations.com/plsqldev1100.exe 注册码&#xff1a; Product Code&#xff1a;4t46t6vydkvsxekkvf3fjnpzy5wbuhphqz serial Number&#xff1a;601769 password&#xff1a;xs374ca

touch 命令

touch命令有两个功能&#xff1a;一是用于把已存在文件的时间标签更新为系统当前的时间&#xff08;默认方式&#xff09;&#xff0c;它们的数据将原封不动地保留下来&#xff1b;二是用来创建新的空文件。语法touch &#xff08;选项) (参数)选项-a&#xff1a;或--timeatime…