排列(Arrangement),简单讲是从N个不同元素中取出M个,按照一定顺序排成一列,通常用A(M,N)表示。当M=N时,称为全排列(Permutation)。从数学角度讲,全排列的个数A(N,N)=(N)*(N-1)*...*2*1=N!,但从编程角度,如何获取所有排列?那么就必须按照某种顺序逐个获得下一个排列,通常按照升序顺序(字典序)获得下一个排列。

例如对于一个集合A={1,2,3,},首先获取全排列a1: 1,2,3,;然后获取下一个排列a2: 1,3,2,;按此顺序,A的全排列如下:

a1: 1,2,3;  a2: 1,3,2;  a3: 2,1,3;  a4: 2,3,1;  a5: 3,1,2;  a6: 3,2,1;  共6种。

1)下一个全排列(Next Permutation)

对于给定的任意一种全排列,如果能求出下一个全排列的情况,那么求得所有全排列情况就容易了。好在STL中的algorithm已经给出了一种健壮、高效的方法,下面进行介绍。

设目前有一个集合的一种全排列情况A : 3,7,6,2,5,4,3,1,求取下一个排列的步骤如下:

/** Tips: next permuation based on the ascending order sort
* sketch :
* current: 3 7 6 2 5 4 3 1 .
* | | | |
* find i----+ j k +----end
* swap i and k :
* 3 7 6 3 5 4 2 1 .
* | | | |
* i----+ j k +----end
* reverse j to end :
* 3 7 6 3 1 2 4 5 .
* | | | |
* find i----+ j k +----end
* */

具体方法为:

a)从后向前查找第一个相邻元素对(i,j),并且满足A[i] < A[j]。

易知,此时从j到end必然是降序。可以用反证法证明,请自行证明。

b)在[j,end)中寻找一个最小的k使其满足A[i]<A[k]。

由于[j,end)是降序的,所以必然存在一个k满足上面条件;并且可以从后向前查找第一个满足A[i]<A[k]关系的k,此时的k必是待找的k。

c)将i与k交换。

此时,i处变成比i大的最小元素,因为下一个全排列必须是与当前排列按照升序排序相邻的排列,故选择最小的元素替代i。

易知,交换后的[j,end)仍然满足降序排序。因为在(k,end)中必然小于i,在[j,k)中必然大于k,并且大于i。

d)逆置[j,end)

由于此时[j,end)是降序的,故将其逆置。最终获得下一全排序。

注意:如果在步骤a)找不到符合的相邻元素对,即此时i=begin,则说明当前[begin,end)为一个降序顺序,即无下一个全排列,STL的方法是将其逆置成升序。

2)Next Permutation代码

// STL next permutation base idea
int next_permutation(int *begin, int *end)
{
int *i=begin, *j, *k;
if (i==end || ++i==end) return 0; // 0 or 1 element, no next permutation
for (i=end-1; i!=begin;) {
j = i--; // find last increasing pair (i,j)
if (!(*i < *j)) continue;
// find last k which not less than i,
for (k=end; !(*i < *(--k)););
iter_swap(i,k);
// now the range [j,end) is in descending order
reverse(j,end);
return 1;
}
// current is in descending order
reverse(begin,end);
return 0;
}

上面仅仅是STL中next_permutation的主要思路,原版是C++迭代器版,这里为了便于理解,改成了C的指针版本。

当返回为1时,表示找到了下一全排列;返回0时,表示无下一全排列。注意,如果从begin到end为降序,则表明全排列结束,逆置使其还原到升序。

3)使用next_permutation

如何获取所有全排列情况?STL中的代码非常精妙,利用next_permutation的返回值,判断是否全排列结束(否则将死循环)。对于给定的一个数组,打印其所有全排列只需如下:

// Display All Permutation
void all_permutation(int arr[], int n)
{
sort(arr,arr+n); // sort arr[] in ascending order
do{
for(int i=0; i<n; printf("%d ",arr[i++]));
printf("\n");
}while(next_permutation(arr,arr+n));
}

如果一个数组arr[]中存在重复元素,next_permutation是否工作正常呢?注意第8和10行,STL使用“!(*i < *j)”进行判断大小,若相等则继续寻找,这样就会跳过重复的元素,进而跳过重复的全排列(如:1,2,2; 和1,2,2)。有人会认为直接使用“*i>=*j”更清晰,对于int这种进本数据类型而言,这并没问题。然而,对于结构体甚至C++而言,元素是一个用户自定义数据类型,如何判断其大小?再退一步讲,如何进行排序?STL追求健壮、高效和精妙,对于用户自定义数据类型的排序,可以增加函数指针或者仿函数(Functional),只需要给定“a<b”的方法(如less(a,b))即可。如需求“a>b”可以转化成“b<a”;求“a==b”可以转化成“!(a<b) && !(b<a)”;求“a>=b”可以转化成“!(a<b)”。因此,一般自定义比较器只需要给定less()即可(对于C++而言,即重载操作符operator<)。

