一个非常奇妙的非递归全排列算法

蜡笔小新啦 2012-11-08 02:03:16
下面是一个哥们写的一个非递归全排列算法,
确实看不懂下面红色字体那部分。大家谁看懂了?



下午偶尔翻到以前的写到的一些代码,不觉心血来潮,看了一下其中关于全排列算法的一个实现.联想到高中时数学课上学到关于组合和排列的一些公式,于是在纸上涂鸦着一些计算方法,突然灵感潮来,想借助小白鼠的那个思路也能将全排列解决出来~

下班回到家便赶紧吃饭玩一局sc后,开始实现,终于赶在睡觉之前可以写这篇blog,介绍我的这种还算奇妙的全排列算法:
1. 算法思路:
试想N个自然数A1,A2,...,AN的全排列是N!个,那么,对于每种排列,我将其打印出来,遍历一遍至少是O(N!)的复杂度,那么能不能在就在这个复杂度内用非递归解决这个问题呢?我的想法就是从这点开始成形.同时借助了建模的思想分析的.
无论如何,我首先选取一个自然数 A1 出来,放到存放地址的中间, 那么在A1的周围存在两个位置,一个是A1的左边,一个是A1的右边,那么,我抽出另一自然数出来(假设是AK),便有两种存放的可能性:
(AK,A1) , (A1,AK).
如果我将1周围的两个位置用0和1来表示,如果放在左边,那么左边用1表示,反之用0表示.反正只能放一个地方,那么这两个位置合起来不是10,就是01,化成十进制便是2或者1.
同理,如果第第二个数A2放在A1的左边,也就是第一轮获取了2,排列就是(A2,A1),当第三个数(假设是A3)到来时,那么它就存在三个位置可放了,分别是A2的左边,A2和A1之间,A1的右边,同样用0和1标志每个位置,如果A3放在A2左边,那么构成100(十进制4),如果中间则构成010(十进制2),A1的右边001(十进制1),那么可以用第二个值来表征第三个数摆放的位置.
如此,细心的玩家已经大概想明白了我要干嘛了,对,如此以往,可以将全部N个自然数用N-1个数值来替代(这些数字是有规律的,2的次方),接下来,我想到的便是,一定要找到这些数字和它在全排列中一一对应的关系,那么问题便解决了.
我们拿3个自然数的例子来分析,3!=6,对于3个自然数的全排列我将其编号1-6组,每组按上面的规则应该可以唯一对应一个序列,如将第一组对应于(1,1), 是按上面规则来的. 第二组对应于(1,2), 第三组对应于(2,1), 第四组对应于(2,2), 第五组对应于(4,1), 第六组对应于(4,2). 于是, 我只要分析出一个计算规则便可以只要遍历这N!个分组,便可以求出每个分组对应的排列了.
很显然, 由P(M,M)的计算规则,P(M,M)=M*(M-1)*...*1可以看出,如果我在第2个到来时,将m = P(M,M)%2 同时,P=P/2,那么,可以很好的处理第二个到来时所做的寻址选择:如果m是0,那么就放右边,如果m是1,就放左边,而对应于第三个到来时,同样如此,那么这个存放规则便生成了.于是遍历N!次,按辗转法则,便可以直接生成每个组对应的全排列了.
应该明白了吧?


2.算法实现(C语言版):
结合上面的思路,编写代码,其中逻辑还是挺麻烦的.

引用
[yhzh@localhost test]$ cat test_permute1.c
#include < stdio.h>
#include < stdlib.h>

#define N 10 //偷懒,定义个宏吧.用数组省事
int permute(int perm);

int main(int argc,char *argv[])
{
int ret = 0;

ret = permute(N);

return ret;
}

