概念

全排列的生成算法有很多种,有递归遍例,也有循环移位法等等。C++/STL中定义的next_permutation和prev_permutation函数则是非常灵活且高效的一种方法,它被广泛的应用于为指定序列生成不同的排列。本文将详细的介绍prev_permutation函数的内部算法。

按照STL文档的描述,next_permutation函数将按字母表顺序生成给定序列的下一个较大的序列,直到整个序列为减序为止。prev_permutation函数与之相反,是生成给定序列的上一个较小的序列。二者原理相同,仅遍例顺序相反,这里仅以next_permutation为例介绍算法。

下文内容都基于一个假设,即序列中不存在相同元素。对序列大小的比较做出定义:两个长度相同的序列,从两者的第一个元素开始向后比较,直到出现一个不同元素(也可能就是第它们的第一个元素),该元素较大的序列为大,反之序列为小;若一直到最后一个元素都相同,那么两个序列相等。

设当前序列为pn,下一个较大的序列为pn+1,那么不存在pm,使得pn < pm < pn+1

问题

给定任意非空序列,生成下一个较大或较小的序列。

数学推导

根据上述概念易知,对于一个任意序列,最小的序列是增序,最大的序列为减序。那么给定一个pn要如何才能生成pn+1呢?先来看下面的例子:

我们用<a1 a2 ... am>来表示m个数的一种序列。设序列pn=<3 6 4 2>,根据定义可算得下一个序列pn+1=<4 2 3 6>。观察pn可以发现,其子序列<6 4 2>已经为减序,那么这个子序列不可能通过交换元素位置得出更大的序列了,因此必须移动最高位3(即a1)的位置,且要在子序列<6 4 2>中找一个数来取代3的位置。子序列<6 4 2>中6和4都比3大,但6大于4。如果用6去替换3得到的序列一定会大于4替换3得到的序列,因此只能选4。将4和3的位置对调后形成排列<4 6 3 2>。对调后得到的子序列<6 3 2>仍保持减序,即这3个数能够生成的最大的一种序列。而4是第1次作为首位的,需要右边的子序列最小,因此4右边的子序列应为<2 3 6>,这样就得到了正确的一个序列pn+1=<4 2 3 6>。

下面归纳分析该过程。假设一个有m个元素的序列pn,其下一个较大序列为pn+1

1) 若pn最右端的2个元素构成一个增序子序列,那么直接反转这2个元素使该子序列成为减序,即可得到pn+1

2) 若pn最右端一共有连续的s个元素构成一个减序子序列,令i = m - s,则有pn(i) < pn(i+1),其中pn(i)表示排列pn的第i个元素。例如pn=<1 2 5 4 3>,那么pn的右端最多有3个元素构成一个减序子集<5 4 3>,i=5-3=2,则有pn(i)=2 < 5=pn(i+1)。因此若将pn(i)和其右边的子集s {pn(i+1), pn(i+2), ..., pn(m)}中任意一个元素调换必能得到一个较大的序列(不一定是下一个)。要保证是下一个较大的序列,必须保持pn(i)左边的元素不动,并在子集s {pn(i+1), pn(i+2), ..., pn(m)}中找出所有比pn(i)大的元素中最小的一个pn(j),即不存在pn(k) ∈ s且pn(i) < pn(k) < pn(j),然后将二者调换位置。现在只要使新子集{pn(i+1), pn(i+2), ..., pn(i), ...,pn(m)}成为最小序列即得到pn+1。注意到新子集仍保持减序,那么此时直接将其反转即可得到pn+1 {pn(1), pn(2), ..., pn(j), pn(m), pn(m-1), ..., pn(i), ..., pn(i+2), pn(i+1)}。

复杂度

最好的情况为pn的最右边的2个元素构成一个最小的增序子集,交换次数为1,复杂度为O(1),最差的情况为1个元素最小,而右面的所有元素构成减序子集,这样需要先将第1个元素换到最右,然后反转右面的所有元素。交换次数为1+(n-1)/2,复杂度为O(n)。因为各种排列等可能出现,所以平均复杂度即为O(n)。

扩展

1. 能否直接算出集合{1, 2, ..., m}的第n个排列?

设某个集合{a1, a2, ..., am}(a1<a2<...<am)构成的某种序列pn,基于以上分析易证得:若as<at,那么将as作为第1个元素的所有序列一定都小于at作为第1个元素的任意序列。同理可证得:第1个元素确定后,剩下的元素中若as'<at',那么将as'作为第2个元素的所有序列一定都小于作为第2个元素的任意序列。例如4个数的集合{2, 3, 4, 6}构成的序列中,以3作为第1个元素的序列一定小于以4或6作为第1个元素的序列;3作为第1个元素的前题下,2作为第2个元素的序列一定小于以4或6作为第2个元素的序列。

