全排列算法

Posted at:
January 9, 2018

1 简介

给定含有N个互不相同元素的序列,输出这N个元素的所有排列(全排列)。

以字符串"abc"为例,全排列包括"abc","acb","bac","bca","cab","cba"这6个结果。
从排列组合来计算6=3!=3*2*1,也就是我们依次选择3个元素:

  • 第一个元素有3种选择(a,b,c),假设选择c
  • 第二个元素只有2种选择(a,b),假设选择b
  • 第三个元素只有1种选择(a)

2 递归

首先我们采用递归的方式实现。
假设我们已经完成N-1个元素的全排列,那么怎么完成N个元素的全排列?
由于上面的简介可知,N个元素的全排列结果数目等于N! = N*(N-1)!。
也就是说遍历任选N个元素中的一个后再与剩下的N-1个元素的全排列组合。

2.1 版本一

代码实现如下:

对于"abc",上述代码输出结果为"abc","acb","bac","bca","cba","cab"。
细心的朋友可能会发现先输出"cba",然后再输出"cab"。
是的,上述代码输出次序并不是按照 字典序

2.2 版本二

在序列本身有序(升序)的前提下,把上面的swap改成如下操作即可实现字典序输出:

  • 把[selIdx, idx)区间的元素整体后移一位
  • 再把idx元素放到selIdx位置

代码实现如下:

当然从效率上来说,相比swap交换,上述代码效率有所降低。

3 非递归

要以非递归方式实现全排列,首先要解决的是根据当前排列得到下一个排列。
另外需要注意的是全排列中所有排列也是有顺序的,比较自然的想法就是某种意义上的“升序”。
以"abc"序列来说,"bac"在"bca"之前是比较自然的,因为这两个排列都以b开头,应该以第二元素a和c决定顺序。

3.1 字典序

上述顺序其实就是 字典序 ,简单来说就是以字母的顺序比较序列中的元素,相同则比较下一元素,直到决出顺序。
那么如何得到字典序的下一个排列?算法如下:

  • 从排列末尾寻找相邻升序子序列seq[i]<seq[i+1],如果不存在说明是最后的排列
  • 从排列末尾寻找seq[j]>seq[i] (由上述步骤可知j一定存在)
  • 交换seq[i]与seq[j]
  • 反转seq[i+1]之后的元素

代码实现如下:

3.2 STL之next_permutation

实际上STL标准模板库已经提供了这样的算法,通过next_permutation可以获取下一个排列。
示例代码如下:

4 变种

上述讨论的序列中元素是互不相同的,其中一种变种就是允许重复元素。
由于非递归采用的是字典序保持“升序”,所以不存在问题。
而递归方法会重复打印,需要调整为在待选择元素中选择下一个与当前选择不同的元素。

Modified at: January 9, 2018