int permute(int perm)
{
char i = 1;
int j = 1;
int k = 1;
int ptr[2*N] = {0}; //其实只需要N的空间即可,未来偷懒,定义2N的数组取代队列,这也是数组替代队列的典例

while((i <= perm) && (j = j*i)) //这个while和下面第一个for的意思是遍历N!个数字,从1到N!,添加print语句试试
{
for(;k <= j;k++)
{
int begin = N; //队列的头位置(处于2N数组中的位置)
int end = N; //队列的尾位置(处于数组中的位置),后面需要注意end在左边,begin在右边
int l = 1; //代排列的自然数,偷懒,就用1到N吧.方便,可以++就行了
int n = k; //辗转法则之基数
int m = 0; //辗转法则之分离数
int t = 1; //控制生成1到N个自然数的一个和K对应的排列

ptr[N] = l; //将数组的N位置设置为队列的头和尾所在地.同时放入第一数:1

while(t < perm) //生成和K对应队列,一个一个放入2到N
{
t++;
l++;

m = n%t+1; //辗转,+1为什么?呵呵,测试一下吧(注意求余可是有余数0啊)
n = n/t;

if( m == 1) //如果m==1,意味着n正好被t整除,那么意味着下一个数是永远放在最右边一个数的右边,因此begin++了.end不动.
{
ptr[++begin] = l;
}
else //放左边了.至于是放在左边过去的第几个位置,有m的值而定,因此会有move = begin - (m -1),因此从end到该move位置之间的所有数据要向后移动.因此会有下面的for语句,如果用队列,则不需要,数组模拟队列就是有些麻烦
{
int move = 0;

move = begin - (m - 1); //确定放置位置
if(move < end) //过end了,其实也只是过去一位,可以在这里assert一下,那么直接加.等于是在队列末尾添加数字.
{
ptr[move] = l;
end = move;
}
else //在队列中间加入数字
{
int index = end;

for(;index<=move;index++)
{
ptr[index-1] = ptr[index];
}
ptr[move] = l;
end = end - 1;
}
}
}//while t //循环结束

int tmp;
for(tmp=end;tmp<=begin;tmp++) //打印结果
printf("%d ",ptr[tmp]);
printf(" ");
}
i++;
}

return 0;
}

...全文
1410 12 打赏 收藏 转发到动态 举报
写回复
用AI写文章
12 条回复
切换为时间正序
请发表友善的回复…
发表回复
clariones 2013-01-02
  • 打赏
  • 举报
回复
研究了一下午,自觉终于搞明白了。 先向各位致敬了! 我的理解是这样: 1 .N个数的排列数是 N! 个。 算法就是把N!个数映射到N!个排列上。 例如 N=3,N!=6,其中4我映射到123,5映射到321..... 当然这映射算法可以自己定,所以原作者的实现并不唯一,可以按照我们自己的需要来改动; 2. 这个映射算法的要求是: 从一个数字 X 得到的排列必须唯一。 也就是排列不能重复,否则就不对了;反过来也是一样,一个排列可以计算出一个确定的值来。 或者这么说:数字X代表了第X种排列。 3. 原作者的算法是这样: 1. 假设放入数字的顺序是 1 2 3 ...,(对应代码里的L), 2. 决定排列的数字X在代码里是n; 3. 当我放数字L到n所对应的结果数组里的时候,它应该有L种位置可以放置 (条件1:放入顺序是1,2,3。。。那放第L个数字时,在第X种排列中,已经有L-1个数字放进去了,所以有L个可选的位置) 4. 那么第X种排列中,它应该在哪个位置呢? 就是 m=n%t 所决定的了。+1是为了适应编程需要引入的。 5. n = n/t 的作用是为下一次计算L+1做准备。X这个值的算法是这样: X = x1*0!+x2*1!+x3*2!+....xn*(n-1)! xn 表示第n个数的某个位置。 这么说可能比较混乱,举个例子。 假设n=4, X=X1*1+X2*2+X3*3+X4*4 X1 表示第一个数的某个位置,....X4代表第4个数的某个位置 从条件1可知,第一个数就是1,第4个数就是4, 再从条件4可知,数字1的位置只有一个可能,数字2的位置有两个可能,数字3的位置有3个可能,数字4的位置有4个可能。 1不用说了,就是1,所以第一步,得到一个结果“1”;此时X=1+X2... 2有两个可能,假设我们取第2种,就是放左边(左,右),那么得到结果 “12”,此时X=1+2*1(第二种)+X3... 3有3个可能,假设我们去第2种,就是放中间(左,中,右),那么得到结果 "132",此时X=1+2×1+2×2+X4。。。 4有4个可能,假设我们取第2种(左,左1,右1,右),就是放在第一个数字和第二个数字之间,此时有“1432”,而X=1+2×1+2×2+2×6=20. 所以“1432”对应的数字就是20,数字20对应的排列就是 “1432”。 假设我们所有数字都取最后一种可能,即全部放在左边,X=1+2*1+3*2+4*6=33,(1234) 假设我么所有数字都取第一种可能,即全部放在右边,X=1+1*1+1*2+1*6=10. (4321) 一共有33-10+1=24种排列。
xibeitianlang 2012-11-15
  • 打赏
  • 举报