推广可知,在确定前i(i<n)个元素后,在剩下的m-i=s个元素的集合{aq1, aq2, ..., aq3}(aq1<aq2<...<aqm)中,以aqj作为第i+1个元素的序列一定小于以aqj+1作为第i+1个元素的序列。由此可知:在确定前i个元素后,一共可生成s!种连续大小的序列。

根据以上分析,对于给定的n(必有n<=m!)可以从第1位开始向右逐位地确定每一位元素。在第1位不变的前题下,后面m-1位一共可以生成(m-1)!中连续大小的序列。若n>(m-1)!,则第1位不会是a1,n中可以容纳x个(m-1)!即代表第1位是ax。在确定第1位后,将第1位从原集合中删除,得到新的集合{aq1, aq2, ..., aq3}(aq1<aq2<...<aqm),然后令n1=n-x(m-1)!,求这m-1个数中生成的第n1个序列的第1位。

举例说明:如7个数的集合为{1, 2, 3, 4, 5, 6, 7},要求出第n=1654个排列。

(1654 / 6!)取整得2,确定第1位为3,剩下的6个数{1, 2, 4, 5, 6, 7},求第1654 % 6!=214个序列;

(214 / 5!)取整得1,确定第2位为2,剩下5个数{1, 4, 5, 6, 7},求第214 % 5!=94个序列;

(94 / 4!)取整得3,确定第3位为6,剩下4个数{1, 4, 5, 7},求第94 % 4!=22个序列;

(22 / 3!)取整得3,确定第4位为7,剩下3个数{1, 4, 5},求第22 % 3!=4个序列;

(4 / 2!)得2,确定第5为5,剩下2个数{1, 4};由于4 % 2!=0,故第6位和第7位为增序<1 4>;

因此所有排列为:3267514。

2. 给定一种排列,如何算出这是第几个排列呢?

和前一个问题的推导过程相反。例如3267514:

后6位的全排列为6!,3为{1, 2, 3 ,4 , 5, 6, 7}中第2个元素(从0开始计数),故2*720=1440;

后5位的全排列为5!,2为{1, 2, 4, 5, 6, 7}中第1个元素,故1*5!=120;

后4位的全排列为4!,6为{1, 4, 5, 6, 7}中第3个元素,故3*4!=72;

后3位的全排列为3!,7为{1, 4, 5, 7}中第3个元素,故3*3!=18;

后2位的全排列为2!,5为{1, 4, 5}中第2个元素,故2*2!=4;

最后2位为增序,因此计数0,求和得:1440+120+72+18+4=1654

next_Permutation Code 如下

 class Solution {
public:
void nextPermutation(vector<int> &num) {
// Start typing your C/C++ solution below
// DO NOT write int main() function
int i;
vector<int>::iterator iter = num.end();
int maxNum = INT_MIN; //step 1, find the first number which violate the increase form right to left, we call it AAA
// take 5 6 7 8 4 3 2 1 for a example, find AAA = 7;
for(i = num.size() - ; i >= ; i--)
{
if (num[i] < num[i+])
break;
} if (i >= ) //i found
{
int j;
int minNum = INT_MAX;
//step 2, find the first number which is large than AAA form right to left, we call it BBB
// take 5 6 7 8 4 3 2 1 for a example, find BBB = 8;
for(j = num.size() - ; j >= ; j--)
{
if (num[j] > num[i])
break;
} //step 3, swap AAA and BBB
// take 5 6 7 8 4 3 2 1 for a example, exchanged array is 5 6 8 7 4 3 2 1
int t = num[i];
num[i] = num[j];
num[j] = t; //step 4, reverse all digit after AAA's position
// take 5 6 7 8 4 3 2 1 for a example, exchanged array is 5 6 8 7 4 3 2 1, then reverse 7 4 3 2 1
// the output array is 5 6 8 1 2 3 4 7
int k = i + ;
j = num.size() - ;
while(k < j)
{
t = num[k];
num[k] = num[j];
num[j] = t; k++;
j--;
}
}
else
//sort(num.begin(), num.end());
{
int k = ;
int j = num.size() - ;
while(k < j)
{
int t = num[k];
num[k] = num[j];
num[j] = t; k++;
j--;
}
}
}
};

总结:

next_permutation的实现过程如下:

首先,从最尾端开始往前寻找两个相邻的元素,令第一个元素是i, 第二个元素是ii,且满足i<ii; (找破坏了递增的那个元素AAA,位置m)

然后,再从最尾端开始往前搜索,找出第一个大于i的元素,设其为j;(找比AAA大的元素BBB)

然后,将i和j对调,再将ii及其后面的所有元素反转。(交换AAA 和BBB,然后reverse m右边的元素)

这样得到的新序列就是“下一个排列”。

  prev_permutation的实现过程如下:

首先,从最尾端开始向前寻找两个相邻的元素,令第一个元素为i,第二个元素为ii,且满足i>ii(找破坏了递减的那个元素AAA,位置m)

