曾看到百度迅雷笔试题中有全排列的问题。全排列在笔试面试中很热门,因为它难度适中,既可以考察递归实现,又能进一步考察非递归的实现,便于区分出考生的水平。所以在百度和迅雷的校园招聘以及程序员和软件设计师的考试中都考到了,同时,作为acm的一员,在数据结构相关知识中排列问题经常出现,为了方便自己和大家,本文对全排列作下总结帮助大家更好的学习和理解。
本文章根据博客http://blog.csdn.net/morewindows/article/details/7370155/,http://blog.csdn.net/acdreamers/article/details/8544505,http://blog.sina.com.cn/s/blog_9f7ea4390101101u.html,http://blog.csdn.net/ydxzmhy/article/details/37815017,整理并添加我自己的理解整理得到的,如果对本文有任何补充之处,欢迎大家指出。
题目要求:打印全排列。
为方便起见,用123来示例下。123的全排列有123、132、213、231、312、321这六种。首先考虑213和321这二个数是如何得出的。显然这二个都是123中的1与后面两数交换得到的。然后可以将123的第二个数和每三个数交换得到132。同理可以根据213和321来得231和312。因此可以知道——全排列就是从第一个数字起每个数分别与它后面的数字交换。找到这个规律后,递归的代码就很容易写出来了:(字符型)
//全排列的递归实现 #include <stdio.h> #include <string.h> void Swap(char *a, char *b) { char t = *a; *a = *b; *b = t; } //k表示当前选取到第几个数,m表示共有多少数. void AllRange(char *pszStr, int k, int m) { if (k == m) { static int s_i = 1; printf(" 第%3d个排列\t%s\n", s_i++, pszStr); } else { for (int i = k; i <= m; i++) //第i个数分别与它后面的数字交换就能得到新的排列 { Swap(pszStr + k, pszStr + i); AllRange(pszStr, k + 1, m); Swap(pszStr + k, pszStr + i); } } } void Foo(char *pszStr) { AllRange(pszStr, 0, strlen(pszStr) - 1); } int main() { char szTextStr[] = "123"; printf("%s的全排列如下:\n", szTextStr); Foo(szTextStr); return 0; }
注意这样的方法没有考虑到重复数字,如122将会输出:
这种输出绝对不符合要求,因此现在要想办法来去掉重复的数列。
我们先尝试加个这样的判断——如果一个数与后面的数字相同那么这二个数就不交换了。如122,第一个数与后面交换得212、221。然后122中第二数就不用与第三个数交换了,但对212,它第二个数与第三个数是不相同的,交换之后得到221。与由122中第一个数与第三个数交换所得的221重复了。所以这个方法不行。
换种思维,对122,第一个数1与第二个数2交换得到212,然后考虑第一个数1与第三个数2交换,此时由于第三个数等于第二个数,所以第一个数不再与第三个数交换。再考虑212,它的第二个数与第三个数交换可以得到解决221。此时全排列生成完毕。
这样我们也得到了在全排列中去掉重复的规则——去重的全排列就是从第一个数字起每个数分别与它后面非重复出现的数字交换。用编程的话描述就是第i个数与第j个数交换时,要求[i,j)中没有与第j个数相等的数。下面给出完整代码:
<span style="font-size:12px;">//去重全排列的递归实现 #include <stdio.h> #include <string.h> void Swap(char *a, char *b) { char t = *a; *a = *b; *b = t; } //在pszStr数组中,[nBegin,nEnd)中是否有数字与下标为nEnd的数字相等 bool IsSwap(char *pszStr, int nBegin, int nEnd) { for (int i = nBegin; i < nEnd; i++) if (pszStr[i] == pszStr[nEnd]) return false; return true; } //k表示当前选取到第几个数,m表示共有多少数. void AllRange(char *pszStr, int k, int m) { if (k == m) { static int s_i = 1; printf(" 第%3d个排列\t%s\n", s_i++, pszStr); } else { for (int i = k; i <= m; i++) //第i个数分别与它后面的数字交换就能得到新的排列 { if (IsSwap(pszStr, k, i)) { Swap(pszStr + k, pszStr + i); AllRange(pszStr, k + 1, m); Swap(pszStr + k, pszStr + i); } } } } void Foo(char *pszStr) { AllRange(pszStr, 0, strlen(pszStr) - 1); } int main() { char szTextStr[] = "122"; printf("%s的全排列如下:\n", szTextStr); Foo(szTextStr); return 0; }</span>OK,到现在我们已经能熟练写出递归的方法了,并且考虑了字符串中的重复数据可能引发的重复数列问题。那么如何使用非递归的方法来得到全排列了?
全排列的非递归就是由后向前找替换数和替换点,然后由后向前找第一个比替换数大的数与替换数交换,最后颠倒替换点后的所有数据。
查找下一个排列的方法如下:
例如:对23541这个字符串进行全排列。参考STL源码中的算法。
从后向前查找出一对相邻的递增数字,41、54均不符合,35符合相邻的递增数字要求,因此,将3作为替换数,替换点为第二个,然后从后向前查找比替换数大的最小数(这个数必然存在),1不行,4符合,因此4 和 3 替换,变为24531排列,然后将替换点后的数字进行颠倒顺序,即得到24135,显然这组排列便是我们要的下一个排列。
对于像54321这种已经最大的排列,我们找不到相邻递增的一对数字了,我们就将这组排列全部颠倒顺序得到12345即最小排列。
这便实现了对任意排列输入,得到其下一个排列输出的要求。
同时我们还可以得到其任意排列输入,全部排列输出的情况,只要在排列前将输入的排列排一下序就好了,可以自己写一个高效地排序算法,也可以用排序函数sort(),qsort(),具体使用方法查阅百度或相关资料。
<span style="font-size:12px;">//全排列的非递归实现 #include <stdio.h> #include <stdlib.h> #include <string.h> void Swap(char *a, char *b) { char t = *a; *a = *b; *b = t; } //反转区间 void Reverse(char *a, char *b) { while (a < b) Swap(a++, b--); } //下一个排列 bool Next_permutation(char a[]) { char *pEnd = a + strlen(a); if (a == pEnd) return false; char *p, *q, *pFind; pEnd--; p = pEnd; while (p != a) { q = p; --p; if (*p < *q) //找降序的相邻2数,前一个数即替换数 { //从后向前找比替换点大的第一个数 pFind = pEnd; while (*pFind <= *p) --pFind; //替换 Swap(pFind, p); //替换点后的数全部反转 Reverse(q, pEnd); return true; } } Reverse(p, pEnd);//如果没有下一个排列,全部反转后返回true return false; } int QsortCmp(const void *pa, const void *pb) { return *(char*)pa - *(char*)pb; } int main() { char szTextStr[] = "abc"; printf("%s的全排列如下:\n", szTextStr); //加上排序 qsort(szTextStr, strlen(szTextStr), sizeof(szTextStr[0]), QsortCmp); int i = 1; do{ printf("第%3d个排列\t%s\n", i++, szTextStr); }while (Next_permutation(szTextStr)); return 0; }</span>这样,我们便实现了使用非递归的方法来获得下一个排列了。
三、使用库(类)函数。
在<algorithm>头文件中有专门用于排列的函数next_permutation.
标准库全排列next_permutation()
在标准库算法中,next_permutation应用在数列操作上比较广泛.这个函数可以计算一组数据的全排列.但是怎么用,原理如何,我做了简单的剖析.
首先查看stl中相关信息.
函数原型:
template<class BidirectionalIterator>
bool next_permutation(
BidirectionalIterator _First,
BidirectionalIterator _Last
);
template<class BidirectionalIterator, class BinaryPredicate>
bool next_permutation(
BidirectionalIterator _First,
BidirectionalIterator _Last,
BinaryPredicate _Comp
);
两个重载函数,第二个带谓词参数_Comp,其中只带两个参数的版本,默认谓词函数为"小于".
返回值:bool类型
分析next_permutation函数执行过程:
假设数列 d1,d2,d3,d4……
范围由[first,last)标记,调用next_permutation使数列逐次增大,这个递增过程按照字典序。例如,在字母表中,abcd的下一单词排列为abdc,但是,有一关键点,如何确定这个下一排列为字典序中的next,而不是next->next->next……
若当前调用排列到达最大字典序,比如dcba,就返回false,同时重新设置该排列为最小字典序。
返回为true表示生成下一排列成功。下面着重分析此过程:
根据标记从后往前比较相邻两数据,若前者小于(默认为小于)后者,标志前者为X1(位置PX)表示将被替换,再次重后往前搜索第一个不小于X1的数据,标记为X2。交换X1,X2,然后把[PX+1,last)标记范围置逆。完成。
要点:为什么这样就可以保证得到的为最小递增。
从位置first开始原数列与新数列不同的数据位置是PX,并且新数据为X2。[PX+1,last)总是递减的,[first,PX)没有改变,因为X2>X1,所以不管X2后面怎样排列都比原数列大,反转[PX+1,last)使此子数列(递增)为最小。从而保证的新数列为原数列的字典序排列next。
这种方法与第二种非递归的方法类似,或者说非递归的方法参考的此种方法。
明白了这个原理后,看下面例子:
int main(){
int a[] = {3,1,2};
do{
cout << a[0] << " " << a[1] << " " << a[2] << endl;
}
while (next_permutation(a,a+3));
return 0;
}
输出:312/321 因为原数列不是从最小字典排列开始。
所以要想得到所有全排列
int a[] = {3,1,2}; change to int a[] = {1,2,3};
另外,库中另一函数prev_permutation与next_permutation相反,由原排列得到字典序中上一次最近排列。
所以
int main(){
int a[] = {3,2,1};
do{
cout << a[0] << " " << a[1] << " " << a[2] << endl;
}
while (prev_permutation(a,a+3));
return 0;
}
才能得到123的所有排列。
而prev_permutation()函数功能是输出所有比当前排列小的排列,顺序是从大到小。
next_permutation函数的原理如下:
在当前序列中,从尾端向前寻找两个相邻元素,前一个记为*i,后一个记为*t,并且满足*i < *t。然后再从尾端
寻找另一个元素*j,如果满足*i < *j,即将第i个元素与第j个元素对调,并将第t个元素之后(包括t)的所有元
素颠倒排序,即求出下一个序列了。
代码:
<span style="font-size:12px;">template<class Iterator> bool next_permutation(Iterator first,Iterator last) { if(first == last) return false; Iterator i = first; ++i; if(i == last) return false; i = last; --i; while(1) { Iterator t = i; --i; if(*i < *t) { Iterator j = last; while(!(*i < *--j)); iter_swap(i,j); reverse(t,last); return true; } if(i == first) { reverse(first,last); return false; } } }</span>
<span style="font-size:12px;">int main() { int a[3]; a[0]=1;a[1]=2;a[2]=3; do { cout<<a[0]<<""<<a[1]<<""<<a[2]<<endl; } while (next_permutation(a,a+3)); //参数3指的是要进行排列的长度 //如果存在a之后的排列,就返回true。如果a是最后一个排列没有后继,返回false,每执行一次,a就变成它的后继 }</span>
<span style="font-size:12px;">int main() { char ch[205]; cin >> ch; sort(ch, ch + strlen(ch) ); //该语句对输入的数组进行字典升序排序。如输入9874563102cout<<ch;将输出0123456789,这样就能输出全排列了 char *first = ch; char *last = ch + strlen(ch); do { cout<< ch<< endl; }while(next_permutation(first, last)); return 0; }</span>
<span style="font-size:12px;">int main() { string line; while(cin>>line&&line!="#") { if(next_permutation(line.begin(),line.end()))//从当前输入位置开始 cout<<line<<endl; elsecout<<"Nosuccesor\n"; } } int main() { string line; while(cin>>line&&line!="#") { sort(line.begin(),line.end());//全排列 cout<<line<<endl; while(next_permutation(line.begin(),line.end())) cout<<line<<endl; } }</span>
<span style="font-size:12px;">#include<iostream> //poj 1256Anagram #include<string> #include<algorithm> using namespace std; int cmp(char a,char b)//'A'<'a'<'B'<'b'<...<'Z'<'z'. { if(tolower(a)!=tolower(b)) returntolower(a)<tolower(b); else return a<b; } int main() { char ch[20]; int n; cin>>n; while(n--) { scanf("%s",ch); sort(ch,ch+strlen(ch),cmp); do { printf("%s\n",ch); }while(next_permutation(ch,ch+strlen(ch),cmp)); } return 0; }</span>