目标
STL中的next_permutation 函数和 prev_permutation 两个函数提供了对于一个特定排列P,求出其后一个排列P+1和前一个排列P-1的功能。
这里我们以next_permutation 为例分析STL中实现的原理,prev_permutation 的原理与之类似,我们在最后给出它们实现上差异的比较
问题:
给定一个排列P,求出其后一个排列P+1是什么。
思路
按照字典序的定义不难推出,正序,是一组排列中最小的排列,而逆序,则是一组排列中最大的排列。
从字典序的定义出发,我们可以看到,n个元素的排列全排列中,从给定排列P 求解下一排列P+1 ,假设两个排列有前k位是相同的(0<=k<=n),那么我们只需要在后面n-k个元素的排列 P(n-k)中求得下一个排列即可。
既然我们关心的是后面 n-k位的排列,那么不妨开开脑洞,从后向前考察排列。
首先我们举一个比较极端的例子:排列 1 2 3 4 5
很显然,这是一个正序排列(递增序列),因此这是这几个数字所组成的排列中最小的排列,记为P1.
现在我们要求出P2,P2是 1 2 3 5 4. 我们可以看到,P2的前三位和P1的前三位的排列完全相同,唯一的变化是最后两位颠倒顺序,这一顺序的颠倒有何玄机呢?——使得最后两位从正序的 4 5 变成了逆序的 5 4.
接着求P3.P3是 1 2 4 3 5. 我们看到,最后两位已经是逆序,不可能有字典序更大的排列,因此必须考虑更多的位,在后3个元素中,3 5 4 显然不是逆序,因此一定存在字典序更大的排列方式,我们由此确定了n-k==3
我们现在要在 3 5 4 中求得下一个排列,3 5 4 不是一个逆序,是因为 3 后面有元素大于3 。我们要在大于3的数字中选择最小的那个,和3交换,因为这样可以保证得到最小的首位元素。对于这个例子,我们选择将3和4进行交换,而不是3 和 5,这样得到的首位元素是4. 现在我们得到了排列 4 5 3 。
显然,4 5 3 并不是我们想要的下一个排列,下一个排列是 4 3 5. 观察区别,不难看出,首位元素一定是4,但是5 3 这个子排列是一个逆序排列。
为什么会是逆序排列?
因为我们寻找的时候就以是不是逆序为分割点,3 恰好是第一个非逆序的元素,而4作为与3 交换的元素,又比3要大,因此交换后得到的 5 3 一定是逆序的排列。
逆序排列没有下一排列,但是将逆序排列反向后,我们就得到了对应的正序排列,而正序排列是当前元素所能形成的最小排列,因此,4 3 5 是4 为首位元素所能形成最小排列,而前3 位没有变化,故我们得到了下一排列P3.
另外,大于3的最小元素,即4 ,也是第一个大于3的元素,因为 5 4 是个逆序排列。
更一般地,例如对于可重集排列 1 2 3 7 6 5 2 1
我们首先寻找第一个非逆序元素,这里是3,然后从后向前寻找第一个大于3的元素,这里是5,交换,得到 5 7 6 3 2 1 的子排列,然后反向,即可得到下一排列。如果没有找到第一个非逆序元素,那么说明该排列已经是最大排列。
代码实现:
以字符串为例,实现next_permutation,这里的空的for语句主要是为了压行
1 /* 2 *算法实现:STL中的next_permutation实现 3 */ 4 #include<cstdio> 5 #include<cstring> 6 7 void inline swap(char *s1,char *s2){ 8 char t=*s1; 9 *s1=*s2; 10 *s2=t; 11 } 12 /** 13 *反转字符串函数,s,e分别执行字符串的开始和结尾,不能反转中文 14 **/ 15 void reverse(char *s,char* e){ 16 for(e--;s<e;s++,e--)swap(s,e); 17 } 18 19 bool next_permutation(char *start,char *end){ 20 char *cur = end-1, *pre=cur-1; 21 while(cur>start && *pre>=*cur)cur--,pre--; 22 if(cur<=start)return false; 23 24 for(cur=end-1;*cur<=*pre;cur--);//找到逆序中大于*pre的元素的最小元素 25 swap(cur,pre); 26 reverse(pre+1,end);//将尾部的逆序变成正序 27 return true; 28 } 29 30 int main(){ 31 char s1[]="01224",s2[]="8000"; 32 reverse(s1,s1+strlen(s1)); 33 printf("%s ",s1); 34 int n=strlen(s2); 35 puts("下一个排列:"); 36 int cnt=0; 37 do{ 38 puts(s2); 39 cnt++; 40 }while(next_permutation(s2,s2+n)); 41 printf("%d",cnt); 42 }
将上述代码的next_permutation 中的所有有关pre和cur指针内容比较的部分的符号反转,就得到了prev_nextpermutation
代码如下:
1 bool prev_permutation(char *start,char *end){ 2 char *cur = end-1, *pre=cur-1; 3 while(cur>start && *pre<=*cur)cur--,pre--;//这里符号有变化 4 if(cur<=start)return false; 5 6 for(cur=end-1;*cur>=*pre;cur--);//这里符号有变化 7 swap(cur,pre); 8 reverse(pre+1,end); 9 return true; 10 }