代码测试模板
1 |
|
1. 交换元素法
《剑指Offer》第一版的面试题28:字符串的排列,书上给出的递归算法实现如下:
1 | void Permutation(int k) { |
这种做法的问题是,得出的结果无法保持字典序,比如n=4时,1432在1423前面,原因是4和2交换后,后面的3和2就失去了有序性。
那么对后面进行排序呢?也不行,如果排序之后,2的位置变了,之后调用的swap(a[1], a[3])
交换的便是4和3而非4和2。
2. DFS
其实字典序排序的就是找到N叉树的所有根节点到叶子节点的路径,以123为例
1 | [ ] |
构造出这棵树,然后DFS即可。问题是如何构造?实际上是N叉树剪枝。剪枝通过为每个节点设置标志来判断,无需用哈希表,每个节点对应数组的一个下标。实现如下:
先添加全局变量以及修改init()
函数
1 | #include <algorithm> |
然后实现递归函数
1 | void dfs(int k) { |
3. STL算法
STL提供了现成的函数来提供下一个字典序(默认)排列
1 | sort(a, a + n); |
因此上述代码就足以实现算法了,这里主要研究下next_permutation()
的实现。next_permutation()
生成的是下一个较大的序列,比如对123生成的就是132而非213、231,因为不存在大小范围在123和132之间的排列。
设当前序列为a[n],且存在i使得a[i]>a[i+1]>…>a[n-1]。此时无法通过交换a[i]到a[n-1]之间的两个元素使其字典序更大。
因此必须将a[i-1]和a[i]..a[n-1]之间一个比a[i-1]大的数交换,比如对153642,后缀642单调递减,因此得交换3和6、4、2的一个数。而交换2会使字典序减小,因此交换4,得到154632,由于6>4>3>2,相当于3插入2和4之间,因此后缀632是单调递减,需要将其逆置来获得最小字典序。
具体证明这里不详述,可参考全排列生成算法: next_permutation
1 | bool NextPermutation() { |
这种做法还有个优点就是避免了重复输出。
比如对序列112,用DFS做会生成两个112。而用这种方法做:
- a[1]和a[2]交换,得到121;
- a[0]和a[1]交换,得到211,结束。
因为每次交换的是a[i-1]和大于a[i-1]的a[j],因此不会像交换法或DFS一样将a[i-1]和相等的a[j]交换。
这里使用lower_bound()
在降序序列中进行二分查找,找到用来和a[i-1]交换的a[j]。而STL直接是从后往前查找,原因是next_permutation()
是基于双向迭代器的,无法应用于二分查找。虽然理论上我这种做法更快,但实际上生成全排列的时间复杂度是O(n!),n不可能很大,充其量上两位数,相比起全排列本身的复杂度,O(n)到O(logn)的效率提升实际上无关痛痒。