有了全排列,那么排列问题A(M,N)则解决了一半,直接从A中选择选择M个元素,然后对这M个元素进行全排列。其中前一步为组合(Combination),记为(M,N),感兴趣的可以自己解决。

4)前一个全排列(prev_permutation)

与next_permutation类似,STL也提供一个版本:

// STL prev permutation base idea
int prev_permutation(int *begin, int *end)
{
int *i=begin, *j, *k;
if (i==end || ++i==end) return 0; // 0 or 1 element, no prev permutation
for (i=end-1; i!=begin;) {
j = i--; // find last decreasing pair (i,j)
if (!(*i > *j)) continue;
// find last k which less than i,
for (k=end; !(*i > *(--k)););
iter_swap(i,k);
// now the range [j,end) is in ascending order
reverse(j,end);
return 1;
}
// current is in ascending order
reverse(begin,end);
return 0;
}

这里不再详细介绍。

5)STL源码next_permutation分析

前面说到STL非常健壮、高效和精妙,下面以next_permutation作分析:

// STL next_permutation
template <class BidirectionalIterator>
bool next_permutation(
BidirectionalIterator first, // iterator, like the C point
BidirectionalIterator last
)
{
if(first == last) return false; // no element BidirectionalIterator i = first;
if(++i == last) return false; // only one element i = last;
--i; // do not use i--, why? for(;;) { // no statemnet loop, why do not use line 29 ?
BidirectionalIterator j = i; // do not use j=i--; why?
--i;
// find the last neighbor pair (i,j) which element i < j
if(*i < *j) {
BidirectionalIterator k = last;
while(!(*i < *--k)); // find last k >= i
iter_swap(i, k); // swap i and k
reverse(j, last); // reverse [j,last)
return true;
} if(i == first) {
reverse(first, last); // current is in descending order
return false;
}
}
}

STL中首先判断是否为空,如果为空则直接返回false,因为没有下一个全排列。是否可以跟第11行调换呢?显然不行。那么是否可以跟第10行调换呢?虽然这样并不影响运行结果,但是对于为空的情况,多了对象的实例化(构造)和清理(析构)两个过程。可见STL对高效的炽热追求。

紧接着,第14行使用“--i;”而不是“i--;”,简言之,前者是先自减再使用,后者是先使用再自减。在这里虽然对结果也不影响,但是这两种实现方法还是有区别的。对于“i--;”来说,编译器首先会将i的值拷贝到临时变量中,然后对i进行自减,最后将临时变量返回;对于“--i”来说,编译器直接将i的值自减,然后将i的值返回。显然,“--i”只执行了两个指令操作,而“i--”执行了三个指令操作。所以能用“--i”的时候尽量不要使用“i--”。(PS:目前编译器已经十分智能了,对于上面的情况,即便写成“i--”仍然会按照“--i”进行编译,但请记住,不要指望任何版本的编译器都能帮你优化代码!)

注意:第17、18两句,并没有合并成一句,因为此时编译器无法进行合理优化,所以写成两句要比写成一句的少了一个指令操作。具体如下:

// C source  1                     |             2
int main(){ |int main(){
int i=0; | int i=0;
int j=i--; | int j=i;
| --i;
return 0; | return 0;
} |}
// assembly without optimization |
_main: 1 |_main: 2
pushl %ebp | pushl %ebp
movl %esp, %ebp | movl %esp, %ebp
andl $-16, %esp | andl $-16, %esp
subl $16, %esp | subl $16, %esp
call ___main | call ___main
movl $0, 12(%esp) | movl $0, 12(%esp)
movl 12(%esp), %eax | movl 12(%esp), %eax
leal -1(%eax), %edx |
movl %edx, 12(%esp) | movl %eax, 8(%esp)
movl %eax, 8(%esp) | subl $1, 12(%esp)
movl $0, %eax | movl $0, %eax
leave | leave
ret | ret
.ident "GCC: (GNU) 4.8.3" | .ident "GCC: (GNU) 4.8.3"

因此,不要指望任何版本的编译器都能帮你优化代码!

然后看第16行的for语句,为什么不用while语句?从语法上讲,“while(1)”与“for(;;)”是相同的,都是死循环。但是后者是一个无条件跳转,即不需要条件判断直接循环;而前者多了条件判断,虽然这个条件判断永远为真,但是多了一个机器指令操作。(PS:目前编译器已经十分智能,对于这两种写法编译结果都是无条件跳转,并不需要额外的条件判断,还是那句话,不要指望任何版本的编译器都能帮你优化代码!)

尽管如此,第28行仍然需要条件判断,何不写在for中?抛开无条件跳转的优势之外,这样写有什么不同?仔细分析可知,如果循环到5次时,找到了满足条件的连续元素对(i,j),那么第28行的条件判断只执行了4次;如果将28行条件判断写在for中,则需要5次条件判断。由此可见,STL源码对健壮、高效和精妙的卓越追求!

此外,STL同样提供了带比较器的next_permutation:

template <class BidirectionalIterator,
class BinaryPredicate>
bool next_permutation(
BidirectionalIterator _First,
BidirectionalIterator _Last,
BinaryPredicate _Comp
);