回复
笔误:即第14(+1)个排列为CBAD。
xibeitianlang 2012-11-15
  • 打赏
  • 举报
回复
两个字:费劲;三个字:真费劲。 数学好的可以这样按字典序排列: 假设4个数ABCD全排列,一、6个一组分4组;二、每组2个1小组分3组;三、小组中大的排前,小的排后。 0-23循环,比如循环到14时,14/6=2..2,取第2+1个C排在首位,C剩下ABD, 2/2=1..0,取第1+1个B排在次席CB 余数为0 补上AD,即第14(+1)个排列为CDAD。 编程时从(N-1)!开始递降分组^_^,可预先算好阶乘值放在一个数组中,免得重复计算。
蜡笔小新啦 2012-11-14
  • 打赏
  • 举报
回复
若是排列数多的话,递归很影响性能的。 所以这个兄弟的非递归我觉得很好。就是不理解啥意思。
蜡笔小新啦 2012-11-13
  • 打赏
  • 举报
回复
引用 5 楼 sduxiaoxiang 的回复:
引用 4 楼 pengliangchina 的回复:引用 2 楼 sduxiaoxiang 的回复:数学思维 数学好 很容易懂的 红色字体那段你看懂了? 为什么那么做呢? 没看代码 早看过数学思维实现全排的思想 理解原理了 自己写 代码比这简单
什么书上看的啊?
cnmhx 2012-11-13
  • 打赏
  • 举报
回复
incursive, or iterative, is not the point.
sduxiaoxiang 2012-11-13
  • 打赏
  • 举报
回复
引用 6 楼 pengliangchina 的回复:
引用 5 楼 sduxiaoxiang 的回复:引用 4 楼 pengliangchina 的回复:引用 2 楼 sduxiaoxiang 的回复:数学思维 数学好 很容易懂的 红色字体那段你看懂了? 为什么那么做呢? 没看代码 早看过数学思维实现全排的思想 理解原理了 自己写 代码比这简单 什么书上看的啊?
网上找到的 书上全排 都是递归
sduxiaoxiang 2012-11-12
  • 打赏
  • 举报
回复
引用 4 楼 pengliangchina 的回复:
引用 2 楼 sduxiaoxiang 的回复:数学思维 数学好 很容易懂的 红色字体那段你看懂了? 为什么那么做呢?
没看代码 早看过数学思维实现全排的思想 理解原理了 自己写 代码比这简单
蜡笔小新啦 2012-11-10
  • 打赏
  • 举报
回复
引用 2 楼 sduxiaoxiang 的回复:
数学思维 数学好 很容易懂的
红色字体那段你看懂了? 为什么那么做呢?
BrightPi 2012-11-09
  • 打赏
  • 举报
回复

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>

int factorial(int n);
void emernuation(char** des, char* src);
int nWidth(int n);

