1.简介
说起全排列,想必大家都不会陌生。而全排列的生成算法,也一直是各类 OJ 网站常见的题目,可见其经典程度有多高。为了快速掌握、复习算法思想,节省时间。现推出各种经典算法的实例分析。本次带来的是全排列生成算法。
2.非递归算法
在 C++ STL 中,通过包含 algorithm 头文件,可以使用 std::next_permutation 和 std::prev_permutation 算法。
这两个算法,输入一对迭代器 [first,last) 标记的范围。假设输入数组[1,2,3,4], std::next_permutation 求得该数组的下一个字典序排列即[1,2,4,3],并且返回ture.
因此,求一个序列的全排列,可以使用如下 C++ 代码:
#include<iostream>
#include<algorithm>
#include<vector>
int main()
{
vector<int> vec{1,2,3};
do
{
for(auto elem:vec)
std::cout<<elem<<' ';
std::cout<<std::endl;
}while(std::next_permutation(vec.begin(),vec.end()));
return 0;
}
求得结果如下:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
通过这种方法求全排,关键点就是:给定一个序列,例如[1,3,2],如何找到它的下一个字典序排列?
从上述代码的运行结果可以看到:
①没有相邻逆序对的序列,在字典序中排在最前。例如[1,2,3]中,(1,2) 和 (2,3)都是顺序的。
②没有相邻顺序对的序列,在字典序中排在最后。例如[3,2,1]中,(3,2) 和 (2,1)都是逆序的。
利用这两个结论,可以帮我们找到指定序列的下一个字典序。
来看[1,3,2]这个序列。我们看到子序列[3,2]是逆序的,使用结论②可以得出这个子序列排在最后。因此,这是以1开头的最后一个序列。那么排在[1,3,2]之后的序列,必然是以2开头,并且接下来的子序列是顺序的。可以得出是[2,1,3]。
接着,形式化一下这种方法。有序列S = [n0,n1,n2,...n(i-1),ni,nj,...,nk],记序列L = [n0,n1,n2,...,n(i-1)],序列R = [nj,...,nk].假设ni < nj,并且子序列R是逆序的。
此时,R没有任何相邻数对是顺序的,由结论②可得序列S是以[n0,n1,...,ni]开头的最后一个序列。
序列[ni,nj,...,nk]还不是逆序,通过变化必然可以得到下一个字典序。所以,构造序列S的下一个字典序列时,必须得保持L不变然后加上[ni,nj,...,nk]的下一个字典序。 举例来说,当序列S = [1,4,3,5,2],L = [1,4],R=[5,2].序列[3,5,2]并不是逆序,所以还不能改变L,只能改变序列[3,5,2]来得到S的下一个字典序。
那么序列M = [ni,nj,...,nk]的下一个字典序什么呢?可以看到,这个序列是以 ni 开头的最后一个序列,那么下一字典序如何构造呢?下一个字典序的第一位,一定是大于ni的最小值。由于R逆序且ni < nj,所以这个值一定存在于序列R中。假设这个大于ni的最小值为nx,那么只要将剩下的数按顺序排在后面。就一定可以得到以 nx开头的第一个字典序。
这种按字典序求下一个排列的方法,已经由C++ STL 库函数 std::next_permutation实现
link(http://en.cppreference.com/w/cpp/algorithm/next_permutation)
代码如下:
template<class BidirIt>
bool next_permutation(BidirIt first, BidirIt last)
{
if (first == last) return false;
BidirIt i = last;
if (first == --i) return false;
while (true) {
BidirIt i1, i2;
i1 = i;
if (*--i < *i1) { //从后边起,寻找第一个顺序对(i,i1)
i2 = last;
while (!(*i < *--i2))//从[il1,last)找到大于i的最小值i2
;
std::iter_swap(i, i2);
std::reverse(i1, last);//交换 i,i2 后,[i1,last)仍然是逆序,所以这里要 reverse 处理
return true;
}
if (i == first) {
std::reverse(first, last);
return false;
}
}
}
在leetcode中,可以找到 permutation 这个问题进行练习。可以看到,当前运行最快的算法,是递归版的。看来,递归版的算法,确实值得研究。
3.总结
求一个序列的下一个字典序排列,首先,最重要的点就是记住顺序和逆序这两种序列的特点;然后,当一个序列包含逆序的子序列时,就可以很方便地构造下一个字典序排列。
在实现算法时,也要充分利用已有的信息。