这里不再进行分析。

注:本文涉及的源码:permutation : https://git.oschina.net/eudiwffe/codingstudy/tree/master/src/permutation/permutation.c

STL permutation : https://git.oschina.net/eudiwffe/codingstudy/tree/master/src/permutation/permutation_stl.cpp

[算法]——全排列(Permutation)以及next_permutation的更多相关文章

  1. 打印全排列和stl::next_permutation

    打印全排列是个有点挑战的编程问题.STL提供了stl::next_permutation完美的攻克了这个问题. 可是,假设不看stl::next_permutation,尝试自己解决,怎么做? 非常自 ...

  2. C++ 全排列函数 std::next_permutation与std::prev_permutation

    C++ STL中提供了std::next_permutation与std::prev_permutation可以获取数字或者是字符的全排列,其中std::next_permutation提供升序.st ...

  3. 全排列函数(next_permutation)

    顾名思义,这个函数就是用来求数组的全排列的,至于怎么用,看下面的介绍: 这是一个c++函数,包含在头文件algorithm里面,这个函数可以从当前的数组的大小按照字典序逐个递增的顺序排列 看下面的模板 ...

  4. 全排列函数(next_permutation())

    平常需要全排列的时候,一般都是dfs然后字符串匹配啥的……今天看题解的时候突然发现了这个神器. next_permutation()函数在c++的algorithm库里,作用是传入一个数组,输出这个数 ...

  5. 全排列 permutation

    给定一个数字列表,返回其所有可能的排列 lintcode package www.dxb.com; import java.util.List;import java.util.ArrayList; ...

  6. next_permutation(全排列算法)

    STL提供了两个用来计算排列组合关系的算法,分别是next_permutation和prev_permutation.首先我们必须了解什么是"下一个"排列组合,什么是"前 ...

  7. 【LeetCode】Permutation全排列

    1. Next Permutation 实现C++的std::next_permutation函数,重新排列范围内的元素,返回按照 字典序 排列的下一个值较大的组合.若其已经是最大排列,则返回最小排列 ...

  8. 排列算法汇总(下一个排列,全排列,第K个排列)

    一.下一个排列 首先,STL提供了两个用来计算排列组合关系的算法,分别是next_permutation和prev_permutation. next_permutation(nums.begin() ...

  9. 全排列 ( next_permutation)

    SLT: C++的STL有一个函数可以方便地生成全排列,这就是next_permutation 在C++ Reference中查看了一下next_permutation的函数声明: #include ...

随机推荐

  1. Swift中的Optional类型 (可选类型)与强制解包 ? !

    我们在swift的开发中会经常遇见?和! ,理解这两个符号深层次的内容对我们的开发是相当有利的: 目前网上对swift3.0的教程还相当的少,如果去搜索会发现早期的说法,在定义变量的时候,swift是 ...

  2. 让 Terminal/vim 使用 Solarized 配色

    经过亲身体验,终于使用上了solarized的配色,之前配出来相差太多,于是找到这篇参考博文:http://blog.csdn.net/angle_birds/article/details/1169 ...

  3. Java和C++中的static

    1.Java类中的static变量和static方法会在类装载的过程中就得到内存分配,然后就会进行初始化工作.最多可能会被初始化3次,静态代码块的执行在main方法之前. static变量不可以在构造 ...

  4. 通过IL分析C#中的委托、事件、Func、Action、Predicate之间的区别与联系

    先说一下个人理解的结论吧: delegate是C#中的一种类型,它实际上是一个能够持有对某个方法的引用的类. delegate声明的变量与delegate声明的事件,并没有本质的区别,事件是在dele ...

  5. 一个最简单的登录页面测试case

    具体需求: 有一个登陆页面, (假如上面有2个textbox, 一个提交按钮. 请针对这个页面设计30个以上的testcase.) 此题的考察目的:面试者是否熟悉各种测试方法,是否有丰富的Web测试经 ...

  6. 安装mysql5.5时候的报错解决办法:

    每次安装mysql5.5的时候总会报出一下错误: -- Could NOT find OpenSSL (missing: OPENSSL_LIBRARIES OPENSSL_INCLUDE_DIR) ...

  7. JAVA定义接口格式:

    [public]interface 接口名称 [extends父接口名列表] { //静态常量 [public] [static] [final] 数据类型变量名=常量值; //抽象方法 [publi ...

  8. DevOps的修炼之路!

    作为一名产品经理,首先要知道产品对于所属公司来说意味着什么,要探寻这个问题,我们又得知道和公司息息相关的是什么,在我的理解来看,与公司状况相关的因素有以下这些: 市场份额 平均订单金额 盈利能力 资产 ...

  9. oracle 驱动安装备忘

    ubuntu 从oracle官网下载两个必须的rpm包(这里选择的是version12.1.0.2.0, 64位操作系统) oracle-instantclient12.1-basic-12.1.0. ...

  10. Cobar-Client 实现策略总结

    1. 数据源 DataSource CobarClient 的 DataSource 分为三层 ICobarDataSourceService: 封装了多个 DataSourceDescriptor, ...