然后,从最尾端开始往前寻找第一个小于i的元素,令它为j(找比AAA小的元素BBB)

然后,将i和j对调,再将ii及其之后的所有元素反转。(交换AAA 和BBB,然后reverse m右边的元素)

    这样得到的序列就是该排列的上一个排列。

[LeetCode] next_permutation的更多相关文章

  1. 【LeetCode】数组排列问题(permutations)(附加next_permutation解析)

    描述 Given a collection of distinct integers, return all possible permutations. Example: Input: [1,2,3 ...

  2. [LeetCode] Palindrome Permutation II 回文全排列之二

    Given a string s, return all the palindromic permutations (without duplicates) of it. Return an empt ...

  3. [LeetCode] Permutations II 全排列之二

    Given a collection of numbers that might contain duplicates, return all possible unique permutations ...

  4. [LeetCode] Permutations 全排列

    Given a collection of numbers, return all possible permutations. For example,[1,2,3] have the follow ...

  5. leetcode bugfree note

    463. Island Perimeterhttps://leetcode.com/problems/island-perimeter/就是逐一遍历所有的cell,用分离的cell总的的边数减去重叠的 ...

  6. 九度OJ 1120 全排列 -- 实现C++STL中next_permutation()

    题目地址:http://ac.jobdu.com/problem.php?pid=1120 题目描述: 给定一个由不同的小写字母组成的字符串,输出这个字符串的所有全排列. 我们假设对于小写字母有'a' ...

  7. LeetCode:60. Permutation Sequence,n全排列的第k个子列

    LeetCode:60. Permutation Sequence,n全排列的第k个子列 : 题目: LeetCode:60. Permutation Sequence 描述: The set [1, ...

  8. [LeetCode] Next Greater Element III 下一个较大的元素之三

    Given a positive 32-bit integer n, you need to find the smallest 32-bit integer which has exactly th ...

  9. 【一天一道LeetCode】#60. Permutation Sequence.

    一天一道LeetCode系列 (一)题目 The set [1,2,3,-,n] contains a total of n! unique permutations. By listing and ...

随机推荐

  1. Matlab中reshape函数的使用

    reshape把指定的矩阵改变形状,但是元素个数不变, 例如,行向量: a = [1 2 3 4 5 6] 执行下面语句把它变成3行2列: b = reshape(a,3,2) 执行结果: b = 1 ...

  2. Java系列,《Java核心技术 卷1》,chapter 13,集合

    13.1.2 Java类库中的集合接口和迭代器接口     删除元素,对于next和remove的调用是互相依赖的,如果调用remove之前没有调用next,则会跑出IllegalStateExcep ...

  3. Django添加Last-Modified和ETag

    用Django REST Framework做的REST API,其中有个API有这样的需求: APP端请求这个API,服务器端从数据库读数据,返回json.返回的数据量稍微有些大,但是可能一年才修改 ...

  4. 实验5 简单嵌入式WEB服务器实验 实验报告 20135303 20135326

    北京电子科技学院(BESTI) 实     验    报     告 课程:信息安全系统设计基础                班级:  1353 姓名:20135303 魏昊卿 学号:2013532 ...

  5. iOS——百度统计

    百度移动统计(http://mtj.baidu.com)是一款专业的移动应用统计分析工具,支持ios和android平台.开发者可以方便地通过嵌入统计SDK,对移动应用进行全方位监测,实时监控产品表现 ...

  6. python为什么会有@classmethod?

    今天被问了这么个问题 python为什么要有classmethod? 被问倒了,只能回答:classmethod不需要实例化类,用起来比较方便.这么回答没有什么底细,于是查看了一下python的官方文 ...

  7. JavaScript里面三个等号和两个等号有什么区别?

    1.对于string,number等基础类型,==和===是有区别的 a)不同类型间比较,==之比较“转化成同一类型后的值”看“值”是否相等,===如果类型不同,其结果就是不等 b)同类型比较,直接进 ...

  8. jQuery使用之(三)处理页面的元素

    对于页面的元素,在DOM编程中可以通过各种查询.修改手段进行管理,非常麻烦.jQuery提供了一整套的方法来处理页面的元素.包括元素的内容.复制.移动和替换等.本节将介绍一些常用的内容. 1.直接获取 ...

  9. 每天一个linux命令(48):netstat命令

    netstat命令用于显示与IP.TCP.UDP和ICMP协议相关的统计数据,一般用于检验本机各端口的网络连接情况.netstat是在内核中访问网络及相关信息的程序,它能提供TCP连接,TCP和UDP ...

  10. 每天一个linux命令(12):head 命令

    head 与 tail 就像它的名字一样的浅显易懂,它是用来显示开头或结尾某个数量的文字区块,head 用来显示档案的开头至标准输出中,而 tail 想当然尔就是看档案的结尾. 1.命令格式: hea ...