int main(int argc, char* argv[]){
	int i;
	char *strPattern;
	int nPattern;
	char **strDes;
	int nDes;
	int nPrintWidth;

	//strPattern = "abc";
	strPattern = argv[1];
	nPattern = strlen( strPattern );
	nDes = factorial( nPattern );

	//memory allocation
	strDes = (char **)malloc(sizeof(char*) * nDes);
	assert(strDes);

	for(i=0; i<nDes; i++){
		strDes[i] = (char *)malloc(sizeof(char) * (1+nPattern));
		assert(strDes[i]);
		strDes[i][0] = '\0';
	}

	//address: send the strPattern to a function and get the result in the strDes
	emernuation(strDes, strPattern);

	//print the result
	nPrintWidth = nWidth(factorial(nPattern));
	for(i=0; i<nDes; i++){ 
		fprintf(stdout, "%0*d %s\n",nPrintWidth, i+1, strDes[i]);
	}

	//free memory
	for(i=0; i<nDes; i++){
		if(strDes[i]){
			free(strDes[i]);
		}
	}
	
	if(strDes){
		free(strDes);
	}

	return 0;
}

int nWidth(int n){
	int ans;
	ans = 1;
	while(n/10!=0){
		ans++;
		n=n/10;
	}
	return ans;
}


int factorial(int n){
assert(n >= 0);
	if(0 == n || 1 == n){
		return 1;
	}
	else{
		return n * factorial( n-1 );
	}
}

void emernuation(char** des, char* src){
	int h,i,j,k;
	int nSection;
	int nSrc;
	char **strDes;
	char *strSrc;
	char **strSub;
	char ***pStrSection;

	strDes = des;
	strSrc = src;
	nSrc = strlen(strSrc);
	nSection = factorial(nSrc-1);

	//exit
	if(nSrc < 2){
		for(j=0;;j++){
			if(strDes[0][j] == '\0'){
				strDes[0][j] = strSrc[0];
				strDes[0][j+1] = '\0';
				break;
			}
		}
		return ;
	}

	//memory allocation
	strSub = (char **)malloc(sizeof(char *) * nSrc);
	assert(strSub);
	
	for(j=0;j<nSrc;j++){
		strSub[j] = (char *)malloc(sizeof(char) * nSrc);
		assert(strSub[j]);
	}
	
	pStrSection = (char ***)malloc(sizeof(char **) * nSrc);
	assert(pStrSection);

	//address
	for(h=0;h<nSrc;h++){
		//sub strings
		for(i=0,j=0;j<nSrc;j++){
			if(j!=h){
				strSub[h][i] = strSrc[j];
				i++;
			}
		}
		strSub[h][i] = '\0';
		

		//sub address
		pStrSection[h] = strDes + h * nSection;
		
		//copy the current character scanned
		for(i=0;i<nSection;i++){
			//strconchar( pStrSection[h][i], strSrc[h]);
			for(j=0;;j++){
				if(pStrSection[h][i][j] == '\0'){
					pStrSection[h][i][j] = strSrc[h];
					pStrSection[h][i][j+1] = '\0';
					break;

				}
			}
		}
		
		//recursion to address the sub strings
		emernuation(pStrSection[h], strSub[h]);
	}
	
	//memory unallocation
	for(j=0;j<nSrc;j++){
		if(strSub[j]){
			free(strSub[j]);
		}
	}
	
	if(strSub){
		free(strSub);
	}

	if(pStrSection){
		free(pStrSection);
	}
	
	return;
}

sduxiaoxiang 2012-11-09
  • 打赏
  • 举报
回复
数学思维 数学好 很容易懂的
636f6c696e 2012-11-08
  • 打赏
  • 举报
回复
反正我是没看懂...

33,008

社区成员

发帖
与我相关
我的任务
社区描述
数据结构与算法相关内容讨论专区
社区管理员
  • 数据结构与算法社区
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